+
+ Popover Right
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit.
+ Voluptas reprehenderit doloremque mollitia esse eveniet dolor.
+ Eos quasi amet, assumenda omnis aliquid cum tenetur ratione
+ tempore similique, itaque maiores et vel.
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default TooltipPage;
diff --git a/src/pages/components/typography.jsx b/src/pages/components/typography.jsx
new file mode 100644
index 0000000..4d9803f
--- /dev/null
+++ b/src/pages/components/typography.jsx
@@ -0,0 +1,498 @@
+import React from "react";
+import Card from "@/components/ui/Card";
+import Icon from "@/components/ui/Icon";
+const lists = [
+ {
+ id: 1,
+ },
+ {
+ id: 2,
+ },
+ {
+ id: 3,
+ },
+ {
+ id: 4,
+ },
+ {
+ id: 5,
+ },
+ {
+ id: 6,
+ },
+];
+const typography = () => {
+ return (
+
+
+
+
+ All HTML headings, are available. .h1 through .h7 classes are also
+ available, for when you want to match the font styling of a heading.
+
+
+ PREVIEW
+ FONT SIZE
+
+
+
+
Heading 1
+
+ 60px
+
+
+
+
+
Heading 2
+
+ 48px
+
+
+
+
+
Heading 3
+
+ 36px
+
+
+
+
Heading 4
+
+ 30px
+
+
+
+
Heading 5
+
+ 24px
+
+
+
+
Heading 6
+
+ 20px
+
+
+
+
+
+ All HTML headings are available with light and bold font-weight. Use
+ .font-weight-normal for light heading and .
+
+
+ LIGHT HEADINGS
+ BOLD HEADINGS
+
+
+
+
Heading
+ Heading
+
+
+
+
Heading 2
+ Heading 2
+
+
+
+
Heading 3
+ Heading 3
+
+
+
Heading 4
+ Heading 4
+
+
+
Heading 5
+ Heading 5
+
+
+
Heading 6
+ Heading 6
+
+
+
+
+
+
+
+
+
+
+ Type
+
+
+
+
+ Class
+
+
+
+
+ Text
+
+
+
+
+
+
+
+ Title
+
+
+ text-lg
+
+
+
+ Cupcake ipsum dolor sit amet fruitcake donut chocolate.
+
+
+ font-size: 18px / line-height: 28px / font-weight: 500
+
+
+
+
+
+ Sub Title
+
+
+ text-base
+
+
+
+ Cupcake ipsum dolor sit amet fruitcake donut chocolate.
+
+
+ font-size: 16px / line-height: 24px / font-weight: 400
+
+
+
+
+
+ Body Text
+
+
+ text-sm
+
+
+
+ Cupcake ipsum dolor sit amet fruitcake donut chocolate.
+
+
+ font-size: 14px / line-height: 20px / font-weight: 400
+
+
+
+
+
+ Small Text
+
+
+ text-xs
+
+
+
+ Cupcake ipsum dolor sit amet fruitcake donut chocolate.
+
+
+ font-size: 12px / line-height: 18px / font-weight: 400
+
+
+
+
+
+
+
+
+
+
+ Use the included utility classes to recreate the small secondary-500
+ heading text.
+
+
+
+
Display heading
+
+
+
Display heading
+
+
+
Display heading
+
+
+
Display heading
+
+
+
Display heading
+
+
+
Display heading
+
+
+
+
+ Use the included utility classes to recreate the small secondary-500
+ heading text.
+
+
+
+
Display heading
+
+
+
Display heading
+
+
+
Display heading
+
+
+
Display heading
+
+
+
Display heading
+
+
+
Display heading
+
+
+
+
+
+ Traditional heading elements are designed to work best in the meat
+ of your page content. When you need a heading to stand out, consider
+ using a display heading—a larger, slightly more opinionated heading
+ style.
+
+
+
+
Display 1
+
+
+
Display 2
+
+
+
Display 3
+
+
+
Display 4
+
+
+
+
+
+
+
+ Styling for common inline HTML5 elements.
+
+
+
+ This line of text is meant to be treated as deleted text.
+
+
+
+
+ This line of text is meant to be treated as an addition to the
+ document.
+
+
+
+
+ This line of text is meant to be treated as deleted text.
+
+
+
+
+
+
+
+
+
+ Styling for common inline HTML5 elements.
+
+
+ Styling for common inline HTML5 elements.
+
+
+ Styling for common inline HTML5 elements.
+
+
+ This is warning-500 text You can archive this adding
+ .text-warning-500 class
+
+
+ This is danger-500 text You can archive this adding
+ .text-danger-500 class
+
+
+
+
+
+
+
+
+
+
+ Unorder list.
+
+
+ {lists.map((item, i) => (
+
+ Lorem ipsum dolor sit amet.
+
+ ))}
+
+
+
+
+
+ list-decimal
+
+
+
+ Lorem ipsum dolor sit amet.
+
+ Lorem ipsum dolor sit amet.
+
+
+
+ Lorem ipsum dolor sit amet.
+
+
+ Lorem ipsum dolor sit amet.
+
+
+ Lorem ipsum dolor sit amet.
+
+
+ Lorem ipsum dolor sit amet.
+
+
+
+
+
+
+ Dash list
+
+
+
+
+ {" "}
+ Lorem ipsum dolor sit amet.
+
+
+ Lorem ipsum dolor sit amet.
+
+
+
+
+ {" "}
+ Lorem ipsum dolor sit amet.
+
+
+ Lorem ipsum dolor sit amet.
+
+
+
+
+ {" "}
+ Lorem ipsum dolor sit amet.
+
+
+ Lorem ipsum dolor sit amet.
+
+
+
+
+
+
+ Icon List 1
+
+
+ {lists.map((item, i) => (
+
+
+
+
+ Lorem ipsum dolor sit amet.
+
+ ))}
+
+
+
+
+ Icon List 2
+
+
+ {lists.map((item, i) => (
+
+
+
+
+ Lorem ipsum dolor sit amet.
+
+ ))}
+
+
+
+
+ Icon List 3
+
+
+ {lists.map((item, i) => (
+
+
+ Lorem ipsum dolor sit amet.
+
+ ))}
+
+
+
+
+
+
+
+
+
+ Lorem, ipsum dolor sit amet consectetur adipisicing elit.
+ Accusamus laudantium omnis fugit ducimus nulla libero temporibus
+ corrupti non voluptatem harum?
+
+ Dashcode Admin Template
+
+
+
+ Lorem, ipsum dolor sit amet consectetur adipisicing elit.
+ Accusamus laudantium omnis fugit ducimus nulla libero temporibus
+ corrupti non voluptatem harum?
+
+ Dashcode Admin Template
+
+
+
+ Lorem, ipsum dolor sit amet consectetur adipisicing elit.
+ Accusamus laudantium omnis fugit ducimus nulla libero temporibus
+ corrupti non voluptatem harum?
+
+ Dashcode Admin Template
+
+
+
+
+
+
+
+ );
+};
+
+export default typography;
diff --git a/src/pages/components/video.jsx b/src/pages/components/video.jsx
new file mode 100644
index 0000000..7c3fcd6
--- /dev/null
+++ b/src/pages/components/video.jsx
@@ -0,0 +1,12 @@
+import React from "react";
+import VideoPlayer from "@/components/ui/VideoPlayer";
+
+const VideoPage = () => {
+ return (
+
+
+
+ );
+};
+
+export default VideoPage;
diff --git a/src/pages/csv/index.jsx b/src/pages/csv/index.jsx
new file mode 100644
index 0000000..ab4b0eb
--- /dev/null
+++ b/src/pages/csv/index.jsx
@@ -0,0 +1,281 @@
+import { useEffect, useState } from "react";
+import DataTable from "react-data-table-component";
+import { ChevronDown, Download } from "lucide-react";
+import { ToastContainer, toast } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
+import { VITE_API_BASE_URL } from "../../constant/config";
+
+const CsvExport = () => {
+ const authToken = sessionStorage.getItem("token");
+ const [reportList, setReportList] = useState([]);
+ const [filteredList, setFilteredList] = useState([]);
+ const [searchName, setSearchName] = useState("");
+ const [searchType, setSearchType] = useState("");
+ const [searchDate, setSearchDate] = useState("");
+ const [resetPaginationToggle, setResetPaginationToggle] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const fetchExcelReport = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}settings/report-list`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) throw new Error("Failed to fetch Excel Report list");
+
+ const data = await response.json();
+
+ if (data.status === 200 || data.status === "success") {
+ setReportList(data.result || []);
+ setFilteredList(data.result || []);
+ } else {
+ toast.error("Failed to load CSV exports list");
+ }
+ } catch (error) {
+ console.error(error);
+ toast.error("An error occurred");
+ }
+ };
+
+ useEffect(() => {
+ fetchExcelReport();
+ }, []);
+
+ const handleSearch = () => {
+ let filtered = [...reportList];
+ if (searchName.trim() !== "") {
+ filtered = filtered.filter((r) =>
+ r.file_name.toLowerCase().includes(searchName.toLowerCase())
+ );
+ }
+
+ if (searchType !== "") {
+ filtered = filtered.filter(
+ (r) => r.file_type.toLowerCase() === searchType.toLowerCase()
+ );
+ }
+ if (searchDate !== "") {
+ const now = new Date();
+
+ filtered = filtered.filter((r) => {
+ const reportDate = new Date(r.created_at.replace(" ", "T"));
+
+ if (searchDate === "today") {
+ return reportDate.toDateString() === now.toDateString();
+ } else if (searchDate === "this-week") {
+ const startOfWeek = new Date(now);
+ startOfWeek.setDate(now.getDate() - now.getDay() + 1);
+ const endOfWeek = new Date(startOfWeek);
+ endOfWeek.setDate(startOfWeek.getDate() + 6);
+ return reportDate >= startOfWeek && reportDate <= endOfWeek;
+ } else if (searchDate === "this-month") {
+ return (
+ reportDate.getMonth() === now.getMonth() &&
+ reportDate.getFullYear() === now.getFullYear()
+ );
+ }
+ return true;
+ });
+ }
+
+ setFilteredList(filtered);
+ setResetPaginationToggle(!resetPaginationToggle);
+ };
+
+ const columns = [
+ {
+ name: "Action",
+ minWidth: "50px",
+ center: true,
+ cell: (row) => (
+
+
+
+ ),
+ },
+ {
+ name: "Title",
+ sortable: true,
+ minWidth: "200px",
+ selector: (row) => row.file_name || "-",
+ center: true,
+ },
+ {
+ name: "Type",
+ sortable: true,
+ selector: (row) => row.file_type || "-",
+ minWidth: "200px",
+ center: true,
+ },
+ {
+ name: "Exported Date",
+ sortable: true,
+ minWidth: "180px",
+ center: true,
+ selector: (row) =>
+ row.created_at
+ ? new Date(row.created_at.replace(" ", "T")).toLocaleString()
+ : "-",
+ },
+ {
+ name: "Status",
+ sortable: true,
+ minWidth: "120px",
+ center: true,
+ cell: (row) => {
+ let color = "gray";
+ if (row.status === "active") color = "green";
+ else if (row.status === "inactive") color = "red";
+
+ return (
+
+ {row.status}
+
+ );
+ },
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#312e81",
+ color: "white",
+ fontSize: "16px",
+ },
+ },
+ headCells: {
+ style: {
+ justifyContent: "center",
+ textAlign: "center",
+ },
+ },
+ rows: {
+ style: {
+ minHeight: "60px",
+ fontSize: "15px",
+ justifyContent: "center",
+ textAlign: "center",
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: "#f9fafb",
+ },
+ },
+ cells: {
+ style: {
+ justifyContent: "center",
+ textAlign: "center",
+ },
+ },
+ pagination: {
+ style: {
+ borderTopStyle: "solid",
+ borderTopWidth: "1px",
+ borderTopColor: "#e5e7eb",
+ },
+ },
+ };
+
+ return (
+
+
+
+ CSV Export List
+
+
+
+
Search
+
+
+
+
+
+
+ Report Name :
+
+ setSearchName(e.target.value)}
+ placeholder="Enter file name..."
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
+ />
+
+
+
+ Report Type :
+
+ setSearchType(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
+ >
+ All
+ Store Discount
+ Outlet
+ Promo Code
+ Promo
+ Member
+ Voucher
+ Topup
+ Order
+ Sales Report
+ Product Report
+ Promo Report
+
+
+
+
+ Export Date :
+
+ setSearchDate(e.target.value)}
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
+ >
+ All
+ Today
+ This Week
+ This Month
+
+
+
+
+
+
+ {loading ? "Searching..." : "Search"}
+
+
+
+
+
+ }
+ responsive
+ />
+
+
+ );
+};
+
+export default CsvExport;
diff --git a/src/pages/dashboard/index.jsx b/src/pages/dashboard/index.jsx
new file mode 100644
index 0000000..7526ff2
--- /dev/null
+++ b/src/pages/dashboard/index.jsx
@@ -0,0 +1,292 @@
+import React, { useState, useMemo } from "react";
+import DataTable from "react-data-table-component";
+import { Filter } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data";
+
+const priorityColorMap = {
+ High: "bg-red-100 text-red-800",
+ Medium: "bg-yellow-100 text-yellow-800",
+ Low: "bg-green-100 text-green-800",
+};
+
+const InlineFilterComponent = ({ filters, setFilters, onClear }) => {
+ return (
+
+ {/* Search */}
+
+ Search
+
+ setFilters((prev) => ({ ...prev, searchText: e.target.value }))
+ }
+ />
+
+
+ {/* Category */}
+
+ Category
+
+ setFilters((prev) => ({ ...prev, category: e.target.value }))
+ }
+ >
+ All
+ FLSS
+ TH
+ TP
+ LM
+ TL
+
+
+
+ {/* Priority */}
+
+ Priority
+
+ setFilters((prev) => ({ ...prev, priority: e.target.value }))
+ }
+ >
+ All
+ High
+ Medium
+ Low
+
+
+
+ {/* Status */}
+
+ Status
+
+ setFilters((prev) => ({ ...prev, status: e.target.value }))
+ }
+ >
+ All
+ Pending
+ Approved
+ Rejected
+
+
+
+ {/* Clear */}
+
+
+ Clear Filters
+
+
+
+ );
+};
+
+const Dashboard = () => {
+ const [data] = useState(initialCasesSeed);
+ const [filters, setFilters] = useState({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ });
+ const [showFilters, setShowFilters] = useState(false);
+
+ // Filter logic
+ const filteredData = useMemo(
+ () =>
+ data.filter((item) => {
+ const matchSearch =
+ item.name.toLowerCase().includes(filters.searchText.toLowerCase()) ||
+ item.id.toLowerCase().includes(filters.searchText.toLowerCase());
+ const matchCategory =
+ filters.category === "" || item.category === filters.category;
+ const matchPriority =
+ filters.priority === "" || item.priority === filters.priority;
+ const matchStatus =
+ filters.status === "" || item.status === filters.status;
+ return matchSearch && matchCategory && matchPriority && matchStatus;
+ }),
+ [data, filters]
+ );
+
+ // Summary counts
+ const summary = useMemo(() => {
+ const total = filteredData.length;
+ const pending = filteredData.filter((x) => x.status === "Pending").length;
+ const approved = filteredData.filter((x) => x.status === "Approved").length;
+ const scheduled = filteredData.filter((x) => x.status === "Scheduled").length;
+ return { total, pending, approved, scheduled };
+ }, [filteredData]);
+
+ const columns = [
+ { name: "Case ID", selector: (row) => row.id, sortable: true, width: "120px" },
+ { name: "Category", selector: (row) => row.category, sortable: true, width: "120px" },
+ { name: "Name", selector: (row) => row.name, sortable: true, wrap: true },
+ { name: "Requested Date", selector: (row) => row.requested, sortable: true, width: "200px" },
+ {
+ name: "Priority",
+ selector: (row) => row.priority,
+ sortable: true,
+ width: "120px",
+ cell: (row) => (
+
+ {row.priority}
+
+ ),
+ },
+ {
+ name: "Status",
+ selector: (row) => row.status,
+ sortable: true,
+ width: "140px",
+ cell: (row) => {
+ let bg = "bg-gray-100 text-gray-800";
+ if (row.status === "Pending") bg = "bg-yellow-100 text-yellow-800";
+ else if (row.status === "Approved") bg = "bg-green-100 text-green-800";
+ else if (row.status === "Rejected") bg = "bg-red-100 text-red-800";
+ else if (row.status === "Scheduled") bg = "bg-blue-100 text-blue-800";
+ return (
+
+ {row.status}
+
+ );
+ },
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "#ffffff",
+ fontSize: "14px",
+ fontWeight: "600",
+ },
+ },
+ rows: {
+ style: {
+ fontSize: "14px",
+ "&:nth-of-type(odd)": { backgroundColor: "#f9fafb" },
+ },
+ },
+ };
+
+ return (
+
+
+
File Upload
+
+
+
+
+
+
+
+ Click to upload or drag & drop
+
+ PNG, JPG, or PDF (max 10MB)
+
+
+
+
+ Submit
+
+
+
+
+ {/* Header */}
+
+
Case List
+ setShowFilters(!showFilters)}
+ className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 rounded-md hover:bg-blue-700 text-sm"
+ >
+
+ Filter
+
+
+
+ {/* Summary Section */}
+
+
+
Total Cases
+
{summary.total}
+
+
+
Pending
+
{summary.pending}
+
+
+
Approved
+
{summary.approved}
+
+
+
Scheduled
+
{summary.scheduled}
+
+
+
+ {/* Filter panel */}
+ {showFilters && (
+
+ setFilters({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ })
+ }
+ />
+ )}
+
+ {/* DataTable */}
+ No matching cases found
+ }
+ />
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/src/pages/icons.jsx b/src/pages/icons.jsx
new file mode 100644
index 0000000..32a5781
--- /dev/null
+++ b/src/pages/icons.jsx
@@ -0,0 +1,158 @@
+import React from "react";
+import Tooltip from "@/components/ui/Tooltip";
+import Icon from "@/components/ui/Icon";
+import useSkin from "@/hooks/useSkin";
+const icons = [
+ {
+ name: "heroicons:academic-cap",
+ },
+ {
+ name: "heroicons:adjustments-horizontal",
+ },
+ {
+ name: "heroicons:adjustments-vertical",
+ },
+ {
+ name: "heroicons:archive-box",
+ },
+ {
+ name: "heroicons:archive-box-arrow-down",
+ },
+ {
+ name: "heroicons:archive-box-x-mark",
+ },
+ {
+ name: "heroicons:arrow-down",
+ },
+ {
+ name: "heroicons:arrow-down-circle",
+ },
+ {
+ name: "heroicons:arrow-down-left",
+ },
+ {
+ name: "heroicons:arrow-down-on-square",
+ },
+ {
+ name: "heroicons:arrow-up-tray",
+ },
+ {
+ name: "heroicons:arrows-pointing-in",
+ },
+ {
+ name: "heroicons:cloud",
+ },
+ {
+ name: "heroicons:cog",
+ },
+ {
+ name: "heroicons:command-line",
+ },
+ {
+ name: "heroicons:computer-desktop",
+ },
+ {
+ name: "heroicons:cpu-chip",
+ },
+ {
+ name: "heroicons:document-arrow-down",
+ },
+ {
+ name: "heroicons:envelope",
+ },
+ {
+ name: "heroicons:envelope-open",
+ },
+ {
+ name: "heroicons:exclamation-circle",
+ },
+ {
+ name: "heroicons:exclamation-triangle",
+ },
+ {
+ name: "heroicons:eye",
+ },
+ {
+ name: "heroicons:eye-dropper",
+ },
+ {
+ name: "heroicons:film",
+ },
+ {
+ name: "heroicons:heart",
+ },
+ {
+ name: "heroicons:inbox",
+ },
+ {
+ name: "heroicons:inbox",
+ },
+ {
+ name: "heroicons:information-circle",
+ },
+ {
+ name: "heroicons:lifebuoy",
+ },
+ {
+ name: "heroicons:identification",
+ },
+ {
+ name: "heroicons:key",
+ },
+ {
+ name: "heroicons:link",
+ },
+ {
+ name: "heroicons:pencil-square",
+ },
+ { name: "heroicons:rectangle-stack" },
+ { name: "heroicons:rocket-launch" },
+ { name: "heroicons:window" },
+ { name: "heroicons:wifi" },
+ { name: "heroicons:wallet" },
+ { name: "heroicons:variable" },
+ { name: "heroicons:users" },
+ { name: "heroicons:user-plus" },
+ { name: "heroicons:user-minus" },
+ { name: "heroicons:user-group" },
+ { name: "heroicons:user-circle" },
+ { name: "heroicons:user" },
+ { name: "heroicons:square-2-stack" },
+ { name: "heroicons:shopping-bag" },
+ { name: "heroicons:shield-check" },
+ { name: "heroicons:share" },
+ { name: "heroicons:wrench" },
+];
+const IconPage = () => {
+ const [skin] = useSkin();
+ return (
+
+ );
+};
+
+export default IconPage;
diff --git a/src/pages/member/index.jsx b/src/pages/member/index.jsx
new file mode 100644
index 0000000..dc66b0e
--- /dev/null
+++ b/src/pages/member/index.jsx
@@ -0,0 +1,772 @@
+import { useEffect, useState, useMemo } from "react";
+import DataTable from "react-data-table-component";
+import {
+ PenSquare,
+ LayoutGrid,
+ Trash,
+ Plus,
+ Download,
+ ChevronDown,
+ Search,
+ X,
+ Filter,
+} from "lucide-react";
+import { useNavigate, useLocation } from "react-router-dom";
+import DeleteConfirmationModal from "../../components/ui/DeletePopUp";
+import { VITE_API_BASE_URL } from "../../constant/config";
+import { CSVLink } from "react-csv";
+import { ToastContainer, toast } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
+import UserService from "../../store/api/userService";
+
+const MemberPage = () => {
+ const authToken = sessionStorage.getItem("token");
+ const [customerData, setCustomerData] = useState([]);
+ const [filteredCustomerData, setFilteredCustomerData] = useState([]);
+ const [tierData, setTierData] = useState([]);
+ const navigate = useNavigate();
+ const [userPermissions, setUserPermissions] = useState({});
+ const [hasCreatePermission, setHasCreatePermission] = useState(false);
+ const [hasUpdatePermission, setHasUpdatePermission] = useState(false);
+ const [hasDeletePermission, setHasDeletePermission] = useState(false);
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ // Filter states
+ const [nameFilter, setNameFilter] = useState("");
+ const [emailFilter, setEmailFilter] = useState("");
+ const [phoneFilter, setPhoneFilter] = useState("");
+ const [tierFilter, setTierFilter] = useState("");
+ const [referralFilter, setReferralFilter] = useState("");
+ const [showFilters, setShowFilters] = useState(false);
+
+ //pagination
+ const [page, setPage] = useState(1);
+ const [limit, setLimit] = useState(20);
+ const [totalRows, setTotalRows] = useState(0);
+
+ const userData = useMemo(() => {
+ try {
+ const userStr = localStorage.getItem("user");
+ return userStr ? JSON.parse(userStr) : null;
+ } catch (error) {
+ console.error("Error parsing user data:", error);
+ return null;
+ }
+ }, []);
+
+ const user_id = userData?.user?.user_id || null;
+
+ const exportToCSV = async () => {
+ if (isDisabled) return;
+ setIsDisabled(true);
+
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}outlets/export-excel`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ Authorization: `Bearer ${authToken}`,
+ },
+ body: new URLSearchParams({
+ user_id: user_id,
+ type: "member",
+ }),
+ });
+
+ const result = await response.json();
+ if (result.status === 200) {
+ toast.success(
+ "Export file has been processed, this may take a while! You can find it in the Excel Report tab."
+ );
+ } else {
+ toast.error("Failed to export file.");
+ }
+ } catch (err) {
+ toast.error("Something went wrong.");
+ } finally {
+ setTimeout(() => setIsDisabled(false), 1000);
+ }
+ };
+
+ const fetchFilteredCustomers = async () => {
+ try {
+ const params = new URLSearchParams();
+
+ if (nameFilter) params.append("name", nameFilter);
+ if (emailFilter) params.append("email", emailFilter);
+ if (phoneFilter) params.append("phone", phoneFilter);
+ if (tierFilter) params.append("tier", tierFilter);
+ if (referralFilter) params.append("referral", referralFilter);
+
+ const response = await fetch(
+ `${VITE_API_BASE_URL}customers?${params.toString()}`,
+ {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch customers");
+ }
+
+ const result = await response.json();
+ setFilteredCustomerData(result.data || []);
+ } catch (err) {
+ console.error("Error fetching filtered customers:", err);
+ toast.error("Failed to load filtered data");
+ }
+ };
+
+ const fetchUserPermissions = async () => {
+ try {
+ const userStr = localStorage.getItem("user");
+ if (!userStr) return;
+
+ const userObj = JSON.parse(userStr);
+ const userId = userObj?.user.user_id;
+ if (!userId) return;
+
+ const userDataRes = await UserService.getUser(userId);
+ const userData = userDataRes?.data;
+ if (!userData) return;
+
+ // Check user is admin
+ if (userData.role && userData.role.toLowerCase() === "admin") {
+ setIsAdmin(true);
+ setHasCreatePermission(true);
+ setHasUpdatePermission(true);
+ setHasDeletePermission(true);
+ return;
+ }
+
+ let permissions = {};
+ if (userData.user_permissions) {
+ try {
+ permissions = JSON.parse(userData.user_permissions);
+ setUserPermissions(permissions);
+
+ // Check Topup module permissions
+ if (permissions.Member) {
+ if (permissions.Member.create === true) {
+ setHasCreatePermission(true);
+ }
+ if (permissions.Member.update === true) {
+ setHasUpdatePermission(true);
+ }
+ if (permissions.Member.delete === true) {
+ setHasDeletePermission(true);
+ }
+ }
+ } catch (e) {
+ console.error("Error parsing user permissions:", e);
+ }
+ }
+ } catch (err) {
+ console.error("Error fetching user permissions:", err);
+ }
+ };
+
+ const deleteItem = async (customerId) => {
+ try {
+ const response = await fetch(
+ VITE_API_BASE_URL + "customers/delete/" + customerId,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${authToken}`,
+ },
+ }
+ );
+ let data = response;
+
+ if (!response.ok) {
+ console.error("Error deleteing customer:", data);
+ throw new Error("This customer cant be found in the database");
+ }
+
+ setShowDeleteModal(false);
+ setSelectedMemberId(null);
+
+ setTimeout(() => window.location.reload(), 3500);
+ } catch (err) {
+ console.error("Error deleteing customer:", err);
+ toast.error(err.message || "Unexpected error");
+ }
+ };
+
+ // const fetchCustomerWalletHistory = async (customerId) => {
+ // try {
+ // const response = await fetch(
+ // `${VITE_API_BASE_URL}customer-wallet/history/${customerId}`,
+ // {
+ // method: "GET",
+ // headers: {
+ // "Content-Type": "application/json",
+ // Authorization: `Bearer ${authToken}`,
+ // },
+ // }
+ // );
+
+ // if (!response.ok) {
+ // throw new Error("Network response was not ok");
+ // }
+
+ // const walletHistory = await response.json();
+ // return walletHistory.data;
+ // } catch (error) {
+ // console.error("Error fetching wallet history:", error);
+ // }
+ // };
+
+ const fetchCustomers = async (page = 1, limit = 25) => {
+ try {
+ setLoading(true);
+
+ const params = new URLSearchParams({
+ page: page.toString(),
+ limit: limit.toString(),
+ });
+
+ if (nameFilter) params.append("name", nameFilter);
+ if (emailFilter) params.append("email", emailFilter);
+ if (phoneFilter) params.append("phone", phoneFilter);
+ if (tierFilter) params.append("tier", tierFilter);
+ if (referralFilter) params.append("referral", referralFilter);
+
+ const response = await fetch(`${VITE_API_BASE_URL}customers?${params}`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) throw new Error("Failed to fetch customers");
+
+ const result = await response.json();
+
+ setCustomerData(result.data || []);
+ setFilteredCustomerData(result.data || []);
+ setTotalRows(result.total || 0);
+ setPage(result.page || 1);
+ setLimit(result.limit || 25);
+ } catch (error) {
+ console.error("Error fetching customers:", error);
+ toast.error("Failed to load customer list");
+ } finally {
+ setLoading(false);
+ }
+};
+
+
+
+ const handlePageChange = (newPage) => {
+ fetchCustomers(newPage, limit);
+ };
+
+ const handlePerRowsChange = async (newLimit, newPage) => {
+ setLimit(newLimit);
+ fetchCustomers(newPage, newLimit);
+ };
+
+ const fetchTierData = async () => {
+ try {
+ const response = await fetch(
+ VITE_API_BASE_URL + "settings/membership-tiers",
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${authToken}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error("Network response was not ok");
+ }
+
+ const tierData = await response.json();
+ setTierData(tierData.data);
+ } catch (error) {
+ console.error("Error fetching tier data:", error);
+ }
+ };
+
+ useEffect(() => {
+ console.log("token", authToken);
+ fetchCustomers();
+ fetchTierData();
+ fetchUserPermissions();
+ }, []);
+
+ // Apply filters whenever filter values change
+ // useEffect(() => {
+ // applyFilters();
+ // }, [nameFilter, emailFilter, phoneFilter, tierFilter, referralFilter, customerData]);
+
+ // const applyFilters = () => {
+ // let filteredData = [...customerData];
+
+ // if (nameFilter) {
+ // filteredData = filteredData.filter(customer =>
+ // customer.name && customer.name.toLowerCase().includes(nameFilter.toLowerCase())
+ // );
+ // }
+
+ // if (emailFilter) {
+ // filteredData = filteredData.filter(customer =>
+ // customer.email && customer.email.toLowerCase().includes(emailFilter.toLowerCase())
+ // );
+ // }
+
+ // if (phoneFilter) {
+ // filteredData = filteredData.filter(customer =>
+ // customer.phone && customer.phone.includes(phoneFilter)
+ // );
+ // }
+
+ // if (tierFilter) {
+ // filteredData = filteredData.filter(customer =>
+ // customer.customer_tier && customer.customer_tier.toLowerCase() === tierFilter.toLowerCase()
+ // );
+ // }
+
+ // if (referralFilter) {
+ // filteredData = filteredData.filter(customer =>
+ // customer.customer_referral_code &&
+ // customer.customer_referral_code.toLowerCase().includes(referralFilter.toLowerCase())
+ // );
+ // }
+
+ // setFilteredCustomerData(filteredData);
+ // };
+
+ const clearFilters = () => {
+ setNameFilter("");
+ setEmailFilter("");
+ setPhoneFilter("");
+ setTierFilter("");
+ setReferralFilter("");
+ };
+
+ const [selectedMemberId, setSelectedMemberId] = useState(null);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+ const tierColorClasses = {
+ gold: "text-yellow-600 font-medium",
+ silver: "text-gray-500 font-medium",
+ bronze: "text-amber-700 font-medium",
+ platinum: "text-purple-600 font-medium",
+ };
+
+ const getTierColor = (tier) => {
+ const key = tier?.toLowerCase();
+ return key ? tierColorClasses[key] : "";
+ };
+
+ function getTierName(tierIdStr) {
+ const normalizedId = String(tierIdStr).trim();
+ const tier = tierData.find((t) => String(t.id).trim() === normalizedId);
+ return tier ? tier.name : null;
+ }
+
+ const csvData = [
+ [
+ "Name",
+ "Email",
+ "Phone",
+ "Membership Tier",
+ "Total Transaction (RM)",
+ "Last Transaction Date",
+ "Wallet Value",
+ "Unclaimed Vouchers",
+ "Subscription",
+ "Date Joined",
+ "Referral",
+ ],
+ ...filteredCustomerData.map((c) => [
+ c.name,
+ c.email,
+ c.phone,
+ c.customer_tier,
+ c.totalTransaction,
+ c.lastTransactionDate,
+ c.customer_wallet,
+ c.unclaimedVouchers,
+ c.subscription,
+ c.created_at,
+ c.customer_referral_code,
+ ]),
+ ];
+
+ const handleAddMember = () => {
+ navigate("/member/add_new_member/");
+ };
+
+ const handleEditMember = (id) => {
+ navigate(`/member/member_overview/${id}`);
+ };
+
+ const handleOrgChart = () => {
+ navigate("/member/org_chart");
+ };
+
+ const handleDeleteClick = (id) => {
+ setSelectedMemberId(id);
+ setShowDeleteModal(true);
+ };
+
+ const ActionButtons = ({ row }) => {
+ return (
+
+ {(isAdmin || hasUpdatePermission) && (
+
handleEditMember(row.id)}
+ title="Edit Member"
+ >
+
+
+ )}
+
+
+
+
+
+ {(isAdmin || hasDeletePermission) && (
+
handleDeleteClick(row.id)}
+ title="Delete Member"
+ >
+
+
+ )}
+
+ );
+ };
+
+ const columns = [
+ {
+ name: "Action",
+ cell: (row) => ,
+ button: true,
+ width: "180px",
+ right: true,
+ },
+ {
+ name: "Name",
+ selector: (row) => row.name || "-",
+ minWidth: "200px",
+ wrap: true,
+ sortable: true,
+ },
+ {
+ name: "Phone Number",
+ selector: (row) => row.phone || "-",
+ minWidth: "180px",
+ sortable: true,
+ },
+ {
+ name: "Email Address",
+ selector: (row) => row.email || "-",
+ minWidth: "280px",
+ sortable: true,
+ },
+ {
+ name: "Membership Tier",
+ selector: (row) => row.customer_tier_id,
+ cell: (row) =>
+ row.customer_tier ? (
+
+ {row.customer_tier}
+
+ ) : (
+ "-"
+ ),
+ minWidth: "180px",
+ sortable: true,
+ },
+ {
+ name: "Wallet Value",
+ selector: (row) =>
+ row.customer_wallet ? `RM ${row.customer_wallet}` : "RM0.00",
+ minWidth: "150px",
+ sortable: true,
+ },
+ {
+ name: "Total Order",
+ selector: (row) =>
+ row.total_order ? row.total_order : 0,
+ minWidth: "150px",
+ sortable: true,
+ },
+ {
+ name: "Total Spend",
+ selector: (row) =>
+ row.total_order_spend ? `RM ${row.total_order_spend}` : "RM0.00",
+ minWidth: "150px",
+ sortable: true,
+ },
+ {
+ name: "Active Voucher",
+ selector: (row) =>
+ row.active_voucher ? row.active_voucher : 0,
+ minWidth: "150px",
+ sortable: true,
+ },
+ {
+ name: "Used Voucher",
+ selector: (row) =>
+ row.used_voucher ? row.used_voucher : 0,
+ minWidth: "150px",
+ sortable: true,
+ },
+ {
+ name: "Latest Order Date",
+ selector: (row) =>
+ row.latest_order_date ? row.latest_order_date : "-",
+ minWidth: "200px",
+ sortable: true,
+ },
+ {
+ name: "Date Joined",
+ selector: (row) => row.created_at || "-",
+ minWidth: "200px",
+ sortable: true,
+ },
+ {
+ name: "Referral",
+ selector: (row) =>
+ row.customer_referral_code ? row.customer_referral_code : "-",
+ sortable: true,
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#312e81",
+ color: "white",
+ minHeight: "50px",
+ fontSize: "16px",
+ justifyContent: "center",
+ },
+ },
+ headCells: {
+ style: {
+ paddingLeft: "16px",
+ paddingRight: "16px",
+ fontWeight: "500",
+ justifyContent: "center",
+ textAlign: "center",
+ subHeaderWrap: true,
+ },
+ },
+ rows: {
+ style: {
+ minHeight: "60px",
+ fontSize: "15px",
+ "&:hover": {
+ backgroundColor: "#f9fafb",
+ },
+ justifyContent: "center",
+ center: true,
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: "#f9fafb",
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: "16px",
+ paddingRight: "16px",
+ justifyContent: "center",
+ textAlign: "center",
+ alignItems: "center",
+ center: true,
+ },
+ },
+ pagination: {
+ style: {
+ borderTopStyle: "solid",
+ borderTopWidth: "1px",
+ borderTopColor: "#e5e7eb",
+ },
+ },
+ };
+
+ return (
+
+
+
+
Members
+
+
+
Member
+
+ {(isAdmin || hasCreatePermission) && (
+
+
+ Add New Member
+
+ )}
+
+
+ Export Report
+
+
setShowFilters(!showFilters)}
+ >
+
+ {showFilters ? "Hide Filters" : "Show Filters"}
+
+
+
+
+ {/* Filter Section */}
+ {showFilters && (
+
+
+
+
+ fetchCustomers(1, limit)}
+ className="px-8 py-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
+ >
+ Submit
+
+
+
+ Clear Filters
+
+
+
+ )}
+
+
}
+ progressPending={loading}
+ responsive
+ />
+
+
+
setShowDeleteModal(false)}
+ onConfirm={() => {
+ deleteItem(selectedMemberId);
+ setShowDeleteModal(false);
+ }}
+ />
+
+
+ );
+};
+
+export default MemberPage;
diff --git a/src/pages/member/member-add.jsx b/src/pages/member/member-add.jsx
new file mode 100644
index 0000000..723007a
--- /dev/null
+++ b/src/pages/member/member-add.jsx
@@ -0,0 +1,460 @@
+import { useRef, useState, useEffect } from 'react';
+import { X, Eye, ChevronDown, EyeOff } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import md5 from 'md5';
+import { ToastContainer, toast } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+
+const AddMember = () => {
+ const [showPassword, setShowPassword] = useState(false);
+ const [formData, setFormData] = useState({
+ customer_type: '',
+ customer_tier: '',
+ phone: '',
+ password_hash: '',
+ name: '',
+ email: '',
+ birthday: '',
+ customer_referral_id: '',
+ status: '',
+ profile_picture: ''
+ });
+ const [tierData, setTierData] = useState([]);
+ // const authToken = localStorage.getItem('authToken');
+ const authToken = sessionStorage.getItem('token');
+ const [customerType, setCustomerType] = useState([]);
+ const [customerData, setCustomerData] = useState([]);
+
+ const fetchCustomers = async () => {
+ try {
+
+ const response = await fetch(VITE_API_BASE_URL + "customers", {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerData = await response.json();
+ // console.log('response', customerData.data);
+
+ setCustomerData(customerData.data);
+
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ }
+ }
+
+ const fetchTierData = async () => {
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "settings/membership-tiers", {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const tierData = await response.json();
+
+ setTierData(tierData.data);
+ } catch (error) {
+ console.error("Error fetching tier data:", error);
+ }
+ }
+
+ const fetchCustomerType = async () => {
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "settings/customer-types", {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerTypeData = await response.json();
+
+ setCustomerType(customerTypeData.data);
+ } catch (error) {
+ console.error("Error fetching customer type data:", error);
+ }
+ }
+
+
+ useEffect(() => {
+ fetchTierData();
+ fetchCustomerType();
+ fetchCustomers();
+ // console.log("Tier data fetched:", tierData);
+ }, []);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ if (name === 'password_hash') {
+ setFormData(prev => ({
+ ...prev,
+ [name]: md5(value)
+ }));
+ }
+ else {
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ console.log('Form submitted:', formData);
+ const formData1 = new FormData();
+ Object.entries(formData).forEach(([key, value]) => {
+ formData1.append(key, value);
+ });
+
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "customers/create", {
+ method: 'POST',
+ headers: {
+ // 'Content-Type': 'multipart/form-data',
+ Authorization: `Bearer ${authToken}`,
+ },
+ // body: JSON.stringify(formData)
+ body: formData1
+ });
+
+ // console.log('Response status:', response.status);
+ let data = response;
+
+ if (!response.ok) {
+ console.error("Error creating member:", data);
+ throw new Error('Fill in all required fields');
+ }
+
+ toast.success(data.message || "Created successfully", {
+ position: "top-right",
+ autoClose: 1500,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ onClose: () => {
+ navigate("/member");
+ },
+ });
+
+ handleReset();
+ } catch (err) {
+ console.error("Error creating member:", err);
+ toast.error(err.message || "Unexpected error");
+ }
+ };
+
+ const handleImageUpload = (e) => {
+ // console.log('Image selected:', e.target.files[0]);
+ const name = e.target.name;
+ const file = e.target.files[0];
+ if (file) {
+ setPreview(URL.createObjectURL(file));
+ // onImageSelect(file);
+ }
+
+ setFormData((prev) => ({
+ ...prev,
+ profile_picture: file,
+ }));
+ };
+
+ const navigate = useNavigate();
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const handleReset = () => {
+ setFormData({
+ customer_type: '',
+ customer_tier: '',
+ phone: '',
+ password_hash: '',
+ name: '',
+ email: '',
+ birthday: '',
+ customer_referral_id: '',
+ status: '',
+ profile_picture: ''
+ });
+
+ setPreview(null);
+ hiddenInput.current.value = null;
+ }
+
+ const hiddenInput = useRef(null);
+ const [preview, setPreview] = useState(null);
+
+ const handleClick = () => hiddenInput.current.click();
+
+
+ return (
+
+
+
Add New Member
+
+
+
+
+
+
+
+ );
+};
+
+export default AddMember;
\ No newline at end of file
diff --git a/src/pages/member/member-chart.jsx b/src/pages/member/member-chart.jsx
new file mode 100644
index 0000000..c9ad638
--- /dev/null
+++ b/src/pages/member/member-chart.jsx
@@ -0,0 +1,176 @@
+import { useState, useEffect, useRef } from 'react';
+import OrganizationChart from "organization-chart-react";
+import "organization-chart-react/dist/style.css";
+import { useNavigate } from 'react-router-dom';
+
+const orgData = {
+ title: "CEO",
+ titleClass: "title-box",
+ contentClass: "rounded-lg text-center p-3",
+ member: [
+ {
+ name: "Oliver",
+ add: "View Order",
+ },
+ ],
+ children: [
+ {
+ title: "MANAGEMENT",
+ titleClass: "title-box",
+ contentClass: "rounded-lg text-center p-3",
+ member: [
+ {
+ name: "Jake",
+ add: "View Order",
+ },
+ ],
+ children: [
+ {
+ title: "FRONTEND",
+ titleClass: "title-box",
+ contentClass: "rounded-lg text-center p-3",
+ member: [
+ {
+ name: "David",
+ add: "View Order",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ title: "DEVELOPMENT",
+ titleClass: "title-box",
+ contentClass: "rounded-lg text-center p-3",
+ member: [
+ {
+ name: "Emma",
+ add: "View Order",
+ },
+ ],
+ },
+ {
+ title: "DEVELOPMENT",
+ titleClass: "title-box",
+ contentClass: "rounded-lg text-center p-3",
+ member: [
+ {
+ name: "Nick",
+ add: "View Order",
+ },
+ ],
+ },
+ ],
+};
+
+const OrgChart = () => {
+ const [zoom, setZoom] = useState(0.7);
+ const [position, setPosition] = useState({ x: 0, y: 0 });
+ const [isDragging, setIsDragging] = useState(false);
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
+ const chartContainerRef = useRef(null);
+
+ const customCSS = `
+ .org-chart-container {
+ font-family: Arial, sans-serif;
+ }
+
+ .org-extend-arrow {
+ width: 30px;
+ height: 30px;
+ }
+
+ .title-box {
+ display: none;
+ }
+ `;
+
+
+ const handleMouseDown = (e) => {
+ setIsDragging(true);
+ setDragStart({
+ x: e.clientX - position.x,
+ y: e.clientY - position.y
+ });
+ };
+
+ const handleMouseMove = (e) => {
+ if (isDragging) {
+ setPosition({
+ x: e.clientX - dragStart.x,
+ y: e.clientY - dragStart.y
+ });
+ }
+ };
+
+ const handleMouseUp = () => {
+ setIsDragging(false);
+ };
+
+ const handleMouseLeave = () => {
+ setIsDragging(false);
+ };
+
+ const handleWheel = (e) => {
+ e.preventDefault();
+ const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
+ setZoom(prevZoom => {
+ const newZoom = prevZoom * zoomFactor;
+ return Math.min(Math.max(newZoom, 0.5), 2);
+ });
+ };
+
+ useEffect(() => {
+ const container = chartContainerRef.current;
+ if (container) {
+ container.addEventListener('wheel', handleWheel, { passive: false });
+ }
+ return () => {
+ if (container) {
+ container.removeEventListener('wheel', handleWheel);
+ }
+ };
+ }, []);
+
+ const navigate = useNavigate();
+ const handleOrder = () => {
+ navigate('/member/member_overview/member_order');
+ }
+
+
+ return (
+
+
+ Organization Chart
+
+
+
+
+
+
+ );
+};
+
+export default OrgChart;
\ No newline at end of file
diff --git a/src/pages/member/member-e-add-address.jsx b/src/pages/member/member-e-add-address.jsx
new file mode 100644
index 0000000..083c259
--- /dev/null
+++ b/src/pages/member/member-e-add-address.jsx
@@ -0,0 +1,297 @@
+import { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { ToastContainer, toast } from 'react-toastify';
+
+const MemberAddAddress = () => {
+ const authToken = sessionStorage.getItem('token');
+ const { id } = useParams();
+
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ console.log('Form submitted:', formData);
+ // const formData1 = new FormData();
+ // Object.entries(formData).forEach(([key, value]) => {
+ // formData1.append(key, value);
+ // });
+
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "customer-addresses/create/" + id, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ body: JSON.stringify(formData)
+ // body: formData1
+ });
+
+ // console.log('Response status:', response.status);
+ let data = response;
+
+ if (!response.ok) {
+ console.error("Error creating address:", data);
+ throw new Error('Fill in all required fields');
+ }
+
+ toast.success(data.message || "Created successfully", {
+ position: "top-right",
+ autoClose: 1500,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ onClose: () => {
+ navigate(-1);
+ },
+ });
+
+ handleReset();
+ } catch (err) {
+ console.error("Error adding address:", err);
+ toast.error(err.message || "Unexpected error");
+ }
+ };
+
+ const [formData, setFormData] = useState({
+ is_default: 0,
+ name: '',
+ phone: '',
+ emailAddress: '',
+ address: '',
+ postcode: '',
+ area: '',
+ state: '',
+ country: '',
+ status: '',
+ note: '',
+ unit: ''
+ });
+
+ const handleReset = () => {
+ setFormData({
+ is_default: 0,
+ name: '',
+ phone: '',
+ emailAddress: '',
+ address: '',
+ postcode: '',
+ area: '',
+ state: '',
+ country: '',
+ status: '',
+ note: '',
+ unit: ''
+ });
+ }
+
+ const navigate = useNavigate();
+ const handleChange = (e) => {
+ const { name, value, type, checked } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? (checked ? 1 : 0) : value
+ }));
+ };
+
+ // const handleSubmit = (e) => {
+ // e.preventDefault();
+ // console.log('Form submitted:', formData);
+ // };
+
+ const handleClose = () => {
+ navigate(-1);
+ };
+
+ return (
+
+
+
+
Add New Address
+
+
+
+
+
+
+
+
+
NEW ADDRESS
+
+
+
+
+
+ );
+};
+
+export default MemberAddAddress;
\ No newline at end of file
diff --git a/src/pages/member/member-e-edit-address.jsx b/src/pages/member/member-e-edit-address.jsx
new file mode 100644
index 0000000..f333b6e
--- /dev/null
+++ b/src/pages/member/member-e-edit-address.jsx
@@ -0,0 +1,312 @@
+import { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { X } from 'lucide-react';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { set } from 'react-hook-form';
+import { ToastContainer, toast } from 'react-toastify';
+
+const EditMemberAddress = () => {
+ const { addressId } = useParams();
+
+ const authToken = sessionStorage.getItem('token');
+ const [addressData, setAddressData] = useState({});
+
+ const fetchAddressData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customer-addresses/${addressId}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const addressData = await response.json()
+ const addressDetails = addressData.data;
+
+ setAddressData(addressDetails);
+ setFormData((prev) => ({ ...prev, ...addressDetails }));
+
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ }
+ }
+
+ useEffect(() => {
+ fetchAddressData();
+ }, []);
+
+
+ const [formData, setFormData] = useState({
+ is_default: 0,
+ name: '',
+ phone: '',
+ emailAddress: '',
+ address: '',
+ postcode: '',
+ area: '',
+ state: '',
+ country: '',
+ status: ''
+ });
+
+ const handleChange = (e) => {
+ const { name, value, type, checked } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? (checked ? 1 : 0) : value
+ }));
+ };
+
+ // const handleSubmit = (e) => {
+ // e.preventDefault();
+ // console.log('Form submitted:', formData);
+ // };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ console.log('Form submitted:', formData);
+ // const formData1 = new FormData();
+ // Object.entries(formData).forEach(([key, value]) => {
+ // formData1.append(key, value);
+ // });
+
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "customer-addresses/update/" + addressId, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ body: JSON.stringify(formData)
+ // body: formData1
+ });
+
+ // console.log('Response status:', response.status);
+ let data = response;
+
+ if (!response.ok) {
+ console.error("Error updating address:", data);
+ throw new Error('Fill in all required fields');
+ }
+
+ toast.success(data.message || "Save successfully", {
+ position: "top-right",
+ autoClose: 1500,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ onClose: () => {
+ navigate(-1);
+ },
+ });
+
+ // handleReset();
+ } catch (err) {
+ console.error("Error updating address:", err);
+ toast.error(err.message || "Unexpected error");
+ }
+ };
+
+ const navigate = useNavigate();
+ const handleClose = () => {
+ navigate(-1);
+ };
+
+ return (
+
+
+
+
Edit Address
+
+
+
+
+
+
+
ADDRESS
+
+
+
+
+
+ );
+};
+
+export default EditMemberAddress;
\ No newline at end of file
diff --git a/src/pages/member/member-e-edit-order.jsx b/src/pages/member/member-e-edit-order.jsx
new file mode 100644
index 0000000..b9fa5ff
--- /dev/null
+++ b/src/pages/member/member-e-edit-order.jsx
@@ -0,0 +1,190 @@
+import { useState } from 'react';
+import { ChevronLeft } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+
+const EditDetailsMemberOrder = () => {
+ const [selfPickupDate, setSelfPickupDate] = useState('');
+ const navigate = useNavigate();
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ return (
+
+
+
+
+
+ Print
+
+
+ Print Order
+
+
+ Print Pre-Order
+
+
+ Print Receipt
+
+
+
+
+ Go back
+
+
+
+ {/* Order Details Section */}
+
+
+ Order Details
+
+
+
+
+
+
+
+
+
+
+
+ {/* Order Information Section */}
+
+
+ Order #000257 03-Oct-2024 04:06 PM
+
+
+
+
+
+
+
+
+ {/* Self Pickup Details Section */}
+
+
+ Self Pickup Details
+
+
+
+
+
+ Self-Pickup Date
+
+
+
+ {/*
setSelfPickupDate(e.target.value)}
+ >
+ DD/MM/YYYY
+
+
*/}
+
setSelfPickupDate(e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+
+
+
+ {/* Other Items Section */}
+
+
Other Items
+
+
+
+
+ Product
+ Description
+ Amount
+ Quantity
+ Total
+
+
+
+
+
+
+
+
+ Demo Product
+ Code : DP00001
+
+ RM5.00
+ 5
+ RM 25.00
+
+
+
+
+ {/* Order Summary Section */}
+
+
+
+
+ {/* Product Total */}
+
+ Product Total
+ RM 25.00
+
+
+ {/* Courier Fee */}
+
+ Courier Fee
+ RM 5.10
+
+
+
+
+ {/* Grand Total */}
+
+ Grand Total
+ RM 30.10
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const DataRow = ({ label, value, valueColor = "text-gray-800" }) => {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+};
+
+export default EditDetailsMemberOrder;
\ No newline at end of file
diff --git a/src/pages/member/member-edit-address.jsx b/src/pages/member/member-edit-address.jsx
new file mode 100644
index 0000000..25c2f9a
--- /dev/null
+++ b/src/pages/member/member-edit-address.jsx
@@ -0,0 +1,343 @@
+import { useState, useEffect } from 'react';
+import { Pencil, Trash2, ChevronLeft } from 'lucide-react';
+import DataTable from 'react-data-table-component';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
+import DeleteConfirmationModal from '../../components/ui/DeletePopUp';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { use } from 'react';
+import { ToastContainer, toast } from 'react-toastify';
+
+export default function MemberEditAddress() {
+ const { id } = useParams();
+ const authToken = sessionStorage.getItem('token');
+ const [customerAddressData, setCustomerAddressData] = useState([]);
+
+ const fetchCustomerAddressData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customer-addresses/by-customer/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerAddressData = await response.json()
+ const customerAddressDetails = customerAddressData.data;
+
+ setCustomerAddressData(customerAddressDetails);
+
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ }
+ }
+
+ useEffect(() => {
+ fetchCustomerAddressData();
+ }, []);
+
+
+ const [addresses, setAddresses] = useState([
+ {
+ id: 1,
+ name: "Nur Mirleana Binti Abdul Hakim",
+ phone: "+60122455587",
+ address: "15 Apartment, 9A Jalan Van Praagh, Taman Continental, 11600 Jelutong, Penang",
+ createdDate: "19 / JUL / 2023 10:41 AM",
+ status: "Active"
+ },
+ {
+ id: 2,
+ name: "Nur Mirleana Binti Abdul Hakim",
+ phone: "+60122455587",
+ address: "15 Apartment, 9A Jalan Van Praagh, Taman Continental, 11600 Jelutong, Penang",
+ createdDate: "19 / JUL / 2023 10:41 AM",
+ status: "Active"
+ },
+ {
+ id: 3,
+ name: "Nur Mirleana Binti Abdul Hakim",
+ phone: "+60122455587",
+ address: "15 Apartment, 9A Jalan Van Praagh, Taman Continental, 11600 Jelutong, Penang",
+ createdDate: "19 / JUL / 2023 10:41 AM",
+ status: "Active"
+ },
+ {
+ id: 4,
+ name: "Nur Mirleana Binti Abdul Hakim",
+ phone: "+60122455587",
+ address: "15 Apartment, 9A Jalan Van Praagh, Taman Continental, 11600 Jelutong, Penang",
+ createdDate: "19 / JUL / 2023 10:41 AM",
+ status: "Active"
+ },
+ {
+ id: 5,
+ name: "Nur Mirleana Binti Abdul Hakim",
+ phone: "+60122455587",
+ address: "15 Apartment, 9A Jalan Van Praagh, Taman Continental, 11600 Jelutong, Penang",
+ createdDate: "19 / JUL / 2023 10:41 AM",
+ status: "Active"
+ },
+ {
+ id: 6,
+ name: "Nur Mirleana Binti Abdul Hakim",
+ phone: "+60122455587",
+ address: "15 Apartment, 9A Jalan Van Praagh, Taman Continental, 11600 Jelutong, Penang",
+ createdDate: "19 / JUL / 2023 10:41 AM",
+ status: "Active"
+ }
+ ]);
+
+ const [selectedAddressId, setSelectedAddressId] = useState(null);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+
+ const navigate = useNavigate();
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const handleAdd = () => {
+ navigate(`/member/member_overview/member_address/add_member_address/${id}`);
+ };
+
+ const location = useLocation();
+ const { row } = location.state || {};
+ const handleEdit = (addressId) => {
+ navigate(`/member/member_overview/member_address/edit_member_address/${addressId}`);
+ };
+
+ const handleDelete = (addressId) => {
+ setSelectedAddressId(addressId);
+ setShowDeleteModal(true);
+ };
+
+ const deleteItem = async (addressId) => {
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "customer-addresses/delete/" + addressId, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ // body: JSON.stringify(formData)
+ // body: formData1
+ });
+
+ // console.log('Response status:', response.status);
+ let data = response;
+
+ if (!response.ok) {
+ console.error("Error deleteing addres:", data);
+ throw new Error('This address cant be found in the database');
+ }
+
+ setTimeout(() => window.location.reload(), 3500);
+
+ } catch (err) {
+ console.error("Error deleteing address:", err);
+ toast.error(err.message || "Unexpected error");
+ }
+ };
+
+ const columns = [
+ {
+ name: 'Name',
+ selector: row => row.name,
+ // sortable: true,
+ },
+ {
+ name: 'Phone Number',
+ selector: row => row.phone,
+ // sortable: true,
+ },
+ {
+ name: 'Address',
+ selector: row => row.address,
+ // sortable: true,
+ wrap: true,
+ grow: 2,
+ },
+ {
+ name: 'Created Date',
+ selector: row => row.created_at,
+ sortable: true,
+ },
+ {
+ name: 'Status',
+ // selector: row => row.status,
+ // sortable: true,
+ cell: row => (
+
+ Active
+
+ ),
+ },
+ {
+ name: 'Action',
+ cell: row => (
+
+
handleEdit(row.id)}
+ className="p-1.5 border border-gray-300 rounded-md text-gray-600 hover:bg-gray-100"
+ >
+
+
+
handleDelete(row.id)}
+ className="p-1.5 border border-gray-300 rounded-md text-red-500 hover:bg-gray-100"
+ >
+
+
+
+ ),
+ button: true,
+ },
+ ];
+
+ // const customStyles = {
+ // headRow: {
+ // style: {
+ // backgroundColor: '#262259',
+ // color: 'white',
+ // borderRadius: '0',
+ // minHeight: '52px',
+ // },
+ // },
+ // headCells: {
+ // style: {
+ // paddingLeft: '16px',
+ // paddingRight: '16px',
+ // fontWeight: '500',
+ // },
+ // },
+ // rows: {
+ // style: {
+ // minHeight: '64px',
+ // '&:hover': {
+ // backgroundColor: '#f9fafb',
+ // },
+ // },
+ // },
+ // cells: {
+ // style: {
+ // paddingLeft: '16px',
+ // paddingRight: '16px',
+ // },
+ // },
+ // pagination: {
+ // style: {
+ // borderTop: 'none',
+ // marginTop: '16px',
+ // },
+ // pageButtonsStyle: {
+ // border: '1px solid #e5e7eb',
+ // borderRadius: '6px',
+ // height: '32px',
+ // width: '32px',
+ // padding: '4px',
+ // margin: '5px',
+ // },
+ // },
+ // };
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: '#312e81',
+ color: 'white',
+ minHeight: '50px',
+ fontSize: '16px',
+ justifyContent: 'center',
+ },
+ },
+ headCells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ fontWeight: '500',
+ justifyContent: 'center',
+ textAlign: 'center',
+ subHeaderWrap: true,
+ },
+ },
+ rows: {
+ style: {
+ minHeight: '60px',
+ fontSize: '15px',
+ '&:hover': {
+ backgroundColor: '#f9fafb',
+ },
+ justifyContent: 'center',
+ center: true,
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: '#f9fafb',
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ justifyContent: 'center',
+ textAlign: 'center',
+ alignItems: 'center',
+ center: true,
+ },
+ },
+ pagination: {
+ style: {
+ borderTopStyle: 'solid',
+ borderTopWidth: '1px',
+ borderTopColor: '#e5e7eb',
+ },
+ },
+ };
+
+ return (
+
+
+
+
+
+ Go back
+
+
+
+
+
+
Address List
+
+ + Add New Address
+
+
+
+
+
+
+
setShowDeleteModal(false)}
+ onConfirm={() => {
+ deleteItem(selectedAddressId);
+ setShowDeleteModal(false);
+ }}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/member/member-edit-adjustpoint.jsx b/src/pages/member/member-edit-adjustpoint.jsx
new file mode 100644
index 0000000..fdee031
--- /dev/null
+++ b/src/pages/member/member-edit-adjustpoint.jsx
@@ -0,0 +1,369 @@
+import { ChevronLeft } from 'lucide-react';
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { ToastContainer, toast } from 'react-toastify';
+
+const MemberEditAdjustPoint = () => {
+ const { id } = useParams();
+ const authToken = sessionStorage.getItem('token');
+ const navigate = useNavigate();
+
+ const [customerPointData, setCustomerPointData] = useState({
+ customerName: '',
+ point: '',
+ });
+
+ const [loading, setLoading] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [pointHistory, setPointHistory] = useState([]);
+
+ const [formData, setFormData] = useState({
+ customer_id: id,
+ related_type: "Adjustments",
+ related_id: 0,
+ action: "",
+ pointValue: "",
+ in: 0.00,
+ out: 0.00,
+ remark: "",
+ });
+
+ const pointOptions = [
+ { value: 'increase', label: 'Add Points' },
+ { value: 'decrease', label: 'Deduct Points' }
+ ];
+
+ const fetchCustomerData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customers/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch customer data');
+ }
+
+ const customerData = await response.json();
+ const customerDetails = customerData.data;
+
+ setCustomerPointData({
+ customerName: customerDetails.name || 'N/A',
+ point: customerPointData.point, // Keep existing point value
+ });
+
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ toast.error('Failed to load customer data');
+ }
+ }
+
+ const fetchCustomerPointHistory = async (dateFrom = '', dateTo = '') => {
+ try {
+ setLoading(true);
+ const response = await fetch(`${VITE_API_BASE_URL}customer-point/history/${id}?date_from=${dateFrom}&date_to=${dateTo}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch point history');
+ }
+
+ const pointHistoryData = await response.json();
+ setPointHistory(pointHistoryData.data || []);
+
+ const pointBalance = getLastBalance(pointHistoryData.data) ?? 0;
+
+ setCustomerPointData(prev => ({
+ ...prev,
+ point: pointBalance,
+ }));
+ } catch (error) {
+ console.error("Error fetching point history:", error);
+ toast.error('Failed to load point history');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ const getLastBalance = (arr) => {
+ if (!arr?.length) return null;
+ const last = arr[0];
+ return parseFloat(last.balance) || 0;
+ };
+
+ useEffect(() => {
+ async function fetchAll() {
+ await fetchCustomerData();
+ await fetchCustomerPointHistory();
+ }
+ fetchAll();
+ }, [id]);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ // Validation
+ if (!formData.action) {
+ toast.error('Please select an action (Add or Deduct Points)');
+ return;
+ }
+
+ if (!formData.pointValue || parseFloat(formData.pointValue) <= 0) {
+ toast.error('Please enter a valid point value');
+ return;
+ }
+
+ if (formData.action === 'decrease' && parseFloat(formData.pointValue) > parseFloat(customerPointData.point)) {
+ toast.error('Deduction amount cannot exceed current points');
+ return;
+ }
+
+ setSubmitting(true);
+
+ const points = Number(parseFloat(formData.pointValue).toFixed(2));
+ const submitData = {
+ ...formData,
+ in: formData.action === 'increase' ? points : 0.00,
+ out: formData.action === 'decrease' ? points : 0.00,
+ };
+
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "customer/points/create", {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ body: JSON.stringify(submitData)
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Failed to adjust points');
+ }
+
+ toast.success(data.message || "Points adjusted successfully", {
+ position: "top-right",
+ autoClose: 1500,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ onClose: () => {
+ // Refresh data after successful submission
+ fetchCustomerPointHistory();
+ // Reset form
+ setFormData({
+ customer_id: id,
+ related_type: "Adjustments",
+ related_id: 0,
+ action: "",
+ pointValue: "",
+ in: 0.00,
+ out: 0.00,
+ remark: "",
+ });
+ },
+ });
+
+ } catch (err) {
+ console.error("Error adjusting points:", err);
+ toast.error(err.message || "Failed to adjust points");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prevState => ({
+ ...prevState,
+ [name]: value
+ }));
+ };
+
+ const handleClose = () => {
+ navigate(-1);
+ };
+
+ return (
+
+
+
+ {/* Header with back button */}
+
+
+
+ Go back
+
+
+
+ {/* Details Section */}
+
+
+
Details
+
+
+ {loading ? (
+
+ ) : (
+ <>
+
+ Customer Name
+ : {customerPointData.customerName}
+
+
+ Points Balance
+ : {customerPointData.point}
+
+ >
+ )}
+
+
+
+ {/* Adjust Points Form Section */}
+
+
+ {/* Recent Points History Section */}
+ {pointHistory.length > 0 && (
+
+
+
Recent Points History
+
+
+
+
+
+
+ Date
+ Action
+ Points In
+ Points Out
+ Balance
+ Remark
+
+
+
+ {pointHistory.slice(0, 5).map((history, index) => (
+
+
+ {new Date(history.created_at).toLocaleDateString()}
+
+
+ {history.action_type}
+
+
+ {history.in > 0 ? `+${history.in}` : ''}
+
+
+ {history.out > 0 ? `-${history.out}` : ''}
+
+
+ {history.balance}
+
+
+ {history.remark || '-'}
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default MemberEditAdjustPoint;
\ No newline at end of file
diff --git a/src/pages/member/member-edit-adjustwallet.jsx b/src/pages/member/member-edit-adjustwallet.jsx
new file mode 100644
index 0000000..3a40550
--- /dev/null
+++ b/src/pages/member/member-edit-adjustwallet.jsx
@@ -0,0 +1,442 @@
+import React, { useState, useEffect } from 'react';
+import DataTable from 'react-data-table-component';
+import { ChevronDown, ChevronLeft } from 'lucide-react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { ToastContainer, toast } from 'react-toastify';
+
+const MemberAdjustWallet = () => {
+ const { id } = useParams();
+ const [formData, setFormData] = useState({
+ action: '',
+ in: 0.00,
+ out: 0.00,
+ remark: '',
+ value: null,
+ customer_id: id,
+ related_id: "0",
+ related_type: 'Adjustments',
+ });
+ const authToken = sessionStorage.getItem('token');
+
+ const [walletData, setWalletData] = useState({
+ customerName: '',
+ wallet: '',
+ walletCredit: '',
+ totalWallet: '',
+ });
+
+ const [transactions, setTransactions] = useState({ all: [], credit: [] });
+ const [dateFrom, setDateFrom] = useState('');
+ const [dateTo, setDateTo] = useState('');
+ const [customerData, setCustomerData] = useState([]);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ }
+
+ const fetchCustomerData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customers/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerData = await response.json()
+ const customerDetails = customerData.data;
+
+
+ setWalletData({
+ customerName: customerDetails.name || 'N/A',
+ wallet: `RM ${customerDetails.wallet || 0}`,
+ });
+``
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ }
+ }
+
+ const fetchCustomerWalletHistory = async (dateFrom = '', dateTo = '') => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customer-wallet/history/${id}?date_from=${dateFrom}&date_to=${dateTo}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const walletHistory = await response.json();
+ // console.log('Wallet History:', walletHistory);
+ const walletData = walletHistory.data;
+
+ const allTransactions = walletData.all || [];
+ const creditTransactions = walletData.credit || [];
+
+ const allBalance = getLastBalance(allTransactions) ?? 0;
+ const creditBalance = getLastBalance(creditTransactions) ?? 0;
+ const totalBalance = allBalance + creditBalance;
+
+ setTransactions({
+ all: allTransactions,
+ credit: creditTransactions
+ });
+
+ setWalletData(prev => ({
+ ...prev,
+ wallet: `RM ${allBalance}`,
+ }));
+
+ } catch (error) {
+ console.error("Error fetching wallet history:", error);
+
+ }
+ }
+
+ const getLastBalance = (arr) => {
+ if (!arr?.length) return null;
+ const last = arr[0];
+ console.log(parseFloat(last.balance));
+ const balance = parseFloat(last.balance);
+ return balance;
+ };
+
+ useEffect(() => {
+ async function fetchAll() {
+ await fetchCustomerData();
+ await fetchCustomerWalletHistory();
+ }
+ fetchAll();
+ }, []);
+
+ const emptyData = [];
+
+ const data = [];
+
+ const columns = [
+ {
+ name: 'No',
+ cell: (row, rowIndex) => rowIndex + 1,
+ width: '70px'
+ },
+ {
+ name: 'Date Created',
+ selector: row => row.created_at,
+ // sortable: true,
+ minWidth: '200px',
+ },
+ {
+ name: 'Type',
+ selector: row => row.related_type,
+ // sortable: true,
+ minWidth: '150px',
+ },
+ {
+ name: 'Action',
+ selector: row => row.action.toUpperCase(),
+ // sortable: true,
+ minWidth: '100px',
+ },
+ {
+ name: 'Current',
+ selector: row => row.current,
+ // sortable: true,
+ },
+ {
+ name: 'In',
+ selector: row => row.in,
+ // sortable: true,
+ },
+ {
+ name: 'Out',
+ selector: row => row.out,
+ // sortable: true,
+ },
+ {
+ name: 'Balance',
+ selector: row => row.balance,
+ // sortable: true,
+ },
+ // {
+ // name: 'Reason',
+ // selector: row => row.reason,
+ // sortable: true,
+ // grow: 2,
+ // },
+ {
+ name: 'Remark',
+ selector: row => row.remark,
+ sortable: true,
+ minWidth: '250px',
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: '#312e81',
+ color: 'white',
+ minHeight: '50px',
+ fontSize: '16px',
+ justifyContent: 'center',
+ },
+ },
+ headCells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ fontWeight: '500',
+ justifyContent: 'center',
+ textAlign: 'center',
+ subHeaderWrap: true,
+ },
+ },
+ rows: {
+ style: {
+ minHeight: '60px',
+ fontSize: '15px',
+ '&:hover': {
+ backgroundColor: '#f9fafb',
+ },
+ justifyContent: 'center',
+ center: true,
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: '#f9fafb',
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ justifyContent: 'center',
+ textAlign: 'center',
+ alignItems: 'center',
+ center: true,
+ },
+ },
+ pagination: {
+ style: {
+ borderTopStyle: 'solid',
+ borderTopWidth: '1px',
+ borderTopColor: '#e5e7eb',
+ },
+ },
+ };
+
+ const NoDataComponent = () => (
+ No points history found
+ );
+
+ const navigate = useNavigate();
+ const handleBack = () => {
+ navigate(-1);
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ // Validation
+ if (!formData.action || !formData.value || parseFloat(formData.value) <= 0) {
+ toast.error("Please select an action and enter a valid amount");
+ return;
+ }
+
+ const amount = Number(parseFloat(formData.value).toFixed(2));
+ const submitData = {
+ ...formData,
+ in: formData.action === 'in' ? amount : 0,
+ out: formData.action === 'out' ? amount : 0,
+ value: undefined // Remove the value field as it's not needed in the API
+ };
+
+ delete submitData.value; // Remove the value field
+
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "customer-wallets/create", {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ body: JSON.stringify(submitData)
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.message || 'Failed to process wallet adjustment');
+ }
+
+ // Custom success message based on action
+ const successMessage = formData.action === 'in'
+ ? "Top Up Successful"
+ : "Deduct Successful";
+
+ toast.success(successMessage, {
+ position: "top-right",
+ autoClose: 1500,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ onClose: () => {
+ // Refresh wallet data after successful submission
+ fetchCustomerWalletHistory();
+ // Reset form
+ setFormData({
+ action: '',
+ in: 0.00,
+ out: 0.00,
+ remark: '',
+ value:'',
+ customer_id: id,
+ related_id: "0",
+ related_type: 'Adjustments',
+ });
+ },
+ });
+
+ } catch (err) {
+ console.error("Error creating wallet action:", err);
+ toast.error(err.message || "Unexpected error");
+ }
+ };
+
+ return (
+
+
+
+
+ Go back
+
+
+
+ {/* Customer Details Section */}
+
+
+ Details
+
+
+
+
+
Customer Name
+
:
+
{walletData.customerName}
+
+
+
Wallet
+
:
+
{walletData.wallet}
+
+
+
Wallet Credit
+
:
+
{walletData.walletCredit}
+
+
+
Total Wallet
+
:
+
{walletData.totalWallet}
+
+
+
+
+
+ {/* Listing Form Section */}
+
+
+ Listing
+
+
+
+
+
Wallet
+
+
+
+ Choose
+ Top Up
+ Deduct
+
+
+
+
+
+
+
+
+
+
{/* Empty div for grid alignment */}
+
+
+ Remark
+
+
+
+
+
+ Submit
+
+
+
+
+
+
+
+
Listing (Credit)
+
+ }
+ pagination
+ responsive
+ />
+
+
+
+ );
+};
+
+export default MemberAdjustWallet;
\ No newline at end of file
diff --git a/src/pages/member/member-edit-order.jsx b/src/pages/member/member-edit-order.jsx
new file mode 100644
index 0000000..e6b28e1
--- /dev/null
+++ b/src/pages/member/member-edit-order.jsx
@@ -0,0 +1,636 @@
+import { useState, useEffect, useMemo } from 'react';
+import DataTable from 'react-data-table-component';
+import { ChevronLeft, Edit, Search, Filter, X } from 'lucide-react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { ToastContainer, toast } from 'react-toastify';
+
+export default function MemberEditOrder() {
+ const [orders, setOrders] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [filterLoading, setFilterLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const { id } = useParams();
+ const authToken = sessionStorage.getItem('token');
+ const [customerData, setCustomerData] = useState({
+ customerName: '',
+ code: '',
+ });
+
+ const [filters, setFilters] = useState({
+ orderNumber: '',
+ status: '',
+ paymentType: '',
+ dateFrom: '',
+ dateTo: '',
+ outlet: '', // Added outlet filter
+ });
+
+ const [filteredOrders, setFilteredOrders] = useState([]);
+ const navigate = useNavigate();
+ const [outletData, setOutletData] = useState([]);
+
+ const fetchCustomerData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customers/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerData = await response.json();
+ const customerDetails = customerData.data;
+
+ setCustomerData({
+ customerName: customerDetails.name || 'N/A',
+ code: customerDetails.customer_referral_code || 'N/A',
+ });
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ toast.error('Failed to load customer details');
+ }
+ };
+
+ const fetchCustomerOrders = async () => {
+ try {
+ setLoading(true);
+ const user = JSON.parse(localStorage.getItem('user'));
+ const token = user?.token || authToken;
+
+ const response = await fetch(`${VITE_API_BASE_URL}order/customer-orderlist/${id}`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch order list');
+ }
+
+ const result = await response.json();
+
+ // Filter orders by customer_id to get only this customer's orders
+ const customerOrders = result.data.filter(order => order.customer_id === id);
+
+ // Format the data for display
+ const formattedOrders = customerOrders.map(order => ({
+ id: order.id,
+ orderNumber: order.order_so,
+ status: order.status,
+ orderType: order.order_type,
+ orderDate: new Date(order.created_at),
+ formattedDate: formatDate(order.created_at),
+ paymentType: getPaymentType(order.payments),
+ totalAmount: `RM ${parseFloat(order.grand_total).toFixed(2)}`,
+ totalQuantity: calculateTotalQuantity(order.items),
+ shippingStatus: getShippingStatus(order.deliveries),
+ trackingDetails: getTrackingDetails(order.deliveries),
+ rawData: order, // Keep original data for potential future use
+ outlet_title: order.outlet_title || 'N/A',
+ outlet_id: order.outlet_id || null, // Add outlet_id for filtering
+ }));
+
+ setOrders(formattedOrders);
+ setFilteredOrders(formattedOrders);
+
+ } catch (error) {
+ console.error('Error fetching orders:', error);
+ setError(error.message);
+ toast.error('Failed to load orders');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const userData = useMemo(() => {
+ try {
+ const userStr = localStorage.getItem("user");
+ return userStr ? JSON.parse(userStr) : null;
+ } catch (error) {
+ console.error("Error parsing user data:", error);
+ return null;
+ }
+ }, []);
+
+ const user_id = userData?.user?.user_id || null;
+
+ const fetchOutletData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}outlets/list?user_id=${user_id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const outletData = await response.json();
+ setOutletData(outletData.result);
+ } catch (error) {
+ console.error("Error fetching outlet data:", error);
+ }
+ }
+
+ useEffect(() => {
+ fetchCustomerData();
+ fetchCustomerOrders();
+ fetchOutletData();
+ }, [id]);
+
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const handleEdit = (orderId) => {
+ navigate(`/orders/order_lists/order_overview/${orderId}`);
+ };
+
+ // Format date function
+ const formatDate = (dateString) => {
+ if (!dateString) return 'N/A';
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ };
+
+ // Calculate total quantity
+ const calculateTotalQuantity = (items) => {
+ if (!items) return 0;
+ return items.reduce((total, item) => total + parseInt(item.quantity || 0), 0);
+ };
+
+ // Get payment type
+ const getPaymentType = (payments) => {
+ if (!payments || payments.length === 0) return '-';
+ const method = payments[0].payment_method || '-';
+ return method.charAt(0).toUpperCase() + method.slice(1);
+ };
+
+ // Get shipping status
+ const getShippingStatus = (deliveries) => {
+ if (!deliveries || deliveries.length === 0) return '-';
+ const status = deliveries[0].status || '-';
+ return status.charAt(0).toUpperCase() + status.slice(1);
+ };
+
+ // Get tracking details
+ const getTrackingDetails = (deliveries) => {
+ if (!deliveries || deliveries.length === 0) return '-';
+ return deliveries[0].provider_order_id || '-';
+ };
+
+ // Apply filters
+ useEffect(() => {
+ setFilterLoading(true);
+
+ let filteredOrders = orders;
+
+ if (filters.orderNumber) {
+ filteredOrders = filteredOrders.filter(order =>
+ order.orderNumber.toLowerCase().includes(filters.orderNumber.toLowerCase())
+ );
+ }
+
+ if (filters.status && filters.status !== 'Choose') {
+ filteredOrders = filteredOrders.filter(order =>
+ order.status.toLowerCase() === filters.status.toLowerCase()
+ );
+ }
+
+ if (filters.paymentType && filters.paymentType !== 'Choose') {
+ filteredOrders = filteredOrders.filter(order =>
+ order.paymentType.toLowerCase() === filters.paymentType.toLowerCase()
+ );
+ }
+
+ if (filters.outlet && filters.outlet !== 'Choose') {
+ filteredOrders = filteredOrders.filter(order =>
+ order.outlet_id === filters.outlet
+ );
+ }
+
+ if (filters.dateFrom) {
+ const fromDate = new Date(filters.dateFrom);
+ filteredOrders = filteredOrders.filter(order =>
+ order.orderDate >= fromDate
+ );
+ }
+
+ if (filters.dateTo) {
+ const toDate = new Date(filters.dateTo);
+ toDate.setHours(23, 59, 59, 999);
+ filteredOrders = filteredOrders.filter(order =>
+ order.orderDate <= toDate
+ );
+ }
+
+ setFilteredOrders(filteredOrders);
+
+ // Small delay to show loading state for better UX
+ setTimeout(() => setFilterLoading(false), 300);
+ }, [filters, orders]);
+
+ const handleFilterChange = (key, value) => {
+ setFilters(prev => ({ ...prev, [key]: value }));
+ };
+
+ const clearFilters = () => {
+ setFilters({
+ orderNumber: '',
+ status: '',
+ paymentType: '',
+ dateFrom: '',
+ dateTo: '',
+ outlet: '', // Reset outlet filter too
+ });
+ };
+
+ // Check if any filters are active
+ const hasActiveFilters = Object.values(filters).some(value => value !== '' && value !== 'Choose');
+
+ // Define columns for DataTable
+ const columns = [
+ {
+ name: 'Action',
+ selector: row => row.id,
+ cell: row => (
+ handleEdit(row.id)}
+ title="Edit Order"
+ >
+
+
+ ),
+ width: '80px',
+ },
+ {
+ name: 'Status',
+ selector: row => row.status,
+ cell: row => (
+
+ {row.status.charAt(0).toUpperCase() + row.status.slice(1)}
+
+ ),
+ width: '120px',
+ },
+ {
+ name: 'Order No.',
+ selector: row => row.orderNumber,
+ cell: row => (
+
+ #{row.orderNumber}
+
+ ),
+ width: '150px',
+ },
+ {
+ name: 'Outlet',
+ selector: row => row.outlet_title,
+ cell: row => (
+
+ {row.outlet_title}
+
+ ),
+ width: '150px',
+ },
+ {
+ name: 'Order Type',
+ selector: row => row.orderType,
+ cell: row => {
+ if (!row.orderType) return "-";
+ const type = row.orderType.toLowerCase();
+ if (type === "dinein") return "Dine in";
+ if (type === "pickup") return "Pick up";
+ if (type === "delivery") return "Delivery";
+ return type.charAt(0).toUpperCase() + type.slice(1);
+ },
+ width: '120px',
+ },
+ {
+ name: 'Order Date',
+ selector: row => row.formattedDate,
+ width: '120px',
+ },
+ {
+ name: 'Payment Type',
+ selector: row => row.paymentType,
+ width: '130px',
+ },
+ {
+ name: 'Total Amount',
+ selector: row => row.totalAmount,
+ width: '130px',
+ cell: row => (
+
+ {row.totalAmount}
+
+ ),
+ },
+ {
+ name: 'Total Quantity',
+ selector: row => row.totalQuantity,
+ width: '130px',
+ },
+ {
+ name: 'Shipping Status',
+ selector: row => row.shippingStatus,
+ width: '130px',
+ },
+ {
+ name: 'Tracking Details',
+ selector: row => row.trackingDetails,
+ width: '150px',
+ cell: row => (
+
+ {row.trackingDetails}
+
+ ),
+ },
+ ];
+
+ // Custom styles for DataTable to match your design
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: '#2e2c67',
+ color: 'white',
+ fontWeight: 'bold',
+ minHeight: '56px',
+ },
+ },
+ headCells: {
+ style: {
+ color: 'white',
+ fontSize: '14px',
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ },
+ },
+ rows: {
+ style: {
+ minHeight: '60px',
+ fontSize: '14px',
+ '&:not(:last-of-type)': {
+ borderBottomStyle: 'solid',
+ borderBottomWidth: '1px',
+ borderBottomColor: '#E5E7EB',
+ },
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: '#f3f4f6',
+ transition: 'background-color 0.2s ease',
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ },
+ },
+ pagination: {
+ style: {
+ minHeight: '56px',
+ marginTop: '0',
+ borderTop: '1px solid #E5E7EB',
+ },
+ },
+ noData: {
+ style: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '200px',
+ fontSize: '16px',
+ color: '#6B7280',
+ },
+ },
+ progress: {
+ style: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '200px',
+ },
+ },
+ };
+
+ // Custom pagination options text
+ const paginationComponentOptions = {
+ rowsPerPageText: 'Rows per page:',
+ rangeSeparatorText: 'of',
+ selectAllRowsItem: false,
+ selectAllRowsItemText: 'All',
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Header with back button */}
+
+
+
+ Go back
+
+
+
+ {/* Details section */}
+
+
+
Details
+
+
+
+
+ Code
+ : {customerData.code}
+
+
+ Customer Name
+ : {customerData.customerName}
+
+
+
+
+
+ {/* Search Filter section */}
+
+
+
Search Filter
+ {hasActiveFilters && (
+
+
+ Clear Filters
+
+ )}
+
+
+
+
+
+
+ Order Number
+
+ handleFilterChange('orderNumber', e.target.value)}
+ />
+
+
+ Date From
+ handleFilterChange('dateFrom', e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"
+ />
+
+
+ Date To
+ handleFilterChange('dateTo', e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"
+ />
+
+
+
+
+ Status
+
+ handleFilterChange('status', e.target.value)}
+ >
+ All Statuses
+ Pending
+ Completed
+ Cancelled
+
+
+
+ Payment Type
+ handleFilterChange('paymentType', e.target.value)}
+ >
+ All Payment Types
+ Cash
+ Card
+ Online
+
+
+ {/* Added Outlet Filter */}
+
+ Outlet
+ handleFilterChange('outlet', e.target.value)}
+ >
+ All Outlets
+ {outletData.map((outlet) => (
+
+ {outlet.title}
+
+ ))}
+
+
+
+
+
+
+ {/* Order List section */}
+
+
+
Order List
+ {filteredOrders.length > 0 && (
+
+ Showing {filteredOrders.length} order{filteredOrders.length !== 1 ? 's' : ''}
+
+ )}
+
+
+
+
+
+ }
+ noDataComponent={
+
+ {orders.length === 0
+ ? 'No orders found for this customer'
+ : 'No orders match your filters'
+ }
+
+ }
+ highlightOnHover
+ pointerOnHover
+ />
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/member/member-edit-overview.jsx b/src/pages/member/member-edit-overview.jsx
new file mode 100644
index 0000000..d070439
--- /dev/null
+++ b/src/pages/member/member-edit-overview.jsx
@@ -0,0 +1,292 @@
+import React, { useEffect, useState } from 'react';
+import { X, User, MapPin, ShoppingCart, CreditCard, Gift, Star, Wallet, Calendar, Clock, Home, CreditCard as CreditCardIcon, Activity } from 'lucide-react';
+import { useNavigate, Link, useParams } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import { VITE_API_BASE_URL } from '../../constant/config';
+
+export default function MemberEditOverview() {
+ const navigate = useNavigate();
+ const authToken = sessionStorage.getItem('token');
+ const { id } = useParams();
+ const [dashboardData, setDashboardData] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchDashboardData();
+ }, [id]);
+
+ const fetchDashboardData = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch(`${VITE_API_BASE_URL}customer-dashboard/${id}`, {
+ headers: {
+ 'Authorization': `Bearer ${authToken}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch dashboard data');
+ }
+
+ const data = await response.json();
+ setDashboardData(data.data);
+ } catch (error) {
+ console.error('Error fetching dashboard data:', error);
+ toast.error('Failed to load customer dashboard');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ // Format date for display
+ const formatDate = (dateString) => {
+ if (!dateString) return '-';
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-US', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric'
+ });
+ };
+
+ // Get status color and text
+ const getStatusInfo = (status) => {
+ switch (status) {
+ case 'active':
+ return { color: 'text-green-600', bg: 'bg-green-100', text: 'Active' };
+ case 'dormant':
+ return { color: 'text-yellow-600', bg: 'bg-yellow-100', text: 'Dormant' };
+ case 'churned':
+ return { color: 'text-red-600', bg: 'bg-red-100', text: 'Churned' };
+ case 'new':
+ return { color: 'text-blue-600', bg: 'bg-blue-100', text: 'New' };
+ default:
+ return { color: 'text-gray-600', bg: 'bg-gray-100', text: 'Unknown' };
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!dashboardData) {
+ return (
+
+
+
Failed to load dashboard data
+
+
+ );
+ }
+
+ const { orders, topups, wallet, points, vouchers, lifecycle, checkins } = dashboardData;
+ const statusInfo = getStatusInfo(lifecycle?.customer_status);
+
+ return (
+
+
+
Overview
+
+
+
+
+
+
+ {[
+ { icon: User, label: "Profile", link: `member/member_overview/member_profile/${id}`, key: 'profile' },
+ { icon: MapPin, label: "Address", link: `member/member_overview/member_address/${id}`, key: 'address' },
+ { icon: ShoppingCart, label: "Order Record", link: `member/member_overview/member_order/${id}`, key: 'order' },
+ { icon: CreditCard, label: "Topup Record", link: `member/member_overview/member_topup/${id}`, key: 'topup' },
+ { icon: Gift, label: "Voucher Record", link: `member/member_overview/member_voucher/${id}`, key: 'voucher' },
+ { icon: Star, label: "Point", link: `member/member_overview/member_point/${id}`, key: 'point' },
+ { icon: Wallet, label: "Wallet", link: `member/member_overview/member_wallet/${id}`, key: 'wallet' },
+ ].map((tab) => (
+
+
+
+
+
{tab.label}
+
+ ))}
+
+
+
+ {/* Left section - Customer Overview */}
+
+
Customer Overview
+
+ {/* Revenue, Transaction Count, Activity Count */}
+
+
+
Total Lifetime Revenue
+
RM{orders?.total_grand || '0.00'}
+
{orders?.total_orders || 0} orders
+
+
+
Total Transaction Count
+
{orders?.total_orders || 0}
+
Orders completed
+
+
+
Total Topup Amount
+
RM{topups?.total_topup_amount || '0.00'}
+
{topups?.total_topups || 0} topups
+
+
+
+
+
+ {/* Wallet, Vouchers, Credits */}
+
+
+
Wallet Balance
+
RM{wallet?.total_wallet_balance || '0.00'}
+
Current balance
+
+
+
Active Vouchers
+
{vouchers?.summary?.active_vouchers || 0}
+
Total: {vouchers?.summary?.total_vouchers || 0}
+
+
+
Total Points
+
+ {points?.current_points ? Math.floor(points.current_points) : '0'}
+
+
From points
+
+
+
+
+ {/* Right section - Wallet Summary */}
+
+
Wallet Summary
+
+
+
+
Total In
+
+RM{topups?.total_in || '0.00'}
+
+
+
Total Out
+
-RM{topups?.total_out || '0.00'}
+
+
+
Customer Status
+
+ {statusInfo.text}
+
+ {lifecycle?.days_since_last_activity && (
+
+ {lifecycle.days_since_last_activity} days since last activity
+
+ )}
+
+
+
+
+
+ {/* Lifecycle Insight */}
+ {lifecycle && (
+
+
+
+ Lifecycle Insight
+
+
+
+ {/* First Activity */}
+
+
{formatDate(lifecycle.first_activity)}
+
+
+
+
First Seen
+
+
+ {/* First Order */}
+
+
{formatDate(lifecycle.first_order)}
+
+
+
+
First Order
+
+
+ {/* First Topup */}
+
+
{formatDate(lifecycle.first_topup)}
+
+
+
+
First Topup
+
+
+ {/* Last Activity */}
+
+
{formatDate(lifecycle.last_activity)}
+
+
+
+
Last Activity
+
+
+
+ {/* Timeline */}
+
+
+
+ {[
+ { label: 'First Seen', date: lifecycle.first_activity },
+ { label: 'First Order', date: lifecycle.first_order },
+ { label: 'First Topup', date: lifecycle.first_topup },
+ { label: 'Latest Activity', date: lifecycle.last_activity }
+ ].map((milestone, index) => (
+
+
+
{milestone.label}
+
+ {formatDate(milestone.date)}
+
+
+ ))}
+
+
+
+ {/* Status Summary */}
+
+
+
+
Customer Status: {statusInfo.text}
+ {lifecycle.days_since_last_activity && (
+
+ {lifecycle.days_since_last_activity} days since last activity
+
+ )}
+
+
+
Total Orders: {orders?.total_orders || 0}
+
Total Topups: {topups?.total_topups || 0}
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/member/member-edit-point.jsx b/src/pages/member/member-edit-point.jsx
new file mode 100644
index 0000000..ac88716
--- /dev/null
+++ b/src/pages/member/member-edit-point.jsx
@@ -0,0 +1,347 @@
+import { useState, useEffect } from 'react';
+import DataTable from 'react-data-table-component';
+import { ArrowLeft, ChevronLeft } from 'lucide-react';
+import { useNavigate, useParams } from 'react-router-dom';
+// import { apiUrl } from '../../constant/constants';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { min } from 'd3-array';
+import { ToastContainer, toast } from 'react-toastify';
+
+export default function MemberEditPoint() {
+ const { id } = useParams();
+ const authToken = sessionStorage.getItem('token');
+
+ const [dateFrom, setDateFrom] = useState('');
+ const [dateTo, setDateTo] = useState('');
+ const [customerPointData, setCustomerPointData] = useState({
+ customerName: '',
+ point: '',
+ });
+
+ const [pointData, setPointData] = useState([]);
+
+ const fetchCustomerData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customers/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerData = await response.json()
+ const customerDetails = customerData.data;
+ // if (customerDetails.profile_picture_url) {
+ // setPreview(customerDetails.profile_picture_url);
+ // }
+ // console.log("Customer data fetched:", customerDetails);
+
+ setCustomerPointData({
+ customerName: customerDetails.name || 'N/A',
+ });
+
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ }
+ }
+
+ const fetchCustomerPointHistory = async (dateFrom = '', dateTo = '') => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customer-point/history/${id}?date_from=${dateFrom}&date_to=${dateTo}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const pointHistory = await response.json();
+ // console.log('Point History:', pointHistory);
+ const pointData = pointHistory.data;
+
+ setPointData(pointData);
+
+ const pointBalance = getLastBalance(pointData) ?? 0;
+
+ setCustomerPointData(prev => ({
+ ...prev,
+ point: pointBalance,
+ }));
+ } catch (error) {
+ console.error("Error fetching point history:", error);
+ }
+ }
+
+ const getLastBalance = (arr) => {
+ if (!arr?.length) return null;
+ const last = arr[0];
+ console.log(parseFloat(last.balance));
+ const balance = parseFloat(last.balance);
+ return balance;
+ };
+
+ useEffect(() => {
+ async function fetchAll() {
+ await fetchCustomerData();
+ await fetchCustomerPointHistory();
+ }
+ fetchAll();
+ }, []);
+
+ // useEffect(() => {
+ // // Fetch point history whenever dateFrom or dateTo changes
+ // console.log("Fetching point history with dateFrom:", dateFrom, "dateTo:", dateTo);
+ // if (dateFrom || dateTo) {
+ // fetchCustomerPointHistory(dateFrom, dateTo);
+ // }
+ // }, [dateFrom, dateTo]);
+
+ const columns = [
+ {
+ name: 'No',
+ // selector: row => row.no,
+ // sortable: true,
+ cell: (row, rowIndex) => rowIndex + 1,
+ width: '70px'
+ },
+ {
+ name: 'Date Created',
+ selector: row => row.created_at,
+ // sortable: true,
+ minWidth: '200px',
+ },
+ {
+ name: 'Type',
+ selector: row => row.related_type,
+ // sortable: true,
+ minWidth: '150px',
+ },
+ {
+ name: 'Action',
+ selector: row => row.action.toUpperCase(),
+ // sortable: true,
+ minWidth: '150px',
+ },
+ {
+ name: 'Current',
+ selector: row => row.current,
+ // sortable: true,
+ },
+ {
+ name: 'In',
+ selector: row => row.in,
+ // sortable: true,
+ },
+ {
+ name: 'Out',
+ selector: row => row.out,
+ // sortable: true,
+ },
+ {
+ name: 'Balance',
+ selector: row => row.balance,
+ // sortable: true,
+ },
+ // {
+ // name: 'Reason',
+ // selector: row => row.reason,
+ // sortable: true,
+ // grow: 2,
+ // },
+ {
+ name: 'Remark',
+ selector: row => row.remark,
+ sortable: true,
+ minWidth: '250px',
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: '#312e81',
+ color: 'white',
+ minHeight: '50px',
+ fontSize: '16px',
+ justifyContent: 'center',
+ },
+ },
+ headCells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ fontWeight: '500',
+ justifyContent: 'center',
+ textAlign: 'center',
+ subHeaderWrap: true,
+ },
+ },
+ rows: {
+ style: {
+ minHeight: '60px',
+ fontSize: '15px',
+ '&:hover': {
+ backgroundColor: '#f9fafb',
+ },
+ justifyContent: 'center',
+ center: true,
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: '#f9fafb',
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ justifyContent: 'center',
+ textAlign: 'center',
+ alignItems: 'center',
+ center: true,
+ },
+ },
+ pagination: {
+ style: {
+ borderTopStyle: 'solid',
+ borderTopWidth: '1px',
+ borderTopColor: '#e5e7eb',
+ },
+ },
+ };
+
+ const NoDataComponent = () => (
+ No points history found
+ );
+
+ const navigate = useNavigate();
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const handleAdjustPoint = () => {
+ navigate(`/member/member_overview/member_point/adjust_point/${id}`);
+ }
+
+ return (
+
+ {/* Header with back button */}
+
+
+
+ Go back
+
+
+
+ {/* Details section */}
+
+
+ Details
+
+
+
+
Customer Name
+
:
+
{customerPointData.customerName}
+
+
+
Point
+
:
+
{customerPointData.point}
+
+
+ {/* Adjust Point button */}
+
+
+ Adjust Point
+
+
+
+
+
+ {/* Search Filter section */}
+
+
+ Search Filter
+
+
+
+
+ Date From
+
+
+ {/*
*/}
+
setDateFrom(e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+ Date To
+
+
+ {/*
*/}
+
setDateTo(e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ />
+
+
+
+ {/* Search button */}
+
+ fetchCustomerPointHistory(dateFrom, dateTo)}>
+ Search
+
+
+
+
+
+ {/* Listing section */}
+
+
Listing
+
+ }
+ pagination
+ responsive
+ />
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/member/member-edit-profile.jsx b/src/pages/member/member-edit-profile.jsx
new file mode 100644
index 0000000..db9cb80
--- /dev/null
+++ b/src/pages/member/member-edit-profile.jsx
@@ -0,0 +1,602 @@
+import { useRef, useState, useEffect } from 'react';
+import { X, Eye } from 'lucide-react';
+import { useNavigate, useLocation, useParams } from 'react-router-dom';
+// import { apiUrl } from '../../constant/constants';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { set } from 'lodash';
+import { ToastContainer, toast } from 'react-toastify';
+
+const EditProfileMember = () => {
+
+ // const { state } = useLocation();
+ // const { customerId } = state || {};
+ const { id } = useParams();
+ const authToken = sessionStorage.getItem('token');
+ const [showPassword, setShowPassword] = useState(false);
+ const [customerData, setCustomerData] = useState({});
+ const [preview, setPreview] = useState(null);
+ // const [formData, setFormData] = useState({});
+ const [formData, setFormData] = useState({
+ referenceLink: '',
+ referenceCode: '',
+ memberCard: '',
+ customer_type: '',
+ customer_tier: '',
+ customer_tier_id: '',
+ name: '',
+ companyName: '',
+ phone: '',
+ email: '',
+ nric: '',
+ gender: '',
+ race: '',
+ birthday: '',
+ status: '',
+ // profile_picture: null,
+ profile_picture_url: ''
+ });
+
+ const handleReset = () => {
+ setFormData({
+ referenceLink: '',
+ referenceCode: '',
+ memberCard: '',
+ customer_type: '',
+ customer_tier: '',
+ customer_tier_id: '',
+ name: '',
+ companyName: '',
+ phone: '',
+ email: '',
+ nric: '',
+ gender: '',
+ race: '',
+ birthday: '',
+ status: '',
+ profile_picture: null,
+ profile_picture_url: ''
+ });
+
+ setPreview(null);
+ hiddenInput.current.value = null;
+ }
+
+ function getTierName(tierIdStr) {
+ const normalizedId = String(tierIdStr).trim();
+
+ const tier = tierData.find(
+ t => String(t.id).trim() === normalizedId
+ );
+
+ // console.log('tier', tier);
+ return tier ? tier.name : null;
+ }
+
+ const [tierData, setTierData] = useState([]);
+ // const authToken = localStorage.getItem('authToken');
+ const [customerType, setCustomerType] = useState([]);
+
+ const fetchTierData = async () => {
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "settings/membership-tiers", {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const tierData = await response.json();
+
+ setTierData(tierData.data);
+ } catch (error) {
+ console.error("Error fetching tier data:", error);
+ }
+ }
+
+ const fetchCustomerType = async () => {
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "settings/customer-types", {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerTypeData = await response.json();
+
+ setCustomerType(customerTypeData.data);
+ } catch (error) {
+ console.error("Error fetching customer type data:", error);
+ }
+ }
+
+ const fetchCustomerData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customers/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerData = await response.json()
+ const customerDetails = customerData.data;
+ if (customerDetails.profile_picture_url) {
+ setPreview(customerDetails.profile_picture_url);
+ }
+ console.log("Customer data fetched:", customerDetails);
+ setCustomerData(customerDetails);
+ setFormData(prev => ({
+ ...prev,
+ ...customerDetails
+ }));
+
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ }
+ }
+
+ useEffect(() => {
+ fetchTierData();
+ fetchCustomerType();
+ fetchCustomerData();
+ }, []);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ console.log('Form submitted:', formData);
+ const formData1 = new FormData();
+ Object.entries(formData).forEach(([key, value]) => {
+ formData1.append(key, value);
+ });
+
+ try {
+ const response = await fetch(VITE_API_BASE_URL + "customers/update/" + id, {
+ method: 'POST',
+ headers: {
+ // 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ // body: JSON.stringify(formData)
+ body: formData1
+ });
+
+ // console.log('Response status:', response.status);
+ let data = response;
+
+ if (!response.ok) {
+ console.error("Error creating member:", data);
+ throw new Error('Fill in all required fields');
+ }
+
+ toast.success(data.message || "Save successfully", {
+ position: "top-right",
+ autoClose: 1500,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ onClose: () => {
+ navigate(-1);
+ },
+ });
+
+ // handleReset();
+ } catch (err) {
+ console.error("Error creating member:", err);
+ toast.error(err.message || "Unexpected error");
+ }
+ };
+
+ // const handleImageUpload = (e) => {
+ // console.log('Image selected:', e.target.files[0]);
+ // };
+
+ const navigate = useNavigate();
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const hiddenInput = useRef(null);
+
+
+ const handleClick = () => hiddenInput.current.click();
+
+ const handleImageUpload = (e) => {
+ // console.log('Image selected:', e.target.files[0]);
+ const name = e.target.name;
+ const file = e.target.files[0];
+
+ if (!file) return;
+
+ // if (file) {
+ // setPreview(URL.createObjectURL(file));
+ // onImageSelect(file);
+ // }
+
+ setPreview(URL.createObjectURL(file));
+
+ setFormData((prev) => ({
+ ...prev,
+ profile_picture: file,
+ }));
+ };
+
+ return (
+
+
+
Edit Member
+
+
+
+
+
+
+ {/* Referral Information */}
+
+
+ REFERRAL INFORMATION
+
+ {/*
+ Reference Link
+
+
*/}
+
+ Reference Code
+
+
+
+
+ {/* Membership Details */}
+
+
+ MEMBERSHIP DETAILS
+
+ {/*
+ Member Card
+
+
*/}
+
+
+
+
+
Customer Type
+
+
+ Choose
+ {customerType.map(type => (
+ {type.name}
+ ))}
+
+
+
+
+
+
+
Tier Level
+
+
+ Choose
+ {tierData.map(type => (
+ {type.name}
+ ))}
+
+
+
+
+
+
+ {/* Personal & Contact Information */}
+
+
+ PERSONAL & CONTACT INFORMATION
+
+
+
+ Full Name
+
+
+
+ {/*
+ Company Name
+
+
*/}
+
+
+ Phone Number
+
+
+
+
+ Email Address
+
+
+
+ {/*
+ NRIC
+
+
+
+
+
Gender
+
+
+ Choose
+ Male
+ Female
+ Other
+
+
+
+
+
+
+
Race
+
+
+ Choose
+ Malay
+ Chinese
+ Indian
+ Other
+
+
+
+
*/}
+
+
+ Birthday
+
+
+
+
+
+ {/* Account Status */}
+
+
+ ACCOUNT STATUS
+
+
+
Status
+ {/*
*/}
+
+
+ Choose
+ Active
+ Inactive
+ Pending
+
+
+
+
+
+
Photo
+ {/*
*/}
+
+
+ {preview ? (
+
+ ) : (
+
+ )}
+
+
+ {/*
+
+
Add Image
*/}
+ {/*
*/}
+
+
+
+
+
+
+ {/* Submit Button */}
+
+
+ Cancel
+
+
+
+ Save Changes
+
+
+
+
+ );
+};
+
+export default EditProfileMember;
\ No newline at end of file
diff --git a/src/pages/member/member-edit-topup.jsx b/src/pages/member/member-edit-topup.jsx
new file mode 100644
index 0000000..2ed6964
--- /dev/null
+++ b/src/pages/member/member-edit-topup.jsx
@@ -0,0 +1,446 @@
+import { useState, useEffect } from 'react';
+import DataTable from 'react-data-table-component';
+import { ChevronLeft, Search, Filter, X, Download } from 'lucide-react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { ToastContainer, toast } from 'react-toastify';
+
+export default function MemberTopup() {
+ const authToken = sessionStorage.getItem('token');
+ const { id } = useParams();
+ const navigate = useNavigate();
+
+ const [customerData, setCustomerData] = useState({
+ customerName: '',
+ code: '',
+ });
+
+ const [topupData, setTopupData] = useState([]);
+ const [filteredData, setFilteredData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [filterLoading, setFilterLoading] = useState(false);
+
+ const [filters, setFilters] = useState({
+ topUpNumber: '',
+ dateFrom: '',
+ dateTo: '',
+ status: '',
+ });
+
+ // Check if any filters are active
+ const hasActiveFilters = Object.values(filters).some(value => value !== '');
+
+ // Fetch customer data
+ const fetchCustomerData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customers/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerData = await response.json();
+ const customerDetails = customerData.data;
+
+ setCustomerData({
+ customerName: customerDetails.name || 'N/A',
+ code: customerDetails.customer_referral_code || 'N/A',
+ });
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ toast.error('Failed to load customer data');
+ }
+ };
+
+ // Fetch topup data for this customer
+ const fetchTopupData = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch(`${VITE_API_BASE_URL}customer/topup/list`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`,
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ // Filter topup data for this specific customer
+ if (data.data && Array.isArray(data.data)) {
+ const customerTopups = data.data.filter(
+ item => item.customer_id === id
+ );
+
+ // Format the data for display
+ const formattedData = customerTopups.map(item => ({
+ id: item.id,
+ topUpNumber: item.topup_number,
+ amount: `RM ${parseFloat(item.amount).toFixed(2)}`,
+ redeem: item.credit ? `RM ${parseFloat(item.credit).toFixed(2)}` : 'N/A',
+ paymentType: item.payment_method || 'N/A',
+ date: new Date(item.created_at).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ }),
+ status: item.status,
+ rawDate: new Date(item.created_at), // For filtering
+ rawAmount: parseFloat(item.amount) // For sorting
+ }));
+
+ setTopupData(formattedData);
+ setFilteredData(formattedData);
+ }
+ } catch (error) {
+ console.error('Error fetching topup data:', error);
+ toast.error('Failed to load topup history');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Apply filters whenever they change
+ useEffect(() => {
+ setFilterLoading(true);
+
+ let result = topupData;
+
+ // Filter by topup number
+ if (filters.topUpNumber) {
+ result = result.filter(item =>
+ item.topUpNumber.toLowerCase().includes(filters.topUpNumber.toLowerCase())
+ );
+ }
+
+ // Filter by status
+ if (filters.status) {
+ result = result.filter(item =>
+ item.status.toLowerCase() === filters.status.toLowerCase()
+ );
+ }
+
+ // Filter by date range
+ if (filters.dateFrom) {
+ const fromDate = new Date(filters.dateFrom);
+ result = result.filter(item => item.rawDate >= fromDate);
+ }
+
+ if (filters.dateTo) {
+ const toDate = new Date(filters.dateTo);
+ // Set to end of day
+ toDate.setHours(23, 59, 59, 999);
+ result = result.filter(item => item.rawDate <= toDate);
+ }
+
+ setFilteredData(result);
+
+ // Small delay to show loading state for better UX
+ setTimeout(() => setFilterLoading(false), 300);
+ }, [filters, topupData]);
+
+ // Fetch data on component mount
+ useEffect(() => {
+ fetchCustomerData();
+ fetchTopupData();
+ }, [id]);
+
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const handleFilterChange = (key, value) => {
+ setFilters(prev => ({ ...prev, [key]: value }));
+ };
+
+ const clearFilters = () => {
+ setFilters({
+ topUpNumber: '',
+ dateFrom: '',
+ dateTo: '',
+ status: '',
+ });
+ };
+
+ const columns = [
+ {
+ name: 'Top Up Number',
+ selector: row => row.topUpNumber,
+ sortable: true,
+ width: '20%',
+ cell: row => (
+
+ {row.topUpNumber}
+
+ ),
+ },
+ {
+ name: 'Amount',
+ selector: row => row.amount,
+ sortable: true,
+ width: '15%',
+ sortFunction: (a, b) => a.rawAmount - b.rawAmount,
+ },
+ {
+ name: 'Credit',
+ selector: row => row.redeem,
+ sortable: true,
+ width: '15%',
+ },
+ {
+ name: 'Payment Type',
+ selector: row => row.paymentType,
+ sortable: true,
+ width: '15%',
+ },
+ {
+ name: 'Date',
+ selector: row => row.date,
+ sortable: true,
+ width: '15%',
+ },
+ {
+ name: 'Status',
+ selector: row => row.status,
+ sortable: true,
+ width: '10%',
+ cell: row => (
+
+ {row.status}
+
+ ),
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: '#2e2c67',
+ color: 'white',
+ fontWeight: 'bold',
+ minHeight: '56px',
+ },
+ },
+ headCells: {
+ style: {
+ color: 'white',
+ fontSize: '14px',
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ },
+ },
+ rows: {
+ style: {
+ minHeight: '60px',
+ fontSize: '14px',
+ '&:not(:last-of-type)': {
+ borderBottomStyle: 'solid',
+ borderBottomWidth: '1px',
+ borderBottomColor: '#E5E7EB',
+ },
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: '#f3f4f6',
+ transition: 'background-color 0.2s ease',
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ },
+ },
+ pagination: {
+ style: {
+ minHeight: '56px',
+ marginTop: '0',
+ borderTop: '1px solid #E5E7EB',
+ },
+ },
+ noData: {
+ style: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '200px',
+ fontSize: '16px',
+ color: '#6B7280',
+ },
+ },
+ progress: {
+ style: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '200px',
+ },
+ },
+ };
+
+ // Custom pagination options text
+ const paginationComponentOptions = {
+ rowsPerPageText: 'Rows per page:',
+ rangeSeparatorText: 'of',
+ selectAllRowsItem: false,
+ selectAllRowsItemText: 'All',
+ };
+
+ return (
+
+
+
+ {/* Header with back button */}
+
+
+
+ Go back
+
+
+
+ {/* Details section */}
+
+
+
Details
+
+
+
+
+ Code
+ : {customerData.code}
+
+
+ Customer Name
+ : {customerData.customerName}
+
+
+
+
+
+ {/* Search Filter section */}
+
+
+
Search Filter
+ {hasActiveFilters && (
+
+
+ Clear Filters
+
+ )}
+
+
+
+
+
+
+ Top Up Number
+
+ handleFilterChange('topUpNumber', e.target.value)}
+ />
+
+
+ Date From
+ handleFilterChange('dateFrom', e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"
+ />
+
+
+ Date To
+ handleFilterChange('dateTo', e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"
+ />
+
+
+
+
+ Status
+
+ handleFilterChange('status', e.target.value)}
+ >
+ All Statuses
+ Success
+ Pending
+ Failed
+
+
+
+
+
+
+ {/* Listing section */}
+
+
+
Topup History
+
+
+ {loading ? (
+
+ ) : (
+
+
+
+ }
+ noDataComponent={
+
+ {topupData.length === 0
+ ? 'No topup records found for this customer'
+ : 'No records match your filters'
+ }
+
+ }
+ highlightOnHover
+ pointerOnHover
+ />
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/member/member-edit-voucher.jsx b/src/pages/member/member-edit-voucher.jsx
new file mode 100644
index 0000000..ff5c48e
--- /dev/null
+++ b/src/pages/member/member-edit-voucher.jsx
@@ -0,0 +1,229 @@
+import { useState, useEffect } from 'react';
+import DataTable from 'react-data-table-component';
+import { ChevronLeft } from 'lucide-react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { VITE_API_BASE_URL } from '../../constant/config';
+
+export default function MemberEditVoucher() {
+ const { id } = useParams();
+ const authToken = sessionStorage.getItem('token');
+ const [customerData, setCustomerData] = useState({
+ customerName: '',
+ code: '',
+ });
+ const [voucherData, setVoucherData] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const fetchCustomerData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customers/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerData = await response.json();
+ const customerDetails = customerData.data;
+
+ setCustomerData({
+ customerName: customerDetails.name || 'N/A',
+ code: customerDetails.customer_referral_code || 'N/A',
+ });
+
+ // Transform voucher data to match table structure
+ if (customerDetails.vouchers && customerDetails.vouchers.length > 0) {
+ const transformedVouchers = customerDetails.vouchers.map(voucher => ({
+ id: voucher.id || voucher.voucher_code, // Use a unique identifier
+ voucherCode: voucher.voucher_code,
+ amount: voucher.amount ? `RM ${parseFloat(voucher.amount).toFixed(2)}` : 'N/A',
+ minimumPurchase: voucher.minimum_purchase ? `RM ${parseFloat(voucher.minimum_purchase).toFixed(2)}` : 'N/A',
+ dateStart: voucher.start_date || 'N/A',
+ dateExpired: voucher.voucher_expiry_date || 'N/A',
+ status: voucher.voucher_status || 'N/A',
+ remark: voucher.remark || 'N/A'
+ }));
+ setVoucherData(transformedVouchers);
+ } else {
+ setVoucherData([]);
+ }
+
+ setLoading(false);
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ fetchCustomerData();
+ }, [id]);
+
+ const navigate = useNavigate();
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ // Define columns for DataTable
+ const columns = [
+ {
+ name: 'Voucher Code',
+ selector: row => row.voucherCode,
+ sortable: true,
+ width: '45%',
+ },
+ {
+ name: 'Date Expired',
+ selector: row => row.dateExpired,
+ sortable: true,
+ width: '45%',
+ },
+ {
+ name: 'Status',
+ selector: row => row.status,
+ sortable: true,
+ width: '12%',
+ cell: row => (
+
+ {row.status}
+
+ ),
+ },
+ ];
+
+ // Custom styles for DataTable to match your design
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: '#2e2c67',
+ color: 'white',
+ fontWeight: 'bold',
+ minHeight: '56px',
+ },
+ },
+ headCells: {
+ style: {
+ color: 'white',
+ fontSize: '14px',
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ },
+ },
+ rows: {
+ style: {
+ minHeight: '60px',
+ fontSize: '14px',
+ '&:not(:last-of-type)': {
+ borderBottomStyle: 'solid',
+ borderBottomWidth: '1px',
+ borderBottomColor: '#E5E7EB',
+ },
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ },
+ },
+ pagination: {
+ style: {
+ minHeight: '56px',
+ marginTop: '0',
+ borderTop: '1px solid #E5E7EB',
+ },
+ },
+ noData: {
+ style: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '200px',
+ fontSize: '16px',
+ color: '#6B7280',
+ },
+ },
+ };
+
+ // Custom pagination options text
+ const paginationComponentOptions = {
+ rowsPerPageText: 'Rows per page:',
+ rangeSeparatorText: 'of',
+ selectAllRowsItem: false,
+ selectAllRowsItemText: 'All',
+ };
+
+ return (
+
+
+
+
+ Go back
+
+
+
+ {/* Details section */}
+
+
+
Details
+
+
+
+
+ Code
+ : {customerData.code}
+
+
+ Customer Name
+ : {customerData.customerName}
+
+
+
+
+
+ {/* Listing section */}
+
+
Voucher Listing
+
+ {loading ? (
+
+ ) : (
+
+ No vouchers found for this customer
+
+ }
+ highlightOnHover
+ pointerOnHover
+ />
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/member/member-edit-wallet.jsx b/src/pages/member/member-edit-wallet.jsx
new file mode 100644
index 0000000..81baca5
--- /dev/null
+++ b/src/pages/member/member-edit-wallet.jsx
@@ -0,0 +1,601 @@
+import { ChevronLeft, Search, Filter, X } from 'lucide-react';
+import { useState, useEffect } from 'react';
+import DataTable from 'react-data-table-component';
+import { useNavigate, useParams } from 'react-router-dom';
+import { VITE_API_BASE_URL } from '../../constant/config';
+import { ToastContainer, toast } from 'react-toastify';
+
+export default function MemberEditWallet() {
+ const authToken = sessionStorage.getItem('token');
+ const { id } = useParams();
+ const [walletData, setWalletData] = useState({
+ customerName: '',
+ wallet: '',
+ walletCredit: '',
+ totalWallet: '',
+ code: '',
+ });
+
+ const [transactions, setTransactions] = useState({ all: [], credit: [] });
+ const [topupData, setTopupData] = useState([]);
+ const [filteredTopupData, setFilteredTopupData] = useState([]);
+ const [filteredWalletData, setFilteredWalletData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [filterLoading, setFilterLoading] = useState(false);
+
+ const [filters, setFilters] = useState({
+ searchTerm: '',
+ dateFrom: '',
+ dateTo: '',
+ status: '',
+ type: 'all', // 'all', 'wallet', 'topup'
+ });
+
+ // Check if any filters are active
+ const hasActiveFilters = Object.values(filters).some(value => value !== '' && value !== 'all');
+
+ const fetchCustomerData = async () => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customers/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const customerData = await response.json()
+ const customerDetails = customerData.data;
+
+ setWalletData(prev => ({
+ ...prev,
+ customerName: customerDetails.name || 'N/A',
+ code: customerDetails.customer_referral_code || 'N/A',
+ }));
+
+ } catch (error) {
+ console.error("Error fetching customer data:", error);
+ toast.error('Failed to load customer data');
+ }
+ }
+
+ const fetchCustomerWalletHistory = async (dateFrom = '', dateTo = '') => {
+ try {
+ const response = await fetch(`${VITE_API_BASE_URL}customer-wallet/history/${id}?date_from=${dateFrom}&date_to=${dateTo}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const walletHistory = await response.json();
+ const walletData = walletHistory.data;
+
+ const allTransactions = walletData.all || [];
+ const creditTransactions = walletData.credit || [];
+
+ const allBalance = getLastBalance(allTransactions) ?? 0;
+ const creditBalance = getLastBalance(creditTransactions) ?? 0;
+ const totalBalance = allBalance + creditBalance;
+
+ setTransactions({
+ all: allTransactions,
+ credit: creditTransactions
+ });
+
+ setFilteredWalletData(allTransactions);
+
+ setWalletData(prev => ({
+ ...prev,
+ wallet: `RM ${allBalance.toFixed(2)}`,
+ walletCredit: `RM ${creditBalance.toFixed(2)}`,
+ totalWallet: `RM ${totalBalance.toFixed(2)}`,
+ }));
+
+ } catch (error) {
+ console.error("Error fetching wallet history:", error);
+ toast.error('Failed to load wallet history');
+ }
+ }
+
+ // Fetch topup data for this customer
+ const fetchTopupData = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch(`${VITE_API_BASE_URL}customer/topup/list`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${authToken}`,
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+
+ // Filter topup data for this specific customer
+ if (data.data && Array.isArray(data.data)) {
+ const customerTopups = data.data.filter(
+ item => item.customer_id.toString() === id.toString()
+ );
+
+ // Format the data for display
+ const formattedData = customerTopups.map(item => ({
+ id: item.id,
+ topUpNumber: item.topup_number,
+ amount: `RM ${parseFloat(item.amount).toFixed(2)}`,
+ redeem: item.credit ? `RM ${parseFloat(item.credit).toFixed(2)}` : 'N/A',
+ paymentType: item.payment_method || 'N/A',
+ date: new Date(item.created_at).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ }),
+ rawDate: new Date(item.created_at),
+ status: item.status,
+ type: 'topup',
+ rawAmount: parseFloat(item.amount)
+ }));
+
+ setTopupData(formattedData);
+ setFilteredTopupData(formattedData);
+ }
+ } catch (error) {
+ console.error('Error fetching topup data:', error);
+ toast.error('Failed to load topup history');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Apply filters whenever they change
+ useEffect(() => {
+ setFilterLoading(true);
+
+ // Filter topup data
+ let topupResult = topupData;
+
+ // Filter by search term
+ if (filters.searchTerm) {
+ topupResult = topupResult.filter(item =>
+ item.topUpNumber.toLowerCase().includes(filters.searchTerm.toLowerCase()) ||
+ item.paymentType.toLowerCase().includes(filters.searchTerm.toLowerCase())
+ );
+ }
+
+ // Filter by status
+ if (filters.status) {
+ topupResult = topupResult.filter(item =>
+ item.status.toLowerCase() === filters.status.toLowerCase()
+ );
+ }
+
+ // Filter by date range
+ if (filters.dateFrom) {
+ const fromDate = new Date(filters.dateFrom);
+ topupResult = topupResult.filter(item => item.rawDate >= fromDate);
+ }
+
+ if (filters.dateTo) {
+ const toDate = new Date(filters.dateTo);
+ toDate.setHours(23, 59, 59, 999);
+ topupResult = topupResult.filter(item => item.rawDate <= toDate);
+ }
+
+ setFilteredTopupData(topupResult);
+
+ // Filter wallet data
+ let walletResult = transactions.all;
+
+ // Filter by search term
+ if (filters.searchTerm) {
+ walletResult = walletResult.filter(item =>
+ (item.related_type && item.related_type.toLowerCase().includes(filters.searchTerm.toLowerCase())) ||
+ (item.action && item.action.toLowerCase().includes(filters.searchTerm.toLowerCase())) ||
+ (item.remark && item.remark.toLowerCase().includes(filters.searchTerm.toLowerCase()))
+ );
+ }
+
+ // Filter by date range
+ if (filters.dateFrom) {
+ const fromDate = new Date(filters.dateFrom);
+ walletResult = walletResult.filter(item => new Date(item.created_at) >= fromDate);
+ }
+
+ if (filters.dateTo) {
+ const toDate = new Date(filters.dateTo);
+ toDate.setHours(23, 59, 59, 999);
+ walletResult = walletResult.filter(item => new Date(item.created_at) <= toDate);
+ }
+
+ setFilteredWalletData(walletResult);
+
+ // Small delay to show loading state for better UX
+ setTimeout(() => setFilterLoading(false), 300);
+ }, [filters, topupData, transactions.all]);
+
+ const getLastBalance = (arr) => {
+ if (!arr?.length) return null;
+ const last = arr[0];
+ const balance = parseFloat(last.balance);
+ return isNaN(balance) ? 0 : balance;
+ };
+
+ useEffect(() => {
+ async function fetchAll() {
+ await fetchCustomerData();
+ await fetchCustomerWalletHistory();
+ await fetchTopupData();
+ }
+ fetchAll();
+ }, [id]);
+
+ const handleFilterChange = (key, value) => {
+ setFilters(prev => ({ ...prev, [key]: value }));
+ };
+
+ const clearFilters = () => {
+ setFilters({
+ searchTerm: '',
+ dateFrom: '',
+ dateTo: '',
+ status: '',
+ type: 'all',
+ });
+ };
+
+ const topupColumns = [
+ {
+ name: 'Top Up Number',
+ selector: row => row.topUpNumber,
+ sortable: true,
+ width: '20%',
+ cell: row => (
+
+ {row.topUpNumber}
+
+ ),
+ },
+ {
+ name: 'Amount',
+ selector: row => row.amount,
+ sortable: true,
+ width: '15%',
+ sortFunction: (a, b) => a.rawAmount - b.rawAmount,
+ },
+ {
+ name: 'Credit',
+ selector: row => row.redeem,
+ sortable: true,
+ width: '15%',
+ },
+ {
+ name: 'Payment Type',
+ selector: row => row.paymentType,
+ sortable: true,
+ width: '15%',
+ },
+ {
+ name: 'Date',
+ selector: row => row.date,
+ sortable: true,
+ width: '15%',
+ },
+ {
+ name: 'Status',
+ selector: row => row.status,
+ sortable: true,
+ width: '10%',
+ cell: row => (
+
+ {row.status}
+
+ ),
+ },
+ ];
+
+ const walletColumns = [
+ {
+ name: 'No',
+ cell: (row, rowIndex) => rowIndex + 1,
+ width: '70px'
+ },
+ {
+ name: 'Date Created',
+ selector: row => new Date(row.created_at).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ }),
+ minWidth: '150px',
+ sortable: true,
+ },
+ {
+ name: 'Type',
+ selector: row => row.related_type,
+ minWidth: '150px',
+ sortable: true,
+ },
+ {
+ name: 'Action',
+ selector: row => row.action?.toUpperCase() || '',
+ minWidth: '100px',
+ sortable: true,
+ },
+ {
+ name: 'Current',
+ selector: row => `RM ${parseFloat(row.current || 0).toFixed(2)}`,
+ sortable: true,
+ },
+ {
+ name: 'In',
+ selector: row => `RM ${parseFloat(row.in || 0).toFixed(2)}`,
+ sortable: true,
+ },
+ {
+ name: 'Out',
+ selector: row => `RM ${parseFloat(row.out || 0).toFixed(2)}`,
+ sortable: true,
+ },
+ {
+ name: 'Balance',
+ selector: row => `RM ${parseFloat(row.balance || 0).toFixed(2)}`,
+ sortable: true,
+ },
+ {
+ name: 'Remark',
+ selector: row => row.remark,
+ minWidth: '250px',
+ sortable: true,
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: '#312e81',
+ color: 'white',
+ minHeight: '50px',
+ fontSize: '16px',
+ },
+ },
+ headCells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ fontWeight: '500',
+ },
+ },
+ rows: {
+ style: {
+ minHeight: '60px',
+ fontSize: '15px',
+ '&:hover': {
+ backgroundColor: '#f9fafb',
+ },
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: '#f9fafb',
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ },
+ },
+ pagination: {
+ style: {
+ borderTopStyle: 'solid',
+ borderTopWidth: '1px',
+ borderTopColor: '#e5e7eb',
+ },
+ },
+ };
+
+ const NoDataComponent = () => (
+ No data found
+ );
+
+ const navigate = useNavigate();
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ const handleAdjustWallet = () => {
+ navigate(`/member/member_overview/member_wallet/adjust_wallet/${id}`);
+ }
+
+ return (
+
+
+
+
+
+
+ Go back
+
+
+
+ {/* Main Content */}
+
+ {/* Details Card */}
+
+
+ Details
+
+
+
+
Code
+
:
+
{walletData.code}
+
+
+
Customer Name
+
:
+
{walletData.customerName}
+
+
+
Wallet
+
:
+
{walletData.wallet}
+
+
+
+ Adjust Wallet
+
+
+
+
+
+ {/* Unified Search Filter Card */}
+
+
+ Search Filter
+ {hasActiveFilters && (
+
+
+ Clear Filters
+
+ )}
+
+
+
+
+
+
+ Search
+
+ handleFilterChange('searchTerm', e.target.value)}
+ />
+
+
+ Date From
+ handleFilterChange('dateFrom', e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"
+ />
+
+
+ Date To
+ handleFilterChange('dateTo', e.target.value)}
+ className="w-full border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500"
+ />
+
+
+
+
+ Status
+
+ handleFilterChange('status', e.target.value)}
+ >
+ All Statuses
+ Success
+ Pending
+ Failed
+
+
+
+
+
+
+ {/* Topup History Listing */}
+
+
Topup History
+
+ }
+ noDataComponent={
+
+ {topupData.length === 0
+ ? 'No topup records found for this customer'
+ : 'No records match your filters'
+ }
+
+ }
+ highlightOnHover
+ pointerOnHover
+ />
+
+
+
+ {/* Wallet History Listing */}
+
+
Wallet History
+
+ }
+ noDataComponent={
+
+ {transactions.all.length === 0
+ ? 'No wallet records found for this customer'
+ : 'No records match your filters'
+ }
+
+ }
+ highlightOnHover
+ pointerOnHover
+ />
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/report/product.jsx b/src/pages/report/product.jsx
new file mode 100644
index 0000000..2cf8d9d
--- /dev/null
+++ b/src/pages/report/product.jsx
@@ -0,0 +1,865 @@
+import reportService from '@/store/api/reportService';
+import outletService from '@/store/api/outletService';
+import { BarChart3, ChevronDown, ChevronUp, Download, Loader2, Package, Store, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
+import { useEffect, useMemo, useState, useCallback, memo } from 'react';
+import { useTable, useSortBy, usePagination } from 'react-table';
+import { Pie } from 'react-chartjs-2';
+import {
+ Chart as ChartJS,
+ ArcElement,
+ Title,
+ Tooltip,
+ Legend
+} from 'chart.js';
+import UserService from '@/store/api/userService';
+import { toast } from 'react-toastify';
+
+ChartJS.register(ArcElement, Title, Tooltip, Legend);
+
+const ProductReport = () => {
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [topProducts, setTopProducts] = useState([]);
+ const [filters, setFilters] = useState({
+ startDate: '',
+ endDate: '',
+ outlet: 'All',
+ orderMethod: 'All',
+ reportMode: 'total',
+ year: new Date().getFullYear().toString() // Default to current year
+ });
+ const [outletOptions, setOutletOptions] = useState([]);
+ const [showFilters, setShowFilters] = useState(false);
+ const [userPermissions, setUserPermissions] = useState({});
+ const [hasCreatePermission, setHasCreatePermission] = useState(false);
+ const [hasUpdatePermission, setHasUpdatePermission] = useState(false);
+ const [hasDeletePermission, setHasDeletePermission] = useState(false);
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [legendPage, setLegendPage] = useState(0);
+ const itemsPerPage = 10;
+
+ const userData = useMemo(() => {
+ try {
+ const userStr = localStorage.getItem('user');
+ return userStr ? JSON.parse(userStr) : null;
+ } catch (error) {
+ console.error('Error parsing user data:', error);
+ return null;
+ }
+ }, []);
+
+ const user_id = userData?.user?.user_id || null;
+
+ const fetchUserPermissions = async () => {
+ try {
+ if (!user_id) return;
+
+ const userDataRes = await UserService.getUser(user_id);
+ const userData = userDataRes?.data;
+ if (!userData) return;
+
+ if (userData.role && userData.role.toLowerCase() === 'admin') {
+ setIsAdmin(true);
+ setHasCreatePermission(true);
+ setHasUpdatePermission(true);
+ setHasDeletePermission(true);
+ return;
+ }
+
+ let permissions = {};
+ if (userData.user_permissions) {
+ try {
+ permissions = JSON.parse(userData.user_permissions);
+ setUserPermissions(permissions);
+
+ if (permissions.Outlets && permissions.Outlets.subItems && permissions.Outlets.subItems.Lists) {
+ if (permissions.Outlets.subItems.Lists.create === true) {
+ setHasCreatePermission(true);
+ }
+ if (permissions.Outlets.subItems.Lists.update === true) {
+ setHasUpdatePermission(true);
+ }
+ if (permissions.Outlets.subItems.Lists.delete === true) {
+ setHasDeletePermission(true);
+ }
+ }
+ } catch (e) {
+ console.error("Error parsing user permissions:", e);
+ }
+ }
+ } catch (err) {
+ console.error("Error fetching user permissions:", err);
+ }
+ };
+
+ const fetchOutlets = async () => {
+ try {
+ if (!user_id) return;
+ const res = await outletService.getOutlets(user_id);
+ if (res.status === 200) {
+ const list = res.result;
+ setOutletOptions(list);
+ }
+ } catch (e) {
+ console.error('Failed to fetch outlets', e);
+ }
+ };
+
+ const buildSearchParams = (f) => {
+ const params = {};
+
+ // Handle date parameters based on report mode
+ if (f.reportMode === 'daily') {
+ if (f.startDate) params.start_date = f.startDate;
+ if (f.endDate) params.end_date = f.endDate;
+ } else if (f.reportMode === 'monthly') {
+ // For monthly, use year to set start_date (YYYY-01-01)
+ if (f.year) {
+ params.start_date = `${f.year}-01-01`;
+ }
+ }
+ // For yearly and total modes, no date parameters needed
+
+ if (f.outlet && f.outlet !== 'All') params.outlet_id = f.outlet;
+ if (f.orderMethod && f.orderMethod !== 'All') params.order_type = f.orderMethod;
+ if (f.reportMode) params.report_mode = f.reportMode;
+ if (user_id) params.user_id = user_id;
+ return params;
+ };
+
+ const fetchReport = async (applied = filters) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await reportService.getProductReport(buildSearchParams(applied));
+ if(response.status == 200){
+ const products = response.data;
+ console.log(products)
+ setTopProducts(products);
+ }
+ } catch (e) {
+ setError('Failed to fetch product report. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchOutlets();
+ fetchReport();
+ fetchUserPermissions();
+ }, []);
+
+ // Generate year options (last 10 years + next 1 year)
+ const yearOptions = useMemo(() => {
+ const currentYear = new Date().getFullYear();
+ const years = [];
+ for (let i = currentYear - 10; i <= currentYear + 1; i++) {
+ years.push(i.toString());
+ }
+ return years.reverse(); // Show most recent years first
+ }, []);
+
+ // Month number to name mapping
+ const monthNames = {
+ '01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr', '05': 'May', '06': 'Jun',
+ '07': 'Jul', '08': 'Aug', '09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'
+ };
+
+ // Generate dynamic columns based on report mode
+ const columns = useMemo(() => {
+ const baseColumns = [
+ {
+ Header: 'Product',
+ accessor: 'item_title',
+ Cell: ({ value }) => (
+
+ ),
+ }
+ ];
+
+ // For Total mode - single column
+ if (filters.reportMode === 'total') {
+ baseColumns.push({
+ Header: 'Total Qty Sold',
+ accessor: 'quantity',
+ Cell: ({ value }) => (
+
+ {value}
+
+ ),
+ });
+ }
+ // For comparative modes (daily, monthly, yearly)
+ else if (topProducts.length > 0 && topProducts[0].comparative_data) {
+ const periods = Object.keys(topProducts[0].comparative_data).sort();
+
+ // Add period columns
+ periods.forEach(period => {
+ let headerName = period;
+
+ // Format monthly headers (01 -> Jan, 02 -> Feb, etc.)
+ if (filters.reportMode === 'monthly' && monthNames[period]) {
+ headerName = monthNames[period];
+ }
+
+ baseColumns.push({
+ Header: headerName,
+ accessor: `comparative_data.${period}`,
+ Cell: ({ value }) => (
+
+ {value || 0}
+
+ ),
+ });
+ });
+ } else {
+ // Fallback to single column if no comparative data
+ baseColumns.push({
+ Header: 'Qty Sold',
+ accessor: 'quantity',
+ Cell: ({ value }) => (
+
+ {value}
+
+ ),
+ });
+ }
+
+ return baseColumns;
+ }, [topProducts]);
+
+ const data = useMemo(() => topProducts, [topProducts]);
+
+ const {
+ getTableProps,
+ getTableBodyProps,
+ headerGroups,
+ prepareRow,
+ page,
+ canPreviousPage,
+ canNextPage,
+ pageOptions,
+ pageCount,
+ gotoPage,
+ nextPage,
+ previousPage,
+ setPageSize,
+ state: { pageIndex, pageSize },
+ } = useTable(
+ {
+ columns,
+ data,
+ initialState: { pageIndex: 0, pageSize: 10 },
+ },
+ useSortBy,
+ usePagination
+ );
+
+ const orderMethodOptions = [
+ { value: 'All', title: 'All' },
+ { value: 'delivery', title: 'Delivery' },
+ { value: 'pickup', title: 'Pickup' },
+ { value: 'dinein', title: 'Dine-in' },
+ ];
+
+ const reportModeOptions = [
+ { value: 'daily', title: 'Daily' },
+ { value: 'monthly', title: 'Monthly' },
+ { value: 'yearly', title: 'Yearly' },
+ { value: 'total', title: 'Total' },
+ ];
+
+ const chartData = useMemo(() => {
+ if (!topProducts || topProducts.length === 0) {
+ return {
+ labels: [],
+ datasets: [{
+ label: 'Qty Sold',
+ data: [],
+ backgroundColor: [],
+ hoverBackgroundColor: [],
+ borderColor: '#ffffff',
+ borderWidth: 3,
+ hoverBorderWidth: 4,
+ hoverBorderColor: '#ffffff'
+ }]
+ };
+ }
+
+ const getRandomColor = () => {
+ const letters = '0123456789ABCDEF';
+ let color = '#';
+ for (let i = 0; i < 6; i++) {
+ color += letters[Math.floor(Math.random() * 16)];
+ }
+ return color;
+ };
+
+ const darkenColor = (hex, percent) => {
+ const num = parseInt(hex.slice(1), 16);
+ const amt = Math.round(2.55 * percent);
+ const R = (num >> 16) - amt;
+ const G = ((num >> 8) & 0x00FF) - amt;
+ const B = (num & 0x0000FF) - amt;
+
+ return '#' + (
+ 0x1000000 +
+ (Math.max(0, R) << 16) +
+ (Math.max(0, G) << 8) +
+ Math.max(0, B)
+ ).toString(16).slice(1);
+ };
+
+ const backgroundColor = [];
+ const hoverBackgroundColor = [];
+
+ for (let i = 0; i < topProducts.length; i++) {
+ const color = getRandomColor();
+ backgroundColor.push(color);
+ hoverBackgroundColor.push(darkenColor(color, 15));
+ }
+
+ // For chart, use quantities based on report mode
+ const chartQuantities = topProducts.map(product => {
+ if (filters.reportMode === 'total') {
+ return product.quantity;
+ } else if (product.comparative_data) {
+ // For comparative modes, sum all periods
+ return Object.values(product.comparative_data)
+ .reduce((sum, qty) => sum + parseInt(qty || 0), 0);
+ }
+ return product.quantity;
+ });
+
+ return {
+ labels: topProducts.map(p => p.item_title),
+ datasets: [
+ {
+ label: 'Qty Sold',
+ data: chartQuantities,
+ backgroundColor,
+ hoverBackgroundColor,
+ borderColor: '#ffffff',
+ borderWidth: 3,
+ hoverBorderWidth: 4,
+ hoverBorderColor: '#ffffff'
+ }
+ ]
+ };
+ }, [topProducts, filters.reportMode]);
+
+ const chartOptions = useMemo(() => ({
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ title: {
+ display: true,
+ text: `Top Sales Products - ${filters.reportMode.charAt(0).toUpperCase() + filters.reportMode.slice(1)}${filters.reportMode === 'monthly' ? ` ${filters.year}` : ''}`,
+ font: {
+ size: 18,
+ weight: 'bold'
+ },
+ color: '#374151',
+ padding: 20
+ },
+ tooltip: {
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ titleColor: '#ffffff',
+ bodyColor: '#ffffff',
+ borderColor: '#e5e7eb',
+ borderWidth: 1,
+ cornerRadius: 8,
+ displayColors: true,
+ callbacks: {
+ title: (context) => {
+ return context[0].label;
+ },
+ label: (context) => {
+ const total = context.dataset.data.reduce((a, b) => parseInt(a) + parseInt(b), 0);
+ const val = context.parsed;
+ const pct = total ? ((val / total) * 100).toFixed(1) : 0;
+ return `Qty: ${val} units (${pct}%)`;
+ }
+ }
+ }
+ },
+ elements: {
+ arc: {
+ borderWidth: 3,
+ borderColor: '#ffffff'
+ }
+ },
+ animation: {
+ animateRotate: true,
+ animateScale: true,
+ duration: 1000,
+ easing: 'easeInOutQuart'
+ },
+ interaction: {
+ intersect: false,
+ mode: 'index'
+ }
+ }), [filters.reportMode, filters.year]);
+
+ const CustomLegend = memo(({ labels, colors, data }) => {
+ const total = useMemo(() => data.reduce((sum, value) => sum + parseInt(value), 0), [data]);
+
+ const totalPages = Math.ceil(labels.length / itemsPerPage);
+ const startIndex = legendPage * itemsPerPage;
+ const endIndex = Math.min(startIndex + itemsPerPage, labels.length);
+ const currentItems = labels.slice(startIndex, endIndex);
+
+ const handlePrevious = useCallback(() => {
+ setLegendPage(prev => Math.max(0, prev - 1));
+ }, []);
+
+ const handleNext = useCallback(() => {
+ setLegendPage(prev => Math.min(totalPages - 1, prev + 1));
+ }, [totalPages]);
+
+ return (
+
+
+
+
Product Breakdown
+
+
+
+ {currentItems.map((label, i) => {
+ const originalIndex = startIndex + i;
+ const value = data[originalIndex];
+ const percentage = total ? ((parseInt(value) / total) * 100).toFixed(1) : 0;
+
+ return (
+
+
+
+
+ {percentage}%
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+ {startIndex + 1}-{endIndex} of {labels.length}
+
+ = totalPages - 1}
+ className="p-1.5 rounded border border-gray-300 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ title="Next page"
+ >
+
+
+
+
+ {legendPage + 1}/{totalPages}
+
+
+
+
+ Total:
+ {total.toLocaleString()}
+
+
+
+
+ );
+ });
+
+ const exportToCSV = async() => {
+ let searchParams = buildSearchParams(filters);
+ searchParams = {
+ ...searchParams,
+ type: 'product-report'
+ };
+
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await reportService.getExportExcel(searchParams);
+ if(response.status == 200){
+ toast.success(response.message);
+ }
+ setLoading(false);
+ } catch (err) {
+ setError('Failed to export product data. Please try again.');
+ setLoading(false);
+ }
+ };
+
+ const handleApplyFilters = () => {
+ gotoPage(0);
+ fetchReport(filters);
+ };
+
+ const handleResetFilters = () => {
+ const resetFilters = {
+ startDate: '',
+ endDate: '',
+ outlet: 'All',
+ orderMethod: 'All',
+ reportMode: 'total',
+ year: new Date().getFullYear().toString()
+ };
+ setFilters(resetFilters);
+ gotoPage(0);
+ fetchReport(resetFilters);
+ };
+
+ // Handle report mode change
+ const handleReportModeChange = (mode) => {
+ setFilters(prev => ({ ...prev, reportMode: mode }));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ setShowFilters(v => !v)}
+ >Filter
+
+
+ Export Report
+
+
+
+
+ {showFilters && (
+
+
+
+ Report Mode
+ handleReportModeChange(e.target.value)}
+ >
+ {reportModeOptions.map((mode, i) => (
+ {mode.title}
+ ))}
+
+
+
+ {/* Show year selector for monthly mode */}
+ {filters.reportMode === 'monthly' && (
+
+ Year
+ setFilters(prev => ({ ...prev, year: e.target.value }))}
+ >
+ {yearOptions.map((year) => (
+ {year}
+ ))}
+
+
+ )}
+
+ {/* Show date range only for daily mode */}
+ {filters.reportMode === 'daily' && (
+ <>
+
+ Start Date
+ setFilters(prev => ({ ...prev, startDate: e.target.value }))}
+ />
+
+
+ End Date
+ setFilters(prev => ({ ...prev, endDate: e.target.value }))}
+ />
+
+ >
+ )}
+
+ {/* Show empty space when inputs are hidden to maintain layout */}
+ {filters.reportMode !== 'daily' && filters.reportMode !== 'monthly' && (
+ <>
+
+ Start Date
+
+
+
+ End Date
+
+
+ >
+ )}
+
+
+ Outlet
+ setFilters(prev => ({ ...prev, outlet: e.target.value }))}>
+ All
+ {outletOptions.map(opt => (
+ {opt.title}
+ ))}
+
+
+
+ Order Method
+ setFilters(prev => ({ ...prev, orderMethod: e.target.value }))}>
+ {orderMethodOptions.map((m, i) => ({m.title} ))}
+
+
+
+
+ Reset
+ Apply
+
+
+ )}
+
+
+
+ {loading ? (
+
+
+
+
Loading product data...
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {filters.reportMode === 'total'
+ ? 'Top Products - Total Sales'
+ : filters.reportMode === 'monthly'
+ ? `Top Products Table - Monthly ${filters.year}`
+ : `Top Products Table - ${filters.reportMode.charAt(0).toUpperCase() + filters.reportMode.slice(1)}`
+ }
+
+
+
+ {/* React Table v7 */}
+
+
+
+ {headerGroups.map(headerGroup => (
+
+ {headerGroup.headers.map(column => (
+
+
+ {column.render('Header')}
+
+ {column.isSorted ? (
+ column.isSortedDesc ? (
+
+ ) : (
+
+ )
+ ) : (
+ ''
+ )}
+
+
+
+ ))}
+
+ ))}
+
+
+ {loading ? (
+
+
+
+
+
+ ) : page.length === 0 ? (
+
+
+ No product data found.
+
+
+ ) : (
+ page.map(row => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map(cell => (
+
+ {cell.render('Cell')}
+
+ ))}
+
+ );
+ })
+ )}
+
+
+
+
+ {/* Pagination Controls */}
+
+
+
+ Page {pageIndex + 1} of {pageOptions.length}
+
+
+ ({topProducts.length} total items)
+
+
+
+
+
{
+ setPageSize(Number(e.target.value));
+ }}
+ className="border border-gray-300 rounded-md text-sm px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ {[10, 20, 30, 50, 100].map(size => (
+
+ Show {size}
+
+ ))}
+
+
+
+ gotoPage(0)}
+ disabled={!canPreviousPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="First page"
+ >
+
+
+ previousPage()}
+ disabled={!canPreviousPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Previous page"
+ >
+
+
+ nextPage()}
+ disabled={!canNextPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Next page"
+ >
+
+
+ gotoPage(pageCount - 1)}
+ disabled={!canNextPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Last page"
+ >
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ProductReport;
\ No newline at end of file
diff --git a/src/pages/report/promo.jsx b/src/pages/report/promo.jsx
new file mode 100644
index 0000000..838fd2d
--- /dev/null
+++ b/src/pages/report/promo.jsx
@@ -0,0 +1,705 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { useTable, useSortBy, usePagination } from 'react-table';
+import { Edit, Download, Trash2, Plus, PenLine, ChevronDown, ChevronUp, Loader2, TrendingUp, Users, Percent, BarChart3, TicketSlash, TicketCheck, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import DeleteConfirmationModal from '../../components/ui/DeletePopUp';
+import OutletApiService from '../../store/api/outletService';
+import UserService from '../../store/api/userService';
+import reportService from '@/store/api/reportService';
+import { set } from 'react-hook-form';
+import { toast } from 'react-toastify';
+import outletService from '../../store/api/outletService';
+
+const PromoReport = () => {
+ const [outlets, setOutlets] = useState([]);
+ const [filteredOutlets, setFilteredOutlets] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [selectedOutletId, setSelectedOutletId] = useState(null);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [deleting, setDeleting] = useState(false);
+ const navigate = useNavigate();
+ const [userPermissions, setUserPermissions] = useState({});
+ const [hasCreatePermission, setHasCreatePermission] = useState(false);
+ const [hasUpdatePermission, setHasUpdatePermission] = useState(false);
+ const [hasDeletePermission, setHasDeletePermission] = useState(false);
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [reportData, setReportData] = useState([]);
+ const [filteredReportData, setFilteredReportData] = useState([]);
+ const [summaryData, setSummaryData] = useState([]);
+ const [filters, setFilters] = useState({
+ startDate: '',
+ endDate: '',
+ outlet: 'All',
+ reportMode: 'total',
+ year: new Date().getFullYear().toString()
+ });
+ const [outletOptions, setOutletOptions] = useState([]);
+ const [showFilters, setShowFilters] = useState(false);
+
+ const userData = useMemo(() => {
+ try {
+ const userStr = localStorage.getItem('user');
+ return userStr ? JSON.parse(userStr) : null;
+ } catch (error) {
+ console.error('Error parsing user data:', error);
+ return null;
+ }
+ }, []);
+
+ const user_id = userData?.user?.user_id || null;
+
+ // Generate year options (last 10 years + next 1 year)
+ const yearOptions = useMemo(() => {
+ const currentYear = new Date().getFullYear();
+ const years = [];
+ for (let i = currentYear - 10; i <= currentYear + 1; i++) {
+ years.push(i.toString());
+ }
+ return years.reverse();
+ }, []);
+
+ // Month number to name mapping
+ const monthNames = {
+ '01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr', '05': 'May', '06': 'Jun',
+ '07': 'Jul', '08': 'Aug', '09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'
+ };
+
+ const reportModeOptions = [
+ { value: 'daily', title: 'Daily' },
+ { value: 'monthly', title: 'Monthly' },
+ { value: 'yearly', title: 'Yearly' },
+ { value: 'total', title: 'Total' },
+ ];
+
+ const fetchUserPermissions = async () => {
+ try {
+ if (!user_id) return;
+
+ const userDataRes = await UserService.getUser(user_id);
+ const userData = userDataRes?.data;
+ if (!userData) return;
+
+ if (userData.role && userData.role.toLowerCase() === 'admin') {
+ setIsAdmin(true);
+ setHasCreatePermission(true);
+ setHasUpdatePermission(true);
+ setHasDeletePermission(true);
+ return;
+ }
+
+ let permissions = {};
+ if (userData.user_permissions) {
+ try {
+ permissions = JSON.parse(userData.user_permissions);
+ setUserPermissions(permissions);
+
+ if (permissions.Outlets && permissions.Outlets.subItems && permissions.Outlets.subItems.Lists) {
+ if (permissions.Outlets.subItems.Lists.create === true) {
+ setHasCreatePermission(true);
+ }
+ if (permissions.Outlets.subItems.Lists.update === true) {
+ setHasUpdatePermission(true);
+ }
+ if (permissions.Outlets.subItems.Lists.delete === true) {
+ setHasDeletePermission(true);
+ }
+ }
+ } catch (e) {
+ console.error("Error parsing user permissions:", e);
+ }
+ }
+ } catch (err) {
+ console.error("Error fetching user permissions:", err);
+ }
+ };
+
+ const fetchOutlets = async () => {
+ try {
+ if (!user_id) return;
+ const res = await outletService.getOutlets(user_id);
+ if (res.status === 200) {
+ const list = res.result;
+ setOutletOptions(list);
+ }
+ } catch (e) {
+ console.error('Failed to fetch outlets', e);
+ }
+ };
+
+ useEffect(() => {
+ if (user_id) {
+ fetchOutlets();
+ fetchReport();
+ fetchUserPermissions();
+ }
+ }, [user_id]);
+
+ // Filter outlets based on user's outlet ID
+ useEffect(() => {
+ setFilteredReportData(reportData);
+ }, [reportData]);
+
+ const buildSearchParams = (f) => {
+ const params = {};
+
+ // Handle date parameters based on report mode
+ if (f.reportMode === 'daily') {
+ if (f.startDate) params.start_date = f.startDate;
+ if (f.endDate) params.end_date = f.endDate;
+ } else if (f.reportMode === 'monthly') {
+ // For monthly, use year to set start_date (YYYY-01-01)
+ if (f.year) {
+ params.start_date = `${f.year}-01-01`;
+ }
+ }
+ // For yearly and total modes, no date parameters needed
+
+ if (f.outlet && f.outlet !== 'All') params.outlet_id = f.outlet;
+ if (f.reportMode) params.report_mode = f.reportMode;
+ if (user_id) params.user_id = user_id;
+ return params;
+ };
+
+ const fetchReport = async (applied = filters) => {
+ try {
+ setLoading(true);
+ setError(null);
+ const response = await reportService.getPromoReport(buildSearchParams(applied));
+ if(response.status == 200){
+ console.log(response.data)
+ const report_data = response.data.promos;
+ const summary_data = response.data.summary;
+ console.log(summary_data)
+ console.log(report_data);
+ setReportData(report_data);
+ setSummaryData(summary_data);
+ }
+ } catch (err) {
+ setError('Failed to fetch promo report. Please try again.');
+ console.error('Error fetching promo report:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const exportToCSV = async() => {
+ let searchParams = buildSearchParams(filters);
+ searchParams = {
+ ...searchParams,
+ type: 'promo-report'
+ };
+
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await reportService.getExportExcel(searchParams);
+ if(response.status == 200){
+ toast.success(response.message);
+ }
+ setLoading(false);
+ } catch (err) {
+ setError('Failed to export promo data. Please try again.');
+ setLoading(false);
+ }
+ };
+
+ // Generate dynamic columns based on report mode
+ const columns = useMemo(() => {
+ // Base columns
+ const baseColumns = [
+ {
+ Header: 'Promo Name',
+ accessor: 'promo_name',
+ Cell: ({ value }) => (
+
+ {value}
+
+ ),
+ },
+ {
+ Header: 'Code',
+ accessor: 'code',
+ Cell: ({ value }) => (
+
+ {value}
+
+ ),
+ }
+ ];
+
+ // For Total mode - simple column
+ if (filters.reportMode === 'total') {
+ return [
+ ...baseColumns,
+ {
+ Header: 'Promo Used',
+ accessor: 'promo_used',
+ Cell: ({ value }) => (
+
+ {value}
+
+ ),
+ }
+ ];
+ }
+
+ // For comparative modes (daily, monthly, yearly)
+ if (reportData.length > 0 && reportData[0].comparative_data) {
+ const periods = Object.keys(reportData[0].comparative_data).sort();
+
+ // Create grouped columns for each period
+ periods.forEach(period => {
+ let headerName = period;
+
+ // Format monthly headers (01 -> Jan, 02 -> Feb, etc.)
+ if (filters.reportMode === 'monthly' && monthNames[period]) {
+ headerName = monthNames[period];
+ }
+
+ // Create a group column for this period
+ baseColumns.push({
+ Header: headerName,
+ accessor: `comparative_data.${period}.promo_used`,
+ Cell: ({ value }) => (
+
+ {value || 0}
+
+ ),
+ });
+ });
+ } else {
+ // Fallback to simple column if no comparative data
+ return [
+ ...baseColumns,
+ {
+ Header: 'Promo Used',
+ accessor: 'promo_used',
+ Cell: ({ value }) => (
+
+ {value}
+
+ ),
+ }
+ ];
+ }
+
+ return baseColumns;
+ }, [reportData]);
+
+ const data = useMemo(() => reportData, [reportData]);
+
+ // React Table v7 configuration
+ const {
+ getTableProps,
+ getTableBodyProps,
+ headerGroups,
+ prepareRow,
+ page,
+ canPreviousPage,
+ canNextPage,
+ pageOptions,
+ pageCount,
+ gotoPage,
+ nextPage,
+ previousPage,
+ setPageSize,
+ state: { pageIndex, pageSize },
+ } = useTable(
+ {
+ columns,
+ data,
+ initialState: { pageIndex: 0, pageSize: 10 },
+ },
+ useSortBy,
+ usePagination
+ );
+
+ const handleApplyFilters = () => {
+ gotoPage(0);
+ fetchReport(filters);
+ };
+
+ const handleResetFilters = () => {
+ const resetFilters = {
+ startDate: '',
+ endDate: '',
+ outlet: 'All',
+ reportMode: 'total',
+ year: new Date().getFullYear().toString()
+ };
+ setFilters(resetFilters);
+ gotoPage(0);
+ fetchReport(resetFilters);
+ };
+
+ const handleReportModeChange = (mode) => {
+ setFilters(prev => ({ ...prev, reportMode: mode }));
+ };
+
+ return (
+
+ {/* Header Section */}
+
+
+
+
+
+
+
+
+
+
Promo Report
+
+
+
+
+
+
+
+ {/* Summary Cards */}
+
+
+
+
+
+
+
+
+
Total Promos
+
{summaryData.total_promo}
+
+
+
+
+
+
+
+
+
+
+
Active Promos
+
{summaryData.active_promo}
+
+
+
+
+
+
+
+
+
+
+
Expired Promos
+
{summaryData.expired_promo}
+
+
+
+
+
+
+
+
+
+
+
Promos Used
+
{summaryData.promo_used}
+
+
+
+
+
+
+ {error && (
+
+
+
+
+
+
{error}
+
+ Try again
+
+
+
+
+
+ )}
+
+ {/* Promo Usage Table */}
+
+
+
+
+
+
+
+
+
+
+ {filters.reportMode === 'total'
+ ? 'Promo Usage - Total'
+ : filters.reportMode === 'monthly'
+ ? `Promo Usage - Monthly ${filters.year}`
+ : `Promo Usage - ${filters.reportMode.charAt(0).toUpperCase() + filters.reportMode.slice(1)}`
+ }
+
+
+
+
+ setShowFilters(v => !v)}
+ >Filter
+
+
+ Export Report
+
+
+
+
+
+ {showFilters && (
+
+
+
+ Report Mode
+ handleReportModeChange(e.target.value)}
+ >
+ {reportModeOptions.map((mode, i) => (
+ {mode.title}
+ ))}
+
+
+
+ {/* Show year selector for monthly mode */}
+ {filters.reportMode === 'monthly' && (
+
+ Year
+ setFilters(prev => ({ ...prev, year: e.target.value }))}
+ >
+ {yearOptions.map((year) => (
+ {year}
+ ))}
+
+
+ )}
+
+ {/* Show date range only for daily mode */}
+ {filters.reportMode === 'daily' && (
+ <>
+
+ Start Date
+ setFilters(prev => ({ ...prev, startDate: e.target.value }))} />
+
+
+ End Date
+ setFilters(prev => ({ ...prev, endDate: e.target.value }))} />
+
+ >
+ )}
+
+ {/* Show empty space when inputs are hidden to maintain layout */}
+ {filters.reportMode !== 'daily' && filters.reportMode !== 'monthly' && (
+ <>
+
+ Start Date
+
+
+
+ End Date
+
+
+ >
+ )}
+
+
+ Outlet
+ setFilters(prev => ({ ...prev, outlet: e.target.value }))}>
+ All
+ {outletOptions.map(opt => (
+ {opt.title}
+ ))}
+
+
+
+
+ Reset
+ Apply
+
+
+ )}
+
+ {/* React Table v7 */}
+
+
+
+ {headerGroups.map(headerGroup => (
+
+ {headerGroup.headers.map(column => (
+
+
+ {column.render('Header')}
+
+ {column.isSorted ? (
+ column.isSortedDesc ? (
+
+ ) : (
+
+ )
+ ) : (
+ ''
+ )}
+
+
+
+ ))}
+
+ ))}
+
+
+ {loading ? (
+
+
+
+
+
+
Loading promo data...
+
+
+
+
+ ) : page.length === 0 ? (
+
+
+
+
+
+
+
No promo data found
+
Try refreshing the page or check your connection
+
+
+
+ ) : (
+ page.map(row => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map(cell => (
+
+ {cell.render('Cell')}
+
+ ))}
+
+ );
+ })
+ )}
+
+
+
+
+ {/* Pagination Controls */}
+
+
+
+ Page {pageIndex + 1} of {pageOptions.length}
+
+
+ ({reportData.length} total items)
+
+
+
+
+
{
+ setPageSize(Number(e.target.value));
+ }}
+ className="border border-gray-300 rounded-md text-sm px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ {[10, 20, 30, 50, 100].map(size => (
+
+ Show {size}
+
+ ))}
+
+
+
+ gotoPage(0)}
+ disabled={!canPreviousPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="First page"
+ >
+
+
+ previousPage()}
+ disabled={!canPreviousPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Previous page"
+ >
+
+
+ nextPage()}
+ disabled={!canNextPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Next page"
+ >
+
+
+ gotoPage(pageCount - 1)}
+ disabled={!canNextPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Last page"
+ >
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PromoReport;
\ No newline at end of file
diff --git a/src/pages/report/sales.jsx b/src/pages/report/sales.jsx
new file mode 100644
index 0000000..b1551c3
--- /dev/null
+++ b/src/pages/report/sales.jsx
@@ -0,0 +1,952 @@
+import outletService from '@/store/api/outletService';
+import reportService from '@/store/api/reportService';
+import UserService from '@/store/api/userService';
+import { BarElement, CategoryScale, Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, Title, Tooltip } from 'chart.js';
+import { Activity, BarChart3, ChevronDown, ChevronUp, Clock, DollarSign, Download, Loader2, ShoppingCart, Store, Users, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight } from 'lucide-react';
+import { useEffect, useMemo, useState } from 'react';
+import { useTable, useSortBy, usePagination } from 'react-table';
+import { Line } from 'react-chartjs-2';
+import { toast } from 'react-toastify';
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+ Filler
+);
+
+const SalesReport = () => {
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [salesData, setSalesData] = useState([]);
+ const [outletData, setOutletData] = useState([]);
+ const [hourlyData, setHourlyData] = useState([]);
+ const [summaryData, setSummaryData] = useState({});
+ const [userPermissions, setUserPermissions] = useState({});
+ const [hasCreatePermission, setHasCreatePermission] = useState(false);
+ const [hasUpdatePermission, setHasUpdatePermission] = useState(false);
+ const [hasDeletePermission, setHasDeletePermission] = useState(false);
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [showFilters, setShowFilters] = useState(false);
+ const [filters, setFilters] = useState({
+ startDate: '',
+ endDate: '',
+ outlet: 'All',
+ orderMethod: 'All',
+ reportMode: 'total',
+ year: new Date().getFullYear().toString()
+ });
+ const [outletOptions, setOutletOptions] = useState([]);
+
+ const userData = useMemo(() => {
+ try {
+ const userStr = localStorage.getItem('user');
+ return userStr ? JSON.parse(userStr) : null;
+ } catch (error) {
+ console.error('Error parsing user data:', error);
+ return null;
+ }
+ }, []);
+
+ const user_id = userData?.user?.user_id || null;
+
+ // Generate year options (last 10 years + next 1 year)
+ const yearOptions = useMemo(() => {
+ const currentYear = new Date().getFullYear();
+ const years = [];
+ for (let i = currentYear - 10; i <= currentYear + 1; i++) {
+ years.push(i.toString());
+ }
+ return years.reverse();
+ }, []);
+
+ // Month number to name mapping
+ const monthNames = {
+ '01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr', '05': 'May', '06': 'Jun',
+ '07': 'Jul', '08': 'Aug', '09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'
+ };
+
+ const fetchUserPermissions = async () => {
+ try {
+ if (!user_id) return;
+
+ const userDataRes = await UserService.getUser(user_id);
+ const userData = userDataRes?.data;
+ if (!userData) return;
+
+ if (userData.role && userData.role.toLowerCase() === 'admin') {
+ setIsAdmin(true);
+ setHasCreatePermission(true);
+ setHasUpdatePermission(true);
+ setHasDeletePermission(true);
+ return;
+ }
+
+ let permissions = {};
+ if (userData.user_permissions) {
+ try {
+ permissions = JSON.parse(userData.user_permissions);
+ setUserPermissions(permissions);
+
+ if (permissions.Outlets && permissions.Outlets.subItems && permissions.Outlets.subItems.Lists) {
+ if (permissions.Outlets.subItems.Lists.create === true) {
+ setHasCreatePermission(true);
+ }
+ if (permissions.Outlets.subItems.Lists.update === true) {
+ setHasUpdatePermission(true);
+ }
+ if (permissions.Outlets.subItems.Lists.delete === true) {
+ setHasDeletePermission(true);
+ }
+ }
+ } catch (e) {
+ console.error("Error parsing user permissions:", e);
+ }
+ }
+ } catch (err) {
+ console.error("Error fetching user permissions:", err);
+ }
+ };
+
+ const buildSearchParams = (f) => {
+ const params = {};
+
+ // Handle date parameters based on report mode
+ if (f.reportMode === 'daily') {
+ if (f.startDate) params.start_date = f.startDate;
+ if (f.endDate) params.end_date = f.endDate;
+ } else if (f.reportMode === 'monthly') {
+ // For monthly, use year to set start_date (YYYY-01-01)
+ if (f.year) {
+ params.start_date = `${f.year}-01-01`;
+ }
+ }
+ // For yearly and total modes, no date parameters needed
+
+ if (f.outlet && f.outlet !== 'All') params.outlet_id = f.outlet;
+ if (f.orderMethod && f.orderMethod !== 'All') params.order_type = f.orderMethod;
+ if (f.reportMode) params.report_mode = f.reportMode;
+ if (user_id) params.user_id = user_id;
+ return params;
+ };
+
+ const fetchSalesData = async (appliedFilters = filters) => {
+ const searchParams = buildSearchParams(appliedFilters);
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await reportService.getSalesReport(searchParams);
+ if(response.status == 200){
+ const sales_report_data = response.data.outlets;
+ const summary_data = response.data.summary;
+ const hourly_data = response.data.hourly;
+ setSummaryData(summary_data);
+ setHourlyData(hourly_data);
+ setOutletData(sales_report_data);
+ }
+ setLoading(false);
+ } catch (err) {
+ setError('Failed to fetch sales data. Please try again.');
+ setLoading(false);
+ }
+ };
+
+ const fetchOutletData = async () => {
+ try {
+ if (!user_id) return;
+ const response = await outletService.getOutlets(user_id);
+ if(response.status == 200){
+ const outlet_data = response.result;
+ setOutletOptions(outlet_data);
+ }
+ } catch (error) {
+ console.error('Error fetching outlet data:', error);
+ }
+ };
+
+ useEffect(() => {
+ if(user_id){
+ fetchSalesData();
+ fetchUserPermissions();
+ fetchOutletData();
+ }
+ }, [user_id]);
+
+ const orderMethodOptions = [
+ { value: 'All', title: 'All' },
+ { value: 'delivery', title: 'Delivery' },
+ { value: 'pickup', title: 'Pickup' },
+ { value: 'dinein', title: 'Dine-in' }
+ ];
+
+ const reportModeOptions = [
+ { value: 'daily', title: 'Daily' },
+ { value: 'monthly', title: 'Monthly' },
+ { value: 'yearly', title: 'Yearly' },
+ { value: 'total', title: 'Total' },
+ ];
+
+ // Generate dynamic columns based on report mode
+ const columns = useMemo(() => {
+ // Base outlet column
+ const baseColumns = [
+ {
+ Header: 'Outlet',
+ accessor: 'outlet_name',
+ Cell: ({ value }) => (
+
+ ),
+ }
+ ];
+
+ // For Total mode - simple columns
+ if (filters.reportMode === 'total') {
+ return [
+ ...baseColumns,
+ {
+ Header: 'Sales',
+ accessor: 'total_sales',
+ Cell: ({ value }) => (
+
+
+ RM {value || '0.00'}
+
+
+ ),
+ },
+ {
+ Header: 'Orders',
+ accessor: 'total_orders',
+ Cell: ({ value }) => (
+
+ ),
+ },
+ {
+ Header: 'Avg Sales/Customer',
+ accessor: 'average_sales_per_customer',
+ Cell: ({ value }) => (
+
+ ),
+ },
+ {
+ Header: 'Avg Sales/Order',
+ accessor: 'average_sales_per_order',
+ Cell: ({ value }) => (
+
+ ),
+ },
+ ];
+ }
+
+ // For comparative modes (daily, monthly, yearly)
+ if (outletData.length > 0 && outletData[0].comparative_data) {
+ const periods = Object.keys(outletData[0].comparative_data).sort();
+
+ // Create grouped columns for each period
+ periods.forEach(period => {
+ let headerName = period;
+
+ // Format monthly headers (01 -> Jan, 02 -> Feb, etc.)
+ if (filters.reportMode === 'monthly' && monthNames[period]) {
+ headerName = monthNames[period];
+ }
+
+ // Create a group column for this period
+ baseColumns.push({
+ Header: headerName,
+ columns: [
+ {
+ Header: 'Orders',
+ accessor: `comparative_data.${period}.orders`,
+ Cell: ({ value }) => (
+
+ ),
+ },
+ {
+ Header: 'Sales',
+ accessor: `comparative_data.${period}.sales`,
+ Cell: ({ value }) => (
+
+
+ RM {value || '0.00'}
+
+
+ ),
+ },
+ {
+ Header: 'Avg Sales/Customer',
+ accessor: `comparative_data.${period}.avg_sales_per_customer`,
+ Cell: ({ value }) => (
+
+
+ RM {value || '0.00'}
+
+
+ ),
+ },
+ {
+ Header: 'Avg Sales/Order',
+ accessor: `comparative_data.${period}.avg_sales_per_order`,
+ Cell: ({ value }) => (
+
+
+ RM {value || '0.00'}
+
+
+ ),
+ },
+ ],
+ });
+ });
+ } else {
+ // Fallback to simple columns if no comparative data
+ return [
+ ...baseColumns,
+ {
+ Header: 'Sales',
+ accessor: 'total_sales',
+ Cell: ({ value }) => (
+
+
+ RM {value || '0.00'}
+
+
+ ),
+ },
+ {
+ Header: 'Orders',
+ accessor: 'total_orders',
+ Cell: ({ value }) => (
+
+ ),
+ },
+ {
+ Header: 'Avg Sales/Customer',
+ accessor: 'average_sales_per_customer',
+ Cell: ({ value }) => (
+
+ ),
+ },
+ {
+ Header: 'Avg Sales/Order',
+ accessor: 'average_sales_per_order',
+ Cell: ({ value }) => (
+
+ ),
+ },
+ ];
+ }
+
+ return baseColumns;
+ }, [outletData]);
+
+ const data = useMemo(() => outletData, [outletData]);
+
+ // React Table v7 configuration
+ const {
+ getTableProps,
+ getTableBodyProps,
+ headerGroups,
+ prepareRow,
+ page,
+ canPreviousPage,
+ canNextPage,
+ pageOptions,
+ pageCount,
+ gotoPage,
+ nextPage,
+ previousPage,
+ setPageSize,
+ state: { pageIndex, pageSize },
+ } = useTable(
+ {
+ columns,
+ data,
+ initialState: { pageIndex: 0, pageSize: 10 },
+ },
+ useSortBy,
+ usePagination
+ );
+
+ // Chart data for hourly orders
+ const hourlyChartData = {
+ labels: hourlyData.map(item => item.hour),
+ datasets: [
+ {
+ label: 'Orders',
+ data: hourlyData.map(item => item.orders),
+ borderColor: 'rgb(59, 130, 246)',
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ tension: 0.4,
+ fill: true,
+ pointBackgroundColor: 'rgb(59, 130, 246)',
+ pointBorderColor: '#fff',
+ pointBorderWidth: 2,
+ pointRadius: 4
+ }
+ ]
+ };
+
+ const hourlyChartOptions = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'top',
+ labels: {
+ usePointStyle: true,
+ padding: 20
+ }
+ },
+ title: {
+ display: true,
+ text: `Orders by Hour - ${filters.reportMode.charAt(0).toUpperCase() + filters.reportMode.slice(1)}${filters.reportMode === 'monthly' ? ` ${filters.year}` : ''}`,
+ font: {
+ size: 16,
+ weight: 'bold'
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: {
+ color: 'rgba(0, 0, 0, 0.1)'
+ }
+ },
+ x: {
+ grid: {
+ display: false
+ }
+ }
+ },
+ elements: {
+ point: {
+ hoverRadius: 6
+ }
+ }
+ };
+
+ const exportToCSV = async() => {
+ let searchParams = buildSearchParams(filters);
+ searchParams = {
+ ...searchParams,
+ type: 'sales-report'
+ };
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await reportService.getExportExcel(searchParams);
+ if(response.status == 200){
+ toast.success(response.message);
+ }
+ setLoading(false);
+ } catch (err) {
+ setError('Failed to export sales data. Please try again.');
+ setLoading(false);
+ }
+ };
+
+ const handleApplyFilters = () => {
+ gotoPage(0);
+ fetchSalesData(filters);
+ };
+
+ const handleResetFilters = () => {
+ const resetFilters = {
+ startDate: '',
+ endDate: '',
+ outlet: 'All',
+ orderMethod: 'All',
+ reportMode: 'total',
+ year: new Date().getFullYear().toString()
+ };
+ setFilters(resetFilters);
+ gotoPage(0);
+ fetchSalesData(resetFilters);
+ };
+
+ const handleReportModeChange = (mode) => {
+ setFilters(prev => ({ ...prev, reportMode: mode }));
+ };
+
+ return (
+
+ {/* Header Section */}
+
+
+
+
+
+
+
+
+
+
Sales Report
+
+
+
+
+
+
+
+ {error && (
+
+
+
+
+
+
{error}
+
+ Try again
+
+
+
+
+
+ )}
+
+ {/* Summary Cards */}
+
+
+
+
+
+
+
+
+
Total Orders
+
{summaryData.total_orders_all || 0}
+
+
+
+
+
+
+
+
+
+
+
Total Sales
+
+ RM {summaryData.total_sales_all || '0.00'}
+
+
+
+
+
+
+
+
+
+
+
+
Avg Sales/Customer
+
+ RM {summaryData.average_sales_per_customer || '0.00'}
+
+
+
+
+
+
+
+
+
+
+
+
Avg Sales/Order
+
+ RM {summaryData.average_sales_per_order || '0.00'}
+
+
+
+
+
+
+
+
+
+
+
+
Peak Hour
+
+ {summaryData.highest_hour?.hour || '-'}
+
+
+
+
+
+
+
+ {/* Hourly Orders Chart */}
+
+
+
+
+
+
+
+
Peek Hour Trends
+
+
+
+
+
+
+ {loading ? (
+
+
+
+
Loading chart data...
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Outlet Sales Table */}
+
+
+
+
+
+
+
+
+
+
+ {filters.reportMode === 'total'
+ ? 'Sales Report - Total'
+ : filters.reportMode === 'monthly'
+ ? `Sales Report - Monthly ${filters.year}`
+ : `Sales Report - ${filters.reportMode.charAt(0).toUpperCase() + filters.reportMode.slice(1)} `
+ }
+
+
+
+
+ setShowFilters(v => !v)}
+ >
+ Filter
+
+
+
+ Export Report
+
+
+
+
+
+ {showFilters && (
+
+
+
+ Report Mode
+ handleReportModeChange(e.target.value)}
+ >
+ {reportModeOptions.map((mode, i) => (
+ {mode.title}
+ ))}
+
+
+
+ {/* Show year selector for monthly mode */}
+ {filters.reportMode === 'monthly' && (
+
+ Year
+ setFilters(prev => ({ ...prev, year: e.target.value }))}
+ >
+ {yearOptions.map((year) => (
+ {year}
+ ))}
+
+
+ )}
+
+ {/* Show date range only for daily mode */}
+ {filters.reportMode === 'daily' && (
+ <>
+
+ Start Date
+ setFilters(prev => ({ ...prev, startDate: e.target.value }))}
+ />
+
+
+ End Date
+ setFilters(prev => ({ ...prev, endDate: e.target.value }))}
+ />
+
+ >
+ )}
+
+ {/* Show empty space when inputs are hidden to maintain layout */}
+ {filters.reportMode !== 'daily' && filters.reportMode !== 'monthly' && (
+ <>
+
+ Start Date
+
+
+
+ End Date
+
+
+ >
+ )}
+
+
+ Outlet
+ setFilters(prev => ({ ...prev, outlet: e.target.value }))}
+ >
+ All
+ {outletOptions.map(opt => (
+ {opt.title}
+ ))}
+
+
+
+ Order Method
+ setFilters(prev => ({ ...prev, orderMethod: e.target.value }))}
+ >
+ {orderMethodOptions.map((method, index) => (
+ {method.title}
+ ))}
+
+
+
+
+
+ Reset
+
+
+ Apply
+
+
+
+ )}
+
+ {/* React Table v7 with Grouped Headers */}
+
+
+
+ {headerGroups.map(headerGroup => (
+
+ {headerGroup.headers.map(column => (
+
+
+ {column.render('Header')}
+
+ {column.isSorted ? (
+ column.isSortedDesc ? (
+
+ ) : (
+
+ )
+ ) : (
+ ''
+ )}
+
+
+
+ ))}
+
+ ))}
+
+
+
+ {loading ? (
+
+ acc + (col.columns ? col.columns.length : 1), 0)} className="px-6 py-12">
+
+
+
+
Loading table data...
+
+
+
+
+ ) : page.length === 0 ? (
+
+ acc + (col.columns ? col.columns.length : 1), 0)} className="px-6 py-12 text-center text-gray-500">
+
+
+
+
+
No outlet data found
+
Try refreshing the page or check your connection
+
+
+
+ ) : (
+ page.map(row => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map(cell => (
+
+ {cell.render('Cell')}
+
+ ))}
+
+ );
+ })
+ )}
+
+
+
+
+ {/* Pagination Controls */}
+
+
+
+ Page {pageIndex + 1} of {pageOptions.length}
+
+
+ ({outletData.length} total items)
+
+
+
+
+
{
+ setPageSize(Number(e.target.value));
+ }}
+ className="border border-gray-300 rounded-md text-sm px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ {[10, 20, 30, 50, 100].map(size => (
+
+ Show {size}
+
+ ))}
+
+
+
+ gotoPage(0)}
+ disabled={!canPreviousPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="First page"
+ >
+
+
+ previousPage()}
+ disabled={!canPreviousPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Previous page"
+ >
+
+
+ nextPage()}
+ disabled={!canNextPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Next page"
+ >
+
+
+ gotoPage(pageCount - 1)}
+ disabled={!canNextPage}
+ className="p-2 border border-gray-300 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
+ title="Last page"
+ >
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SalesReport;
\ No newline at end of file
diff --git a/src/pages/settings/slideshow/index.jsx b/src/pages/settings/slideshow/index.jsx
new file mode 100644
index 0000000..90ebe45
--- /dev/null
+++ b/src/pages/settings/slideshow/index.jsx
@@ -0,0 +1,383 @@
+import React, { useState, useEffect } from "react";
+import DataTable from "react-data-table-component";
+import { Edit, Trash2, Plus, Menu, ChevronDown } from "lucide-react";
+import { useNavigate } from "react-router-dom";
+import { VITE_API_BASE_URL } from "../../../constant/config";
+import { ToastContainer, toast } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
+
+const SlideshowSettings = () => {
+ const [data, setData] = useState([]);
+ const [filteredData, setFilteredData] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [searchTitle, setSearchTitle] = useState("");
+ const [searchStatus, setSearchStatus] = useState("");
+ const [searchDate, setSearchDate] = useState("");
+ const authToken = sessionStorage.getItem("token");
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const fetchSlideshows = async () => {
+ setLoading(true);
+ try {
+ const token = sessionStorage.getItem("token");
+ const response = await fetch(VITE_API_BASE_URL + "settings/slideshow", {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ const resData = await response.json();
+ if (response.ok) {
+ setData(resData.result || []);
+ setFilteredData(resData.result || []); // Initialize filtered data
+ } else {
+ setData([]);
+ setFilteredData([]);
+ toast.error(resData.message || "Failed to fetch slideshows.");
+ }
+ } catch (err) {
+ setData([]);
+ setFilteredData([]);
+ toast.error("Error fetching slideshow data.");
+ }
+ setLoading(false);
+ };
+
+ fetchSlideshows();
+ }, []);
+
+ const handleSearch = () => {
+ const filtered = data.filter((item) =>
+ item.title.toLowerCase().includes(searchTitle.toLowerCase())
+ );
+ const filteredByStatus = filtered.filter((item) =>
+ searchStatus
+ ? item.status.toLowerCase() === searchStatus.toLowerCase()
+ : true
+ );
+ const filteredByDate = filteredByStatus.filter((item) =>
+ item.updated_at.toLowerCase().includes(searchDate.toLowerCase())
+ );
+ setFilteredData(filteredByDate);
+ };
+
+ const handleEdit = (row) => {
+ navigate(`/settings/edit_slideshow/${row.id}`, { state: { data: row } });
+ };
+
+ const handleDelete = async (row) => {
+ try {
+ setLoading(true);
+ const response = await fetch(
+ VITE_API_BASE_URL + `settings/slideshow/delete/${row.id}`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ }
+ );
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ toast.error(result.message || "Failed to delete slideshow");
+ setLoading(false);
+ return;
+ }
+
+ toast.success(result.message || "Slideshow deleted successfully!");
+ setData((prev) => prev.filter((item) => item.id !== row.id));
+ setFilteredData((prev) => prev.filter((item) => item.id !== row.id)); // Update filtered data
+ setLoading(false);
+ } catch (err) {
+ toast.error("Unexpected error: " + err.message);
+ setLoading(false);
+ }
+ };
+
+ const handleAddNew = () => {
+ navigate("/settings/add_new_slideshow");
+ };
+
+ const columns = [
+ {
+ name: "Action",
+ width: "300px",
+ cell: (row) => (
+
+ handleEdit(row)}
+ className="p-1 text-blue-600 hover:text-blue-800 transition-colors"
+ title="Edit"
+ >
+
+
+ handleDelete(row)}
+ className="p-1 text-red-600 hover:text-red-800 transition-colors"
+ title="Delete"
+ >
+
+
+
+ ),
+ ignoreRowClick: true,
+ allowOverflow: true,
+ button: true,
+ width: "120px",
+ },
+ {
+ name: "Title",
+ selector: (row) => row.title,
+ sortable: true,
+ width: "300px",
+ },
+ {
+ name: "URL",
+ selector: (row) => row.url,
+ sortable: true,
+ center: true,
+ width: "500px",
+ cell: (row) => (
+
+ {row.url}
+
+ ),
+ },
+
+ {
+ name: "Slide Image",
+ selector: (row) => row.url,
+ center: true,
+ width: "300px",
+ cell: (row) => (
+
+ ),
+ },
+ {
+ name: "Status",
+ selector: (row) => row.status,
+ sortable: true,
+ center: true,
+ width: "150px",
+ cell: (row) => (
+
+ {row.status}
+
+ ),
+ },
+ {
+ name: "Updated Date",
+ selector: (row) => row.updated_at,
+ sortable: true,
+ center: true,
+ width: "300px",
+ cell: (row) => (
+
+ {row.updated_at
+ ? new Date(row.updated_at).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+ : "-"}
+
+ ),
+ },
+ {
+ name: "Created Date",
+ selector: (row) => row.created_at,
+ sortable: true,
+ center: true,
+ width: "300px",
+ cell: (row) => (
+
+ {row.created_at
+ ? new Date(row.created_at).toLocaleDateString("en-US", {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+ : "-"}
+
+ ),
+ },
+ ];
+
+ const customStyles = {
+ header: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "white",
+ fontSize: "18px",
+ fontWeight: "bold",
+ minHeight: "60px",
+ paddingLeft: "24px",
+ paddingRight: "24px",
+ },
+ },
+ headRow: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "white",
+ fontSize: "14px",
+ fontWeight: "600",
+ minHeight: "50px",
+ borderBottomWidth: "1px",
+ borderBottomColor: "#e5e7eb",
+ },
+ },
+ headCells: {
+ style: {
+ color: "white",
+ fontSize: "14px",
+ fontWeight: "600",
+ paddingLeft: "16px",
+ paddingRight: "16px",
+ },
+ },
+ rows: {
+ style: {
+ fontSize: "14px",
+ color: "#374151",
+ "&:hover": {
+ backgroundColor: "#f9fafb",
+ },
+ },
+ stripedStyle: {
+ backgroundColor: "#f8fafc",
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: "16px",
+ paddingRight: "16px",
+ paddingTop: "12px",
+ paddingBottom: "12px",
+ },
+ },
+ pagination: {
+ style: {
+ fontSize: "14px",
+ color: "#6b7280",
+ backgroundColor: "white",
+ borderTopColor: "#e5e7eb",
+ borderTopWidth: "1px",
+ },
+ },
+ };
+
+ return (
+
+
+
+ Slideshow Setting
+
+
+
+
+
Search
+
+
+
+
+
+ Search by Title:
+
+ setSearchTitle(e.target.value)}
+ placeholder="Enter title"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
+ disabled={loading}
+ />
+
+
+
+ Search by Status:
+
+ setSearchStatus(e.target.value)}
+ placeholder="Enter status"
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
+ disabled={loading}
+ >
+ Select status
+ Active
+ Inactive
+
+
+
+
+
+ {loading ? "Searching..." : "Search"}
+
+
+
+
+
+
+
Listing
+
+
+ Add New Slideshow
+
+
+
+
+
+ No slideshow settings found
+
+ }
+ sortIcon={
}
+ />
+
+
+ );
+};
+
+export default SlideshowSettings;
diff --git a/src/pages/settings/slideshow/slideshow-add.jsx b/src/pages/settings/slideshow/slideshow-add.jsx
new file mode 100644
index 0000000..8feb1b4
--- /dev/null
+++ b/src/pages/settings/slideshow/slideshow-add.jsx
@@ -0,0 +1,258 @@
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import Select from "@/components/ui/Select";
+import { VITE_API_BASE_URL } from "../../../constant/config";
+import { ToastContainer, toast } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
+import { X } from "lucide-react";
+
+export default function SlideshowAdd() {
+ const [images, setImages] = useState([]);
+ const authToken = sessionStorage.getItem("token");
+ const navigate = useNavigate();
+
+ const [formData, setFormData] = useState({
+ fileName: "",
+ fileType: "",
+ title: "",
+ description: "",
+ order: 1,
+ status: "active",
+ });
+
+ const handleInputChange = (field, value) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleClear = () => {
+ setFormData({
+ fileName: "",
+ fileType: "",
+ title: "",
+ description: "",
+ order: 1,
+ status: "active",
+ });
+ setImages([]);
+ };
+
+ const fileTypeOptions = [
+ { label: "Images", value: "images" },
+ { label: "Video", value: "video" },
+ ];
+
+ const removeImage = (index) => {
+ setImages((prev) => prev.filter((_, i) => i !== index));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!formData.fileType || !formData.title || images.length === 0) {
+ toast.error("Please fill all required fields and select an image.");
+ return;
+ }
+
+ const data = new FormData();
+ data.append("type", formData.fileType);
+ data.append("title", formData.title);
+ data.append("description", formData.description);
+ data.append("order", formData.order);
+ data.append("status", formData.status);
+ data.append("url", images[0]);
+
+ console.log("formData:", formData);
+
+ try {
+ const response = await fetch(
+ VITE_API_BASE_URL + "settings/slideshow/create",
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ body: data,
+ }
+ );
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ toast.error(result.message || "Failed to add slideshow");
+ return;
+ }
+
+ toast.success(result.message || "Slideshow addede successfully!");
+ navigate("/settings/slideshow_settings");
+ } catch (err) {
+ toast.error("Unexpected error: " + err.message);
+ }
+ };
+
+ const handleBack = () => {
+ navigate("/settings/slideshow_settings");
+ };
+
+ return (
+
+
+
+
+
+
+ Add New Slideshow
+
+
+
+
+
+
+
+
+ Upload Images
+
+
+
+
+
+ 800×800, JPG, PNG, max 10MB
+
+
{
+ const files = Array.from(e.target.files);
+ setImages(files);
+ if (files[0]) {
+ handleInputChange("fileName", files[0].name);
+ }
+ }}
+ />
+
+ + Add Image
+
+
+
+ {/* Display selected images */}
+ {images.length > 0 && (
+
+
+ {images.map((image, index) => (
+
+
+
removeImage(index)}
+ className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm hover:bg-red-600"
+ >
+ ×
+
+
+ ))}
+
+
+ )}
+
+
+
+ Slide Type
+
+
+ handleInputChange("fileType", e.target.value)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400 text-sm"
+ />
+
+
+
+
+ Title
+
+
+ handleInputChange("title", e.target.value)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400 text-sm"
+ />
+
+
+
+ Description
+
+
+ handleInputChange("description", e.target.value)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400 text-sm"
+ />
+
+
+
+ Status
+
+
+ handleInputChange("status", e.target.value)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
+ >
+ Active
+ Inactive
+
+
+
+
+
+
+
+
+
+ Clear
+
+
+ Submit
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/settings/slideshow/slideshow-edit.jsx b/src/pages/settings/slideshow/slideshow-edit.jsx
new file mode 100644
index 0000000..7b160c3
--- /dev/null
+++ b/src/pages/settings/slideshow/slideshow-edit.jsx
@@ -0,0 +1,330 @@
+import React, { useState, useEffect } from "react";
+import { useNavigate, useLocation, useParams } from "react-router-dom";
+import Select from "@/components/ui/Select";
+import { VITE_API_BASE_URL } from "../../../constant/config";
+import { toast } from "react-toastify";
+import "react-toastify/dist/ReactToastify.css";
+import { X } from "lucide-react";
+
+export default function SlideshowEdit() {
+ const [images, setImages] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const authToken = sessionStorage.getItem("token");
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { id } = useParams();
+ const slideshowData = location.state?.data;
+
+ const [formData, setFormData] = useState({
+ fileName: "",
+ fileType: "",
+ title: "",
+ description: "",
+ order: 1,
+ status: "active",
+ });
+
+ // Fetch slideshow data by ID if missing (for refresh/direct link)
+ useEffect(() => {
+ if (!slideshowData && id) {
+ setLoading(true);
+ fetch(VITE_API_BASE_URL + `settings/slideshow/${id}`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ })
+ .then((res) => res.json())
+ .then((data) => {
+ const row = data.result || {};
+ setFormData({
+ fileName: row.fileName || "",
+ fileType: row.fileType || "",
+ title: row.title || "",
+ description: row.description || "",
+ order: row.order || 1,
+ status: row.status || "active",
+ });
+ setImages(row.url ? [row.url] : []);
+ })
+ .catch(() => {
+ toast.error("Failed to fetch slideshow data");
+ })
+ .finally(() => setLoading(false));
+ }
+ }, [slideshowData, id, authToken]);
+
+ useEffect(() => {
+ if (slideshowData) {
+ setFormData({
+ fileName: slideshowData.fileName || "",
+ fileType: slideshowData.fileType || "",
+ title: slideshowData.title || "",
+ description: slideshowData.description || "",
+ order: slideshowData.order || 1,
+ status: slideshowData.status || "active",
+ });
+ setImages(slideshowData.url ? [slideshowData.url] : []);
+ }
+ }, [slideshowData]);
+
+ const handleInputChange = (field, value) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleClear = () => {
+ setFormData({
+ fileName: "",
+ fileType: "",
+ title: "",
+ description: "",
+ order: 1,
+ status: "active",
+ });
+ setImages([]);
+ };
+
+ const fileTypeOptions = [
+ { label: "Images", value: "images" },
+ { label: "Video", value: "video" },
+ ];
+
+ const removeImage = (index) => {
+ setImages((prev) => prev.filter((_, i) => i !== index));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!formData.fileType || !formData.title) {
+ toast.error("Please fill all required fields.");
+ return;
+ }
+
+ const data = new FormData();
+ data.append("type", formData.fileType);
+ data.append("title", formData.title);
+ data.append("description", formData.description);
+ // data.append("order", Number(formData.order));
+ data.append("status", formData.status);
+ // console.log(data)
+
+ if (images[0]) {
+ if (typeof images[0] === "string") {
+ // It's a URL string (user did NOT select new file), send as text, not file
+ data.append("url", images[0]);
+ } else {
+ // It's a File object (user selected new image)
+ data.append("url", images[0]);
+ }
+ }
+
+ setLoading(true);
+ try {
+ const response = await fetch(
+ VITE_API_BASE_URL + `settings/slideshow/update/${id}`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ body: data,
+ }
+ );
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ toast.error(result.message || "Failed to update slideshow");
+ setLoading(false);
+ return;
+ }
+
+ toast.success(result.message || "Slideshow updated successfully!");
+ navigate("/settings/slideshow_settings");
+ } catch (err) {
+ toast.error("Unexpected error: " + err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleBack = () => {
+ navigate("/settings/slideshow_settings");
+ };
+
+ return (
+
+
+
+
+
+
+ Edit Slideshow
+
+
+
+
+
+
+
+
+ Upload Images
+
+
+
+
+
+ 800×800, JPG, PNG, max 10MB
+
+
{
+ const files = Array.from(e.target.files);
+ setImages(files);
+ if (files[0]) {
+ handleInputChange("fileName", files[0].name);
+ }
+ }}
+ />
+
+ + Add Image
+
+
+
+ {/* IMAGE PREVIEW: supports both string (existing) and File */}
+ {images.length > 0 && (
+
+
+ {images.map((image, index) => (
+
+
+
removeImage(index)}
+ className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm hover:bg-red-600"
+ >
+ ×
+
+
+ ))}
+
+
+ )}
+
+
+
+ Slide Type
+
+
+ handleInputChange(
+ "fileType",
+ e.target ? e.target.value : e.value
+ )
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400 text-sm"
+ />
+
+
+
+
+ Title
+
+
+ handleInputChange("title", e.target.value)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400 text-sm"
+ />
+
+
+
+ Description
+
+
+ handleInputChange("description", e.target.value)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder-gray-400 text-sm"
+ />
+
+
+
+
+ Status
+
+
+ handleInputChange("status", e.target.value)
+ }
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
+ >
+ Active
+ Inactive
+
+
+
+
+
+
+
+
+
+ {/* Form Section */}
+
+
+
+
+
+
+ Clear
+
+
+ {loading ? "Saving..." : "Update"}
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/settings/user/index.jsx b/src/pages/settings/user/index.jsx
new file mode 100644
index 0000000..08db735
--- /dev/null
+++ b/src/pages/settings/user/index.jsx
@@ -0,0 +1,449 @@
+import React, { useState, useEffect } from 'react';
+import DataTable from 'react-data-table-component';
+import { Plus, Download, Edit, Trash2, Eye, EyeOff, RefreshCw, ChevronDown } from 'lucide-react';
+import DeleteConfirmationModal from '@/components/ui/DeletePopUp';
+import { useNavigate } from 'react-router-dom';
+import UserService from '../../../store/api/userService';
+import OutletApiService from '../../../store/api/outletService';
+
+const UserDataTable = () => {
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [itemToDelete, setItemToDelete] = useState(null);
+ const [editingUser, setEditingUser] = useState(null);
+ const [editForm, setEditForm] = useState({
+ username: '',
+ name: '',
+ password: '',
+ confirmPassword: '',
+ userRoles: '',
+ activeStatus: '',
+ outlet: ''
+ });
+
+ const [userPermissions, setUserPermissions] = useState({});
+ const [hasCreatePermission, setHasCreatePermission] = useState(false);
+ const [hasUpdatePermission, setHasUpdatePermission] = useState(false);
+ const [hasDeletePermission, setHasDeletePermission] = useState(false);
+ const [isAdmin, setIsAdmin] = useState(false);
+ const [currentUserId, setCurrentUserId] = useState(null);
+
+ const fetchUserPermissions = async () => {
+ try {
+ const userStr = localStorage.getItem("user");
+ if (!userStr) return;
+
+ const userObj = JSON.parse(userStr);
+ const userId = userObj?.user.user_id;
+ if (!userId) return;
+
+ setCurrentUserId(userId);
+
+ const userDataRes = await UserService.getUser(userId);
+ const userData = userDataRes?.data || userDataRes;
+ if (!userData) return;
+
+ // Check if user is admin
+ if (userData.role && userData.role.toLowerCase() === 'admin') {
+ setIsAdmin(true);
+ setHasCreatePermission(true);
+ setHasUpdatePermission(true);
+ setHasDeletePermission(true);
+ return;
+ }
+
+ // Parse and set permissions for non-admin users
+ let permissions = {};
+ if (userData.user_permissions) {
+ try {
+ permissions = JSON.parse(userData.user_permissions);
+ setUserPermissions(permissions);
+
+ if (permissions.Settings &&
+ permissions.Settings.subItems &&
+ permissions.Settings.subItems.User) {
+ if (permissions.Settings.subItems.User.create === true) {
+ setHasCreatePermission(true);
+ }
+ if (permissions.Settings.subItems.User.update === true) {
+ setHasUpdatePermission(true);
+ }
+ if (permissions.Settings.subItems.User.delete === true) {
+ setHasDeletePermission(true);
+ }
+ }
+ } catch (e) {
+ console.error("Error parsing user permissions:", e);
+ }
+ }
+ } catch (err) {
+ console.error("Error fetching user permissions:", err);
+ }
+ };
+
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ fetchUserPermissions();
+ }, []);
+
+ useEffect(() => {
+ if (currentUserId) {
+ fetchUsers();
+ }
+ }, [currentUserId, isAdmin]);
+
+ const fetchUsers = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ // Always fetch all users for both admin and non-admin
+ const allUsers = await UserService.getAllUsers(currentUserId);
+
+ // Transform the data to match expected property names
+ const transformedUsers = allUsers.map(user => ({
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ role: user.userRoles || user.role, // Handle both userRoles and role
+ status: user.activeStatus || user.status, // Handle both activeStatus and status
+ created_at: user.createTime || user.created_at // Handle both createTime and created_at
+ }));
+
+ setUsers(transformedUsers);
+ } catch (err) {
+ console.error('Error fetching users:', err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+};
+
+ const handleDelete = (user) => {
+ // Prevent admin from deleting themselves
+ if (user.id === currentUserId) {
+ alert('You cannot delete your own account!');
+ return;
+ }
+
+ setItemToDelete({ ...user, name: user.username });
+ setShowDeleteModal(true);
+ };
+
+ const handleConfirmDelete = async () => {
+ if (itemToDelete) {
+ try {
+ setLoading(true);
+ await UserService.deleteUser(itemToDelete.id);
+
+ // Refresh the user list after deletion
+ await fetchUsers();
+
+ setShowDeleteModal(false);
+ setItemToDelete(null);
+
+ alert('User deleted successfully!');
+ } catch (error) {
+ console.error('Error deleting user:', error);
+ alert(`Error deleting user: ${error.message}`);
+ } finally {
+ setLoading(false);
+ }
+ }
+ };
+
+ const handleAddUser = () => {
+ navigate('/settings/user/add_new_user');
+ };
+
+ const handleExportCSV = () => {
+ const csvContent = [
+ ['Username', 'Name', 'User Roles', 'Active Status', 'Create Time'],
+ ...users.map(user => [user.username, user.name, user.role, user.status, user.created_at])
+ ].map(row => row.join(',')).join('\n');
+
+ const blob = new Blob([csvContent], { type: 'text/csv' });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = isAdmin ? 'all_users.csv' : 'user_profile.csv';
+ a.click();
+ window.URL.revokeObjectURL(url);
+ };
+
+ const handleRefresh = () => {
+ fetchUsers();
+ };
+
+ const columns = [
+ {
+ name: 'Actions',
+ width: '150px',
+ cell: row => (
+
+ {(isAdmin || hasUpdatePermission) && (
+ navigate(`/settings/user/user-edit/${row.id}`)}
+ className="p-1.5 hover:bg-blue-50 rounded transition-colors"
+ title="Edit User"
+ disabled={loading}
+ >
+
+
+ )}
+ {(isAdmin || hasDeletePermission) && row.id !== currentUserId && (
+ handleDelete(row)}
+ className="p-1.5 hover:bg-red-50 rounded transition-colors"
+ title="Delete User"
+ disabled={loading}
+ >
+
+
+ )}
+
+ ),
+ ignoreRowClick: true,
+ allowOverflow: true,
+ button: true
+ },
+ {
+ name: 'Username',
+ selector: row => row.username,
+ sortable: true,
+ width: '210px',
+ },
+ {
+ name: 'Name',
+ selector: row => row.name,
+ sortable: true,
+ width: '210px',
+ },
+ {
+ name: 'User Roles',
+ selector: row => row.role || row.userRoles || '', // Handle both property names
+ sortable: true,
+ width: '230px',
+ cell: row => {
+ // Get the role value from either property and convert to lowercase for consistent comparison
+ const roleValue = (row.role || row.userRoles || '').toString().toLowerCase();
+
+ return (
+
+ {/* Display the original value, not the lowercase version */}
+ {row.role || row.userRoles || 'N/A'}
+
+ )
+ }
+ },
+ {
+ name: 'Active Status',
+ selector: row => row.status || row.activeStatus || '', // Handle both property names
+ sortable: true,
+ width: '230px',
+ cell: row => {
+ // Get the status value from either property and convert to lowercase for consistent comparison
+ const statusValue = (row.status || row.activeStatus || '').toString().toLowerCase();
+
+ return (
+
+ {/* Display the original value, not the lowercase version */}
+ {row.status || row.activeStatus || 'N/A'}
+
+ )
+ }
+ },
+ {
+ name: 'Create Time',
+ selector: row => row.created_at || row.createTime,
+ sortable: true,
+ width: '210px',
+ cell: row => (
+
+
{row.created_at?.split(' ')[0] || '-'}
+
{row.created_at?.split(' ')[1] || ''}
+
+ )
+ },
+ ];
+
+ const customStyles = {
+ header: {
+ style: {
+ backgroundColor: '#1A237E',
+ color: 'white',
+ fontSize: '18px',
+ fontWeight: 'bold',
+ minHeight: '60px',
+ paddingLeft: '24px',
+ paddingRight: '24px'
+ }
+ },
+ headRow: {
+ style: {
+ backgroundColor: '#1A237E',
+ color: 'white',
+ fontSize: '14px',
+ fontWeight: '600',
+ minHeight: '50px',
+ borderBottomWidth: '1px',
+ borderBottomColor: '#e5e7eb'
+ }
+ },
+ headCells: {
+ style: {
+ color: 'white',
+ fontSize: '14px',
+ fontWeight: '600',
+ paddingLeft: '16px',
+ paddingRight: '16px'
+ }
+ },
+ rows: {
+ style: {
+ fontSize: '14px',
+ color: '#374151',
+ '&:hover': {
+ backgroundColor: '#f9fafb'
+ }
+ },
+ stripedStyle: {
+ backgroundColor: '#f8fafc'
+ }
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ paddingTop: '12px',
+ paddingBottom: '12px'
+ }
+ },
+ pagination: {
+ style: {
+ fontSize: '14px',
+ color: '#6b7280',
+ backgroundColor: 'white',
+ borderTopColor: '#e5e7eb',
+ borderTopWidth: '1px'
+ }
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ {isAdmin ? 'Loading all users...' : 'Loading user profile...'}
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
Error: {error}
+
+
+ Retry
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {isAdmin ? 'User Management' : 'User Profile'}
+
+
+
+
+ Export Report
+
+ {(isAdmin || hasCreatePermission) && (
+
+
+ Add User
+
+ )}
+
+
+
+ {/* Data Table */}
+
10}
+ paginationPerPage={10}
+ paginationRowsPerPageOptions={[10, 25, 50, 100]}
+ striped
+ highlightOnHover
+ responsive
+ customStyles={customStyles}
+ noDataComponent={
+
+
No user data found
+
+
+ Refresh
+
+
+ }
+ />
+
+
+ {/* Delete Confirmation Modal */}
+
setShowDeleteModal(false)}
+ onConfirm={handleConfirmDelete}
+ itemName={itemToDelete?.name || ''}
+ itemType="user"
+ />
+
+ );
+};
+
+export default UserDataTable;
\ No newline at end of file
diff --git a/src/pages/settings/user/user-add.jsx b/src/pages/settings/user/user-add.jsx
new file mode 100644
index 0000000..5c61dbe
--- /dev/null
+++ b/src/pages/settings/user/user-add.jsx
@@ -0,0 +1,565 @@
+import React, { useState, useEffect } from 'react';
+import { ArrowLeft, Eye, EyeOff, RefreshCw, Save, X, User, Mail, Phone, MapPin } from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+import UserService from '../../../store/api/userService';
+import OutletApiService from '../../../store/api/outletService';
+import { toast } from 'react-toastify';
+
+const AddNewUser = () => {
+ const [formData, setFormData] = useState({
+ username: '',
+ name: '',
+ userRoles: '',
+ activeStatus: 'Active',
+ password: '',
+ confirmPassword: '',
+ outlet: '' // New field for outlet selection
+ });
+
+ const [outlets, setOutlets] = useState([]); // State for outlets
+ const [loadingOutlets, setLoadingOutlets] = useState(true); // Loading state for outlets
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [errors, setErrors] = useState({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [currentUserId, setCurrentUserId] = useState(null);
+ const navigate = useNavigate();
+
+ const [menuPermissions, setMenuPermissions] = useState({
+ 'Outlet Dashboard': { read: false, create: false, update: false, delete: false },
+ Orders: {
+ read: false,
+ subItems: {
+ Lists: { read: false, create: false, update: false, delete: false },
+ Pending: { read: false, create: false, update: false, delete: false },
+ Confirmed: { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Topup: {
+ read: false,
+ subItems: {
+ Lists: { read: false, create: false, update: false, delete: false },
+ Settings: { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Outlets: {
+ read: false,
+ subItems: {
+ Lists: { read: false, create: false, update: false, delete: false },
+ 'Outlets Menu': { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Menu: {
+ read: false,
+ subItems: {
+ Item: { read: false, create: false, update: false, delete: false },
+ Category: { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Voucher: {
+ read: false,
+ subItems: {
+ List: { read: false, create: false, update: false, delete: false },
+ 'Send Voucher': { read: false, create: false, update: false, delete: false },
+ Schedule: { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Promo: {
+ read: false,
+ subItems: {
+ 'Promo Lists': { read: false, create: false, update: false, delete: false },
+ PWP: { read: false, create: false, update: false, delete: false },
+ 'Discount List': { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Member: { read: false, create: false, update: false, delete: false },
+ 'Student Card': { read: false, create: false, update: false, delete: false },
+ Settings: {
+ read: false,
+ subItems: {
+ User: { read: false, create: false, update: false, delete: false },
+ Tax: { read: false, create: false, update: false, delete: false },
+ 'Membership Tier': { read: false, create: false, update: false, delete: false },
+ 'Customer Types': { read: false, create: false, update: false, delete: false },
+ Delivery: { read: false, create: false, update: false, delete: false },
+ 'App Settings': { read: false, create: false, update: false, delete: false }
+ }
+ }
+});
+
+useEffect(() => {
+ // Get user ID from localStorage on component mount
+ const userStr = localStorage.getItem("user");
+ if (userStr) {
+ try {
+ const userObj = JSON.parse(userStr);
+ const userId = userObj?.user?.user_id;
+ if (userId) {
+ setCurrentUserId(userId);
+ }
+ } catch (error) {
+ console.error("Error parsing user data from localStorage:", error);
+ }
+ }
+ }, []);
+
+ // Fetch outlets on component mount
+ useEffect(() => {
+ const fetchOutlets = async () => {
+ if (!currentUserId) return; // Don't fetch if no user ID
+
+ try {
+ setLoadingOutlets(true);
+ // Pass the user ID to your outlet service
+ const outletsResponse = await OutletApiService.getOutlets(currentUserId);
+
+ // Handle different response structures
+ const outletsData = Array.isArray(outletsResponse)
+ ? outletsResponse
+ : Array.isArray(outletsResponse.result)
+ ? outletsResponse.result
+ : Array.isArray(outletsResponse.data)
+ ? outletsResponse.data
+ : [];
+
+ setOutlets(outletsData);
+ } catch (err) {
+ console.error('Error fetching outlets:', err);
+ toast.error('Failed to load outlets');
+ } finally {
+ setLoadingOutlets(false);
+ }
+ };
+
+ fetchOutlets();
+ }, [currentUserId]);
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!formData.username.trim()) newErrors.username = 'Username is required';
+ if (!formData.name.trim()) newErrors.name = 'Name is required';
+ if (!formData.userRoles) newErrors.userRoles = 'User role is required';
+ if (!formData.password) newErrors.password = 'Password is required';
+ if (!formData.confirmPassword) newErrors.confirmPassword = 'Confirm password is required';
+
+ // Add validation for outlet if role is Outlet
+ if (formData.userRoles === 'Outlet' && !formData.outlet) {
+ newErrors.outlet = 'Outlet selection is required';
+ }
+
+ if (formData.password && formData.password.length < 8) {
+ newErrors.password = 'Password must be at least 8 characters long';
+ }
+
+ if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match';
+ }
+
+ if (formData.username && !/^[a-zA-Z0-9_]+$/.test(formData.username)) {
+ newErrors.username = 'Username can only contain letters, numbers, and underscores';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const generateRandomPassword = () => {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
+ let password = '';
+ for (let i = 0; i < 12; i++) {
+ password += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ setFormData({ ...formData, password: password, confirmPassword: password });
+ setErrors({ ...errors, password: '', confirmPassword: '' });
+ };
+
+ const handleInputChange = (field, value) => {
+ setFormData({ ...formData, [field]: value });
+ if (errors[field]) {
+ setErrors({ ...errors, [field]: '' });
+ }
+
+ // Clear outlet selection if role changes from Outlet to something else
+ if (field === 'userRoles' && value !== 'Outlet') {
+ setFormData(prev => ({ ...prev, outlet: '' }));
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ try {
+ // Prepare user data
+ const userData = {
+ ...formData,
+ // Only include outlet if role is Outlet
+ outlet: formData.userRoles === 'Outlet' ? formData.outlet : undefined,
+ menuPermissions
+ };
+
+ // Call the API to create user
+ const response = await UserService.createUser(userData);
+
+ console.log('User created successfully:', response);
+
+ // Show success message
+ toast.success('User created successfully!');
+
+ // Navigate back to user list
+ navigate('/settings/user');
+
+ } catch (error) {
+ console.error('Error creating user:', error);
+ toast.error(`Error creating user: ${error.message}`);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (window.confirm('Are you sure you want to cancel? All unsaved changes will be lost.')) {
+ navigate(-1);
+ }
+ };
+
+ const handleBack = () => {
+ navigate(-1);
+ }
+
+ return (
+
+
+
+
Add new user
+
+
+
+
+
+
+
+ Account Information
+
+
+
+
+
+ Username *
+
+
handleInputChange('username', e.target.value)}
+ className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors ${
+ errors.username ? 'border-red-500' : 'border-gray-300'
+ }`}
+ placeholder="Enter username"
+ />
+ {errors.username &&
{errors.username}
}
+
+
+
+
+ Name *
+
+
handleInputChange('name', e.target.value)}
+ className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors ${
+ errors.name ? 'border-red-500' : 'border-gray-300'
+ }`}
+ placeholder="Enter name"
+ />
+ {errors.name &&
{errors.name}
}
+
+
+
+
+ User Role *
+
+
handleInputChange('userRoles', e.target.value)}
+ className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors ${
+ errors.userRoles ? 'border-red-500' : 'border-gray-300'
+ }`}
+ >
+ Select user role
+ Admin
+ Editor
+ Moderator
+ Viewer
+ Account
+ Outlet
+
+ {errors.userRoles &&
{errors.userRoles}
}
+
+
+
+
+ Active Status
+
+ handleInputChange('activeStatus', e.target.value)}
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors"
+ >
+ Active
+ Inactive
+ Suspended
+
+
+
+ {/* Conditional outlet dropdown */}
+ {formData.userRoles === 'Outlet' && (
+
+
+ Outlet *
+
+ {loadingOutlets ? (
+
+ 🌀
+ Loading outlets...
+
+ ) : (
+
handleInputChange('outlet', e.target.value)}
+ className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors ${
+ errors.outlet ? 'border-red-500' : 'border-gray-300'
+ }`}
+ disabled={loadingOutlets}
+ >
+ Select outlet
+ {outlets.map(outlet => (
+
+ {outlet.title || outlet.name || `Outlet ${outlet.id}`}
+
+ ))}
+
+ )}
+ {errors.outlet &&
{errors.outlet}
}
+
+ )}
+
+
+
+
+
+
+
+
+
+ Password *
+
+
+
handleInputChange('password', e.target.value)}
+ className={`w-full px-4 py-3 pr-20 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors ${
+ errors.password ? 'border-red-500' : 'border-gray-300'
+ }`}
+ placeholder="Enter password"
+ />
+
+ setShowPassword(!showPassword)}
+ className="p-2 text-gray-500 hover:text-gray-700 transition-colors"
+ title={showPassword ? "Hide password" : "Show password"}
+ >
+ {showPassword ? : }
+
+
+
+
+
+
+ {errors.password &&
{errors.password}
}
+
Password must be at least 8 characters long
+
+
+
+
+ Confirm Password *
+
+
+ handleInputChange('confirmPassword', e.target.value)}
+ className={`w-full px-4 py-3 pr-12 border rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors ${
+ errors.confirmPassword ? 'border-red-500' : 'border-gray-300'
+ }`}
+ placeholder="Confirm password"
+ />
+ setShowConfirmPassword(!showConfirmPassword)}
+ className="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 text-gray-500 hover:text-gray-700 transition-colors"
+ title={showConfirmPassword ? "Hide password" : "Show password"}
+ >
+ {showConfirmPassword ? : }
+
+
+ {errors.confirmPassword &&
{errors.confirmPassword}
}
+
+
+
+
+
Menu Permissions
+
+
+
+
+ Menu
+ Read
+ Create
+ Update
+ Delete
+
+
+
+ {Object.entries(menuPermissions).map(([parent, value]) => {
+ if (value.subItems) {
+ return (
+
+ {/* Parent row with single checkbox for itself only */}
+
+
+ {parent}
+
+
+ {
+ setMenuPermissions(prev => ({
+ ...prev,
+ [parent]: {
+ ...prev[parent],
+ read: !prev[parent].read,
+ // Don't modify subItems here
+ }
+ }));
+ }}
+ className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
+ />
+
+
+
+ {/* Child rows with their own checkboxes */}
+ {Object.entries(value.subItems).map(([subItem, permissions]) => (
+
+
+ - {subItem}
+
+ {['read', 'create', 'update', 'delete'].map((permission) => (
+
+ {
+ setMenuPermissions(prev => {
+ const newState = { ...prev };
+ newState[parent] = {
+ ...newState[parent],
+ subItems: {
+ ...newState[parent].subItems,
+ [subItem]: {
+ ...newState[parent].subItems[subItem],
+ [permission]: !newState[parent].subItems[subItem][permission]
+ }
+ }
+ };
+ return newState;
+ });
+ }}
+ className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
+ />
+
+ ))}
+
+ ))}
+
+ );
+ } else {
+ // No subItems: show four checkboxes at parent row
+ return (
+
+
+ {parent}
+
+ {['read', 'create', 'update', 'delete'].map((permission) => (
+
+ {
+ setMenuPermissions(prev => ({
+ ...prev,
+ [parent]: {
+ ...prev[parent],
+ [permission]: !prev[parent][permission]
+ }
+ }));
+ }}
+ className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
+ />
+
+ ))}
+
+ );
+ }
+ })}
+
+
+
+
+
+ {/* Action Buttons */}
+
+
+
+ Cancel
+
+
+
+ {isSubmitting ? 'Creating User...' : 'Create User'}
+
+
+
+
+
+ );
+};
+
+export default AddNewUser;
\ No newline at end of file
diff --git a/src/pages/settings/user/user-edit.jsx b/src/pages/settings/user/user-edit.jsx
new file mode 100644
index 0000000..fe3aa42
--- /dev/null
+++ b/src/pages/settings/user/user-edit.jsx
@@ -0,0 +1,607 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { Eye, EyeOff } from 'lucide-react';
+import UserService from '../../../store/api/userService';
+import OutletApiService from '../../../store/api/outletService';
+import { toast } from 'react-toastify';
+
+const UserEdit = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(true);
+ const [loadingOutlets, setLoadingOutlets] = useState(false);
+ const [user, setUser] = useState(null);
+ const [error, setError] = useState(null);
+ const [outlets, setOutlets] = useState([]);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [currentUserRole, setCurrentUserRole] = useState('');
+ const [currentUserId, setCurrentUserId] = useState('');
+ const [formData, setFormData] = useState({
+ username: '',
+ name: '',
+ password: '',
+ confirmPassword: '',
+ userRoles: '',
+ activeStatus: '',
+ outlet: ''
+ });
+
+ const defaultMenuPermissions = {
+ 'Outlet Dashboard': { read: false, create: false, update: false, delete: false },
+ Orders: {
+ read: false,
+ subItems: {
+ Lists: { read: false, create: false, update: false, delete: false },
+ Pending: { read: false, create: false, update: false, delete: false },
+ Confirmed: { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Topup: {
+ read: false,
+ subItems: {
+ Lists: { read: false, create: false, update: false, delete: false },
+ Settings: { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Outlets: {
+ read: false,
+ subItems: {
+ Lists: { read: false, create: false, update: false, delete: false },
+ 'Outlets Menu': { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Menu: {
+ read: false,
+ subItems: {
+ Item: { read: false, create: false, update: false, delete: false },
+ Category: { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Voucher: {
+ read: false,
+ subItems: {
+ List: { read: false, create: false, update: false, delete: false },
+ 'Send Voucher': { read: false, create: false, update: false, delete: false },
+ Schedule: { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Promo: {
+ read: false,
+ subItems: {
+ 'Promo Lists': { read: false, create: false, update: false, delete: false },
+ PWP: { read: false, create: false, update: false, delete: false },
+ 'Discount List': { read: false, create: false, update: false, delete: false }
+ }
+ },
+ Member: { read: false, create: false, update: false, delete: false },
+ 'Student Card': { read: false, create: false, update: false, delete: false },
+ Settings: {
+ read: false,
+ subItems: {
+ User: { read: false, create: false, update: false, delete: false },
+ Tax: { read: false, create: false, update: false, delete: false },
+ 'Membership Tier': { read: false, create: false, update: false, delete: false },
+ 'Customer Types': { read: false, create: false, update: false, delete: false },
+ Delivery: { read: false, create: false, update: false, delete: false },
+ 'App Settings': { read: false, create: false, update: false, delete: false }
+ }
+ }
+ };
+
+ const [menuPermissions, setMenuPermissions] = useState(defaultMenuPermissions);
+
+ useEffect(() => {
+ const userData = localStorage.getItem('user');
+ if (userData) {
+ try {
+ const parsedUser = JSON.parse(userData);
+ const role = parsedUser.user.role;
+ const userId = parsedUser.user.user_id; // Get the current user ID
+
+ setCurrentUserRole(role);
+ setCurrentUserId(userId); // Set the current user ID
+ } catch (e) {
+ console.error('Error parsing user data from localStorage:', e);
+ }
+ }
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const userData = await UserService.getUser(id);
+ setUser(userData);
+
+ setFormData({
+ username: userData.data.username || '',
+ name: userData.data.name || '',
+ password: '',
+ confirmPassword: '',
+ userRoles: userData.data.role || '',
+ activeStatus: userData.data.status || '',
+ outlet: userData.data.outlet_id || ''
+ });
+
+ if (userData.data.user_permissions) {
+ try {
+ const parsedPermissions = JSON.parse(userData.data.user_permissions);
+ const mergedPermissions = {
+ ...defaultMenuPermissions,
+ ...parsedPermissions
+ };
+ console.log('Parsed user permissions:', mergedPermissions);
+ setMenuPermissions(mergedPermissions);
+ } catch (e) {
+ console.error('Error parsing user permissions:', e);
+ setMenuPermissions(defaultMenuPermissions);
+ }
+ } else {
+ setMenuPermissions(defaultMenuPermissions);
+ }
+
+ // Fetch outlets using current user's ID from localStorage
+ setLoadingOutlets(true);
+ const outletsResponse = await OutletApiService.getOutlets(currentUserId); // Use currentUserId instead of id
+ const outletsData = Array.isArray(outletsResponse)
+ ? outletsResponse
+ : Array.isArray(outletsResponse.result)
+ ? outletsResponse.result
+ : Array.isArray(outletsResponse.data)
+ ? outletsResponse.data
+ : [];
+ setOutlets(outletsData);
+ } catch (err) {
+ console.error('Error fetching data:', err);
+ setError(err.message || 'Failed to load user data');
+ } finally {
+ setLoading(false);
+ setLoadingOutlets(false);
+ }
+ };
+
+ fetchData();
+ }, [id, currentUserId]);
+
+ const generateRandomPassword = () => {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
+ let password = '';
+ for (let i = 0; i < 12; i++) {
+ password += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ setFormData({ ...formData, password: password, confirmPassword: password });
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (formData.password && formData.password !== formData.confirmPassword) {
+ toast.error('Passwords do not match!', {
+ position: "top-right",
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ });
+ return;
+ }
+
+ if (!formData.username.trim()) {
+ toast.error('Username is required!', {
+ position: "top-right",
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ });
+ return;
+ }
+
+ if (!formData.name.trim()) {
+ toast.error('Name is required!', {
+ position: "top-right",
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ });
+ return;
+ }
+
+ if (!formData.userRoles) {
+ toast.error('User role is required!', {
+ position: "top-right",
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ });
+ return;
+ }
+
+ if (!formData.activeStatus) {
+ toast.error('Active status is required!', {
+ position: "top-right",
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ });
+ return;
+ }
+
+ if (formData.userRoles === 'outlet' && !formData.outlet) {
+ toast.error('Outlet selection is required for Outlet role', {
+ position: "top-right",
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ });
+ return;
+ }
+
+ try {
+ setLoading(true);
+
+ const updateData = {
+ username: formData.username,
+ name: formData.name,
+ userRoles: formData.userRoles,
+ activeStatus: formData.activeStatus,
+ outlet: formData.userRoles === 'outlet' ? formData.outlet : null,
+ menuPermissions: menuPermissions
+ };
+
+ if (formData.password && formData.password.trim()) {
+ updateData.password = formData.password;
+ }
+
+ await UserService.updateUser(id, updateData);
+ toast.success("User updated successfully!", {
+ position: "top-right",
+ autoClose: 1500,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ onClose: () => {
+ navigate("/settings/user");
+ },
+ });
+ } catch (error) {
+ console.error('Error updating user:', error);
+ toast.error(`Error updating user: ${error.message}`, {
+ position: "top-right",
+ autoClose: 3000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ if (name === 'userRoles' && value !== 'outlet') {
+ setFormData(prev => ({ ...prev, outlet: '' }));
+ }
+ };
+
+ if (!user) {
+ return (
+
+ User not found
+
+ );
+ }
+
+ return (
+
+
+
Edit User
+
+
+
+
+ Username
+
+
+
+
+ Name
+
+
+
+
+ User Role
+
+ Select Role
+ Admin
+ Editor
+ Moderator
+ Account
+ Outlet
+
+
+
+ {formData.userRoles === 'outlet' && (
+
+
+ Outlet *
+
+ {loadingOutlets ? (
+
+ 🌀
+ Loading outlets...
+
+ ) : (
+
+ Select outlet
+ {outlets.map(outlet => (
+
+ {outlet.title || outlet.name || `Outlet ${outlet.id}`}
+
+ ))}
+
+ )}
+
+ )}
+
+
+ Active Status
+
+ Select Status
+ Active
+ Inactive
+ Suspended
+
+
+
+
+
+ New Password (optional)
+
+ Generate Random
+
+
+
+
+ setShowPassword(!showPassword)}
+ className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
+ >
+ {showPassword ? : }
+
+
+
+
+ {formData.password && (
+
+
Confirm Password
+
+
+ setShowConfirmPassword(!showConfirmPassword)}
+ className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
+ >
+ {showConfirmPassword ? : }
+
+
+
+ )}
+
+
+ {currentUserRole === 'admin' && (
+
+
Menu Permissions
+
+
+
+
+ Menu
+ Read
+ Create
+ Update
+ Delete
+
+
+
+ {Object.entries(menuPermissions).map(([parent, value]) => {
+ if (value.subItems) {
+ return (
+
+ {/* Parent row with single checkbox for itself only */}
+
+
+ {parent}
+
+
+ {
+ setMenuPermissions(prev => ({
+ ...prev,
+ [parent]: {
+ ...prev[parent],
+ read: !prev[parent].read,
+ // Don't modify subItems here
+ }
+ }));
+ }}
+ className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
+ />
+
+
+
+ {Object.entries(value.subItems).map(([subItem, permissions]) => (
+
+
+ - {subItem}
+
+ {['read', 'create', 'update', 'delete'].map((permission) => (
+
+ {
+ setMenuPermissions(prev => {
+ const newState = { ...prev };
+ newState[parent] = {
+ ...newState[parent],
+ subItems: {
+ ...newState[parent].subItems,
+ [subItem]: {
+ ...newState[parent].subItems[subItem],
+ [permission]: !newState[parent].subItems[subItem][permission]
+ }
+ }
+ };
+ return newState;
+ });
+ }}
+ className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
+ />
+
+ ))}
+
+ ))}
+
+ );
+ } else {
+ // No subItems: show four checkboxes at parent row
+ return (
+
+
+ {parent}
+
+ {['read', 'create', 'update', 'delete'].map((permission) => (
+
+ {
+ setMenuPermissions(prev => ({
+ ...prev,
+ [parent]: {
+ ...prev[parent],
+ [permission]: !prev[parent][permission]
+ }
+ }));
+ }}
+ className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
+ />
+
+ ))}
+
+ );
+ }
+ })}
+
+
+
+
+ )}
+
+
+ navigate('/settings/user')}
+ className="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
+ disabled={loading}
+ >
+ Cancel
+
+
+ {loading ? 'Saving...' : 'Save Changes'}
+
+
+
+
+
+ );
+};
+
+export default UserEdit;
\ No newline at end of file
diff --git a/src/pages/task/all/index.jsx b/src/pages/task/all/index.jsx
new file mode 100644
index 0000000..5434b2d
--- /dev/null
+++ b/src/pages/task/all/index.jsx
@@ -0,0 +1,765 @@
+import React, { useState, useRef, useEffect, Fragment } from "react";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
+import { Dialog, Transition } from "@headlessui/react";
+import { Plus, Calendar, Search, X } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data";
+import { useNavigate } from "react-router-dom";
+
+/**
+ * Single-file component: CaseScheduler
+ *
+ * Requirements:
+ * - Headless UI Dialog for modals
+ * - for datetime selection
+ * - FullCalendar with external draggable items (Draggable)
+ *
+ * Note: Tailwind CSS classes used for styling.
+ */
+
+const priorityColorMap = {
+ High: "#ef4444", // red
+ Medium: "#f97316", // orange
+ Low: "#3b82f6", // blue
+};
+
+const thicknessOptions = ["5mm", "6mm", "8mm", "10mm", "12mm"];
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+export default function CaseScheduler() {
+ const navigate = useNavigate();
+ const [cases, setCases] = useState(initialCasesSeed);
+ const [events, setEvents] = useState([]);
+ const [filter, setFilter] = useState("");
+ const [category, setCategory] = useState("All");
+ const [calendarView, setCalendarView] = useState("timeGridWeek");
+
+ // Modals state
+ const [showPlaceModal, setShowPlaceModal] = useState(false);
+ const [showNewModal, setShowNewModal] = useState(false);
+
+ // Selected case for Place modal
+ const [selectedCaseId, setSelectedCaseId] = useState(null);
+ const [placeForm, setPlaceForm] = useState({
+ order: "",
+ thickness: "8mm",
+ requested: "",
+ datetime: "", // ISO-like: "2025-10-04T09:00"
+ priority: "Medium",
+ notes: "",
+ });
+
+ // New case form
+ const [newForm, setNewForm] = useState({
+ name: "",
+ category: "TH",
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+
+ const calendarRef = useRef();
+
+ // compute counts
+ const categoryCounts = categoryList.reduce((acc, cat) => {
+ if (cat === "All") {
+ acc[cat] = cases.length;
+ } else {
+ acc[cat] = cases.filter((c) => c.category === cat).length;
+ }
+ return acc;
+ }, {});
+
+ // filtered list
+ const filteredCases = cases.filter((c) => {
+ const matchCat = category === "All" || c.category === category;
+ const text = (filter || "").toLowerCase();
+ const matchSearch =
+ c.id.toLowerCase().includes(text) ||
+ (c.name || "").toLowerCase().includes(text) ||
+ (c.type || "").toLowerCase().includes(text) ||
+ (c.sales || "").toLowerCase().includes(text) ||
+ (c.description || "").toLowerCase().includes(text);
+
+ // Hide rejected and scheduled from list
+ const visibleStatus = c.status !== "Rejected" && c.status !== "Scheduled";
+
+ return matchCat && matchSearch && visibleStatus;
+ });
+
+ // Preload scheduled events based on data
+ useEffect(() => {
+ const scheduledEvents = initialCasesSeed
+ .filter((c) => c.scheduled)
+ .map((c) => ({
+ id: c.id + "-" + new Date(c.scheduled).getTime(),
+ title: `${c.id} - ${c.name}`,
+ start: new Date(c.scheduled),
+ end: new Date(new Date(c.scheduled).getTime() + 2 * 60 * 60 * 1000),
+ backgroundColor: priorityColorMap[c.priority],
+ borderColor: priorityColorMap[c.priority],
+ }));
+ setEvents(scheduledEvents);
+
+ // also make sure their status is "Scheduled"
+ setCases((prev) =>
+ prev.map((c) =>
+ c.scheduled ? { ...c, status: "Scheduled" } : c
+ )
+ );
+ }, []);
+
+ // Setup FullCalendar Draggable for external elements.
+ useEffect(() => {
+ const containerEl = document.getElementById("external-cases");
+ if (!containerEl) return;
+
+ // clean old draggable if exists
+ if (containerEl._draggableInstance) {
+ containerEl._draggableInstance.destroy();
+ }
+
+ const draggable = new Draggable(containerEl, {
+ itemSelector: ".fc-draggable",
+ eventData: (eventEl) => {
+ const id = eventEl.getAttribute("data-id");
+ const found = cases.find((c) => c.id === id);
+ if (!found || found.status === "Rejected") return null;
+ return {
+ id: found.id,
+ title: `${found.id} - ${found.name}`,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ },
+ });
+
+ containerEl._draggableInstance = draggable;
+
+ return () => {
+ draggable.destroy && draggable.destroy();
+ };
+ }, [cases]);
+
+
+ // Handler for external item dropped on calendar
+ // FullCalendar uses `drop` for external elements. The `info` contains .date and .draggedEl
+ const handleExternalDrop = (info) => {
+ const el = info.draggedEl;
+ const caseId = el?.getAttribute("data-id");
+ if (!caseId) return;
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === caseId
+ ? { ...c, status: "Scheduled", color: priorityColorMap[c.priority] }
+ : c
+ )
+ );
+
+ const start = info.date;
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // 2 hours duration
+ const newEvent = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ setEvents((prev) => [...prev, newEvent]);
+ };
+
+
+ // Open Place modal for a specific case (from Place on Calendar button)
+ function openPlaceModal(caseId) {
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+ setSelectedCaseId(caseId);
+ setPlaceForm({
+ order: found.order || "",
+ thickness: found.thickness || "8mm",
+ requested: found.requested || "",
+ datetime: "", // user picks
+ priority: found.priority || "Medium",
+ notes: found.notes || "",
+ });
+ setShowPlaceModal(true);
+ }
+
+ // Approve & Place from the modal
+ function approveAndPlace() {
+ if (!selectedCaseId) return;
+ const found = cases.find((c) => c.id === selectedCaseId);
+ if (!found) return;
+
+ // update case to Scheduled
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? { ...c, status: "Scheduled", priority: placeForm.priority, notes: placeForm.notes, thickness: placeForm.thickness }
+ : c
+ )
+ );
+
+ // add event at selected datetime
+ if (!placeForm.datetime) {
+ // if no datetime supplied, close modal and just mark scheduled
+ setShowPlaceModal(false);
+ return;
+ }
+
+ const start = new Date(placeForm.datetime);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
+
+ const ev = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ borderColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ };
+ setEvents((prev) => [...prev, ev]);
+ setShowPlaceModal(false);
+ }
+
+ // Save as pending (update case fields)
+ function savePlaceAsPending() {
+ if (!selectedCaseId) return;
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? {
+ ...c,
+ priority: placeForm.priority,
+ notes: placeForm.notes,
+ thickness: placeForm.thickness,
+ // keep status as Pending unless it was Approved/Scheduled
+ }
+ : c
+ )
+ );
+ setShowPlaceModal(false);
+ }
+
+ // Open New Case modal
+ function openNewModal() {
+ setNewForm({
+ name: "",
+ category: "TH",
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+ setShowNewModal(true);
+ }
+
+ // Save new case as pending
+ function saveNewAsPending() {
+ // create a simple UC- id by incrementing
+ const maxNum = cases.reduce((max, c) => {
+ const m = c.id && c.id.startsWith("UC-") ? parseInt(c.id.split("-")[1]) || 0 : 0;
+ return Math.max(max, m);
+ }, 1026);
+ const newIdNum = maxNum + 1;
+ const newId = `UC-${newIdNum}`;
+
+ const newCase = {
+ id: newId,
+ order: String(100000 + newIdNum), // fake order
+ category: newForm.category,
+ name: newForm.name || newId,
+ type: newForm.name || newId,
+ requested: "",
+ sales: "",
+ description: newForm.notes || "",
+ priority: newForm.priority,
+ status: "Pending",
+ color: priorityColorMap[newForm.priority],
+ notes: newForm.notes,
+ thickness: "8mm",
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setShowNewModal(false);
+
+ // Optionally auto-place if datetime provided? The user requested only Save as Pending for new case.
+ }
+
+ // Approve and place directly from list approve button
+ function handleApproveFromList(id) {
+ setCases((prev) => prev.map((c) => (c.id === id ? { ...c, status: "Approved" } : c)));
+ }
+
+ function handleRejectFromList(id) {
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === id ? { ...c, status: "Rejected" } : c
+ )
+ );
+ }
+
+ // Place on calendar via button in list: open place modal
+ // Also implement drag behavior: pending or approved should be draggable (Draggable.filter above handles rejection)
+ // For "Place on Calendar" button in the list we open the modal
+ // The modal's Approve & Place will add event at chosen datetime
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/tasks/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+
+ {/* Left panel */}
+
+
+
Urgent Cases ({filteredCases.length})
+
+ New Case
+
+
+
+ {/* Search */}
+
+
+ setFilter(e.target.value)}
+ />
+
+
+ {/* Category buttons with counts */}
+
+ {categoryList.map((cat) => (
+ setCategory(cat)}
+ className={`px-3 py-1 rounded-full text-sm border flex items-center gap-2 ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-gray-100 text-gray-600 border-gray-300 hover:bg-gray-200"
+ }`}
+ >
+ {cat}
+ ({categoryCounts[cat]})
+
+ ))}
+
+
+ {/* External cases container */}
+
+ {filteredCases.map((item) => (
+
+
+
+
{item.id}
+
Order {item.name}
+
+
+
+
+
+ {item.status}
+
+
+
+ {item.priority}
+
+
+
+
+
+
{item.type}
+
Item: {item.order_details}
+
Requested: {item.requested}
+
{item.description}
+
+ {/* Buttons: Approve/Reject (40% width) or Place on Calendar */}
+
+ {item.status === "Pending" && (
+ <>
+
handleApproveFromList(item.id)}
+ className="w-[40%] bg-green-600 hover:bg-green-700 text-white py-1 rounded-lg text-sm"
+ >
+ Approve
+
+
handleRejectFromList(item.id)}
+ className="w-[40%] text-red-600 border border-red-700 hover:bg-red-100 py-1 rounded-lg text-sm"
+ >
+ Reject
+
+ >
+ )}
+
+ {item.status === "Approved" && (
+
openPlaceModal(item.id)}
+ className="w-[40%] text-blue-600 border border-blue-200 py-1 rounded-lg text-sm hover:bg-blue-50"
+ >
+ Place on Calendar
+
+ )}
+
+ {/* If Scheduled show small label */}
+ {item.status === "Scheduled" && (
+
Scheduled
+ )}
+
+
+ ))}
+
+
+
+ {/* Right panel (calendar) */}
+
+
+
Production Calendar
+
+ {
+ setCalendarView(e.target.value);
+ const api = calendarRef.current?.getApi();
+ api?.changeView(e.target.value);
+ }}
+ className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Day
+ Week
+ Month
+
+
+
+
+
{
+ // simple event click: maybe show details or allow removal
+ // optional: remove event
+ }}
+ height="80vh"
+ slotMinTime="08:00:00"
+ slotMaxTime="18:00:00"
+ eventContent={(arg) => {
+ // simple content - title only
+ return (
+
+ );
+ }}
+ />
+
+
+
+ {/* Place on Calendar Modal (Headless UI Dialog) */}
+
+ setShowPlaceModal(false)}>
+
+
+
+
+
+ {/* spacer to center */}
+
+
+
+
+
+ Schedule Urgent Case
+ setShowPlaceModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
+ Configure the scheduling details for urgent case {selectedCaseId}
+
+
+ {/* form grid */}
+
+ {/* Order ID (readonly) */}
+
+ Order ID
+
+
+
+ {/* Thickness */}
+
+ Thickness
+ setPlaceForm((p) => ({ ...p, thickness: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ {thicknessOptions.map((t) => (
+ {t}
+ ))}
+
+
+
+ {/* Requested Delivery Date (readonly) */}
+
+ Requested Delivery Date
+
+
+
+ {/* Proposed Furnace Slot -> handled by datetime-local */}
+
+ Proposed Furnace Slot (Date & time)
+ setPlaceForm((p) => ({ ...p, datetime: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ />
+
+
+ {/* Priority */}
+
+ Priority
+ setPlaceForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+ {/* Notes */}
+
+ Notes
+ setPlaceForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+ {/* buttons */}
+
+ setShowPlaceModal(false)}
+ className="px-4 py-2 rounded-md border bg-white text-sm"
+ >
+ Cancel
+
+
+ Save as Pending
+
+
+ Approve & Place
+
+
+
+
+
+
+
+
+ {/* New Case Modal */}
+
+ setShowNewModal(false)}>
+
+
+
+
+
+
+
+
+
+
+ New Urgent Case
+ setShowNewModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
Create a new urgent case
+
+
+
+ Case Name
+ setNewForm((p) => ({ ...p, name: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ placeholder="e.g. UC-1028"
+ />
+
+
+
+ Category
+ setNewForm((p) => ({ ...p, category: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ TH
+ TL
+ FLSS
+ LM
+ TP
+
+
+
+
+ Priority
+ setNewForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+
+ Notes
+ setNewForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+
+ setShowNewModal(false)} className="px-4 py-2 rounded-md border bg-white text-sm">Cancel
+ Save as Pending
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/task/flss/index.jsx b/src/pages/task/flss/index.jsx
new file mode 100644
index 0000000..6150db4
--- /dev/null
+++ b/src/pages/task/flss/index.jsx
@@ -0,0 +1,732 @@
+import React, { useState, useRef, useEffect, Fragment } from "react";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
+import { Dialog, Transition } from "@headlessui/react";
+import { Plus, Calendar, Search, X } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data";
+import { useNavigate } from "react-router-dom";
+
+/**
+ * Single-file component: CaseScheduler
+ *
+ * Requirements:
+ * - Headless UI Dialog for modals
+ * - for datetime selection
+ * - FullCalendar with external draggable items (Draggable)
+ *
+ * Note: Tailwind CSS classes used for styling.
+ */
+
+const priorityColorMap = {
+ High: "#ef4444", // red
+ Medium: "#f97316", // orange
+ Low: "#3b82f6", // blue
+};
+
+const thicknessOptions = ["5mm", "6mm", "8mm", "10mm", "12mm"];
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+export default function CaseScheduler() {
+ const navigate = useNavigate();
+ const [cases, setCases] = useState(initialCasesSeed);
+ const [events, setEvents] = useState([]);
+ const [filter, setFilter] = useState("");
+ const [category, setCategory] = useState("FLSS");
+ const [calendarView, setCalendarView] = useState("timeGridWeek");
+
+ // Modals state
+ const [showPlaceModal, setShowPlaceModal] = useState(false);
+ const [showNewModal, setShowNewModal] = useState(false);
+
+ // Selected case for Place modal
+ const [selectedCaseId, setSelectedCaseId] = useState(null);
+ const [placeForm, setPlaceForm] = useState({
+ order: "",
+ thickness: "8mm",
+ requested: "",
+ datetime: "", // ISO-like: "2025-10-04T09:00"
+ priority: "Medium",
+ notes: "",
+ });
+
+ // New case form
+ const [newForm, setNewForm] = useState({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+
+ const calendarRef = useRef();
+
+ // filtered list
+ const filteredCases = cases.filter((c) => {
+ const matchCat = c.category === category;
+ const text = (filter || "").toLowerCase();
+ const matchSearch =
+ c.id.toLowerCase().includes(text) ||
+ (c.name || "").toLowerCase().includes(text) ||
+ (c.type || "").toLowerCase().includes(text) ||
+ (c.sales || "").toLowerCase().includes(text) ||
+ (c.description || "").toLowerCase().includes(text);
+
+ // Hide rejected and scheduled from list
+ const visibleStatus = c.status !== "Rejected" && c.status !== "Scheduled";
+
+ return matchCat && matchSearch && visibleStatus;
+ });
+
+ // Preload scheduled events based on data
+ useEffect(() => {
+ const scheduledEvents = initialCasesSeed
+ .filter((c) => c.scheduled && c.category === category)
+ .map((c) => ({
+ id: c.id + "-" + new Date(c.scheduled).getTime(),
+ title: `${c.id} - ${c.name}`,
+ start: new Date(c.scheduled),
+ end: new Date(new Date(c.scheduled).getTime() + 2 * 60 * 60 * 1000),
+ backgroundColor: priorityColorMap[c.priority],
+ borderColor: priorityColorMap[c.priority],
+ }));
+ setEvents(scheduledEvents);
+
+ // also make sure their status is "Scheduled"
+ setCases((prev) =>
+ prev.map((c) =>
+ c.scheduled ? { ...c, status: "Scheduled" } : c
+ )
+ );
+ }, []);
+
+ // Setup FullCalendar Draggable for external elements.
+ useEffect(() => {
+ const containerEl = document.getElementById("external-cases");
+ if (!containerEl) return;
+
+ // clean old draggable if exists
+ if (containerEl._draggableInstance) {
+ containerEl._draggableInstance.destroy();
+ }
+
+ const draggable = new Draggable(containerEl, {
+ itemSelector: ".fc-draggable",
+ eventData: (eventEl) => {
+ const id = eventEl.getAttribute("data-id");
+ const found = cases.find((c) => c.id === id);
+ if (!found || found.status === "Rejected") return null;
+ return {
+ id: found.id,
+ title: `${found.id} - ${found.name}`,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ },
+ });
+
+ containerEl._draggableInstance = draggable;
+
+ return () => {
+ draggable.destroy && draggable.destroy();
+ };
+ }, [cases]);
+
+
+ // Handler for external item dropped on calendar
+ // FullCalendar uses `drop` for external elements. The `info` contains .date and .draggedEl
+ const handleExternalDrop = (info) => {
+ const el = info.draggedEl;
+ const caseId = el?.getAttribute("data-id");
+ if (!caseId) return;
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === caseId
+ ? { ...c, status: "Scheduled", color: priorityColorMap[c.priority] }
+ : c
+ )
+ );
+
+ const start = info.date;
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // 2 hours duration
+ const newEvent = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ setEvents((prev) => [...prev, newEvent]);
+ };
+
+
+ // Open Place modal for a specific case (from Place on Calendar button)
+ function openPlaceModal(caseId) {
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+ setSelectedCaseId(caseId);
+ setPlaceForm({
+ order: found.order || "",
+ thickness: found.thickness || "8mm",
+ requested: found.requested || "",
+ datetime: "", // user picks
+ priority: found.priority || "Medium",
+ notes: found.notes || "",
+ });
+ setShowPlaceModal(true);
+ }
+
+ // Approve & Place from the modal
+ function approveAndPlace() {
+ if (!selectedCaseId) return;
+ const found = cases.find((c) => c.id === selectedCaseId);
+ if (!found) return;
+
+ // update case to Scheduled
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? { ...c, status: "Scheduled", priority: placeForm.priority, notes: placeForm.notes, thickness: placeForm.thickness }
+ : c
+ )
+ );
+
+ // add event at selected datetime
+ if (!placeForm.datetime) {
+ // if no datetime supplied, close modal and just mark scheduled
+ setShowPlaceModal(false);
+ return;
+ }
+
+ const start = new Date(placeForm.datetime);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
+
+ const ev = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ borderColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ };
+ setEvents((prev) => [...prev, ev]);
+ setShowPlaceModal(false);
+ }
+
+ // Save as pending (update case fields)
+ function savePlaceAsPending() {
+ if (!selectedCaseId) return;
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? {
+ ...c,
+ priority: placeForm.priority,
+ notes: placeForm.notes,
+ thickness: placeForm.thickness,
+ // keep status as Pending unless it was Approved/Scheduled
+ }
+ : c
+ )
+ );
+ setShowPlaceModal(false);
+ }
+
+ // Open New Case modal
+ function openNewModal() {
+ setNewForm({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+ setShowNewModal(true);
+ }
+
+ // Save new case as pending
+ function saveNewAsPending() {
+ // create a simple UC- id by incrementing
+ const maxNum = cases.reduce((max, c) => {
+ const m = c.id && c.id.startsWith("UC-") ? parseInt(c.id.split("-")[1]) || 0 : 0;
+ return Math.max(max, m);
+ }, 1026);
+ const newIdNum = maxNum + 1;
+ const newId = `UC-${newIdNum}`;
+
+ const newCase = {
+ id: newId,
+ order: String(100000 + newIdNum), // fake order
+ category: newForm.category,
+ name: newForm.name || newId,
+ type: newForm.name || newId,
+ requested: "",
+ sales: "",
+ description: newForm.notes || "",
+ priority: newForm.priority,
+ status: "Pending",
+ color: priorityColorMap[newForm.priority],
+ notes: newForm.notes,
+ thickness: "8mm",
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setShowNewModal(false);
+
+ // Optionally auto-place if datetime provided? The user requested only Save as Pending for new case.
+ }
+
+ // Approve and place directly from list approve button
+ function handleApproveFromList(id) {
+ setCases((prev) => prev.map((c) => (c.id === id ? { ...c, status: "Approved" } : c)));
+ }
+
+ function handleRejectFromList(id) {
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === id ? { ...c, status: "Rejected" } : c
+ )
+ );
+ }
+
+ // Place on calendar via button in list: open place modal
+ // Also implement drag behavior: pending or approved should be draggable (Draggable.filter above handles rejection)
+ // For "Place on Calendar" button in the list we open the modal
+ // The modal's Approve & Place will add event at chosen datetime
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/tasks/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Left panel */}
+
+
+
Urgent Cases ({filteredCases.length})
+
+ New Case
+
+
+
+ {/* Search */}
+
+
+ setFilter(e.target.value)}
+ />
+
+
+ {/* External cases container */}
+
+ {filteredCases.map((item) => (
+
+
+
+
{item.id}
+
Order {item.name}
+
+
+
+
+
+ {item.status}
+
+
+
+ {item.priority}
+
+
+
+
+
+
{item.type}
+
Item: {item.order_details}
+
Requested: {item.requested}
+
{item.description}
+
+ {/* Buttons: Approve/Reject (40% width) or Place on Calendar */}
+
+ {item.status === "Pending" && (
+ <>
+
handleApproveFromList(item.id)}
+ className="w-[40%] bg-green-600 hover:bg-green-700 text-white py-1 rounded-lg text-sm"
+ >
+ Approve
+
+
handleRejectFromList(item.id)}
+ className="w-[40%] text-red-600 border border-red-700 hover:bg-red-100 py-1 rounded-lg text-sm"
+ >
+ Reject
+
+ >
+ )}
+
+ {item.status === "Approved" && (
+
openPlaceModal(item.id)}
+ className="w-[40%] text-blue-600 border border-blue-200 py-1 rounded-lg text-sm hover:bg-blue-50"
+ >
+ Place on Calendar
+
+ )}
+
+ {/* If Scheduled show small label */}
+ {item.status === "Scheduled" && (
+
Scheduled
+ )}
+
+
+ ))}
+
+
+
+ {/* Right panel (calendar) */}
+
+
+
Production Calendar
+
+ {
+ setCalendarView(e.target.value);
+ const api = calendarRef.current?.getApi();
+ api?.changeView(e.target.value);
+ }}
+ className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Day
+ Week
+ Month
+
+
+
+
+
{
+ // simple event click: maybe show details or allow removal
+ // optional: remove event
+ }}
+ height="80vh"
+ slotMinTime="08:00:00"
+ slotMaxTime="18:00:00"
+ eventContent={(arg) => {
+ // simple content - title only
+ return (
+
+ );
+ }}
+ />
+
+
+
+ {/* Place on Calendar Modal (Headless UI Dialog) */}
+
+ setShowPlaceModal(false)}>
+
+
+
+
+
+ {/* spacer to center */}
+
+
+
+
+
+ Schedule Urgent Case
+ setShowPlaceModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
+ Configure the scheduling details for urgent case {selectedCaseId}
+
+
+ {/* form grid */}
+
+ {/* Order ID (readonly) */}
+
+ Order ID
+
+
+
+ {/* Thickness */}
+
+ Thickness
+ setPlaceForm((p) => ({ ...p, thickness: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ {thicknessOptions.map((t) => (
+ {t}
+ ))}
+
+
+
+ {/* Requested Delivery Date (readonly) */}
+
+ Requested Delivery Date
+
+
+
+ {/* Proposed Furnace Slot -> handled by datetime-local */}
+
+ Proposed Furnace Slot (Date & time)
+ setPlaceForm((p) => ({ ...p, datetime: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ />
+
+
+ {/* Priority */}
+
+ Priority
+ setPlaceForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+ {/* Notes */}
+
+ Notes
+ setPlaceForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+ {/* buttons */}
+
+ setShowPlaceModal(false)}
+ className="px-4 py-2 rounded-md border bg-white text-sm"
+ >
+ Cancel
+
+
+ Save as Pending
+
+
+ Approve & Place
+
+
+
+
+
+
+
+
+ {/* New Case Modal */}
+
+ setShowNewModal(false)}>
+
+
+
+
+
+
+
+
+
+
+ New Urgent Case
+ setShowNewModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
Create a new urgent case
+
+
+
+ Case Name
+ setNewForm((p) => ({ ...p, name: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ placeholder="e.g. UC-1028"
+ />
+
+
+
+ Category
+ setNewForm((p) => ({ ...p, category: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ FLSS
+
+
+
+
+ Priority
+ setNewForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+
+ Notes
+ setNewForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+
+ setShowNewModal(false)} className="px-4 py-2 rounded-md border bg-white text-sm">Cancel
+ Save as Pending
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/task/lm/index.jsx b/src/pages/task/lm/index.jsx
new file mode 100644
index 0000000..e2b22a0
--- /dev/null
+++ b/src/pages/task/lm/index.jsx
@@ -0,0 +1,732 @@
+import React, { useState, useRef, useEffect, Fragment } from "react";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
+import { Dialog, Transition } from "@headlessui/react";
+import { Plus, Calendar, Search, X } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data";
+import { useNavigate } from "react-router-dom";
+
+/**
+ * Single-file component: CaseScheduler
+ *
+ * Requirements:
+ * - Headless UI Dialog for modals
+ * - for datetime selection
+ * - FullCalendar with external draggable items (Draggable)
+ *
+ * Note: Tailwind CSS classes used for styling.
+ */
+
+const priorityColorMap = {
+ High: "#ef4444", // red
+ Medium: "#f97316", // orange
+ Low: "#3b82f6", // blue
+};
+
+const thicknessOptions = ["5mm", "6mm", "8mm", "10mm", "12mm"];
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+export default function CaseScheduler() {
+ const navigate = useNavigate();
+ const [cases, setCases] = useState(initialCasesSeed);
+ const [events, setEvents] = useState([]);
+ const [filter, setFilter] = useState("");
+ const [category, setCategory] = useState("LM");
+ const [calendarView, setCalendarView] = useState("timeGridWeek");
+
+ // Modals state
+ const [showPlaceModal, setShowPlaceModal] = useState(false);
+ const [showNewModal, setShowNewModal] = useState(false);
+
+ // Selected case for Place modal
+ const [selectedCaseId, setSelectedCaseId] = useState(null);
+ const [placeForm, setPlaceForm] = useState({
+ order: "",
+ thickness: "8mm",
+ requested: "",
+ datetime: "", // ISO-like: "2025-10-04T09:00"
+ priority: "Medium",
+ notes: "",
+ });
+
+ // New case form
+ const [newForm, setNewForm] = useState({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+
+ const calendarRef = useRef();
+
+ // filtered list
+ const filteredCases = cases.filter((c) => {
+ const matchCat = c.category === category;
+ const text = (filter || "").toLowerCase();
+ const matchSearch =
+ c.id.toLowerCase().includes(text) ||
+ (c.name || "").toLowerCase().includes(text) ||
+ (c.type || "").toLowerCase().includes(text) ||
+ (c.sales || "").toLowerCase().includes(text) ||
+ (c.description || "").toLowerCase().includes(text);
+
+ // Hide rejected and scheduled from list
+ const visibleStatus = c.status !== "Rejected" && c.status !== "Scheduled";
+
+ return matchCat && matchSearch && visibleStatus;
+ });
+
+ // Preload scheduled events based on data
+ useEffect(() => {
+ const scheduledEvents = initialCasesSeed
+ .filter((c) => c.scheduled && c.category === category)
+ .map((c) => ({
+ id: c.id + "-" + new Date(c.scheduled).getTime(),
+ title: `${c.id} - ${c.name}`,
+ start: new Date(c.scheduled),
+ end: new Date(new Date(c.scheduled).getTime() + 2 * 60 * 60 * 1000),
+ backgroundColor: priorityColorMap[c.priority],
+ borderColor: priorityColorMap[c.priority],
+ }));
+ setEvents(scheduledEvents);
+
+ // also make sure their status is "Scheduled"
+ setCases((prev) =>
+ prev.map((c) =>
+ c.scheduled ? { ...c, status: "Scheduled" } : c
+ )
+ );
+ }, []);
+
+ // Setup FullCalendar Draggable for external elements.
+ useEffect(() => {
+ const containerEl = document.getElementById("external-cases");
+ if (!containerEl) return;
+
+ // clean old draggable if exists
+ if (containerEl._draggableInstance) {
+ containerEl._draggableInstance.destroy();
+ }
+
+ const draggable = new Draggable(containerEl, {
+ itemSelector: ".fc-draggable",
+ eventData: (eventEl) => {
+ const id = eventEl.getAttribute("data-id");
+ const found = cases.find((c) => c.id === id);
+ if (!found || found.status === "Rejected") return null;
+ return {
+ id: found.id,
+ title: `${found.id} - ${found.name}`,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ },
+ });
+
+ containerEl._draggableInstance = draggable;
+
+ return () => {
+ draggable.destroy && draggable.destroy();
+ };
+ }, [cases]);
+
+
+ // Handler for external item dropped on calendar
+ // FullCalendar uses `drop` for external elements. The `info` contains .date and .draggedEl
+ const handleExternalDrop = (info) => {
+ const el = info.draggedEl;
+ const caseId = el?.getAttribute("data-id");
+ if (!caseId) return;
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === caseId
+ ? { ...c, status: "Scheduled", color: priorityColorMap[c.priority] }
+ : c
+ )
+ );
+
+ const start = info.date;
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // 2 hours duration
+ const newEvent = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ setEvents((prev) => [...prev, newEvent]);
+ };
+
+
+ // Open Place modal for a specific case (from Place on Calendar button)
+ function openPlaceModal(caseId) {
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+ setSelectedCaseId(caseId);
+ setPlaceForm({
+ order: found.order || "",
+ thickness: found.thickness || "8mm",
+ requested: found.requested || "",
+ datetime: "", // user picks
+ priority: found.priority || "Medium",
+ notes: found.notes || "",
+ });
+ setShowPlaceModal(true);
+ }
+
+ // Approve & Place from the modal
+ function approveAndPlace() {
+ if (!selectedCaseId) return;
+ const found = cases.find((c) => c.id === selectedCaseId);
+ if (!found) return;
+
+ // update case to Scheduled
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? { ...c, status: "Scheduled", priority: placeForm.priority, notes: placeForm.notes, thickness: placeForm.thickness }
+ : c
+ )
+ );
+
+ // add event at selected datetime
+ if (!placeForm.datetime) {
+ // if no datetime supplied, close modal and just mark scheduled
+ setShowPlaceModal(false);
+ return;
+ }
+
+ const start = new Date(placeForm.datetime);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
+
+ const ev = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ borderColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ };
+ setEvents((prev) => [...prev, ev]);
+ setShowPlaceModal(false);
+ }
+
+ // Save as pending (update case fields)
+ function savePlaceAsPending() {
+ if (!selectedCaseId) return;
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? {
+ ...c,
+ priority: placeForm.priority,
+ notes: placeForm.notes,
+ thickness: placeForm.thickness,
+ // keep status as Pending unless it was Approved/Scheduled
+ }
+ : c
+ )
+ );
+ setShowPlaceModal(false);
+ }
+
+ // Open New Case modal
+ function openNewModal() {
+ setNewForm({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+ setShowNewModal(true);
+ }
+
+ // Save new case as pending
+ function saveNewAsPending() {
+ // create a simple UC- id by incrementing
+ const maxNum = cases.reduce((max, c) => {
+ const m = c.id && c.id.startsWith("UC-") ? parseInt(c.id.split("-")[1]) || 0 : 0;
+ return Math.max(max, m);
+ }, 1026);
+ const newIdNum = maxNum + 1;
+ const newId = `UC-${newIdNum}`;
+
+ const newCase = {
+ id: newId,
+ order: String(100000 + newIdNum), // fake order
+ category: newForm.category,
+ name: newForm.name || newId,
+ type: newForm.name || newId,
+ requested: "",
+ sales: "",
+ description: newForm.notes || "",
+ priority: newForm.priority,
+ status: "Pending",
+ color: priorityColorMap[newForm.priority],
+ notes: newForm.notes,
+ thickness: "8mm",
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setShowNewModal(false);
+
+ // Optionally auto-place if datetime provided? The user requested only Save as Pending for new case.
+ }
+
+ // Approve and place directly from list approve button
+ function handleApproveFromList(id) {
+ setCases((prev) => prev.map((c) => (c.id === id ? { ...c, status: "Approved" } : c)));
+ }
+
+ function handleRejectFromList(id) {
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === id ? { ...c, status: "Rejected" } : c
+ )
+ );
+ }
+
+ // Place on calendar via button in list: open place modal
+ // Also implement drag behavior: pending or approved should be draggable (Draggable.filter above handles rejection)
+ // For "Place on Calendar" button in the list we open the modal
+ // The modal's Approve & Place will add event at chosen datetime
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/tasks/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Left panel */}
+
+
+
Urgent Cases ({filteredCases.length})
+
+ New Case
+
+
+
+ {/* Search */}
+
+
+ setFilter(e.target.value)}
+ />
+
+
+ {/* External cases container */}
+
+ {filteredCases.map((item) => (
+
+
+
+
{item.id}
+
Order {item.name}
+
+
+
+
+
+ {item.status}
+
+
+
+ {item.priority}
+
+
+
+
+
+
{item.type}
+
Item: {item.order_details}
+
Requested: {item.requested}
+
{item.description}
+
+ {/* Buttons: Approve/Reject (40% width) or Place on Calendar */}
+
+ {item.status === "Pending" && (
+ <>
+
handleApproveFromList(item.id)}
+ className="w-[40%] bg-green-600 hover:bg-green-700 text-white py-1 rounded-lg text-sm"
+ >
+ Approve
+
+
handleRejectFromList(item.id)}
+ className="w-[40%] text-red-600 border border-red-700 hover:bg-red-100 py-1 rounded-lg text-sm"
+ >
+ Reject
+
+ >
+ )}
+
+ {item.status === "Approved" && (
+
openPlaceModal(item.id)}
+ className="w-[40%] text-blue-600 border border-blue-200 py-1 rounded-lg text-sm hover:bg-blue-50"
+ >
+ Place on Calendar
+
+ )}
+
+ {/* If Scheduled show small label */}
+ {item.status === "Scheduled" && (
+
Scheduled
+ )}
+
+
+ ))}
+
+
+
+ {/* Right panel (calendar) */}
+
+
+
Production Calendar
+
+ {
+ setCalendarView(e.target.value);
+ const api = calendarRef.current?.getApi();
+ api?.changeView(e.target.value);
+ }}
+ className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Day
+ Week
+ Month
+
+
+
+
+
{
+ // simple event click: maybe show details or allow removal
+ // optional: remove event
+ }}
+ height="80vh"
+ slotMinTime="08:00:00"
+ slotMaxTime="18:00:00"
+ eventContent={(arg) => {
+ // simple content - title only
+ return (
+
+ );
+ }}
+ />
+
+
+
+ {/* Place on Calendar Modal (Headless UI Dialog) */}
+
+ setShowPlaceModal(false)}>
+
+
+
+
+
+ {/* spacer to center */}
+
+
+
+
+
+ Schedule Urgent Case
+ setShowPlaceModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
+ Configure the scheduling details for urgent case {selectedCaseId}
+
+
+ {/* form grid */}
+
+ {/* Order ID (readonly) */}
+
+ Order ID
+
+
+
+ {/* Thickness */}
+
+ Thickness
+ setPlaceForm((p) => ({ ...p, thickness: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ {thicknessOptions.map((t) => (
+ {t}
+ ))}
+
+
+
+ {/* Requested Delivery Date (readonly) */}
+
+ Requested Delivery Date
+
+
+
+ {/* Proposed Furnace Slot -> handled by datetime-local */}
+
+ Proposed Furnace Slot (Date & time)
+ setPlaceForm((p) => ({ ...p, datetime: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ />
+
+
+ {/* Priority */}
+
+ Priority
+ setPlaceForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+ {/* Notes */}
+
+ Notes
+ setPlaceForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+ {/* buttons */}
+
+ setShowPlaceModal(false)}
+ className="px-4 py-2 rounded-md border bg-white text-sm"
+ >
+ Cancel
+
+
+ Save as Pending
+
+
+ Approve & Place
+
+
+
+
+
+
+
+
+ {/* New Case Modal */}
+
+ setShowNewModal(false)}>
+
+
+
+
+
+
+
+
+
+
+ New Urgent Case
+ setShowNewModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
Create a new urgent case
+
+
+
+ Case Name
+ setNewForm((p) => ({ ...p, name: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ placeholder="e.g. UC-1028"
+ />
+
+
+
+ Category
+ setNewForm((p) => ({ ...p, category: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ LM
+
+
+
+
+ Priority
+ setNewForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+
+ Notes
+ setNewForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+
+ setShowNewModal(false)} className="px-4 py-2 rounded-md border bg-white text-sm">Cancel
+ Save as Pending
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/task/th/index.jsx b/src/pages/task/th/index.jsx
new file mode 100644
index 0000000..f0d3881
--- /dev/null
+++ b/src/pages/task/th/index.jsx
@@ -0,0 +1,732 @@
+import React, { useState, useRef, useEffect, Fragment } from "react";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
+import { Dialog, Transition } from "@headlessui/react";
+import { Plus, Calendar, Search, X } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data";
+import { useNavigate } from "react-router-dom";
+
+/**
+ * Single-file component: CaseScheduler
+ *
+ * Requirements:
+ * - Headless UI Dialog for modals
+ * - for datetime selection
+ * - FullCalendar with external draggable items (Draggable)
+ *
+ * Note: Tailwind CSS classes used for styling.
+ */
+
+const priorityColorMap = {
+ High: "#ef4444", // red
+ Medium: "#f97316", // orange
+ Low: "#3b82f6", // blue
+};
+
+const thicknessOptions = ["5mm", "6mm", "8mm", "10mm", "12mm"];
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+export default function CaseScheduler() {
+ const navigate = useNavigate();
+ const [cases, setCases] = useState(initialCasesSeed);
+ const [events, setEvents] = useState([]);
+ const [filter, setFilter] = useState("");
+ const [category, setCategory] = useState("TH");
+ const [calendarView, setCalendarView] = useState("timeGridWeek");
+
+ // Modals state
+ const [showPlaceModal, setShowPlaceModal] = useState(false);
+ const [showNewModal, setShowNewModal] = useState(false);
+
+ // Selected case for Place modal
+ const [selectedCaseId, setSelectedCaseId] = useState(null);
+ const [placeForm, setPlaceForm] = useState({
+ order: "",
+ thickness: "8mm",
+ requested: "",
+ datetime: "", // ISO-like: "2025-10-04T09:00"
+ priority: "Medium",
+ notes: "",
+ });
+
+ // New case form
+ const [newForm, setNewForm] = useState({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+
+ const calendarRef = useRef();
+
+ // filtered list
+ const filteredCases = cases.filter((c) => {
+ const matchCat = c.category === category;
+ const text = (filter || "").toLowerCase();
+ const matchSearch =
+ c.id.toLowerCase().includes(text) ||
+ (c.name || "").toLowerCase().includes(text) ||
+ (c.type || "").toLowerCase().includes(text) ||
+ (c.sales || "").toLowerCase().includes(text) ||
+ (c.description || "").toLowerCase().includes(text);
+
+ // Hide rejected and scheduled from list
+ const visibleStatus = c.status !== "Rejected" && c.status !== "Scheduled";
+
+ return matchCat && matchSearch && visibleStatus;
+ });
+
+ // Preload scheduled events based on data
+ useEffect(() => {
+ const scheduledEvents = initialCasesSeed
+ .filter((c) => c.scheduled && c.category === category)
+ .map((c) => ({
+ id: c.id + "-" + new Date(c.scheduled).getTime(),
+ title: `${c.id} - ${c.name}`,
+ start: new Date(c.scheduled),
+ end: new Date(new Date(c.scheduled).getTime() + 2 * 60 * 60 * 1000),
+ backgroundColor: priorityColorMap[c.priority],
+ borderColor: priorityColorMap[c.priority],
+ }));
+ setEvents(scheduledEvents);
+
+ // also make sure their status is "Scheduled"
+ setCases((prev) =>
+ prev.map((c) =>
+ c.scheduled ? { ...c, status: "Scheduled" } : c
+ )
+ );
+ }, []);
+
+ // Setup FullCalendar Draggable for external elements.
+ useEffect(() => {
+ const containerEl = document.getElementById("external-cases");
+ if (!containerEl) return;
+
+ // clean old draggable if exists
+ if (containerEl._draggableInstance) {
+ containerEl._draggableInstance.destroy();
+ }
+
+ const draggable = new Draggable(containerEl, {
+ itemSelector: ".fc-draggable",
+ eventData: (eventEl) => {
+ const id = eventEl.getAttribute("data-id");
+ const found = cases.find((c) => c.id === id);
+ if (!found || found.status === "Rejected") return null;
+ return {
+ id: found.id,
+ title: `${found.id} - ${found.name}`,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ },
+ });
+
+ containerEl._draggableInstance = draggable;
+
+ return () => {
+ draggable.destroy && draggable.destroy();
+ };
+ }, [cases]);
+
+
+ // Handler for external item dropped on calendar
+ // FullCalendar uses `drop` for external elements. The `info` contains .date and .draggedEl
+ const handleExternalDrop = (info) => {
+ const el = info.draggedEl;
+ const caseId = el?.getAttribute("data-id");
+ if (!caseId) return;
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === caseId
+ ? { ...c, status: "Scheduled", color: priorityColorMap[c.priority] }
+ : c
+ )
+ );
+
+ const start = info.date;
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // 2 hours duration
+ const newEvent = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ setEvents((prev) => [...prev, newEvent]);
+ };
+
+
+ // Open Place modal for a specific case (from Place on Calendar button)
+ function openPlaceModal(caseId) {
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+ setSelectedCaseId(caseId);
+ setPlaceForm({
+ order: found.order || "",
+ thickness: found.thickness || "8mm",
+ requested: found.requested || "",
+ datetime: "", // user picks
+ priority: found.priority || "Medium",
+ notes: found.notes || "",
+ });
+ setShowPlaceModal(true);
+ }
+
+ // Approve & Place from the modal
+ function approveAndPlace() {
+ if (!selectedCaseId) return;
+ const found = cases.find((c) => c.id === selectedCaseId);
+ if (!found) return;
+
+ // update case to Scheduled
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? { ...c, status: "Scheduled", priority: placeForm.priority, notes: placeForm.notes, thickness: placeForm.thickness }
+ : c
+ )
+ );
+
+ // add event at selected datetime
+ if (!placeForm.datetime) {
+ // if no datetime supplied, close modal and just mark scheduled
+ setShowPlaceModal(false);
+ return;
+ }
+
+ const start = new Date(placeForm.datetime);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
+
+ const ev = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ borderColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ };
+ setEvents((prev) => [...prev, ev]);
+ setShowPlaceModal(false);
+ }
+
+ // Save as pending (update case fields)
+ function savePlaceAsPending() {
+ if (!selectedCaseId) return;
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? {
+ ...c,
+ priority: placeForm.priority,
+ notes: placeForm.notes,
+ thickness: placeForm.thickness,
+ // keep status as Pending unless it was Approved/Scheduled
+ }
+ : c
+ )
+ );
+ setShowPlaceModal(false);
+ }
+
+ // Open New Case modal
+ function openNewModal() {
+ setNewForm({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+ setShowNewModal(true);
+ }
+
+ // Save new case as pending
+ function saveNewAsPending() {
+ // create a simple UC- id by incrementing
+ const maxNum = cases.reduce((max, c) => {
+ const m = c.id && c.id.startsWith("UC-") ? parseInt(c.id.split("-")[1]) || 0 : 0;
+ return Math.max(max, m);
+ }, 1026);
+ const newIdNum = maxNum + 1;
+ const newId = `UC-${newIdNum}`;
+
+ const newCase = {
+ id: newId,
+ order: String(100000 + newIdNum), // fake order
+ category: newForm.category,
+ name: newForm.name || newId,
+ type: newForm.name || newId,
+ requested: "",
+ sales: "",
+ description: newForm.notes || "",
+ priority: newForm.priority,
+ status: "Pending",
+ color: priorityColorMap[newForm.priority],
+ notes: newForm.notes,
+ thickness: "8mm",
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setShowNewModal(false);
+
+ // Optionally auto-place if datetime provided? The user requested only Save as Pending for new case.
+ }
+
+ // Approve and place directly from list approve button
+ function handleApproveFromList(id) {
+ setCases((prev) => prev.map((c) => (c.id === id ? { ...c, status: "Approved" } : c)));
+ }
+
+ function handleRejectFromList(id) {
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === id ? { ...c, status: "Rejected" } : c
+ )
+ );
+ }
+
+ // Place on calendar via button in list: open place modal
+ // Also implement drag behavior: pending or approved should be draggable (Draggable.filter above handles rejection)
+ // For "Place on Calendar" button in the list we open the modal
+ // The modal's Approve & Place will add event at chosen datetime
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/tasks/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Left panel */}
+
+
+
Urgent Cases ({filteredCases.length})
+
+ New Case
+
+
+
+ {/* Search */}
+
+
+ setFilter(e.target.value)}
+ />
+
+
+ {/* External cases container */}
+
+ {filteredCases.map((item) => (
+
+
+
+
{item.id}
+
Order {item.name}
+
+
+
+
+
+ {item.status}
+
+
+
+ {item.priority}
+
+
+
+
+
+
{item.type}
+
Item: {item.order_details}
+
Requested: {item.requested}
+
{item.description}
+
+ {/* Buttons: Approve/Reject (40% width) or Place on Calendar */}
+
+ {item.status === "Pending" && (
+ <>
+
handleApproveFromList(item.id)}
+ className="w-[40%] bg-green-600 hover:bg-green-700 text-white py-1 rounded-lg text-sm"
+ >
+ Approve
+
+
handleRejectFromList(item.id)}
+ className="w-[40%] text-red-600 border border-red-700 hover:bg-red-100 py-1 rounded-lg text-sm"
+ >
+ Reject
+
+ >
+ )}
+
+ {item.status === "Approved" && (
+
openPlaceModal(item.id)}
+ className="w-[40%] text-blue-600 border border-blue-200 py-1 rounded-lg text-sm hover:bg-blue-50"
+ >
+ Place on Calendar
+
+ )}
+
+ {/* If Scheduled show small label */}
+ {item.status === "Scheduled" && (
+
Scheduled
+ )}
+
+
+ ))}
+
+
+
+ {/* Right panel (calendar) */}
+
+
+
Production Calendar
+
+ {
+ setCalendarView(e.target.value);
+ const api = calendarRef.current?.getApi();
+ api?.changeView(e.target.value);
+ }}
+ className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Day
+ Week
+ Month
+
+
+
+
+
{
+ // simple event click: maybe show details or allow removal
+ // optional: remove event
+ }}
+ height="80vh"
+ slotMinTime="08:00:00"
+ slotMaxTime="18:00:00"
+ eventContent={(arg) => {
+ // simple content - title only
+ return (
+
+ );
+ }}
+ />
+
+
+
+ {/* Place on Calendar Modal (Headless UI Dialog) */}
+
+ setShowPlaceModal(false)}>
+
+
+
+
+
+ {/* spacer to center */}
+
+
+
+
+
+ Schedule Urgent Case
+ setShowPlaceModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
+ Configure the scheduling details for urgent case {selectedCaseId}
+
+
+ {/* form grid */}
+
+ {/* Order ID (readonly) */}
+
+ Order ID
+
+
+
+ {/* Thickness */}
+
+ Thickness
+ setPlaceForm((p) => ({ ...p, thickness: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ {thicknessOptions.map((t) => (
+ {t}
+ ))}
+
+
+
+ {/* Requested Delivery Date (readonly) */}
+
+ Requested Delivery Date
+
+
+
+ {/* Proposed Furnace Slot -> handled by datetime-local */}
+
+ Proposed Furnace Slot (Date & time)
+ setPlaceForm((p) => ({ ...p, datetime: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ />
+
+
+ {/* Priority */}
+
+ Priority
+ setPlaceForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+ {/* Notes */}
+
+ Notes
+ setPlaceForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+ {/* buttons */}
+
+ setShowPlaceModal(false)}
+ className="px-4 py-2 rounded-md border bg-white text-sm"
+ >
+ Cancel
+
+
+ Save as Pending
+
+
+ Approve & Place
+
+
+
+
+
+
+
+
+ {/* New Case Modal */}
+
+ setShowNewModal(false)}>
+
+
+
+
+
+
+
+
+
+
+ New Urgent Case
+ setShowNewModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
Create a new urgent case
+
+
+
+ Case Name
+ setNewForm((p) => ({ ...p, name: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ placeholder="e.g. UC-1028"
+ />
+
+
+
+ Category
+ setNewForm((p) => ({ ...p, category: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ TH
+
+
+
+
+ Priority
+ setNewForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+
+ Notes
+ setNewForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+
+ setShowNewModal(false)} className="px-4 py-2 rounded-md border bg-white text-sm">Cancel
+ Save as Pending
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/task/tl/index.jsx b/src/pages/task/tl/index.jsx
new file mode 100644
index 0000000..5422efa
--- /dev/null
+++ b/src/pages/task/tl/index.jsx
@@ -0,0 +1,732 @@
+import React, { useState, useRef, useEffect, Fragment } from "react";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
+import { Dialog, Transition } from "@headlessui/react";
+import { Plus, Calendar, Search, X } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data";
+import { useNavigate } from "react-router-dom";
+
+/**
+ * Single-file component: CaseScheduler
+ *
+ * Requirements:
+ * - Headless UI Dialog for modals
+ * - for datetime selection
+ * - FullCalendar with external draggable items (Draggable)
+ *
+ * Note: Tailwind CSS classes used for styling.
+ */
+
+const priorityColorMap = {
+ High: "#ef4444", // red
+ Medium: "#f97316", // orange
+ Low: "#3b82f6", // blue
+};
+
+const thicknessOptions = ["5mm", "6mm", "8mm", "10mm", "12mm"];
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+export default function CaseScheduler() {
+ const navigate = useNavigate();
+ const [cases, setCases] = useState(initialCasesSeed);
+ const [events, setEvents] = useState([]);
+ const [filter, setFilter] = useState("");
+ const [category, setCategory] = useState("TL");
+ const [calendarView, setCalendarView] = useState("timeGridWeek");
+
+ // Modals state
+ const [showPlaceModal, setShowPlaceModal] = useState(false);
+ const [showNewModal, setShowNewModal] = useState(false);
+
+ // Selected case for Place modal
+ const [selectedCaseId, setSelectedCaseId] = useState(null);
+ const [placeForm, setPlaceForm] = useState({
+ order: "",
+ thickness: "8mm",
+ requested: "",
+ datetime: "", // ISO-like: "2025-10-04T09:00"
+ priority: "Medium",
+ notes: "",
+ });
+
+ // New case form
+ const [newForm, setNewForm] = useState({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+
+ const calendarRef = useRef();
+
+ // filtered list
+ const filteredCases = cases.filter((c) => {
+ const matchCat = c.category === category;
+ const text = (filter || "").toLowerCase();
+ const matchSearch =
+ c.id.toLowerCase().includes(text) ||
+ (c.name || "").toLowerCase().includes(text) ||
+ (c.type || "").toLowerCase().includes(text) ||
+ (c.sales || "").toLowerCase().includes(text) ||
+ (c.description || "").toLowerCase().includes(text);
+
+ // Hide rejected and scheduled from list
+ const visibleStatus = c.status !== "Rejected" && c.status !== "Scheduled";
+
+ return matchCat && matchSearch && visibleStatus;
+ });
+
+ // Preload scheduled events based on data
+ useEffect(() => {
+ const scheduledEvents = initialCasesSeed
+ .filter((c) => c.scheduled && c.category === category)
+ .map((c) => ({
+ id: c.id + "-" + new Date(c.scheduled).getTime(),
+ title: `${c.id} - ${c.name}`,
+ start: new Date(c.scheduled),
+ end: new Date(new Date(c.scheduled).getTime() + 2 * 60 * 60 * 1000),
+ backgroundColor: priorityColorMap[c.priority],
+ borderColor: priorityColorMap[c.priority],
+ }));
+ setEvents(scheduledEvents);
+
+ // also make sure their status is "Scheduled"
+ setCases((prev) =>
+ prev.map((c) =>
+ c.scheduled ? { ...c, status: "Scheduled" } : c
+ )
+ );
+ }, []);
+
+ // Setup FullCalendar Draggable for external elements.
+ useEffect(() => {
+ const containerEl = document.getElementById("external-cases");
+ if (!containerEl) return;
+
+ // clean old draggable if exists
+ if (containerEl._draggableInstance) {
+ containerEl._draggableInstance.destroy();
+ }
+
+ const draggable = new Draggable(containerEl, {
+ itemSelector: ".fc-draggable",
+ eventData: (eventEl) => {
+ const id = eventEl.getAttribute("data-id");
+ const found = cases.find((c) => c.id === id);
+ if (!found || found.status === "Rejected") return null;
+ return {
+ id: found.id,
+ title: `${found.id} - ${found.name}`,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ },
+ });
+
+ containerEl._draggableInstance = draggable;
+
+ return () => {
+ draggable.destroy && draggable.destroy();
+ };
+ }, [cases]);
+
+
+ // Handler for external item dropped on calendar
+ // FullCalendar uses `drop` for external elements. The `info` contains .date and .draggedEl
+ const handleExternalDrop = (info) => {
+ const el = info.draggedEl;
+ const caseId = el?.getAttribute("data-id");
+ if (!caseId) return;
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === caseId
+ ? { ...c, status: "Scheduled", color: priorityColorMap[c.priority] }
+ : c
+ )
+ );
+
+ const start = info.date;
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // 2 hours duration
+ const newEvent = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ setEvents((prev) => [...prev, newEvent]);
+ };
+
+
+ // Open Place modal for a specific case (from Place on Calendar button)
+ function openPlaceModal(caseId) {
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+ setSelectedCaseId(caseId);
+ setPlaceForm({
+ order: found.order || "",
+ thickness: found.thickness || "8mm",
+ requested: found.requested || "",
+ datetime: "", // user picks
+ priority: found.priority || "Medium",
+ notes: found.notes || "",
+ });
+ setShowPlaceModal(true);
+ }
+
+ // Approve & Place from the modal
+ function approveAndPlace() {
+ if (!selectedCaseId) return;
+ const found = cases.find((c) => c.id === selectedCaseId);
+ if (!found) return;
+
+ // update case to Scheduled
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? { ...c, status: "Scheduled", priority: placeForm.priority, notes: placeForm.notes, thickness: placeForm.thickness }
+ : c
+ )
+ );
+
+ // add event at selected datetime
+ if (!placeForm.datetime) {
+ // if no datetime supplied, close modal and just mark scheduled
+ setShowPlaceModal(false);
+ return;
+ }
+
+ const start = new Date(placeForm.datetime);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
+
+ const ev = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ borderColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ };
+ setEvents((prev) => [...prev, ev]);
+ setShowPlaceModal(false);
+ }
+
+ // Save as pending (update case fields)
+ function savePlaceAsPending() {
+ if (!selectedCaseId) return;
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? {
+ ...c,
+ priority: placeForm.priority,
+ notes: placeForm.notes,
+ thickness: placeForm.thickness,
+ // keep status as Pending unless it was Approved/Scheduled
+ }
+ : c
+ )
+ );
+ setShowPlaceModal(false);
+ }
+
+ // Open New Case modal
+ function openNewModal() {
+ setNewForm({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+ setShowNewModal(true);
+ }
+
+ // Save new case as pending
+ function saveNewAsPending() {
+ // create a simple UC- id by incrementing
+ const maxNum = cases.reduce((max, c) => {
+ const m = c.id && c.id.startsWith("UC-") ? parseInt(c.id.split("-")[1]) || 0 : 0;
+ return Math.max(max, m);
+ }, 1026);
+ const newIdNum = maxNum + 1;
+ const newId = `UC-${newIdNum}`;
+
+ const newCase = {
+ id: newId,
+ order: String(100000 + newIdNum), // fake order
+ category: newForm.category,
+ name: newForm.name || newId,
+ type: newForm.name || newId,
+ requested: "",
+ sales: "",
+ description: newForm.notes || "",
+ priority: newForm.priority,
+ status: "Pending",
+ color: priorityColorMap[newForm.priority],
+ notes: newForm.notes,
+ thickness: "8mm",
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setShowNewModal(false);
+
+ // Optionally auto-place if datetime provided? The user requested only Save as Pending for new case.
+ }
+
+ // Approve and place directly from list approve button
+ function handleApproveFromList(id) {
+ setCases((prev) => prev.map((c) => (c.id === id ? { ...c, status: "Approved" } : c)));
+ }
+
+ function handleRejectFromList(id) {
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === id ? { ...c, status: "Rejected" } : c
+ )
+ );
+ }
+
+ // Place on calendar via button in list: open place modal
+ // Also implement drag behavior: pending or approved should be draggable (Draggable.filter above handles rejection)
+ // For "Place on Calendar" button in the list we open the modal
+ // The modal's Approve & Place will add event at chosen datetime
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/tasks/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Left panel */}
+
+
+
Urgent Cases ({filteredCases.length})
+
+ New Case
+
+
+
+ {/* Search */}
+
+
+ setFilter(e.target.value)}
+ />
+
+
+ {/* External cases container */}
+
+ {filteredCases.map((item) => (
+
+
+
+
{item.id}
+
Order {item.name}
+
+
+
+
+
+ {item.status}
+
+
+
+ {item.priority}
+
+
+
+
+
+
{item.type}
+
Item: {item.order_details}
+
Requested: {item.requested}
+
{item.description}
+
+ {/* Buttons: Approve/Reject (40% width) or Place on Calendar */}
+
+ {item.status === "Pending" && (
+ <>
+
handleApproveFromList(item.id)}
+ className="w-[40%] bg-green-600 hover:bg-green-700 text-white py-1 rounded-lg text-sm"
+ >
+ Approve
+
+
handleRejectFromList(item.id)}
+ className="w-[40%] text-red-600 border border-red-700 hover:bg-red-100 py-1 rounded-lg text-sm"
+ >
+ Reject
+
+ >
+ )}
+
+ {item.status === "Approved" && (
+
openPlaceModal(item.id)}
+ className="w-[40%] text-blue-600 border border-blue-200 py-1 rounded-lg text-sm hover:bg-blue-50"
+ >
+ Place on Calendar
+
+ )}
+
+ {/* If Scheduled show small label */}
+ {item.status === "Scheduled" && (
+
Scheduled
+ )}
+
+
+ ))}
+
+
+
+ {/* Right panel (calendar) */}
+
+
+
Production Calendar
+
+ {
+ setCalendarView(e.target.value);
+ const api = calendarRef.current?.getApi();
+ api?.changeView(e.target.value);
+ }}
+ className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Day
+ Week
+ Month
+
+
+
+
+
{
+ // simple event click: maybe show details or allow removal
+ // optional: remove event
+ }}
+ height="80vh"
+ slotMinTime="08:00:00"
+ slotMaxTime="18:00:00"
+ eventContent={(arg) => {
+ // simple content - title only
+ return (
+
+ );
+ }}
+ />
+
+
+
+ {/* Place on Calendar Modal (Headless UI Dialog) */}
+
+ setShowPlaceModal(false)}>
+
+
+
+
+
+ {/* spacer to center */}
+
+
+
+
+
+ Schedule Urgent Case
+ setShowPlaceModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
+ Configure the scheduling details for urgent case {selectedCaseId}
+
+
+ {/* form grid */}
+
+ {/* Order ID (readonly) */}
+
+ Order ID
+
+
+
+ {/* Thickness */}
+
+ Thickness
+ setPlaceForm((p) => ({ ...p, thickness: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ {thicknessOptions.map((t) => (
+ {t}
+ ))}
+
+
+
+ {/* Requested Delivery Date (readonly) */}
+
+ Requested Delivery Date
+
+
+
+ {/* Proposed Furnace Slot -> handled by datetime-local */}
+
+ Proposed Furnace Slot (Date & time)
+ setPlaceForm((p) => ({ ...p, datetime: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ />
+
+
+ {/* Priority */}
+
+ Priority
+ setPlaceForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+ {/* Notes */}
+
+ Notes
+ setPlaceForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+ {/* buttons */}
+
+ setShowPlaceModal(false)}
+ className="px-4 py-2 rounded-md border bg-white text-sm"
+ >
+ Cancel
+
+
+ Save as Pending
+
+
+ Approve & Place
+
+
+
+
+
+
+
+
+ {/* New Case Modal */}
+
+ setShowNewModal(false)}>
+
+
+
+
+
+
+
+
+
+
+ New Urgent Case
+ setShowNewModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
Create a new urgent case
+
+
+
+ Case Name
+ setNewForm((p) => ({ ...p, name: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ placeholder="e.g. UC-1028"
+ />
+
+
+
+ Category
+ setNewForm((p) => ({ ...p, category: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ TL
+
+
+
+
+ Priority
+ setNewForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+
+ Notes
+ setNewForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+
+ setShowNewModal(false)} className="px-4 py-2 rounded-md border bg-white text-sm">Cancel
+ Save as Pending
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/task/tp/index.jsx b/src/pages/task/tp/index.jsx
new file mode 100644
index 0000000..313c3cd
--- /dev/null
+++ b/src/pages/task/tp/index.jsx
@@ -0,0 +1,732 @@
+import React, { useState, useRef, useEffect, Fragment } from "react";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
+import { Dialog, Transition } from "@headlessui/react";
+import { Plus, Calendar, Search, X } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data";
+import { useNavigate } from "react-router-dom";
+
+/**
+ * Single-file component: CaseScheduler
+ *
+ * Requirements:
+ * - Headless UI Dialog for modals
+ * - for datetime selection
+ * - FullCalendar with external draggable items (Draggable)
+ *
+ * Note: Tailwind CSS classes used for styling.
+ */
+
+const priorityColorMap = {
+ High: "#ef4444", // red
+ Medium: "#f97316", // orange
+ Low: "#3b82f6", // blue
+};
+
+const thicknessOptions = ["5mm", "6mm", "8mm", "10mm", "12mm"];
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+export default function CaseScheduler() {
+ const navigate = useNavigate();
+ const [cases, setCases] = useState(initialCasesSeed);
+ const [events, setEvents] = useState([]);
+ const [filter, setFilter] = useState("");
+ const [category, setCategory] = useState("TP");
+ const [calendarView, setCalendarView] = useState("timeGridWeek");
+
+ // Modals state
+ const [showPlaceModal, setShowPlaceModal] = useState(false);
+ const [showNewModal, setShowNewModal] = useState(false);
+
+ // Selected case for Place modal
+ const [selectedCaseId, setSelectedCaseId] = useState(null);
+ const [placeForm, setPlaceForm] = useState({
+ order: "",
+ thickness: "8mm",
+ requested: "",
+ datetime: "", // ISO-like: "2025-10-04T09:00"
+ priority: "Medium",
+ notes: "",
+ });
+
+ // New case form
+ const [newForm, setNewForm] = useState({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+
+ const calendarRef = useRef();
+
+ // filtered list
+ const filteredCases = cases.filter((c) => {
+ const matchCat = c.category === category;
+ const text = (filter || "").toLowerCase();
+ const matchSearch =
+ c.id.toLowerCase().includes(text) ||
+ (c.name || "").toLowerCase().includes(text) ||
+ (c.type || "").toLowerCase().includes(text) ||
+ (c.sales || "").toLowerCase().includes(text) ||
+ (c.description || "").toLowerCase().includes(text);
+
+ // Hide rejected and scheduled from list
+ const visibleStatus = c.status !== "Rejected" && c.status !== "Scheduled";
+
+ return matchCat && matchSearch && visibleStatus;
+ });
+
+ // Preload scheduled events based on data
+ useEffect(() => {
+ const scheduledEvents = initialCasesSeed
+ .filter((c) => c.scheduled && c.category === category)
+ .map((c) => ({
+ id: c.id + "-" + new Date(c.scheduled).getTime(),
+ title: `${c.id} - ${c.name}`,
+ start: new Date(c.scheduled),
+ end: new Date(new Date(c.scheduled).getTime() + 2 * 60 * 60 * 1000),
+ backgroundColor: priorityColorMap[c.priority],
+ borderColor: priorityColorMap[c.priority],
+ }));
+ setEvents(scheduledEvents);
+
+ // also make sure their status is "Scheduled"
+ setCases((prev) =>
+ prev.map((c) =>
+ c.scheduled ? { ...c, status: "Scheduled" } : c
+ )
+ );
+ }, []);
+
+ // Setup FullCalendar Draggable for external elements.
+ useEffect(() => {
+ const containerEl = document.getElementById("external-cases");
+ if (!containerEl) return;
+
+ // clean old draggable if exists
+ if (containerEl._draggableInstance) {
+ containerEl._draggableInstance.destroy();
+ }
+
+ const draggable = new Draggable(containerEl, {
+ itemSelector: ".fc-draggable",
+ eventData: (eventEl) => {
+ const id = eventEl.getAttribute("data-id");
+ const found = cases.find((c) => c.id === id);
+ if (!found || found.status === "Rejected") return null;
+ return {
+ id: found.id,
+ title: `${found.id} - ${found.name}`,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ },
+ });
+
+ containerEl._draggableInstance = draggable;
+
+ return () => {
+ draggable.destroy && draggable.destroy();
+ };
+ }, [cases]);
+
+
+ // Handler for external item dropped on calendar
+ // FullCalendar uses `drop` for external elements. The `info` contains .date and .draggedEl
+ const handleExternalDrop = (info) => {
+ const el = info.draggedEl;
+ const caseId = el?.getAttribute("data-id");
+ if (!caseId) return;
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === caseId
+ ? { ...c, status: "Scheduled", color: priorityColorMap[c.priority] }
+ : c
+ )
+ );
+
+ const start = info.date;
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // 2 hours duration
+ const newEvent = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[found.priority],
+ borderColor: priorityColorMap[found.priority],
+ };
+ setEvents((prev) => [...prev, newEvent]);
+ };
+
+
+ // Open Place modal for a specific case (from Place on Calendar button)
+ function openPlaceModal(caseId) {
+ const found = cases.find((c) => c.id === caseId);
+ if (!found) return;
+ setSelectedCaseId(caseId);
+ setPlaceForm({
+ order: found.order || "",
+ thickness: found.thickness || "8mm",
+ requested: found.requested || "",
+ datetime: "", // user picks
+ priority: found.priority || "Medium",
+ notes: found.notes || "",
+ });
+ setShowPlaceModal(true);
+ }
+
+ // Approve & Place from the modal
+ function approveAndPlace() {
+ if (!selectedCaseId) return;
+ const found = cases.find((c) => c.id === selectedCaseId);
+ if (!found) return;
+
+ // update case to Scheduled
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? { ...c, status: "Scheduled", priority: placeForm.priority, notes: placeForm.notes, thickness: placeForm.thickness }
+ : c
+ )
+ );
+
+ // add event at selected datetime
+ if (!placeForm.datetime) {
+ // if no datetime supplied, close modal and just mark scheduled
+ setShowPlaceModal(false);
+ return;
+ }
+
+ const start = new Date(placeForm.datetime);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
+
+ const ev = {
+ id: found.id + "-" + start.getTime(),
+ title: `${found.id} - ${found.name}`,
+ start,
+ end,
+ backgroundColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ borderColor: priorityColorMap[placeForm.priority] || "#3b82f6",
+ };
+ setEvents((prev) => [...prev, ev]);
+ setShowPlaceModal(false);
+ }
+
+ // Save as pending (update case fields)
+ function savePlaceAsPending() {
+ if (!selectedCaseId) return;
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === selectedCaseId
+ ? {
+ ...c,
+ priority: placeForm.priority,
+ notes: placeForm.notes,
+ thickness: placeForm.thickness,
+ // keep status as Pending unless it was Approved/Scheduled
+ }
+ : c
+ )
+ );
+ setShowPlaceModal(false);
+ }
+
+ // Open New Case modal
+ function openNewModal() {
+ setNewForm({
+ name: "",
+ category: category,
+ priority: "Medium",
+ notes: "",
+ datetime: "",
+ });
+ setShowNewModal(true);
+ }
+
+ // Save new case as pending
+ function saveNewAsPending() {
+ // create a simple UC- id by incrementing
+ const maxNum = cases.reduce((max, c) => {
+ const m = c.id && c.id.startsWith("UC-") ? parseInt(c.id.split("-")[1]) || 0 : 0;
+ return Math.max(max, m);
+ }, 1026);
+ const newIdNum = maxNum + 1;
+ const newId = `UC-${newIdNum}`;
+
+ const newCase = {
+ id: newId,
+ order: String(100000 + newIdNum), // fake order
+ category: newForm.category,
+ name: newForm.name || newId,
+ type: newForm.name || newId,
+ requested: "",
+ sales: "",
+ description: newForm.notes || "",
+ priority: newForm.priority,
+ status: "Pending",
+ color: priorityColorMap[newForm.priority],
+ notes: newForm.notes,
+ thickness: "8mm",
+ };
+
+ setCases((prev) => [newCase, ...prev]);
+ setShowNewModal(false);
+
+ // Optionally auto-place if datetime provided? The user requested only Save as Pending for new case.
+ }
+
+ // Approve and place directly from list approve button
+ function handleApproveFromList(id) {
+ setCases((prev) => prev.map((c) => (c.id === id ? { ...c, status: "Approved" } : c)));
+ }
+
+ function handleRejectFromList(id) {
+ setCases((prev) =>
+ prev.map((c) =>
+ c.id === id ? { ...c, status: "Rejected" } : c
+ )
+ );
+ }
+
+ // Place on calendar via button in list: open place modal
+ // Also implement drag behavior: pending or approved should be draggable (Draggable.filter above handles rejection)
+ // For "Place on Calendar" button in the list we open the modal
+ // The modal's Approve & Place will add event at chosen datetime
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/tasks/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Left panel */}
+
+
+
Urgent Cases ({filteredCases.length})
+
+ New Case
+
+
+
+ {/* Search */}
+
+
+ setFilter(e.target.value)}
+ />
+
+
+ {/* External cases container */}
+
+ {filteredCases.map((item) => (
+
+
+
+
{item.id}
+
Order {item.name}
+
+
+
+
+
+ {item.status}
+
+
+
+ {item.priority}
+
+
+
+
+
+
{item.type}
+
Item: {item.order_details}
+
Requested: {item.requested}
+
{item.description}
+
+ {/* Buttons: Approve/Reject (40% width) or Place on Calendar */}
+
+ {item.status === "Pending" && (
+ <>
+
handleApproveFromList(item.id)}
+ className="w-[40%] bg-green-600 hover:bg-green-700 text-white py-1 rounded-lg text-sm"
+ >
+ Approve
+
+
handleRejectFromList(item.id)}
+ className="w-[40%] text-red-600 border border-red-700 hover:bg-red-100 py-1 rounded-lg text-sm"
+ >
+ Reject
+
+ >
+ )}
+
+ {item.status === "Approved" && (
+
openPlaceModal(item.id)}
+ className="w-[40%] text-blue-600 border border-blue-200 py-1 rounded-lg text-sm hover:bg-blue-50"
+ >
+ Place on Calendar
+
+ )}
+
+ {/* If Scheduled show small label */}
+ {item.status === "Scheduled" && (
+
Scheduled
+ )}
+
+
+ ))}
+
+
+
+ {/* Right panel (calendar) */}
+
+
+
Production Calendar
+
+ {
+ setCalendarView(e.target.value);
+ const api = calendarRef.current?.getApi();
+ api?.changeView(e.target.value);
+ }}
+ className="border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Day
+ Week
+ Month
+
+
+
+
+
{
+ // simple event click: maybe show details or allow removal
+ // optional: remove event
+ }}
+ height="80vh"
+ slotMinTime="08:00:00"
+ slotMaxTime="18:00:00"
+ eventContent={(arg) => {
+ // simple content - title only
+ return (
+
+ );
+ }}
+ />
+
+
+
+ {/* Place on Calendar Modal (Headless UI Dialog) */}
+
+ setShowPlaceModal(false)}>
+
+
+
+
+
+ {/* spacer to center */}
+
+
+
+
+
+ Schedule Urgent Case
+ setShowPlaceModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
+ Configure the scheduling details for urgent case {selectedCaseId}
+
+
+ {/* form grid */}
+
+ {/* Order ID (readonly) */}
+
+ Order ID
+
+
+
+ {/* Thickness */}
+
+ Thickness
+ setPlaceForm((p) => ({ ...p, thickness: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ {thicknessOptions.map((t) => (
+ {t}
+ ))}
+
+
+
+ {/* Requested Delivery Date (readonly) */}
+
+ Requested Delivery Date
+
+
+
+ {/* Proposed Furnace Slot -> handled by datetime-local */}
+
+ Proposed Furnace Slot (Date & time)
+ setPlaceForm((p) => ({ ...p, datetime: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ />
+
+
+ {/* Priority */}
+
+ Priority
+ setPlaceForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+ {/* Notes */}
+
+ Notes
+ setPlaceForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+ {/* buttons */}
+
+ setShowPlaceModal(false)}
+ className="px-4 py-2 rounded-md border bg-white text-sm"
+ >
+ Cancel
+
+
+ Save as Pending
+
+
+ Approve & Place
+
+
+
+
+
+
+
+
+ {/* New Case Modal */}
+
+ setShowNewModal(false)}>
+
+
+
+
+
+
+
+
+
+
+ New Urgent Case
+ setShowNewModal(false)} className="text-gray-500 hover:text-gray-700">
+
+
+
+
+
Create a new urgent case
+
+
+
+ Case Name
+ setNewForm((p) => ({ ...p, name: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ placeholder="e.g. UC-1028"
+ />
+
+
+
+ Category
+ setNewForm((p) => ({ ...p, category: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ TP
+
+
+
+
+ Priority
+ setNewForm((p) => ({ ...p, priority: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ >
+ Low
+ Medium
+ High
+
+
+
+
+ Notes
+ setNewForm((p) => ({ ...p, notes: e.target.value }))}
+ className="mt-1 w-full rounded-md border px-3 py-2 bg-white text-sm"
+ rows={3}
+ />
+
+
+
+
+ setShowNewModal(false)} className="px-4 py-2 rounded-md border bg-white text-sm">Cancel
+ Save as Pending
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/taskboard/all/index.jsx b/src/pages/taskboard/all/index.jsx
new file mode 100644
index 0000000..03a116d
--- /dev/null
+++ b/src/pages/taskboard/all/index.jsx
@@ -0,0 +1,327 @@
+import React, { useState, useMemo } from "react";
+import DataTable from "react-data-table-component";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin from "@fullcalendar/interaction";
+import { Filter, Calendar } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data"; // ✅ import your seed file
+import { useNavigate } from "react-router-dom";
+
+// 🎨 Priority color mapping
+const priorityColorMap = {
+ High: "bg-red-100 text-red-800",
+ Medium: "bg-yellow-100 text-yellow-800",
+ Low: "bg-green-100 text-green-800",
+};
+
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+// 🎯 Inline Filter Component
+const InlineFilterComponent = ({ filters, setFilters, onClear }) => (
+
+ {/* Search */}
+
+ Search
+
+ setFilters((prev) => ({ ...prev, searchText: e.target.value }))
+ }
+ />
+
+
+ {/* Category */}
+
+ Category
+
+ setFilters((prev) => ({ ...prev, category: e.target.value }))
+ }
+ >
+ All
+ FLSS
+ TH
+ TP
+ LM
+ TL
+
+
+
+ {/* Priority */}
+
+ Priority
+
+ setFilters((prev) => ({ ...prev, priority: e.target.value }))
+ }
+ >
+ All
+ High
+ Medium
+ Low
+
+
+
+ {/* Status */}
+
+ Status
+
+ setFilters((prev) => ({ ...prev, status: e.target.value }))
+ }
+ >
+ All
+ Pending
+ Approved
+ Rejected
+ Scheduled
+
+
+
+ {/* Clear */}
+
+
+ Clear Filters
+
+
+
+);
+
+const Taskboard = () => {
+ const navigate = useNavigate();
+ const [data] = useState(initialCasesSeed);
+ const [filters, setFilters] = useState({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ });
+ const [showFilters, setShowFilters] = useState(false);
+ const [category, setCategoory] = useState("All");
+
+ const today = new Date().toISOString().split("T")[0]; // e.g. "2025-11-03"
+
+ // 🧠 Only tasks scheduled for today
+ const todayScheduled = useMemo(() => {
+ return data.filter(
+ (item) => item.scheduled && item.scheduled.startsWith(today)
+ );
+ }, [data, today]);
+
+ // 🧠 Apply filters
+ const filteredData = useMemo(() => {
+ return todayScheduled.filter((item) => {
+ const matchSearch =
+ item.name.toLowerCase().includes(filters.searchText.toLowerCase()) ||
+ item.id.toLowerCase().includes(filters.searchText.toLowerCase());
+ const matchCategory =
+ filters.category === "" || item.category === filters.category;
+ const matchPriority =
+ filters.priority === "" || item.priority === filters.priority;
+ const matchStatus =
+ filters.status === "" || item.status === filters.status;
+ return matchSearch && matchCategory && matchPriority && matchStatus;
+ });
+ }, [todayScheduled, filters]);
+
+ // 📊 Summary
+ const summary = useMemo(() => {
+ const total = filteredData.length;
+ const pending = filteredData.filter((x) => x.status === "Pending").length;
+ const approved = filteredData.filter((x) => x.status === "Approved").length;
+ const scheduled = filteredData.filter((x) => x.status === "Scheduled").length;
+ const rejected = filteredData.filter((x) => x.status === "Rejected").length;
+ return { total, pending, approved, scheduled, rejected };
+ }, [filteredData]);
+
+ // 🗓️ Calendar events (color by priority)
+ const calendarEvents = filteredData.map((task) => {
+ const start = new Date(task.scheduled);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // +2 hours
+
+ // 🎨 Priority color map for calendar
+ const priorityColor =
+ task.priority === "High"
+ ? "#ef4444" // red
+ : task.priority === "Medium"
+ ? "#f97316" // orange
+ : "#3b82f6"; // green (low)
+
+ return {
+ id: task.id,
+ title: `${task.id} - ${task.name}`,
+ start,
+ end,
+ color: priorityColor,
+ };
+ });
+
+ // 📋 DataTable
+ const columns = [
+ { name: "Case ID", selector: (row) => row.id, sortable: true, width: "120px" },
+ { name: "Category", selector: (row) => row.category, sortable: true, width: "120px" },
+ { name: "Name", selector: (row) => row.name, sortable: true, wrap: true },
+ {
+ name: "Scheduled Time",
+ selector: (row) => {
+ if (!row.scheduled) return "-";
+ const dateObj = new Date(row.scheduled);
+ const date = dateObj.toLocaleDateString("en-GB", {
+ year: "numeric",
+ month: "short",
+ day: "2-digit",
+ });
+ const time = dateObj.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+ return `${time}`;
+ },
+ sortable: true,
+ width: "200px",
+ },
+ {
+ name: "Priority",
+ selector: (row) => row.priority,
+ sortable: true,
+ width: "120px",
+ cell: (row) => (
+
+ {row.priority}
+
+ ),
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "#ffffff",
+ fontSize: "14px",
+ fontWeight: "600",
+ },
+ },
+ rows: {
+ style: {
+ fontSize: "14px",
+ "&:nth-of-type(odd)": { backgroundColor: "#f9fafb" },
+ },
+ },
+ };
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/taskboards/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Header */}
+
+
+ Scheduled Cases for Today ({today})
+
+ setShowFilters(!showFilters)}
+ className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 rounded-md hover:bg-blue-700 text-sm"
+ >
+
+ Filter
+
+
+
+ {/* Filter */}
+ {showFilters && (
+
+ setFilters({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ })
+ }
+ />
+ )}
+
+ {/* Table */}
+ No scheduled tasks for today
+ }
+ />
+
+ {/* Calendar */}
+
+
+
+
Today's Schedule
+
+
+
+
+
+
+ );
+};
+
+export default Taskboard;
diff --git a/src/pages/taskboard/flss/index.jsx b/src/pages/taskboard/flss/index.jsx
new file mode 100644
index 0000000..4dc1567
--- /dev/null
+++ b/src/pages/taskboard/flss/index.jsx
@@ -0,0 +1,313 @@
+import React, { useState, useMemo } from "react";
+import DataTable from "react-data-table-component";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin from "@fullcalendar/interaction";
+import { Filter, Calendar } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data"; // ✅ import your seed file
+import { useNavigate } from "react-router-dom";
+
+// 🎨 Priority color mapping
+const priorityColorMap = {
+ High: "bg-red-100 text-red-800",
+ Medium: "bg-yellow-100 text-yellow-800",
+ Low: "bg-green-100 text-green-800",
+};
+
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+// 🎯 Inline Filter Component
+const InlineFilterComponent = ({ filters, setFilters, onClear }) => (
+
+ {/* Search */}
+
+ Search
+
+ setFilters((prev) => ({ ...prev, searchText: e.target.value }))
+ }
+ />
+
+
+ {/* Priority */}
+
+ Priority
+
+ setFilters((prev) => ({ ...prev, priority: e.target.value }))
+ }
+ >
+ All
+ High
+ Medium
+ Low
+
+
+
+ {/* Status */}
+
+ Status
+
+ setFilters((prev) => ({ ...prev, status: e.target.value }))
+ }
+ >
+ All
+ Pending
+ Approved
+ Rejected
+ Scheduled
+
+
+
+ {/* Clear */}
+
+
+ Clear Filters
+
+
+
+);
+
+const Taskboard = () => {
+ const navigate = useNavigate();
+ const [data] = useState(initialCasesSeed);
+ const [filters, setFilters] = useState({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ });
+ const [showFilters, setShowFilters] = useState(false);
+ const [category, setCategoory] = useState("FLSS");
+
+ const today = new Date().toISOString().split("T")[0]; // e.g. "2025-11-03"
+
+ // 🧠 Only tasks scheduled for today
+ const todayScheduled = useMemo(() => {
+ return data.filter(
+ (item) => item.scheduled && item.scheduled.startsWith(today)
+ );
+ }, [data, today]);
+
+ // 🧠 Apply filters
+ const filteredData = useMemo(() => {
+ return todayScheduled.filter((item) => {
+ // ✅ Only include TH category
+ if (item.category !== "FLSS") return false;
+
+ const matchSearch =
+ item.name.toLowerCase().includes(filters.searchText.toLowerCase()) ||
+ item.id.toLowerCase().includes(filters.searchText.toLowerCase());
+
+ const matchPriority =
+ filters.priority === "" || item.priority === filters.priority;
+
+ const matchStatus =
+ filters.status === "" || item.status === filters.status;
+
+ return matchSearch && matchPriority && matchStatus;
+ });
+ }, [todayScheduled, filters]);
+
+
+ // 📊 Summary
+ const summary = useMemo(() => {
+ const total = filteredData.length;
+ const pending = filteredData.filter((x) => x.status === "Pending").length;
+ const approved = filteredData.filter((x) => x.status === "Approved").length;
+ const scheduled = filteredData.filter((x) => x.status === "Scheduled").length;
+ const rejected = filteredData.filter((x) => x.status === "Rejected").length;
+ return { total, pending, approved, scheduled, rejected };
+ }, [filteredData]);
+
+ // 🗓️ Calendar events (color by priority)
+ const calendarEvents = filteredData.map((task) => {
+ const start = new Date(task.scheduled);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // +2 hours
+
+ // 🎨 Priority color map for calendar
+ const priorityColor =
+ task.priority === "High"
+ ? "#ef4444" // red
+ : task.priority === "Medium"
+ ? "#f97316" // orange
+ : "#3b82f6"; // green (low)
+
+ return {
+ id: task.id,
+ title: `${task.id} - ${task.name}`,
+ start,
+ end,
+ color: priorityColor,
+ };
+ });
+
+ // 📋 DataTable
+ const columns = [
+ { name: "Case ID", selector: (row) => row.id, sortable: true, width: "120px" },
+ { name: "Category", selector: (row) => row.category, sortable: true, width: "120px" },
+ { name: "Name", selector: (row) => row.name, sortable: true, wrap: true },
+ {
+ name: "Scheduled Time",
+ selector: (row) => {
+ if (!row.scheduled) return "-";
+ const dateObj = new Date(row.scheduled);
+ const date = dateObj.toLocaleDateString("en-GB", {
+ year: "numeric",
+ month: "short",
+ day: "2-digit",
+ });
+ const time = dateObj.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+ return `${time}`;
+ },
+ sortable: true,
+ width: "200px",
+ },
+ {
+ name: "Priority",
+ selector: (row) => row.priority,
+ sortable: true,
+ width: "120px",
+ cell: (row) => (
+
+ {row.priority}
+
+ ),
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "#ffffff",
+ fontSize: "14px",
+ fontWeight: "600",
+ },
+ },
+ rows: {
+ style: {
+ fontSize: "14px",
+ "&:nth-of-type(odd)": { backgroundColor: "#f9fafb" },
+ },
+ },
+ };
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/taskboards/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Header */}
+
+
+ Scheduled Cases for Today ({today})
+
+ setShowFilters(!showFilters)}
+ className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 rounded-md hover:bg-blue-700 text-sm"
+ >
+
+ Filter
+
+
+
+ {/* Filter */}
+ {showFilters && (
+
+ setFilters({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ })
+ }
+ />
+ )}
+
+ {/* Table */}
+ No scheduled tasks for today
+ }
+ />
+
+ {/* Calendar */}
+
+
+
+
Today's Schedule
+
+
+
+
+
+
+ );
+};
+
+export default Taskboard;
diff --git a/src/pages/taskboard/lm/index.jsx b/src/pages/taskboard/lm/index.jsx
new file mode 100644
index 0000000..7e27dd0
--- /dev/null
+++ b/src/pages/taskboard/lm/index.jsx
@@ -0,0 +1,313 @@
+import React, { useState, useMemo } from "react";
+import DataTable from "react-data-table-component";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin from "@fullcalendar/interaction";
+import { Filter, Calendar } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data"; // ✅ import your seed file
+import { useNavigate } from "react-router-dom";
+
+// 🎨 Priority color mapping
+const priorityColorMap = {
+ High: "bg-red-100 text-red-800",
+ Medium: "bg-yellow-100 text-yellow-800",
+ Low: "bg-green-100 text-green-800",
+};
+
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+// 🎯 Inline Filter Component
+const InlineFilterComponent = ({ filters, setFilters, onClear }) => (
+
+ {/* Search */}
+
+ Search
+
+ setFilters((prev) => ({ ...prev, searchText: e.target.value }))
+ }
+ />
+
+
+ {/* Priority */}
+
+ Priority
+
+ setFilters((prev) => ({ ...prev, priority: e.target.value }))
+ }
+ >
+ All
+ High
+ Medium
+ Low
+
+
+
+ {/* Status */}
+
+ Status
+
+ setFilters((prev) => ({ ...prev, status: e.target.value }))
+ }
+ >
+ All
+ Pending
+ Approved
+ Rejected
+ Scheduled
+
+
+
+ {/* Clear */}
+
+
+ Clear Filters
+
+
+
+);
+
+const Taskboard = () => {
+ const navigate = useNavigate();
+ const [data] = useState(initialCasesSeed);
+ const [filters, setFilters] = useState({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ });
+ const [showFilters, setShowFilters] = useState(false);
+ const [category, setCategoory] = useState("LM");
+
+ const today = new Date().toISOString().split("T")[0]; // e.g. "2025-11-03"
+
+ // 🧠 Only tasks scheduled for today
+ const todayScheduled = useMemo(() => {
+ return data.filter(
+ (item) => item.scheduled && item.scheduled.startsWith(today)
+ );
+ }, [data, today]);
+
+ // 🧠 Apply filters
+ const filteredData = useMemo(() => {
+ return todayScheduled.filter((item) => {
+ // ✅ Only include TH category
+ if (item.category !== "LM") return false;
+
+ const matchSearch =
+ item.name.toLowerCase().includes(filters.searchText.toLowerCase()) ||
+ item.id.toLowerCase().includes(filters.searchText.toLowerCase());
+
+ const matchPriority =
+ filters.priority === "" || item.priority === filters.priority;
+
+ const matchStatus =
+ filters.status === "" || item.status === filters.status;
+
+ return matchSearch && matchPriority && matchStatus;
+ });
+ }, [todayScheduled, filters]);
+
+
+ // 📊 Summary
+ const summary = useMemo(() => {
+ const total = filteredData.length;
+ const pending = filteredData.filter((x) => x.status === "Pending").length;
+ const approved = filteredData.filter((x) => x.status === "Approved").length;
+ const scheduled = filteredData.filter((x) => x.status === "Scheduled").length;
+ const rejected = filteredData.filter((x) => x.status === "Rejected").length;
+ return { total, pending, approved, scheduled, rejected };
+ }, [filteredData]);
+
+ // 🗓️ Calendar events (color by priority)
+ const calendarEvents = filteredData.map((task) => {
+ const start = new Date(task.scheduled);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // +2 hours
+
+ // 🎨 Priority color map for calendar
+ const priorityColor =
+ task.priority === "High"
+ ? "#ef4444" // red
+ : task.priority === "Medium"
+ ? "#f97316" // orange
+ : "#3b82f6"; // green (low)
+
+ return {
+ id: task.id,
+ title: `${task.id} - ${task.name}`,
+ start,
+ end,
+ color: priorityColor,
+ };
+ });
+
+ // 📋 DataTable
+ const columns = [
+ { name: "Case ID", selector: (row) => row.id, sortable: true, width: "120px" },
+ { name: "Category", selector: (row) => row.category, sortable: true, width: "120px" },
+ { name: "Name", selector: (row) => row.name, sortable: true, wrap: true },
+ {
+ name: "Scheduled Time",
+ selector: (row) => {
+ if (!row.scheduled) return "-";
+ const dateObj = new Date(row.scheduled);
+ const date = dateObj.toLocaleDateString("en-GB", {
+ year: "numeric",
+ month: "short",
+ day: "2-digit",
+ });
+ const time = dateObj.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+ return `${time}`;
+ },
+ sortable: true,
+ width: "200px",
+ },
+ {
+ name: "Priority",
+ selector: (row) => row.priority,
+ sortable: true,
+ width: "120px",
+ cell: (row) => (
+
+ {row.priority}
+
+ ),
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "#ffffff",
+ fontSize: "14px",
+ fontWeight: "600",
+ },
+ },
+ rows: {
+ style: {
+ fontSize: "14px",
+ "&:nth-of-type(odd)": { backgroundColor: "#f9fafb" },
+ },
+ },
+ };
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/taskboards/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Header */}
+
+
+ Scheduled Cases for Today ({today})
+
+ setShowFilters(!showFilters)}
+ className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 rounded-md hover:bg-blue-700 text-sm"
+ >
+
+ Filter
+
+
+
+ {/* Filter */}
+ {showFilters && (
+
+ setFilters({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ })
+ }
+ />
+ )}
+
+ {/* Table */}
+ No scheduled tasks for today
+ }
+ />
+
+ {/* Calendar */}
+
+
+
+
Today's Schedule
+
+
+
+
+
+
+ );
+};
+
+export default Taskboard;
diff --git a/src/pages/taskboard/th/index.jsx b/src/pages/taskboard/th/index.jsx
new file mode 100644
index 0000000..8efec00
--- /dev/null
+++ b/src/pages/taskboard/th/index.jsx
@@ -0,0 +1,313 @@
+import React, { useState, useMemo } from "react";
+import DataTable from "react-data-table-component";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin from "@fullcalendar/interaction";
+import { Filter, Calendar } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data"; // ✅ import your seed file
+import { useNavigate } from "react-router-dom";
+
+// 🎨 Priority color mapping
+const priorityColorMap = {
+ High: "bg-red-100 text-red-800",
+ Medium: "bg-yellow-100 text-yellow-800",
+ Low: "bg-green-100 text-green-800",
+};
+
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+// 🎯 Inline Filter Component
+const InlineFilterComponent = ({ filters, setFilters, onClear }) => (
+
+ {/* Search */}
+
+ Search
+
+ setFilters((prev) => ({ ...prev, searchText: e.target.value }))
+ }
+ />
+
+
+ {/* Priority */}
+
+ Priority
+
+ setFilters((prev) => ({ ...prev, priority: e.target.value }))
+ }
+ >
+ All
+ High
+ Medium
+ Low
+
+
+
+ {/* Status */}
+
+ Status
+
+ setFilters((prev) => ({ ...prev, status: e.target.value }))
+ }
+ >
+ All
+ Pending
+ Approved
+ Rejected
+ Scheduled
+
+
+
+ {/* Clear */}
+
+
+ Clear Filters
+
+
+
+);
+
+const Taskboard = () => {
+ const navigate = useNavigate();
+ const [data] = useState(initialCasesSeed);
+ const [filters, setFilters] = useState({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ });
+ const [showFilters, setShowFilters] = useState(false);
+ const [category, setCategoory] = useState("TH");
+
+ const today = new Date().toISOString().split("T")[0]; // e.g. "2025-11-03"
+
+ // 🧠 Only tasks scheduled for today
+ const todayScheduled = useMemo(() => {
+ return data.filter(
+ (item) => item.scheduled && item.scheduled.startsWith(today)
+ );
+ }, [data, today]);
+
+ // 🧠 Apply filters
+ const filteredData = useMemo(() => {
+ return todayScheduled.filter((item) => {
+ // ✅ Only include TH category
+ if (item.category !== "TH") return false;
+
+ const matchSearch =
+ item.name.toLowerCase().includes(filters.searchText.toLowerCase()) ||
+ item.id.toLowerCase().includes(filters.searchText.toLowerCase());
+
+ const matchPriority =
+ filters.priority === "" || item.priority === filters.priority;
+
+ const matchStatus =
+ filters.status === "" || item.status === filters.status;
+
+ return matchSearch && matchPriority && matchStatus;
+ });
+ }, [todayScheduled, filters]);
+
+
+ // 📊 Summary
+ const summary = useMemo(() => {
+ const total = filteredData.length;
+ const pending = filteredData.filter((x) => x.status === "Pending").length;
+ const approved = filteredData.filter((x) => x.status === "Approved").length;
+ const scheduled = filteredData.filter((x) => x.status === "Scheduled").length;
+ const rejected = filteredData.filter((x) => x.status === "Rejected").length;
+ return { total, pending, approved, scheduled, rejected };
+ }, [filteredData]);
+
+ // 🗓️ Calendar events (color by priority)
+ const calendarEvents = filteredData.map((task) => {
+ const start = new Date(task.scheduled);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // +2 hours
+
+ // 🎨 Priority color map for calendar
+ const priorityColor =
+ task.priority === "High"
+ ? "#ef4444" // red
+ : task.priority === "Medium"
+ ? "#f97316" // orange
+ : "#3b82f6"; // green (low)
+
+ return {
+ id: task.id,
+ title: `${task.id} - ${task.name}`,
+ start,
+ end,
+ color: priorityColor,
+ };
+ });
+
+ // 📋 DataTable
+ const columns = [
+ { name: "Case ID", selector: (row) => row.id, sortable: true, width: "120px" },
+ { name: "Category", selector: (row) => row.category, sortable: true, width: "120px" },
+ { name: "Name", selector: (row) => row.name, sortable: true, wrap: true },
+ {
+ name: "Scheduled Time",
+ selector: (row) => {
+ if (!row.scheduled) return "-";
+ const dateObj = new Date(row.scheduled);
+ const date = dateObj.toLocaleDateString("en-GB", {
+ year: "numeric",
+ month: "short",
+ day: "2-digit",
+ });
+ const time = dateObj.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+ return `${time}`;
+ },
+ sortable: true,
+ width: "200px",
+ },
+ {
+ name: "Priority",
+ selector: (row) => row.priority,
+ sortable: true,
+ width: "120px",
+ cell: (row) => (
+
+ {row.priority}
+
+ ),
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "#ffffff",
+ fontSize: "14px",
+ fontWeight: "600",
+ },
+ },
+ rows: {
+ style: {
+ fontSize: "14px",
+ "&:nth-of-type(odd)": { backgroundColor: "#f9fafb" },
+ },
+ },
+ };
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/taskboards/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Header */}
+
+
+ Scheduled Cases for Today ({today})
+
+ setShowFilters(!showFilters)}
+ className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 rounded-md hover:bg-blue-700 text-sm"
+ >
+
+ Filter
+
+
+
+ {/* Filter */}
+ {showFilters && (
+
+ setFilters({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ })
+ }
+ />
+ )}
+
+ {/* Table */}
+ No scheduled tasks for today
+ }
+ />
+
+ {/* Calendar */}
+
+
+
+
Today's Schedule
+
+
+
+
+
+
+ );
+};
+
+export default Taskboard;
diff --git a/src/pages/taskboard/tl/index.jsx b/src/pages/taskboard/tl/index.jsx
new file mode 100644
index 0000000..74fe830
--- /dev/null
+++ b/src/pages/taskboard/tl/index.jsx
@@ -0,0 +1,313 @@
+import React, { useState, useMemo } from "react";
+import DataTable from "react-data-table-component";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin from "@fullcalendar/interaction";
+import { Filter, Calendar } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data"; // ✅ import your seed file
+import { useNavigate } from "react-router-dom";
+
+// 🎨 Priority color mapping
+const priorityColorMap = {
+ High: "bg-red-100 text-red-800",
+ Medium: "bg-yellow-100 text-yellow-800",
+ Low: "bg-green-100 text-green-800",
+};
+
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+// 🎯 Inline Filter Component
+const InlineFilterComponent = ({ filters, setFilters, onClear }) => (
+
+ {/* Search */}
+
+ Search
+
+ setFilters((prev) => ({ ...prev, searchText: e.target.value }))
+ }
+ />
+
+
+ {/* Priority */}
+
+ Priority
+
+ setFilters((prev) => ({ ...prev, priority: e.target.value }))
+ }
+ >
+ All
+ High
+ Medium
+ Low
+
+
+
+ {/* Status */}
+
+ Status
+
+ setFilters((prev) => ({ ...prev, status: e.target.value }))
+ }
+ >
+ All
+ Pending
+ Approved
+ Rejected
+ Scheduled
+
+
+
+ {/* Clear */}
+
+
+ Clear Filters
+
+
+
+);
+
+const Taskboard = () => {
+ const navigate = useNavigate();
+ const [data] = useState(initialCasesSeed);
+ const [filters, setFilters] = useState({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ });
+ const [showFilters, setShowFilters] = useState(false);
+ const [category, setCategoory] = useState("TL");
+
+ const today = new Date().toISOString().split("T")[0]; // e.g. "2025-11-03"
+
+ // 🧠 Only tasks scheduled for today
+ const todayScheduled = useMemo(() => {
+ return data.filter(
+ (item) => item.scheduled && item.scheduled.startsWith(today)
+ );
+ }, [data, today]);
+
+ // 🧠 Apply filters
+ const filteredData = useMemo(() => {
+ return todayScheduled.filter((item) => {
+ // ✅ Only include TH category
+ if (item.category !== "TL") return false;
+
+ const matchSearch =
+ item.name.toLowerCase().includes(filters.searchText.toLowerCase()) ||
+ item.id.toLowerCase().includes(filters.searchText.toLowerCase());
+
+ const matchPriority =
+ filters.priority === "" || item.priority === filters.priority;
+
+ const matchStatus =
+ filters.status === "" || item.status === filters.status;
+
+ return matchSearch && matchPriority && matchStatus;
+ });
+ }, [todayScheduled, filters]);
+
+
+ // 📊 Summary
+ const summary = useMemo(() => {
+ const total = filteredData.length;
+ const pending = filteredData.filter((x) => x.status === "Pending").length;
+ const approved = filteredData.filter((x) => x.status === "Approved").length;
+ const scheduled = filteredData.filter((x) => x.status === "Scheduled").length;
+ const rejected = filteredData.filter((x) => x.status === "Rejected").length;
+ return { total, pending, approved, scheduled, rejected };
+ }, [filteredData]);
+
+ // 🗓️ Calendar events (color by priority)
+ const calendarEvents = filteredData.map((task) => {
+ const start = new Date(task.scheduled);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // +2 hours
+
+ // 🎨 Priority color map for calendar
+ const priorityColor =
+ task.priority === "High"
+ ? "#ef4444" // red
+ : task.priority === "Medium"
+ ? "#f97316" // orange
+ : "#3b82f6"; // green (low)
+
+ return {
+ id: task.id,
+ title: `${task.id} - ${task.name}`,
+ start,
+ end,
+ color: priorityColor,
+ };
+ });
+
+ // 📋 DataTable
+ const columns = [
+ { name: "Case ID", selector: (row) => row.id, sortable: true, width: "120px" },
+ { name: "Category", selector: (row) => row.category, sortable: true, width: "120px" },
+ { name: "Name", selector: (row) => row.name, sortable: true, wrap: true },
+ {
+ name: "Scheduled Time",
+ selector: (row) => {
+ if (!row.scheduled) return "-";
+ const dateObj = new Date(row.scheduled);
+ const date = dateObj.toLocaleDateString("en-GB", {
+ year: "numeric",
+ month: "short",
+ day: "2-digit",
+ });
+ const time = dateObj.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+ return `${time}`;
+ },
+ sortable: true,
+ width: "200px",
+ },
+ {
+ name: "Priority",
+ selector: (row) => row.priority,
+ sortable: true,
+ width: "120px",
+ cell: (row) => (
+
+ {row.priority}
+
+ ),
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "#ffffff",
+ fontSize: "14px",
+ fontWeight: "600",
+ },
+ },
+ rows: {
+ style: {
+ fontSize: "14px",
+ "&:nth-of-type(odd)": { backgroundColor: "#f9fafb" },
+ },
+ },
+ };
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/taskboards/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Header */}
+
+
+ Scheduled Cases for Today ({today})
+
+ setShowFilters(!showFilters)}
+ className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 rounded-md hover:bg-blue-700 text-sm"
+ >
+
+ Filter
+
+
+
+ {/* Filter */}
+ {showFilters && (
+
+ setFilters({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ })
+ }
+ />
+ )}
+
+ {/* Table */}
+ No scheduled tasks for today
+ }
+ />
+
+ {/* Calendar */}
+
+
+
+
Today's Schedule
+
+
+
+
+
+
+ );
+};
+
+export default Taskboard;
diff --git a/src/pages/taskboard/tp/index.jsx b/src/pages/taskboard/tp/index.jsx
new file mode 100644
index 0000000..4f27583
--- /dev/null
+++ b/src/pages/taskboard/tp/index.jsx
@@ -0,0 +1,313 @@
+import React, { useState, useMemo } from "react";
+import DataTable from "react-data-table-component";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin from "@fullcalendar/interaction";
+import { Filter, Calendar } from "lucide-react";
+import { initialCasesSeed } from "@/constant/data"; // ✅ import your seed file
+import { useNavigate } from "react-router-dom";
+
+// 🎨 Priority color mapping
+const priorityColorMap = {
+ High: "bg-red-100 text-red-800",
+ Medium: "bg-yellow-100 text-yellow-800",
+ Low: "bg-green-100 text-green-800",
+};
+
+const categoryList = ["All", "TH", "TL", "FLSS", "LM", "TP"];
+
+// 🎯 Inline Filter Component
+const InlineFilterComponent = ({ filters, setFilters, onClear }) => (
+
+ {/* Search */}
+
+ Search
+
+ setFilters((prev) => ({ ...prev, searchText: e.target.value }))
+ }
+ />
+
+
+ {/* Priority */}
+
+ Priority
+
+ setFilters((prev) => ({ ...prev, priority: e.target.value }))
+ }
+ >
+ All
+ High
+ Medium
+ Low
+
+
+
+ {/* Status */}
+
+ Status
+
+ setFilters((prev) => ({ ...prev, status: e.target.value }))
+ }
+ >
+ All
+ Pending
+ Approved
+ Rejected
+ Scheduled
+
+
+
+ {/* Clear */}
+
+
+ Clear Filters
+
+
+
+);
+
+const Taskboard = () => {
+ const navigate = useNavigate();
+ const [data] = useState(initialCasesSeed);
+ const [filters, setFilters] = useState({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ });
+ const [showFilters, setShowFilters] = useState(false);
+ const [category, setCategoory] = useState("TP");
+
+ const today = new Date().toISOString().split("T")[0]; // e.g. "2025-11-03"
+
+ // 🧠 Only tasks scheduled for today
+ const todayScheduled = useMemo(() => {
+ return data.filter(
+ (item) => item.scheduled && item.scheduled.startsWith(today)
+ );
+ }, [data, today]);
+
+ // 🧠 Apply filters
+ const filteredData = useMemo(() => {
+ return todayScheduled.filter((item) => {
+ // ✅ Only include TH category
+ if (item.category !== "TP") return false;
+
+ const matchSearch =
+ item.name.toLowerCase().includes(filters.searchText.toLowerCase()) ||
+ item.id.toLowerCase().includes(filters.searchText.toLowerCase());
+
+ const matchPriority =
+ filters.priority === "" || item.priority === filters.priority;
+
+ const matchStatus =
+ filters.status === "" || item.status === filters.status;
+
+ return matchSearch && matchPriority && matchStatus;
+ });
+ }, [todayScheduled, filters]);
+
+
+ // 📊 Summary
+ const summary = useMemo(() => {
+ const total = filteredData.length;
+ const pending = filteredData.filter((x) => x.status === "Pending").length;
+ const approved = filteredData.filter((x) => x.status === "Approved").length;
+ const scheduled = filteredData.filter((x) => x.status === "Scheduled").length;
+ const rejected = filteredData.filter((x) => x.status === "Rejected").length;
+ return { total, pending, approved, scheduled, rejected };
+ }, [filteredData]);
+
+ // 🗓️ Calendar events (color by priority)
+ const calendarEvents = filteredData.map((task) => {
+ const start = new Date(task.scheduled);
+ const end = new Date(start.getTime() + 2 * 60 * 60 * 1000); // +2 hours
+
+ // 🎨 Priority color map for calendar
+ const priorityColor =
+ task.priority === "High"
+ ? "#ef4444" // red
+ : task.priority === "Medium"
+ ? "#f97316" // orange
+ : "#3b82f6"; // green (low)
+
+ return {
+ id: task.id,
+ title: `${task.id} - ${task.name}`,
+ start,
+ end,
+ color: priorityColor,
+ };
+ });
+
+ // 📋 DataTable
+ const columns = [
+ { name: "Case ID", selector: (row) => row.id, sortable: true, width: "120px" },
+ { name: "Category", selector: (row) => row.category, sortable: true, width: "120px" },
+ { name: "Name", selector: (row) => row.name, sortable: true, wrap: true },
+ {
+ name: "Scheduled Time",
+ selector: (row) => {
+ if (!row.scheduled) return "-";
+ const dateObj = new Date(row.scheduled);
+ const date = dateObj.toLocaleDateString("en-GB", {
+ year: "numeric",
+ month: "short",
+ day: "2-digit",
+ });
+ const time = dateObj.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
+ return `${time}`;
+ },
+ sortable: true,
+ width: "200px",
+ },
+ {
+ name: "Priority",
+ selector: (row) => row.priority,
+ sortable: true,
+ width: "120px",
+ cell: (row) => (
+
+ {row.priority}
+
+ ),
+ },
+ ];
+
+ const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: "#1A237E",
+ color: "#ffffff",
+ fontSize: "14px",
+ fontWeight: "600",
+ },
+ },
+ rows: {
+ style: {
+ fontSize: "14px",
+ "&:nth-of-type(odd)": { backgroundColor: "#f9fafb" },
+ },
+ },
+ };
+
+ return (
+
+
+ {categoryList.map((cat) => (
+
+ navigate(`/taskboards/${cat === "All" ? "all" : cat.toLowerCase()}`)
+ }
+ className={`text-center px-4 py-2 rounded-lg border font-medium text-sm transition
+ ${
+ category === cat
+ ? "bg-blue-600 text-white border-blue-600"
+ : "bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600"
+ }`}
+ >
+ {cat}
+
+ ))}
+
+
+
+ {/* Header */}
+
+
+ Scheduled Cases for Today ({today})
+
+ setShowFilters(!showFilters)}
+ className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 rounded-md hover:bg-blue-700 text-sm"
+ >
+
+ Filter
+
+
+
+ {/* Filter */}
+ {showFilters && (
+
+ setFilters({
+ searchText: "",
+ category: "",
+ priority: "",
+ status: "",
+ })
+ }
+ />
+ )}
+
+ {/* Table */}
+ No scheduled tasks for today
+ }
+ />
+
+ {/* Calendar */}
+
+
+
+
Today's Schedule
+
+
+
+
+
+
+ );
+};
+
+export default Taskboard;
diff --git a/src/pages/utility/coming-soon.jsx b/src/pages/utility/coming-soon.jsx
new file mode 100644
index 0000000..2f521b5
--- /dev/null
+++ b/src/pages/utility/coming-soon.jsx
@@ -0,0 +1,124 @@
+import React from "react";
+import Button from "@/components/ui/Button";
+import Icon from "@/components/ui/Icon";
+import { Link } from "react-router-dom";
+import useDarkMode from "@/hooks/useDarkMode";
+
+import LogoWhite from "@/assets/images/logo/logo-white.svg";
+import Logo from "@/assets/images/logo/logo.svg";
+import SvgImage from "@/assets/images/svg/img-1.svg";
+
+const ComingSoonPage = () => {
+ const [isDark] = useDarkMode();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Coming soon
+
+
+ Get notified when we launch
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+ eiusmod tempor incididunt.
+
+
+
+ *Don’t worry we will not spam you :
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ComingSoonPage;
diff --git a/src/pages/utility/profile.jsx b/src/pages/utility/profile.jsx
new file mode 100644
index 0000000..8f4ddc2
--- /dev/null
+++ b/src/pages/utility/profile.jsx
@@ -0,0 +1,138 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import Icon from "@/components/ui/Icon";
+import Card from "@/components/ui/Card";
+// import BasicArea from "../chart/appex-chart/BasicArea";
+
+// import images
+import ProfileImage from "@/assets/images/users/user-1.jpg";
+
+const profile = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Albert Flores
+
+
+ Front End Developer
+
+
+
+
+
+
+
+
+ $32,400
+
+
+ Total Balance
+
+
+
+
+
+ 200
+
+
+ Board Card
+
+
+
+
+
+ 3200
+
+
+ Calender Events
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LOCATION
+
+
+ Home# 320/N, Road# 71/B, Mohakhali, Dhaka-1207, Bangladesh
+
+
+
+
+
+
+ {/*
+
+
+
+
*/}
+
+
+
+ );
+};
+
+export default profile;
diff --git a/src/pages/utility/under-construction.jsx b/src/pages/utility/under-construction.jsx
new file mode 100644
index 0000000..4dfe9a9
--- /dev/null
+++ b/src/pages/utility/under-construction.jsx
@@ -0,0 +1,101 @@
+import React from "react";
+import Button from "@/components/ui/Button";
+import Icon from "@/components/ui/Icon";
+import { Link } from "react-router-dom";
+import useDarkMode from "@/hooks/useDarkMode";
+
+import LogoWhite from "@/assets/images/logo/logo-white.svg";
+import Logo from "@/assets/images/logo/logo.svg";
+import SvgImage from "@/assets/images/svg/img-2.svg";
+
+const UnderConstructionPage = () => {
+ const [isDark] = useDarkMode();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ We are under maintenance.
+
+
+ We’re making the system more awesome.
+ We’ll be back shortly.
+
+
+
+
+
+ );
+};
+
+export default UnderConstructionPage;
diff --git a/src/server/auth-server.js b/src/server/auth-server.js
new file mode 100644
index 0000000..0e23236
--- /dev/null
+++ b/src/server/auth-server.js
@@ -0,0 +1,92 @@
+const authServerConfig = (server) => {
+ server.post(
+ "/register",
+ (schema, request) => {
+ const requestData = JSON.parse(request.requestBody);
+ const { email, password } = requestData;
+
+ const existingUser = schema.users.findBy({ email });
+ if (existingUser) {
+ return new Response(400, {}, { error: "Email is already registered" });
+ }
+
+ const user = schema.users.create({
+ email,
+ password,
+ });
+
+ const token = "fake-token";
+
+ return {
+ token,
+ user: user.attrs,
+ };
+ }
+ // { timing: 2000 }
+ );
+
+ server.post(
+ "/login",
+ (schema, request) => {
+ const requestData = JSON.parse(request.requestBody);
+ const { username, email, password_hash, password } = requestData;
+
+ const loginField = username || email;
+ const loginPassword = password_hash || password;
+
+ if (!loginField || !loginPassword) {
+ return new Response(
+ 422,
+ {},
+ { error: 422, messages: { error: "Validation failed" }, status: 422 }
+ );
+ }
+
+ const user = schema.users.findBy({ email: loginField });
+
+ if (!user) {
+ return new Response(
+ 404,
+ {},
+ { error: 404, messages: { error: "User not found" }, status: 404 }
+ );
+ }
+
+ if (user.attrs.password !== loginPassword) {
+ return new Response(
+ 400,
+ {},
+ { error: 400, messages: { error: "Invalid username or password" }, status: 400 }
+ );
+ }
+
+ const token = "fake-token";
+
+ return {
+ token,
+ user: user.attrs,
+ };
+ }
+ // { timing: 2000 }
+ );
+
+ server.post("/logout", () => {
+ try {
+ if (typeof sessionStorage !== 'undefined') {
+ sessionStorage.clear();
+ }
+ if (typeof localStorage !== 'undefined') {
+ localStorage.clear();
+ }
+ } catch (error) {
+ console.log('Storage clear error (normal in server environment):', error.message);
+ }
+
+ return new Response(200, {}, {
+ message: "Logout successful",
+ clearStorage: true
+ });
+ });
+};
+
+export default authServerConfig;
\ No newline at end of file
diff --git a/src/server/index.js b/src/server/index.js
new file mode 100644
index 0000000..c82e25d
--- /dev/null
+++ b/src/server/index.js
@@ -0,0 +1,39 @@
+import { createServer, Model } from "miragejs";
+import authServerConfig from "./auth-server";
+
+export function makeMirageServer() {
+ if (import.meta.env.MODE !== "development") {
+ // ✅ Mirage will not run in production
+ return;
+ }
+
+ createServer({
+ models: {
+ user: Model,
+ product: Model,
+ calendarEvent: Model,
+ },
+
+ factories: {},
+ seeds() {},
+
+ routes() {
+ this.namespace = "api";
+
+ // ✅ Allow real APIs to pass through
+ this.passthrough("**/login");
+ this.passthrough("**/users");
+ this.passthrough("https://icom.ipsgroup.com.my/**");
+ this.passthrough("https://pos.ipsgroup.com.my/**");
+ this.passthrough("https://maps.googleapis.com/**");
+ this.passthrough("https://www.google.com/maps/**");
+ this.passthrough("**/order/**"); // ensures /order/list works fine
+
+ // Optional: mock endpoints for local dev only
+ // authServerConfig(this);
+
+ this.timing = 400;
+ this.passthrough();
+ },
+ });
+}
diff --git a/src/store/api/apiSlice.js b/src/store/api/apiSlice.js
new file mode 100644
index 0000000..831e0a3
--- /dev/null
+++ b/src/store/api/apiSlice.js
@@ -0,0 +1,19 @@
+import { VITE_API_BASE_URL } from "@/constant/config";
+import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
+
+export const apiSlice = createApi({
+ reducerPath: "api",
+ baseQuery: fetchBaseQuery({
+ baseUrl: `${VITE_API_BASE_URL}`,
+ prepareHeaders: (headers, { getState }) => {
+ const token = getState().auth.user?.token;
+ if (token) {
+ headers.set('authorization', `Bearer ${token}`);
+ }
+ headers.set('content-type', 'application/json');
+ return headers;
+ },
+ }),
+ tagTypes: ['User'],
+ endpoints: (builder) => ({}),
+});
\ No newline at end of file
diff --git a/src/store/api/auth/authApiSlice.js b/src/store/api/auth/authApiSlice.js
new file mode 100644
index 0000000..afce37a
--- /dev/null
+++ b/src/store/api/auth/authApiSlice.js
@@ -0,0 +1,52 @@
+import { apiSlice } from "../apiSlice";
+import CryptoJS from 'crypto-js';
+
+const hashPassword = (password) => {
+ return CryptoJS.MD5(password).toString();
+};
+
+export const authApi = apiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+
+ registerUser: builder.mutation({
+ query: (userData) => ({
+ url: "users",
+ method: "POST",
+ body: {
+ username: userData.username,
+ name: userData.name,
+ password_hash: hashPassword(userData.password),
+ role: userData.role || "user",
+ status: userData.status || "active"
+ },
+ }),
+ invalidatesTags: ['User'],
+ }),
+
+ // Login endpoint
+ login: builder.mutation({
+ query: (credentials) => ({
+ url: "login",
+ method: "POST",
+ body: {
+ username: credentials.username,
+ password_hash: hashPassword(credentials.password)
+ },
+ }),
+ }),
+
+ // Logout endpoint
+ logout: builder.mutation({
+ query: () => ({
+ url: "logout",
+ method: "POST",
+ }),
+ }),
+ }),
+});
+
+export const {
+ useRegisterUserMutation,
+ useLoginMutation,
+ useLogoutMutation
+} = authApi;
diff --git a/src/store/api/auth/authSlice.js b/src/store/api/auth/authSlice.js
new file mode 100644
index 0000000..ad3996a
--- /dev/null
+++ b/src/store/api/auth/authSlice.js
@@ -0,0 +1,33 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+const storedUser = JSON.parse(localStorage.getItem("user"));
+
+export const authSlice = createSlice({
+ name: "auth",
+ initialState: {
+ user: storedUser || null,
+ isAuth: !!storedUser,
+ expired: false, // Add expired flag
+ },
+ reducers: {
+ setUser: (state, action) => {
+ state.user = action.payload;
+ state.isAuth = true;
+ state.expired = false; // Reset expired flag on login
+ },
+ logOut: (state) => {
+ state.user = null;
+ state.isAuth = false;
+ state.expired = false;
+ sessionStorage.clear();
+ localStorage.clear();
+ },
+ setExpired: (state) => {
+ state.expired = true;
+ state.isAuth = false;
+ }
+ },
+});
+
+export const { setUser, logOut, setExpired } = authSlice.actions;
+export default authSlice.reducer;
\ No newline at end of file
diff --git a/src/store/api/categoryService.js b/src/store/api/categoryService.js
new file mode 100644
index 0000000..9ad483f
--- /dev/null
+++ b/src/store/api/categoryService.js
@@ -0,0 +1,352 @@
+import { VITE_API_BASE_URL } from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+class CategoryService {
+ setToken(token) {
+ sessionStorage.setItem('token', token);
+ }
+
+ getHeaders(isFormData = false) {
+ const headers = {};
+
+ const token = sessionStorage.getItem('token');
+
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+
+ if (!isFormData) {
+ headers['Content-Type'] = 'application/json';
+ }
+
+ return headers;
+ }
+
+ async handleResponse(response) {
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`HTTP ${response.status}: ${error}`);
+ }
+
+ try {
+ return await response.json();
+ } catch (e) {
+ return await response.text();
+ }
+ }
+
+ // Get all categories
+ async getCategories() {
+ try {
+ const response = await fetch(`${BASE_URL}menu-category/list`, {
+ method: 'GET',
+ headers: this.getHeaders()
+ });
+
+ const data = await this.handleResponse(response);
+
+ let categoriesArray;
+ if (Array.isArray(data)) {
+ categoriesArray = data;
+ } else if (data.data && Array.isArray(data.data)) {
+ categoriesArray = data.data;
+ } else if (data.result && Array.isArray(data.result)) {
+ categoriesArray = data.result;
+ } else {
+ categoriesArray = [];
+ }
+
+ return {
+ data: categoriesArray.map(category => this.transformApiCategoryToComponent(category))
+ };
+ } catch (error) {
+ console.error('Error fetching categories:', error);
+ throw error;
+ }
+ }
+
+ // Get single category by
+ async getCategory(id) {
+ try {
+ const response = await fetch(`${BASE_URL}menu-category/${id}`, {
+ method: 'GET',
+ headers: this.getHeaders()
+ });
+
+ const data = await this.handleResponse(response);
+
+ let categoryData;
+ if (data.data && Array.isArray(data.data) && data.data.length > 0) {
+ categoryData = data.data[0];
+ } else if (data.data && !Array.isArray(data.data)) {
+ categoryData = data.data;
+ } else if (!data.data) {
+ categoryData = data;
+ } else {
+ throw new Error('Category not found or invalid response structure');
+ }
+
+ return this.transformApiCategoryToComponent(categoryData);
+ } catch (error) {
+ console.error('Error fetching category:', error);
+ throw error;
+ }
+ }
+
+ async getCategoryById(categoryId) {
+ return this.getCategory(categoryId);
+ }
+
+ async createCategory(categoryData, imageFile = null) {
+ try {
+ const validation = this.validateCategoryData(categoryData);
+ if (!validation.isValid) {
+ throw new Error(`Validation failed: ${Object.values(validation.errors).join(', ')}`);
+ }
+
+ const formData = new FormData();
+
+ const title = categoryData.title || categoryData.name;
+ formData.append('title', title);
+ formData.append('description', categoryData.description || '');
+ formData.append('status', categoryData.status || 'active');
+
+ if (categoryData.orderIndex !== undefined) {
+ formData.append('order_index', categoryData.orderIndex);
+ }
+
+ if (imageFile) {
+ formData.append('image', imageFile);
+ }
+
+ const response = await fetch(`${BASE_URL}menu-category/create`, {
+ method: 'POST',
+ headers: this.getHeaders(true),
+ body: formData
+ });
+ const data = await this.handleResponse(response);
+ return this.transformApiCategoryToComponent(data.data || data);
+ } catch (error) {
+ console.error('Error creating category:', error);
+ throw error;
+ }
+ }
+
+ // Create quick category (text only)
+ async createQuickCategory(title) {
+ try {
+ if (!title || !title.trim()) {
+ throw new Error('Category title is required');
+ }
+
+ const response = await fetch(`${BASE_URL}menu-category/createQuick`, {
+ method: 'POST',
+ headers: this.getHeaders(),
+ body: JSON.stringify({ title: title.trim() })
+ });
+
+ const data = await this.handleResponse(response);
+ return this.transformApiCategoryToComponent(data);
+ } catch (error) {
+ console.error('Error creating quick category:', error);
+ throw error;
+ }
+ }
+
+ // Update category
+ async updateCategory(id, categoryData, imageFile = null) {
+ try {
+ const validation = this.validateCategoryData(categoryData);
+ if (!validation.isValid) {
+ throw new Error(`Validation failed: ${Object.values(validation.errors).join(', ')}`);
+ }
+
+ const formData = new FormData();
+
+ const title = categoryData.title || categoryData.name;
+ formData.append('title', title);
+ formData.append('description', categoryData.description || '');
+ formData.append('status', categoryData.status || 'active');
+
+ if (categoryData.orderIndex !== undefined) {
+ formData.append('order_index', categoryData.orderIndex);
+ }
+
+ if (imageFile) {
+ formData.append('image', imageFile);
+ }
+
+ const response = await fetch(`${BASE_URL}menu-category/update/${id}`, {
+ method: 'POST',
+ headers: this.getHeaders(true),
+ body: formData
+ });
+
+ const data = await this.handleResponse(response);
+ return this.transformApiCategoryToComponent(data);
+ } catch (error) {
+ console.error('Error updating category:', error);
+ throw error;
+ }
+ }
+
+ async updateCategoriesOrder(orderData) {
+ try {
+ const response = await fetch(`${BASE_URL}menu-category/index`, {
+ method: 'POST',
+ headers: this.getHeaders(),
+ body: JSON.stringify({ category_order: orderData })
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error updating categories order:', error);
+ throw error;
+ }
+ }
+
+ // Delete category
+ async deleteCategory(id) {
+ try {
+ const response = await fetch(`${BASE_URL}menu-category/delete/${id}`, {
+ method: 'POST',
+ headers: this.getHeaders()
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error deleting category:', error);
+ throw error;
+ }
+ }
+
+ transformApiCategoryToComponent(apiCategory) {
+ console.log('Transforming API Category:', apiCategory);
+ if (!apiCategory) return null;
+
+ return {
+ id: apiCategory.id,
+ name: apiCategory.name || apiCategory.title,
+ title: apiCategory.title || apiCategory.name,
+ description: apiCategory.description,
+ status: apiCategory.status || 'active',
+ image: apiCategory.image || apiCategory.image_url || null,
+ orderIndex: parseInt(apiCategory.order_index || apiCategory.orderIndex || 0, 10),
+ createdAt: apiCategory.created_at || apiCategory.createdAt,
+ updatedAt: apiCategory.updated_at || apiCategory.updatedAt,
+ viewMode: apiCategory.view_mode || apiCategory.viewMode || '',
+ backgroundColor: apiCategory.background_color || apiCategory.backgroundColor || ''
+ };
+ }
+
+ transformComponentCategoryToApi(componentCategory) {
+ if (!componentCategory) return null;
+
+ return {
+ title: componentCategory.title || componentCategory.name,
+ name: componentCategory.name || componentCategory.title,
+ description: componentCategory.description || '',
+ status: componentCategory.status || 'active',
+ order_index: componentCategory.orderIndex || 0
+ };
+ }
+
+ validateCategoryData(categoryData) {
+ const errors = {};
+
+ const nameField = categoryData.name || categoryData.title;
+ if (!nameField || !nameField.trim()) {
+ errors.name = 'Category name/title is required';
+ }
+
+ if (nameField && nameField.length > 100) {
+ errors.name = 'Category name/title must be less than 100 characters';
+ }
+
+ if (categoryData.status && !['active', 'inactive'].includes(categoryData.status)) {
+ errors.status = 'Status must be either "active" or "inactive"';
+ }
+
+ if (categoryData.orderIndex !== undefined &&
+ (isNaN(categoryData.orderIndex) || categoryData.orderIndex < 0)) {
+ errors.orderIndex = 'Order index must be a non-negative number';
+ }
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors
+ };
+ }
+
+ async createMultipleCategories(categoriesData) {
+ const results = [];
+ const errors = [];
+
+ for (let i = 0; i < categoriesData.length; i++) {
+ try {
+ const result = await this.createCategory(categoriesData[i]);
+ results.push(result);
+ } catch (error) {
+ errors.push({
+ index: i,
+ data: categoriesData[i],
+ error: error.message
+ });
+ }
+ }
+
+ return { results, errors };
+ }
+
+ async updateMultipleCategories(updates) {
+ const results = [];
+ const errors = [];
+
+ for (const update of updates) {
+ try {
+ const result = await this.updateCategory(update.id, update.data, update.imageFile);
+ results.push(result);
+ } catch (error) {
+ errors.push({
+ id: update.id,
+ error: error.message
+ });
+ }
+ }
+
+ return { results, errors };
+ }
+
+ async searchCategories(searchTerm) {
+ try {
+ const categories = await this.getCategories();
+
+ if (!searchTerm || !searchTerm.trim()) {
+ return categories;
+ }
+
+ const term = searchTerm.toLowerCase().trim();
+ return categories.filter(category =>
+ (category.name && category.name.toLowerCase().includes(term)) ||
+ (category.title && category.title.toLowerCase().includes(term)) ||
+ (category.description && category.description.toLowerCase().includes(term))
+ );
+ } catch (error) {
+ console.error('Error searching categories:', error);
+ throw error;
+ }
+ }
+
+ filterCategoriesByStatus(categories, status) {
+ return categories.filter(category => category.status === status);
+ }
+
+ sortCategoriesByOrder(categories) {
+ return [...categories].sort((a, b) => (a.orderIndex || 0) - (b.orderIndex || 0));
+ }
+}
+
+const categoryService = new CategoryService();
+
+export default categoryService;
\ No newline at end of file
diff --git a/src/store/api/cusTypeService.js b/src/store/api/cusTypeService.js
new file mode 100644
index 0000000..03f1b99
--- /dev/null
+++ b/src/store/api/cusTypeService.js
@@ -0,0 +1,128 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+};
+
+const customerTypeService = {
+ getAll: async () => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/customer-types`, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch customer types');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching customer types:', error);
+ throw error;
+ }
+ },
+
+ create: async (data) => {
+ try {
+ const formData = new FormData();
+ formData.append('name', data.name);
+
+ const response = await fetch(`${BASE_URL}settings/customer-types/create`, {
+ method: 'POST',
+ headers: {
+ ...getAuthHeaders(),
+ },
+ body: formData
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to create customer type');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error creating customer type:', error);
+ throw error;
+ }
+ },
+
+ update: async (id, data) => {
+ try {
+ const params = new URLSearchParams();
+ params.append('name', data.name);
+
+ const response = await fetch(`${BASE_URL}settings/customer-types/update/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ ...getAuthHeaders(),
+ },
+ body: params
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to update customer type');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error updating customer type:', error);
+ throw error;
+ }
+ },
+
+ delete: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/customer-types/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to delete customer type');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error deleting customer type:', error);
+ throw error;
+ }
+ },
+
+ getById: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/customer-types/${id}`, {
+ headers: {
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch customer type');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching customer type:', error);
+ throw error;
+ }
+ }
+};
+
+export default customerTypeService;
\ No newline at end of file
diff --git a/src/store/api/dashboardService.js b/src/store/api/dashboardService.js
new file mode 100644
index 0000000..b451430
--- /dev/null
+++ b/src/store/api/dashboardService.js
@@ -0,0 +1,81 @@
+import { VITE_API_BASE_URL } from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+class DashboardService {
+ setToken(token) {
+ sessionStorage.setItem("token", token);
+ }
+
+ getHeaders(isFormData = false) {
+ const headers = {};
+
+ const token = sessionStorage.getItem("token");
+
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`;
+ }
+
+ if (!isFormData) {
+ headers["Content-Type"] = "application/json";
+ }
+
+ return headers;
+ }
+
+ async handleResponse(response) {
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`HTTP ${response.status}: ${error}`);
+ }
+
+ try {
+ return await response.json();
+ } catch (e) {
+ return await response.text();
+ }
+ }
+
+ async dashboard() {
+ try {
+ const response = await fetch(`${BASE_URL}dashboard`, {
+ method: "GET",
+ headers: this.getHeaders(),
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ async dashboardSummary() {
+ try {
+ const response = await fetch(`${BASE_URL}dashboard-summary`, {
+ method: "GET",
+ headers: this.getHeaders(),
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ async liveMonitor(){
+ try {
+ const response = await fetch(`${BASE_URL}live-monitor`, {
+ method: "GET",
+ headers: this.getHeaders(),
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.log(error);
+ }
+ }
+}
+
+const dashboardService = new DashboardService();
+
+export default dashboardService;
diff --git a/src/store/api/exportCsvService.js b/src/store/api/exportCsvService.js
new file mode 100644
index 0000000..4ce7e3a
--- /dev/null
+++ b/src/store/api/exportCsvService.js
@@ -0,0 +1,43 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+class CsvService {
+ getToken() {
+ return sessionStorage.getItem('token');
+ }
+
+ async handleResponse(response) {
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`HTTP ${response.status}: ${error}`);
+ }
+ return await response.json();
+ }
+
+ async fetchCsvReport() {
+ const token = this.getToken();
+ try {
+ const response = await fetch(`${BASE_URL}outlets/export-excel`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch Excel Report');
+ }
+
+ return await response.blob();
+ } catch (error) {
+ console.error('Error fetching Excel Report:', error);
+ throw error;
+ }
+}
+
+}
+
+
+export default new CsvService();
\ No newline at end of file
diff --git a/src/store/api/itemService.js b/src/store/api/itemService.js
new file mode 100644
index 0000000..587a3ca
--- /dev/null
+++ b/src/store/api/itemService.js
@@ -0,0 +1,437 @@
+import { VITE_API_BASE_URL } from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+class ItemService {
+ getToken() {
+ return sessionStorage.getItem("token");
+ }
+
+ async handleResponse(response) {
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ return response.json();
+ }
+
+ async makeFormDataRequest(url, method, formData) {
+ try {
+ const token = this.getToken();
+ const response = await fetch(url, {
+ method,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ body: formData,
+ });
+ return this.handleResponse(response);
+ } catch (error) {
+ console.error("API request failed:", error);
+ throw error;
+ }
+ }
+
+ async makeJsonRequest(url, method, data = null) {
+ try {
+ const token = this.getToken();
+ const config = {
+ method,
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ };
+
+ if (data) {
+ config.body = JSON.stringify(data);
+ }
+
+ const response = await fetch(url, config);
+ return this.handleResponse(response);
+ } catch (error) {
+ console.error("API request failed:", error);
+ throw error;
+ }
+ }
+
+ // Get all menu items
+ async getMenuItems() {
+ const url = `${BASE_URL}menu-item/list`;
+ return this.makeJsonRequest(url, "GET");
+ }
+
+ // Get single menu item by ID
+ async getMenuItem(id) {
+ const url = `${BASE_URL}menu-item/${id}`;
+ return this.makeJsonRequest(url, "GET");
+ }
+
+ // Create quick menu item
+ async createQuickMenuItem(itemData) {
+ const url = `${BASE_URL}menu-item/createQuick`;
+ return this.makeJsonRequest(url, "POST", itemData);
+ }
+
+ // Create full menu item with all details
+ async createMenuItem(itemData) {
+ const url = `${BASE_URL}menu-item/create`;
+ const formData = this.buildFormData(itemData);
+ return this.makeFormDataRequest(url, "POST", formData);
+ }
+
+ // Update menu item
+ async updateMenuItem(id, itemData) {
+ const url = `${BASE_URL}menu-item/update/${id}`;
+ const formData = this.buildFormData(itemData);
+ return this.makeFormDataRequest(url, "POST", formData);
+ }
+
+ // Update menu item order index
+ async updateMenuItemsOrder(orderData) {
+ const url = `${BASE_URL}menu-item/index`;
+ return this.makeJsonRequest(url, "POST", { menu_order: orderData });
+ }
+ // Delete menu item
+ async deleteMenuItem(id) {
+ const url = `${BASE_URL}menu-item/delete/${id}`;
+ return this.makeJsonRequest(url, "POST");
+ }
+ //pwp create
+ async createPwp(itemData) {
+ const url = `${BASE_URL}pwp/create`;
+ return this.makeJsonRequest(url, "POST", itemData);
+ }
+ //pwp edit
+ async updatePwp(id, itemData) {
+ const url = `${BASE_URL}pwp/update/${id}`;
+ return this.makeJsonRequest(url, "POST", itemData);
+ }
+ //pwp show
+ async getPwp(id) {
+ const url = `${BASE_URL}pwp/show/${id}`;
+ return this.makeJsonRequest(url, "GET");
+ }
+ //pwp show list
+ async getPwpList() {
+ const url = `${BASE_URL}pwp/list`;
+ return this.makeJsonRequest(url, "GET");
+ }
+ //pwp delete
+ async deletePwp(id, itemData) {
+ const url = `${BASE_URL}pwp/delete/${id}`;
+ return this.makeJsonRequest(url, "POST", itemData);
+ }
+
+ buildFormData(itemData) {
+ const formData = new FormData();
+
+ // console.log("Building FormData from:", JSON.stringify(itemData, null, 2));
+ // console.log("Deleted images:", deletedImages); // Debug log
+
+ if (itemData.title) formData.append("title", itemData.title);
+ if (itemData.short_description)
+ formData.append("short_description", itemData.short_description);
+ if (itemData.long_description)
+ formData.append("long_description", itemData.long_description);
+ if (itemData.price) formData.append("price", itemData.price.toString());
+ if (itemData.discount_price)
+ formData.append("discount_price", itemData.discount_price.toString());
+ if (itemData.packaging_price)
+ formData.append("packaging_price", itemData.packaging_price.toString());
+ if (itemData.pwp_price !== undefined && itemData.pwp_price !== null) {
+ formData.append("pwp_price", itemData.pwp_price.toString());
+ }
+ if (itemData.status) formData.append("status", itemData.status);
+ if (itemData.order_index !== undefined)
+ formData.append("order_index", itemData.order_index.toString());
+
+ // Categories
+ if (itemData.categories && Array.isArray(itemData.categories)) {
+ if (itemData.categories.length > 0) {
+ itemData.categories.forEach((category, index) => {
+ const categoryId =
+ typeof category === "object" ? category.id : category;
+ formData.append(`category[${index}]`, categoryId.toString());
+ });
+ } else {
+ formData.append("category[]", "");
+ }
+
+ // Menu tags
+ if (itemData.menu_tag && Array.isArray(itemData.menu_tag)) {
+ itemData.menu_tag.forEach((tagId, index) => {
+ formData.append(`menu_tag[${index}]`, tagId.toString());
+ });
+ } else if (itemData.menu_tags && Array.isArray(itemData.menu_tags)) {
+ itemData.menu_tags.forEach((tagId, index) => {
+ formData.append(`menu_tag[${index}]`, tagId.toString());
+ });
+ }
+
+ // Menu option groups
+ if (
+ itemData.menu_option_groups &&
+ Array.isArray(itemData.menu_option_groups)
+ ) {
+ itemData.menu_option_groups.forEach((groupId, index) => {
+ formData.append(`menu_option_group[${index}]`, groupId.toString());
+ });
+ }
+
+ // Availability
+ if (itemData.availability_type) {
+ formData.append("availability[type]", itemData.availability_type);
+
+ if (
+ itemData.availability_type === "seasonal" &&
+ itemData.availability &&
+ Array.isArray(itemData.availability)
+ ) {
+ const seasonalData = itemData.availability[0];
+ if (seasonalData) {
+ if (seasonalData.start_date)
+ formData.append(
+ "availability[seasonal][start_date]",
+ seasonalData.start_date
+ );
+ if (seasonalData.end_date)
+ formData.append(
+ "availability[seasonal][end_date]",
+ seasonalData.end_date
+ );
+ if (seasonalData.start_time)
+ formData.append(
+ "availability[seasonal][start_time]",
+ seasonalData.start_time
+ );
+ if (seasonalData.end_time)
+ formData.append(
+ "availability[seasonal][end_time]",
+ seasonalData.end_time
+ );
+ }
+ }
+
+ if (
+ itemData.availability_type === "regular" &&
+ itemData.availability &&
+ Array.isArray(itemData.availability)
+ ) {
+ const regularData = itemData.availability[0];
+ if (regularData && Array.isArray(regularData)) {
+ regularData.forEach((schedule, index) => {
+ formData.append(
+ `availability[regular][${index}][day_of_week]`,
+ schedule.day_of_week.toString()
+ );
+ formData.append(
+ `availability[regular][${index}][is_enabled]`,
+ schedule.is_enabled ? "1" : "0"
+ );
+ formData.append(
+ `availability[regular][${index}][start_time]`,
+ schedule.start_time
+ );
+ formData.append(
+ `availability[regular][${index}][end_time]`,
+ schedule.end_time
+ );
+ });
+ }
+ }
+ }
+
+ // Variation
+ if (itemData.variations && Array.isArray(itemData.variations)) {
+ itemData.variations.forEach((variation, index) => {
+ if (variation.title)
+ formData.append(`variation[${index}][title]`, variation.title);
+ if (variation.price)
+ formData.append(
+ `variation[${index}][price]`,
+ variation.price.toString()
+ );
+ if (variation.order_index !== undefined)
+ formData.append(
+ `variation[${index}][order_index]`,
+ variation.order_index.toString()
+ );
+
+ formData.append(
+ `variation[${index}][id]`,
+ variation.id ? variation.id.toString() : ""
+ );
+
+ // Variation option groups
+ if (
+ variation.option_groups &&
+ Array.isArray(variation.option_groups)
+ ) {
+ variation.option_groups.forEach((group, groupIndex) => {
+ const groupId = typeof group === "object" ? group.id : group;
+ formData.append(
+ `variation[${index}][option_group][${groupIndex}]`,
+ groupId !== undefined && groupId !== null
+ ? groupId.toString()
+ : ""
+ );
+ });
+ }
+
+ // Variation tags
+ if (variation.tags && Array.isArray(variation.tags)) {
+ variation.tags.forEach((tag, tagIndex) => {
+ const tagId = typeof tag === "object" ? tag.id : tag;
+ formData.append(
+ `variation[${index}][tag][${tagIndex}]`,
+ tagId.toString()
+ );
+ });
+ }
+
+ if (
+ variation.images ||
+ (Array.isArray(variation.images) && variation.images.length > 0)
+ ) {
+ // console.log(`Adding ${variation.images.length} images for variation ${index}`);
+ if (Array.isArray(variation.images)) {
+ variation.images.forEach((image, imageIndex) => {
+ if (image instanceof File) {
+ // console.log(`Adding new variation image for variation ${index}:`, image);
+ formData.append(`variation_image${index}`, image);
+ } else {
+ // Handle string URLs if any
+ // console.log(`Adding new variation image for variation ${index}:`, image);
+ formData.append(`variation[${index}][images]`, image);
+ }
+ });
+ } else {
+ const image = variation.images;
+ if (image instanceof File) {
+ // console.log(`Adding new variation image for variation ${index}:`, image);
+ formData.append(`variation_image${index}`, image);
+ } else {
+ // Handle string URLs if any
+ // console.log(`Adding new variation image for variation ${index}:`, image);
+ formData.append(`variation[${index}][images]`, image);
+ }
+ }
+ } else if (
+ variation.existingImages ||
+ (Array.isArray(variation.existingImages) &&
+ variation.existingImages.length > 0)
+ ) {
+ // If no new images, but we have existing ones
+ // console.log(`Adding ${variation.existingImages.length} existing images for variation ${index}`);
+ if (Array.isArray(variation.existingImages)) {
+ variation.existingImages.forEach((image, imageIndex) => {
+ if (image instanceof File) {
+ // console.log(`Adding new variation image for variation ${index}:`, image);
+ formData.append(`variation_image${index}`, image);
+ } else {
+ // Handle string URLs if any
+ // console.log(`Adding new variation image for variation ${index}:`, image);
+ formData.append(`variation[${index}][images]`, image);
+ }
+ });
+ } else {
+ const image = variation.existingImages;
+ if (image instanceof File) {
+ // console.log(`Adding new variation image for variation ${index}:`, image);
+ formData.append(`variation_image${index}`, image);
+ } else {
+ // Handle string URLs if any
+ // console.log(`Adding new variation image for variation ${index}:`, image);
+ formData.append(`variation[${index}][images]`, image);
+ }
+ }
+ } else {
+ // Explicitly indicate this variation has no image
+ console.log(`No image for variation ${index}`);
+ formData.append(`variation_image${index}`, "");
+ }
+
+ // if (variation.images && Array.isArray(variation.images) && variation.images.length > 0) {
+ // console.log(`Adding ${variation.images.length} images for variation ${index}`);
+ // variation.images.forEach((image, imageIndex) => {
+ // if (image instanceof File) {
+ // console.log(`Adding variation image for variation ${index}:`, image.name);
+ // formData.append(`variation_image${index}`, image);
+ // }
+ // });
+ // }
+
+ // if (variation.existing_images && Array.isArray(variation.existing_images)) {
+ // variation.existing_images.forEach((image, imageIndex) => {
+ // const imageId = typeof image === 'object' ? image.id : image;
+ // formData.append(`variation_image${index}`, imageId.toString());
+ // });
+ // }
+ });
+ }
+
+ // Images
+ if (itemData.images && Array.isArray(itemData.images)) {
+ itemData.images.forEach((image, index) => {
+ if (image instanceof File) {
+ formData.append(`image[${index}]`, image);
+ }
+ });
+ }
+
+ // Existing images
+ if (itemData.existing_images && Array.isArray(itemData.existing_images)) {
+ itemData.existing_images.forEach((image, index) => {
+ formData.append(`existing_image[${index}]`, image.id);
+ });
+ }
+
+ return formData;
+ }
+ }
+
+ transformApiItemToComponent(apiItem) {
+ return {
+ id: apiItem.id,
+ name: apiItem.title,
+ optionGroups: apiItem.menu_option_group || [],
+ price: parseFloat(apiItem.price) || 0,
+ image: apiItem.images?.[0]?.url || apiItem.image,
+ selected: false,
+ categoryId: Array.isArray(apiItem.categories)
+ ? apiItem.categories[0]?.id
+ : apiItem.category_id || apiItem.categoryId || null,
+
+ category: apiItem.category || [],
+ status: apiItem.status,
+ short_description: apiItem.short_description,
+ long_description: apiItem.long_description,
+ variations: apiItem.variation_group || [], // Use variation_group instead of variations
+ availability: apiItem.availability || null,
+ menuOptionGroups: apiItem.menu_option_group || [],
+ order_index: parseInt(apiItem.order_index || 0),
+ hasVariations:
+ (apiItem.variation_group && apiItem.variation_group.length > 0) ||
+ false,
+ };
+ }
+
+ transformComponentItemToApi(componentItem) {
+ return {
+ title: componentItem.name,
+ price: componentItem.price,
+ status: componentItem.status || "active",
+ menu_tag: componentItem.menu_tag || [],
+ short_description: componentItem.short_description || "",
+ long_description: componentItem.long_description || "",
+ categories: componentItem.categoryId ? [componentItem.categoryId] : [],
+ // order_index: componentItem.order_index || 0
+ };
+ }
+}
+
+const itemService = new ItemService();
+export default itemService;
diff --git a/src/store/api/membershipService.js b/src/store/api/membershipService.js
new file mode 100644
index 0000000..6ba24c8
--- /dev/null
+++ b/src/store/api/membershipService.js
@@ -0,0 +1,137 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+};
+
+const membershipTierService = {
+ getAll: async () => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/membership-tiers`, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch membership tiers');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching membership tiers:', error);
+ throw error;
+ }
+ },
+
+ create: async (data) => {
+ try {
+ const formData = new FormData();
+ formData.append('name', data.name);
+ formData.append('min_points', data.min_points);
+ formData.append('discount_rate', data.discount_rate);
+ formData.append('color', data.color);
+ formData.append('category_id', data.category_id); // Add category_id
+
+ const response = await fetch(`${BASE_URL}settings/membership-tiers/create`, {
+ method: 'POST',
+ headers: {
+ ...getAuthHeaders(),
+ },
+ body: formData
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to create membership tier');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error creating membership tier:', error);
+ throw error;
+ }
+ },
+
+ update: async (id, data) => {
+ try {
+ const body = new URLSearchParams({
+ name: data.name,
+ min_points: data.min_points,
+ discount_rate: data.discount_rate,
+ color: data.color,
+ category_id: data.category_id // Add category_id
+ });
+
+ const response = await fetch(`${BASE_URL}settings/membership-tiers/update/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ ...getAuthHeaders(),
+ },
+ body
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to update membership tier');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error updating membership tier:', error);
+ throw error;
+ }
+ },
+
+ delete: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/membership-tiers/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to delete membership tier');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error deleting membership tier:', error);
+ throw error;
+ }
+ },
+
+ getById: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/membership-tiers/${id}`, {
+ headers: {
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch membership tier');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching membership tier:', error);
+ throw error;
+ }
+ }
+};
+
+export default membershipTierService;
\ No newline at end of file
diff --git a/src/store/api/membershipSettingService.js b/src/store/api/membershipSettingService.js
new file mode 100644
index 0000000..7d7ca79
--- /dev/null
+++ b/src/store/api/membershipSettingService.js
@@ -0,0 +1,56 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+};
+
+const membershipSettingService = {
+ get: async (type) => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/membership-settings/${type}`, {
+ headers: {
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch membership settings');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching membership settings:', error);
+ throw error;
+ }
+ },
+ update: async (data) => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/membership-settings/save`, {
+ method: 'POST',
+ headers: {
+ ...getAuthHeaders(),
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to update membership settings');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error updating membership settings:', error);
+ throw error;
+ }
+ }
+ };
+
+ export default membershipSettingService;
\ No newline at end of file
diff --git a/src/store/api/optionGroupService.js b/src/store/api/optionGroupService.js
new file mode 100644
index 0000000..157c390
--- /dev/null
+++ b/src/store/api/optionGroupService.js
@@ -0,0 +1,379 @@
+import { VITE_API_BASE_URL } from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+class OptionGroupService {
+ constructor() {
+ this.baseUrl = BASE_URL;
+ }
+
+ getToken() {
+ return sessionStorage.getItem('token');
+ }
+
+ async handleResponse(response) {
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`API Error: ${response.status} - ${error}`);
+ }
+ return response.json();
+ }
+
+ async makeRequest(endpoint, options = {}) {
+ const url = `${this.baseUrl}${endpoint}`;
+ const token = this.getToken();
+ const config = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ ...options.headers,
+ },
+ ...options,
+ };
+
+ try {
+ const response = await fetch(url, config);
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error(`API call failed for ${endpoint}:`, error);
+ throw error;
+ }
+ }
+
+ async makeFormDataRequest(endpoint, formData, method = 'POST') {
+ const url = `${this.baseUrl}${endpoint}`;
+ const token = this.getToken();
+
+ const config = {
+ method: method,
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Accept': 'application/json',
+ },
+ body: formData,
+ };
+
+ try {
+ const response = await fetch(url, config);
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error(`FormData API call failed for ${endpoint}:`, error);
+ throw error;
+ }
+ }
+
+ async getOptionGroupList() {
+ return await this.makeRequest('option/list', {
+ method: 'GET',
+ });
+ }
+
+ async getOptionGroup(id) {
+ return await this.makeRequest(`option/${id}`, {
+ method: 'GET',
+ });
+ }
+
+ // Create a new option group
+ async createOptionGroup(optionGroupData) {
+ const payload = this.transformToApiFormat(optionGroupData);
+ console.log('Creating option group with payload:', payload);
+ return await this.makeRequest('option/create', {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ });
+ }
+
+ // Update an existing option group
+ async updateOptionGroup(id, optionGroupData) {
+ const payload = this.transformToApiFormat(optionGroupData);
+ console.log('Updating option group with payload:', payload);
+
+ // Make the update request
+ const response = await this.makeRequest(`option/update/${id}`, {
+ method: 'POST',
+ body: payload,
+ });
+
+ console.log('Update response:', response);
+ return response;
+ }
+
+ async updateOptionGroupIndex(orderMap) {
+ return await this.makeRequest('menu-item/option-group/index', {
+ method: 'POST',
+ body: JSON.stringify({
+ option_group_order: orderMap
+ }),
+ });
+ }
+
+ // Delete an option group
+ async deleteOptionGroup(id) {
+ const token = this.getToken();
+ return await this.makeRequest(`option/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: '',
+ });
+ }
+
+ async updateOptionGroupNew(group_id, optionGroupData) {
+ const formData = new FormData();
+
+ formData.append('id', optionGroupData.id || '');
+ formData.append('title', optionGroupData.name.trim());
+ formData.append('min_quantity', optionGroupData.minSelection || 0);
+ formData.append('max_quantity', optionGroupData.maxSelection || 1);
+ formData.append('is_required', optionGroupData.isOptional ? "0" : "1");
+ formData.append('status', 'active');
+ formData.append('order_index', optionGroupData.orderIndex || 0);
+
+ // 🔥 helper for base64 → File
+ function base64ToFile(base64, filename) {
+ const arr = base64.split(',');
+ const mime = arr[0].match(/:(.*?);/)[1];
+ const bstr = atob(arr[1]);
+ let n = bstr.length;
+ const u8arr = new Uint8Array(n);
+ while (n--) {
+ u8arr[n] = bstr.charCodeAt(n);
+ }
+ return new File([u8arr], filename, { type: mime });
+ }
+
+ if (Array.isArray(optionGroupData.options) && optionGroupData.options.length > 0) {
+ optionGroupData.options.forEach((opt, idx) => {
+ // ✅ Handle images for each option
+ if (opt.images) {
+ if (opt.images instanceof File) {
+ formData.append(`image${idx}`, opt.images);
+ } else if (typeof opt.images === 'string') {
+ if (opt.images.startsWith('data:image')) {
+ // Convert base64 string into File
+ const file = base64ToFile(opt.images, `option_${Date.now()}_${idx}.png`);
+ formData.append(`image${idx}`, file);
+ } else {
+ // Normal URL string
+ formData.append(`options[${idx}][images]`, opt.images);
+ }
+ } else {
+ formData.append(`options[${idx}][images]`, '');
+ }
+ } else {
+ formData.append(`options[${idx}][images]`, '');
+ }
+
+ // Other option fields
+ if (opt.id) {
+ formData.append(`options[${idx}][id]`, opt.id);
+ }
+ formData.append(`options[${idx}][title]`, opt.name || opt.title || '');
+ formData.append(
+ `options[${idx}][price_adjustment]`,
+ parseFloat(opt.price || opt.price_adjustment || 0)
+ );
+ formData.append(`options[${idx}][order_index]`, opt.order_index || idx + 1);
+ });
+ }
+
+ const response = await this.makeFormDataRequest(
+ `option/update/${group_id}`,
+ formData,
+ 'POST'
+ );
+
+ return response;
+}
+
+
+ async updateOptionItem(optionItemId, formData, optionGroupData) {
+ try {
+ console.log('Updating option item:', optionItemId, formData);
+ console.log('Option group data:', optionGroupData);
+
+ if (!optionGroupData || !optionGroupData.id) {
+ throw new Error('Option group data is required to update option item');
+ }
+
+ // Get the updated data from FormData
+ const updateData = {
+ title: formData.get('title'),
+ price_adjustment: parseFloat(formData.get('price_adjustment'))
+ };
+
+ const updatedOptions = optionGroupData.options.map(option => {
+ if (option.id.toString() === optionItemId.toString()) {
+ return {
+ ...option,
+ ...updateData,
+ id: option.id
+ };
+ }
+ return option;
+ });
+
+ const optionGroupPayload = {
+ title: optionGroupData.name || optionGroupData.title,
+ min_quantity: optionGroupData.minSelection || 0,
+ max_quantity: optionGroupData.maxSelection || 1,
+ is_required: optionGroupData.isOptional ? "0" : "1",
+ status: 'active',
+ order_index: optionGroupData.orderIndex || 0,
+ options: updatedOptions.map((opt, idx) => ({
+ id: opt.id,
+ title: opt.title || opt.name || '',
+ price_adjustment: parseFloat(opt.price_adjustment || opt.price || 0),
+ order_index: opt.order_index || idx + 1,
+ images: opt.images || null,
+ images_compressed: opt.images_compressed || null,
+ }))
+ };
+
+ console.log('Updating option group with payload:', optionGroupPayload);
+
+ const hasImageFile = formData.get('image') && formData.get('image') instanceof File;
+
+ if (hasImageFile) {
+ const groupFormData = new FormData();
+ groupFormData.append('title', optionGroupPayload.title);
+ groupFormData.append('min_quantity', optionGroupPayload.min_quantity.toString());
+ groupFormData.append('max_quantity', optionGroupPayload.max_quantity.toString());
+ groupFormData.append('is_required', optionGroupPayload.is_required);
+ groupFormData.append('status', optionGroupPayload.status);
+ groupFormData.append('order_index', optionGroupPayload.order_index.toString());
+
+ optionGroupPayload.options.forEach((option, index) => {
+ groupFormData.append(`options[${index}][id]`, option.id);
+ groupFormData.append(`options[${index}][title]`, option.title);
+ groupFormData.append(`options[${index}][price_adjustment]`, option.price_adjustment.toString());
+ groupFormData.append(`options[${index}][order_index]`, option.order_index.toString());
+
+ if (option.id.toString() === optionItemId.toString()) {
+ groupFormData.append(`options[${index}][images]`, formData.get('image'));
+ }
+ });
+
+ return await this.makeFormDataRequest(`option/update/${optionGroupData.id}`, groupFormData);
+ } else {
+ return await this.makeRequest(`option/update/${optionGroupData.id}`, {
+ method: 'POST',
+ body: JSON.stringify(optionGroupPayload),
+ });
+ }
+
+ } catch (error) {
+ console.error('Error updating option item:', error);
+ throw error;
+ }
+ }
+
+ transformToApiFormat(optionGroupData) {
+ if (!optionGroupData.name || !optionGroupData.name.trim()) {
+ throw new Error('Option group name is required');
+ }
+
+ const payload = {
+ id: optionGroupData.id || null,
+ title: optionGroupData.name.trim(),
+ min_quantity: optionGroupData.minSelection || 0,
+ max_quantity: optionGroupData.maxSelection || 1,
+ is_required: optionGroupData.isOptional ? "0" : "1",
+ status: 'active',
+ order_index: optionGroupData.orderIndex || 0,
+ };
+
+
+ if (Array.isArray(optionGroupData.options) && optionGroupData.options.length > 0) {
+ payload.options = optionGroupData.options.map((opt, idx) => {
+ // Check if this is a menu item (has menu item properties)
+ if (opt.id) {
+ return {
+ id: opt.id,
+ title: opt.name || opt.title || '',
+ price_adjustment: parseFloat(opt.price || opt.price_adjustment || 0),
+ order_index: opt.order_index || idx + 1,
+ images: opt.images || opt.image || null // <-- add this line!
+ };
+ } else {
+ const option = {
+ title: opt.name || opt.title || '',
+ price_adjustment: parseFloat(opt.price || opt.price_adjustment || 0),
+ order_index: opt.order_index || idx + 1,
+ images: opt.images || opt.image || null
+ };
+
+ if (opt.id) {
+ option.id = opt.id;
+ }
+
+ return option;
+ }
+ });
+ }
+
+ console.log('Transformed payload:', payload);
+ return payload;
+ }
+
+ transformFromApiFormat(apiData) {
+ if (Array.isArray(apiData)) {
+ return apiData.map(item => this.transformSingleItem(item));
+ }
+ return this.transformSingleItem(apiData);
+ }
+
+ transformSingleItem(item) {
+ return {
+ id: item.id,
+ name: item.title || '',
+ optionCount: item.options ? item.options.length : 0,
+ minSelection: parseInt(item.min_quantity) || 0,
+ maxSelection: parseInt(item.max_quantity) || 1,
+ associatedItems: 0,
+ isSelected: false,
+ isOptional: item.is_required === "0" || item.is_required === 0,
+ status: item.status || 'active',
+ orderIndex: parseInt(item.order_index) || 0,
+ options: (item.options || []).map((option, idx) => ({
+ id: option.id,
+ name: option.title || '',
+ price: parseFloat(option.price_adjustment) || 0,
+ title: option.title || '',
+ price_adjustment: parseFloat(option.price_adjustment) || 0,
+ order_index: parseInt(option.order_index) || idx + 1,
+ optionGroupItemId: option.option_group_item_id,
+ images: option.images || null,
+ images_compressed: option.images_compressed || null,
+ imagePreview: option.images_compressed || option.images || null,
+ isExisting: true
+ }))
+ };
+ }
+
+ async bulkDeleteOptionGroups(ids) {
+ const deletePromises = ids.map(id => this.deleteOptionGroup(id));
+ return await Promise.allSettled(deletePromises);
+ }
+
+ filterOptionGroups(optionGroups, searchQuery) {
+ if (!searchQuery) return optionGroups;
+
+ return optionGroups.filter(group =>
+ group.name.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ }
+
+ async reorderOptionGroups(reorderedGroups) {
+ const updatePromises = reorderedGroups.map((group, index) =>
+ this.updateOptionGroup(group.id, { ...group, orderIndex: index })
+ );
+ return await Promise.allSettled(updatePromises);
+ }
+}
+
+const optionGroupService = new OptionGroupService();
+export default optionGroupService;
\ No newline at end of file
diff --git a/src/store/api/orderService.js b/src/store/api/orderService.js
new file mode 100644
index 0000000..481a02f
--- /dev/null
+++ b/src/store/api/orderService.js
@@ -0,0 +1,270 @@
+const API_BASE_URL = 'https://icom.ipsgroup.com.my/admin/'; // Change to your actual base URL
+
+export const orderService = {
+ getCustomerOrderList: async (user_id, filters = {}, page = 1, perPage = 10) => {
+ try {
+ const user = JSON.parse(localStorage.getItem("user"));
+ const token = user?.token;
+ const params = new URLSearchParams({
+ user_id,
+ page,
+ per_page: perPage,
+ ...(filters.start_date ? { start_date: filters.start_date } : {}),
+ ...(filters.end_date ? { end_date: filters.end_date } : {}),
+ ...(filters.status ? { status: filters.status } : {}),
+ ...(filters.order_type ? { order_type: filters.order_type } : {}),
+ ...(filters.payment_status ? { payment_status: filters.payment_status } : {}),
+ ...(filters.payment_method ? { payment_method: filters.payment_method } : {}),
+ ...(filters.search ? { search: filters.search } : {}),
+ });
+
+ const response = await fetch(`${API_BASE_URL}order/list?${params}`, {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+
+ if (!response.ok) throw new Error("Failed to fetch order list");
+
+ const result = await response.json();
+ return result;
+ } catch (error) {
+ console.error("Error fetching orders:", error);
+ throw error;
+ }
+ },
+
+ getOrderById: async (id) => {
+ try {
+ const user = JSON.parse(localStorage.getItem('user'));
+ const token = user?.token;
+
+ if (!token) {
+ throw new Error('No authentication token found');
+ }
+
+ const response = await fetch(`${API_BASE_URL}/order/${id}`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch order with ID ${id}`);
+ }
+
+ const result = await response.json();
+
+ if (result.status !== 200) {
+ throw new Error(result.message || 'Failed to fetch order');
+ }
+
+ return result.data;
+ } catch (error) {
+ console.error('Error fetching order:', error);
+ throw error;
+ }
+ },
+
+ updateOrderSchedule: async (orderId, selectedDate, selectedTime) => {
+ try {
+ const user = JSON.parse(localStorage.getItem('user'));
+ const token = user?.token;
+
+ if (!token) {
+ throw new Error('No authentication token found');
+ }
+
+ const response = await fetch(`${API_BASE_URL}/order/update-schedule/${orderId}`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ selected_date: selectedDate,
+ selected_time: selectedTime
+ })
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to update order schedule');
+ }
+
+ const result = await response.json();
+
+ if (result.status !== 200) {
+ throw new Error(result.message || 'Failed to update order schedule');
+ }
+
+ return result.data;
+ } catch (error) {
+ console.error('Error updating order schedule:', error);
+ throw error;
+ }
+ },
+
+ updateOrderStatus: async (orderId, status) => {
+ try {
+ const user = JSON.parse(localStorage.getItem('user'));
+ const token = user?.token;
+
+ if (!token) {
+ throw new Error('No authentication token found');
+ }
+
+ const response = await fetch(`${API_BASE_URL}/order/update-status/${orderId}`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ status: status
+ })
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to update order status');
+ }
+
+ const result = await response.json();
+
+ if (result.status !== 200) {
+ throw new Error(result.message || 'Failed to update order status');
+ }
+
+ return result.data;
+ } catch (error) {
+ console.error('Error updating order status:', error);
+ throw error;
+ }
+ },
+
+ createOrderDelivery: async (deliveryData) => {
+ try {
+ const user = JSON.parse(localStorage.getItem('user'));
+ const token = user?.token;
+
+ if (!token) {
+ throw new Error('No authentication token found');
+ }
+
+ // Validate required fields
+ if (!deliveryData.order_id || !deliveryData.actual_fee_amount || !deliveryData.tracking_link) {
+ throw new Error('order_id, actual_fee_amount, and tracking_link are required');
+ }
+
+ const response = await fetch(`${API_BASE_URL}/order/create-deliveries`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(deliveryData)
+ });
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error(result.message || `HTTP error! status: ${response.status}`);
+ }
+
+ return result.data;
+ } catch (error) {
+ console.error('Error creating order delivery:', error);
+ throw error;
+ }
+},
+
+ getOrderDelivery: async (orderId) => {
+ try {
+ const user = JSON.parse(localStorage.getItem('user'));
+ const token = user?.token;
+
+ if (!token) {
+ throw new Error('No authentication token found');
+ }
+
+ if (!orderId) {
+ throw new Error('Order ID is required');
+ }
+
+ const response = await fetch(`${API_BASE_URL}order/get-delivery/${orderId}`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ }
+ });
+
+ if (!response.ok) {
+ // If 404, return null instead of throwing error (assuming not found is a valid case)
+ if (response.status === 404) {
+ return null;
+ }
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to fetch order delivery');
+ }
+
+ const result = await response.json();
+
+ if (result.status !== 200) {
+ throw new Error(result.message || 'Failed to fetch order delivery');
+ }
+
+ return result.data;
+ } catch (error) {
+ console.error('Error fetching order delivery:', error);
+ throw error;
+ }
+ },
+
+ updateOrderDelivery: async (deliveryId, updateData) => {
+ try {
+ const user = JSON.parse(localStorage.getItem('user'));
+ const token = user?.token;
+
+ if (!token) {
+ throw new Error('No authentication token found');
+ }
+
+ if (!deliveryId) {
+ throw new Error('Delivery ID is required');
+ }
+
+ // Filter out undefined values but keep empty strings
+ const payload = Object.fromEntries(
+ Object.entries(updateData).filter(([_, v]) => v !== undefined)
+ );
+
+ const response = await fetch(`${API_BASE_URL}order/update-delivery/${deliveryId}`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload)
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to update order delivery');
+ }
+
+ const result = await response.json();
+
+ if (result.status !== 200) {
+ throw new Error(result.message || 'Failed to update order delivery');
+ }
+
+ return result.data;
+ } catch (error) {
+ console.error('Error updating order delivery:', error);
+ throw error;
+ }
+ },
+
+
+ // You can add more order-related API calls here as needed
+};
\ No newline at end of file
diff --git a/src/store/api/outletService.js b/src/store/api/outletService.js
new file mode 100644
index 0000000..7bdb37c
--- /dev/null
+++ b/src/store/api/outletService.js
@@ -0,0 +1,390 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+class OutletService {
+ getToken() {
+ return sessionStorage.getItem('token');
+ }
+
+ async handleResponse(response) {
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`HTTP ${response.status}: ${error}`);
+ }
+ return await response.json();
+ }
+
+ async createOutlet(outletData) {
+ const token = this.getToken();
+ const formData = new FormData();
+ // Basic fields
+ formData.append('title', outletData.title);
+ formData.append('email', outletData.email);
+ formData.append('phone', outletData.phone);
+ formData.append('password', outletData.password ?? '');
+ formData.append('address', outletData.address);
+ formData.append('state', outletData.state);
+ formData.append('postal_code', outletData.postalCode || outletData.postal_code || '');
+ formData.append('country', outletData.country || 'Malaysia');
+ formData.append('status', outletData.status || 'active');
+ formData.append('latitude', outletData.latitude || '');
+ formData.append('longitude', outletData.longitude || '');
+ formData.append(
+ 'zeoniq_loc_code',
+ outletData.zeoniqLocCode || outletData.outletZeoniqCode || outletData.zeoniq_loc_code || ''
+);
+
+
+ // Serve method and delivery options
+ formData.append('serve_method', outletData.serveMethod || outletData.serve_method || '');
+
+ formData.append(
+ 'delivery_options',
+ Array.isArray(outletData.deliveryOptions)
+ ? outletData.deliveryOptions.join(', ')
+ : (outletData.deliveryOptions || outletData.delivery_options || '')
+ );
+
+ // Delivery coverage with numeric validation
+ const deliveryCoverage = outletData.deliveryCoverage || outletData.delivery_coverage || outletData.outlet_delivery_coverage;
+ const numericCoverage = deliveryCoverage !== undefined && deliveryCoverage !== null
+ ? deliveryCoverage.toString().replace(/[^0-9.]/g, '')
+ : '0';
+ formData.append('outlet_delivery_coverage', numericCoverage);
+
+ // Capacity limits
+ formData.append('order_max_per_hour', outletData.orderMaxPerHour || outletData.order_max_per_hour || '');
+ formData.append('item_max_per_hour', outletData.itemMaxPerHour || outletData.item_max_per_hour || '');
+
+ // Operating schedule
+ let operatingDays = outletData.operating_days || [{}];
+ let operatingHours = outletData.operating_hours || [{}];
+
+ if (outletData.operatingSchedule) {
+ const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ operatingDays = {};
+ operatingHours = {};
+
+ Object.entries(outletData.operatingSchedule).forEach(([dayNumber, dayData]) => {
+ const dayName = dayNames[parseInt(dayNumber)];
+ if (dayName) {
+ operatingDays[dayName] = { is_operated: dayData.is_operated };
+ operatingHours[dayName] = dayData.operating_hours || [];
+ }
+ });
+ }
+
+ const array_operatingday = [];
+ array_operatingday.push(operatingDays);
+ const array_operatinghour = [];
+ array_operatinghour.push(operatingHours);
+ const transformedDays = array_operatingday || {};
+ const transformedHours = array_operatinghour || {};
+
+ formData.append('outlet_operating_days', JSON.stringify(transformedDays));
+ formData.append('outlet_operating_hours', JSON.stringify(transformedHours));
+
+ // Tax handling
+ const taxMap = {
+ sst: 1,
+ service_tax: 2
+ };
+ console.log(outletData.outlet_tax);
+ // Append each ID as outlet_tax[]
+ outletData.outlet_tax.forEach(id => {
+ formData.append("outlet_tax[]", id);
+ });
+
+
+ // Menu items
+ let menuItems = outletData.menuItems || outletData.outlet_menu || [];
+ let menuItemIds = [];
+
+ // Normalize to array of IDs
+ if (Array.isArray(menuItems)) {
+ menuItemIds = menuItems.map(item => (item && typeof item === "object" ? item.id : item)).filter(Boolean);
+ } else if (menuItems && typeof menuItems === "object") {
+ menuItemIds = Object.keys(menuItems)
+ .filter(key => menuItems[key])
+ .map(key => parseInt(key, 10))
+ .filter(Boolean);
+ }
+
+ // Append each ID as outlet_menu[]
+ menuItemIds.forEach(id => {
+ formData.append('outlet_menu[]', id);
+ });
+
+
+ // Image handling
+ if (outletData.images && Array.isArray(outletData.images)) {
+ outletData.images.forEach((image, index) => {
+ if (image instanceof File || image instanceof Blob) {
+ formData.append('outlet_images[]', image);
+ }
+ });
+ }
+
+ // Debug logging
+ console.log('FormData entries for create:');
+ for (let [key, value] of formData.entries()) {
+ console.log(key, ':', value);
+ }
+
+ try {
+ const response = await fetch(`${BASE_URL}outlets/create`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: formData,
+ });
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error creating outlet:', error);
+ throw error;
+ }
+}
+ async getOutlets(user_id) {
+ const token = await this.getToken();
+
+ try {
+ const response = await fetch(`${BASE_URL}outlets/list?user_id=${user_id}`, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Authorization': `Bearer ${token}`,
+ }
+ });
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error fetching outlets:', error);
+ throw error;
+ }
+ }
+
+ async getOutlet(id) {
+ const token = this.getToken();
+
+ try {
+ const response = await fetch(`${BASE_URL}outlets/${id}`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ }
+ });
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error fetching outlet:', error);
+ throw error;
+ }
+ }
+
+async updateOutlet(id, outletData) {
+ const token = this.getToken?.() || sessionStorage.getItem('token');
+
+ // If caller already built FormData, just send it
+ if (outletData instanceof FormData) {
+ if (process.env.NODE_ENV !== 'production') {
+ // console.log('FormData (passed-through) preview:');
+ for (const [k, v] of outletData.entries()) {
+ // console.log(k, v instanceof File ? `[File:${v.name}]` : v);
+ }
+ }
+ const res = await fetch(`${BASE_URL}outlets/update/${id}`, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}` },
+ body: outletData,
+ });
+ if (!res.ok) {
+ let msg = `HTTP ${res.status}`;
+ try { const j = await res.json(); msg = j?.message || JSON.stringify(j?.messages || j); } catch {}
+ throw new Error(msg);
+ }
+ return res.json();
+ }
+
+ // Otherwise, build FormData here
+ const fd = new FormData();
+
+ // Basic fields
+ fd.append('title', outletData.title ?? '');
+ fd.append('email', outletData.email ?? '');
+ fd.append('phone', outletData.phone ?? '');
+ fd.append('address', outletData.address ?? '');
+ fd.append('state', outletData.state ?? '');
+ fd.append('postal_code', outletData.postal_code ?? outletData.postalCode ?? '');
+ fd.append('country', outletData.country ?? 'Malaysia');
+ fd.append('status', outletData.status ?? 'active');
+ fd.append('latitude', String(outletData.latitude ?? '0'));
+ fd.append('longitude', String(outletData.longitude ?? '0'));
+
+ if (outletData.password?.trim()) {
+ fd.append('password', outletData.password.trim());
+ }
+
+ // Serve / delivery
+ const serveMethod = Array.isArray(outletData.serveMethods)
+ ? outletData.serveMethods.join(',')
+ : (outletData.serve_method ?? outletData.serveMethod ?? '');
+ fd.append('serve_method', serveMethod);
+
+ const deliveryOptions = Array.isArray(outletData.deliveryOptions)
+ ? outletData.deliveryOptions.join(',')
+ : (outletData.delivery_options ?? outletData.deliveryOptions ?? '');
+ fd.append('delivery_options', deliveryOptions);
+
+ // Numbers
+ fd.append('outlet_delivery_coverage', String(outletData.outlet_delivery_coverage ?? outletData.deliveryCoverage ?? '0'));
+ fd.append('order_max_per_hour', String(outletData.order_max_per_hour ?? outletData.orderMaxPerHour ?? '0'));
+ fd.append('item_max_per_hour', String(outletData.item_max_per_hour ?? outletData.itemMaxPerHour ?? '0'));
+
+ // Complex objects — backend expects [0], so wrap in single-element array
+ const opDays = outletData.outlet_operating_days ?? {};
+ const opHours = outletData.outlet_operating_hours ?? {};
+ const taxes = outletData.outlet_tax ?? [];
+
+ fd.append('outlet_operating_days', JSON.stringify([opDays]));
+ fd.append('outlet_operating_hours', JSON.stringify([opHours]));
+ fd.append('outlet_tax', JSON.stringify(taxes));
+
+ if (outletData.operating_hours_exceptions) {
+ fd.append(
+ 'outlet_operating_hours_exceptions',
+ JSON.stringify(outletData.operating_hours_exceptions)
+ );
+ }
+
+ // Menu items — backend expects array, not JSON
+ const menuRaw = outletData.outlet_menu ?? outletData.menuItems ?? [];
+ const menuIds = Array.isArray(menuRaw)
+ ? menuRaw.map(m => (typeof m === 'object' ? (m.id ?? m) : m)).filter(Boolean)
+ : [];
+
+ if (menuIds.length) {
+ menuIds.forEach(id => fd.append('outlet_menu[]', String(id)));
+ } else {
+ // optional: send empty to delete all
+ // fd.append('outlet_menu[]', '');
+ }
+
+ // Images
+ const files =
+ outletData.outlet_images
+ ?? (Array.isArray(outletData.images) ? outletData.images.filter(x => x?.file instanceof File).map(x => x.file) : []);
+ files.forEach((file, idx) => fd.append('outlet_images[]', file, file.name || `outlet_${idx}`));
+
+ // Existing image IDs
+ const existingIds =
+ outletData.existing_image
+ ?? (Array.isArray(outletData.images) ? outletData.images.filter(x => x?.id).map(x => x.id) : []);
+ existingIds.forEach(id => fd.append('existing_image[]', String(id)));
+
+ // if (process.env.NODE_ENV !== 'production') {
+ // console.log('FormData (rebuilt) preview:');
+ // for (const [k, v] of fd.entries()) {
+ // console.log(k, v instanceof File ? `[File:${v.name}]` : v);
+ // }
+ // }
+
+ const res = await fetch(`${BASE_URL}outlets/update/${id}`, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}` },
+ body: fd,
+ });
+
+ if (!res.ok) {
+ let msg = `HTTP ${res.status}`;
+ try { const j = await res.json(); msg = j?.message || JSON.stringify(j?.messages || j); } catch {}
+ throw new Error(msg);
+ }
+ return res.json();
+}
+
+
+ async deleteOutlet(id) {
+ const token = this.getToken();
+
+ try {
+ const response = await fetch(`${BASE_URL}outlets/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error deleting outlet:', error);
+ throw error;
+ }
+ }
+
+ async updateOutletPassword(id, password) {
+ const token = this.getToken();
+
+ try {
+ const response = await fetch(`${BASE_URL}outlets/update-password/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify({ password }),
+ });
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error updating outlet password:', error);
+ throw error;
+ }
+ }
+
+
+async addBulk(outletIds, menuItemIds, action = 'activate') {
+ const token = this.getToken();
+ try {
+ const response = await fetch(`${BASE_URL}outlets/update_oulet_menuitem`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ outlet_ids: Array.isArray(outletIds) ? outletIds : [outletIds],
+ menu_item_ids: Array.isArray(menuItemIds) ? menuItemIds : [menuItemIds],
+ action: action // Add this parameter
+ }),
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error in bulk adding menu items to outlets:', error);
+ throw error;
+ }
+}
+
+async deleteBulk(outletIds, menuItemIds) {
+ const token = this.getToken();
+ try {
+ const response = await fetch(`${BASE_URL}outlets/delete_outlet_menuitem`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ outlet_ids: Array.isArray(outletIds) ? outletIds : [outletIds],
+ menu_item_ids: Array.isArray(menuItemIds) ? menuItemIds : [menuItemIds]
+ }),
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error in bulk deleting menu items from outlets:', error);
+ throw error;
+ }
+}
+}
+
+
+
+export default new OutletService();
\ No newline at end of file
diff --git a/src/store/api/promoCodeService.js b/src/store/api/promoCodeService.js
new file mode 100644
index 0000000..a09a376
--- /dev/null
+++ b/src/store/api/promoCodeService.js
@@ -0,0 +1,169 @@
+import { VITE_API_BASE_URL } from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem("token");
+ return {
+ Authorization: `Bearer ${token}`,
+ };
+};
+
+class PromoCodeService {
+ getToken() {
+ return sessionStorage.getItem("token");
+ }
+
+ async handleResponse(response) {
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(
+ errorData.message || `HTTP error! status: ${response.status}`
+ );
+ }
+ return response.json();
+ }
+
+ async makeFormDataRequest(url, method, formData) {
+ try {
+ const token = this.getToken();
+ const response = await fetch(url, {
+ method,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ body: formData,
+ });
+ return this.handleResponse(response);
+ } catch (error) {
+ console.error("API request failed:", error);
+ throw error;
+ }
+ }
+
+ async makeJsonRequest(url, method, data = null) {
+ try {
+ const token = this.getToken();
+ const config = {
+ method,
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ };
+
+ if (data) {
+ config.body = JSON.stringify(data);
+ }
+
+ const response = await fetch(url, config);
+ return this.handleResponse(response);
+ } catch (error) {
+ console.error("API request failed:", error);
+ throw error;
+ }
+ }
+
+ async getAll() {
+ try {
+ const response = await fetch(`${BASE_URL}promo-code/list`, {
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ });
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to fetch promo codes");
+ return await response.json();
+ } catch (error) {
+ console.error("Error fetching promo codes:", error);
+ throw error;
+ }
+ }
+
+ async getById(id) {
+ try {
+ const response = await fetch(`${BASE_URL}promo-code/${id}`, {
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ });
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to fetch promo codes");
+ return await response.json();
+ } catch (error) {
+ console.error("Error fetching promo codes:", error);
+ throw error;
+ }
+ }
+
+ async create(data){
+ try {
+ const response = await fetch(`${BASE_URL}promo-code/create`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(data),
+ });
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to create promo code");
+ return await response.json();
+ } catch (error) {
+ console.error("Error creating promo code:", error);
+ throw error;
+ }
+ }
+
+ async update(id, data) {
+ try {
+ const response = await fetch(`${BASE_URL}promo-code/update/${id}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(data),
+ });
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to update promo code");
+ return await response.json();
+ } catch (error) {
+ console.error("Error updating promo code:", error);
+ throw error;
+ }
+ }
+
+ async delete(id) {
+ try {
+ const response = await fetch(`${BASE_URL}promo-code/delete/${id}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ });
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to delete promo code");
+ return await response.json();
+ } catch (error) {
+ console.error("Error deleting promo code:", error);
+ throw error;
+ }
+ }
+}
+
+const promoCodeService = new PromoCodeService();
+export default promoCodeService;
diff --git a/src/store/api/promoService.js b/src/store/api/promoService.js
new file mode 100644
index 0000000..eb61c2b
--- /dev/null
+++ b/src/store/api/promoService.js
@@ -0,0 +1,307 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+};
+
+const promoService = {
+ getAll: async () => {
+ try {
+ const response = await fetch(`${BASE_URL}promo/list`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch promo codes');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching promo codes:', error);
+ throw error;
+ }
+ },
+
+ getById: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo/${id}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch promo code');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching promo code:', error);
+ throw error;
+ }
+ },
+
+ create: async (data) => {
+ try {
+ const payload = {
+ // Basic promotion info
+ promotionType: data.promotionType || "",
+ promotionName: data.promotionName || "",
+ promotionDescription: data.promotionDescription || "",
+ promotionCode: data.promotionCode || "",
+
+ // Usage and limits
+ usageLimit: data.usageLimit || "multiple",
+ totalRedemptionLimit: data.totalRedemptionLimit ? String(data.totalRedemptionLimit) : "",
+ voucherLimitPerCustomer: data.voucherLimitPerCustomer ? String(data.voucherLimitPerCustomer) : "",
+
+ // Availability settings
+ availableOn: data.availableOn || "all-time",
+ storeStartDate: data.storeStartDate || "",
+ storeEndDate: data.storeEndDate || "",
+ customDayTime: data.customDayTime || {
+ mon: { enabled: false, startTime: "", endTime: "" },
+ tue: { enabled: false, startTime: "", endTime: "" },
+ wed: { enabled: false, startTime: "", endTime: "" },
+ thurs: { enabled: false, startTime: "", endTime: "" },
+ fri: { enabled: false, startTime: "", endTime: "" },
+ sat: { enabled: false, startTime: "", endTime: "" },
+ sun: { enabled: false, startTime: "", endTime: "" }
+ },
+
+ applyToDeliveryPickup: data.applyToDeliveryPickup || "both",
+ promoType: data.promoType || "discount",
+
+ discountAmount: data.discountAmount ? String(data.discountAmount) : "",
+ discountType: data.discountType === "fixed" ? "amount" : data.discountType || "",
+
+ getNumber: data.getNumber ? String(data.getNumber) : "",
+
+ minimumSpend: data.minimumSpend ? String(data.minimumSpend) : "",
+ minimumQuantity: data.minimumQuantity ? String(data.minimumQuantity) : "",
+ minimumAmount: data.minimumAmount ? String(data.minimumAmount) : "",
+ everyQuantity: data.everyQuantity ? String(data.everyQuantity) : "",
+
+ itemCategory1: data.itemCategory1 || "total",
+ itemCategory2: data.itemCategory2 || "total",
+ itemCategoryID1: Array.isArray(data.itemCategory1ID) ? data.itemCategory1ID.map(Number) : [],
+ itemCategoryID2: Array.isArray(data.itemCategory2ID) ? data.itemCategory2ID.map(Number) : [],
+ itemVariations1: data.itemVariations1 || [],
+ itemVariations2: data.itemVariations2 || [],
+
+ isActive: data.isActive !== undefined ? data.isActive : true,
+ updatedBy: data.updatedBy || null
+ };
+
+ console.log('Payload:', payload);
+ const response = await fetch(`${BASE_URL}promo/create`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(payload)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to create promo code');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error creating promo code:', error);
+ throw error;
+ }
+ },
+
+ update: async (id, data) => {
+ try {
+ const payload = {
+ promotionType: data.promotionType || "",
+ promotionName: data.promotionName || "",
+ promotionDescription: data.promotionDescription || "",
+ promotionCode: data.promotionCode || "",
+
+ usageLimit: data.usageLimit || "multiple",
+ totalRedemptionLimit: data.totalRedemptionLimit ? String(data.totalRedemptionLimit) : "",
+ voucherLimitPerCustomer: data.voucherLimitPerCustomer ? String(data.voucherLimitPerCustomer) : "",
+
+ availableOn: data.availableOn || "all-time",
+ storeStartDate: data.storeStartDate || "",
+ storeEndDate: data.storeEndDate || "",
+ customDayTime: data.customDayTime || {
+ mon: { enabled: false, startTime: "", endTime: "" },
+ tue: { enabled: false, startTime: "", endTime: "" },
+ wed: { enabled: false, startTime: "", endTime: "" },
+ thurs: { enabled: false, startTime: "", endTime: "" },
+ fri: { enabled: false, startTime: "", endTime: "" },
+ sat: { enabled: false, startTime: "", endTime: "" },
+ sun: { enabled: false, startTime: "", endTime: "" }
+ },
+
+ applyToDeliveryPickup: data.applyToDeliveryPickup || "both",
+ promoType: data.promoType || "discount",
+
+ discountAmount: data.discountAmount ? String(data.discountAmount) : "",
+ discountType: data.discountType === "fixed" ? "amount" : data.discountType || "",
+
+ getNumber: data.getNumber ? String(data.getNumber) : "",
+
+ minimumSpend: data.minimumSpend ? String(data.minimumSpend) : "",
+ minimumQuantity: data.minimumQuantity ? String(data.minimumQuantity) : "",
+ minimumAmount: data.minimumAmount ? String(data.minimumAmount) : "",
+ everyQuantity: data.everyQuantity ? String(data.everyQuantity) : "",
+
+ itemCategory1: data.itemCategory1 || "total",
+ itemCategory2: data.itemCategory2 || "total",
+ itemCategoryID1: Array.isArray(data.itemCategoryID1) ? data.itemCategoryID1.map(Number) : [],
+ itemCategoryID2: Array.isArray(data.itemCategoryID2) ? data.itemCategoryID2.map(Number) : [],
+ itemVariations1: data.itemVariations1 || [],
+ itemVariations2: data.itemVariations2 || [],
+ isActive: data.isActive !== undefined ? data.isActive : true,
+ updatedBy: data.updatedBy || null
+ };
+
+ console.log('Payload:', payload);
+
+ const response = await fetch(`${BASE_URL}promo/update/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(payload)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to update promo code');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error updating promo code:', error);
+ throw error;
+ }
+ },
+
+ delete: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to delete promo code');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error deleting promo code:', error);
+ throw error;
+ }
+ },
+
+ toggleStatus: async (id, isActive) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo/toggle-status/${id}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify({ isActive })
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to toggle promo status');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error toggling promo status:', error);
+ throw error;
+ }
+ },
+
+ validateCode: async (code, orderData = {}) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo/validate`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify({
+ promotionCode: code,
+ orderTotal: orderData.orderTotal || 0,
+ customerId: orderData.customerId || null,
+ products: orderData.products || []
+ })
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to validate promo code');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error validating promo code:', error);
+ throw error;
+ }
+ },
+
+ getUsageStats: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo/usage-stats/${id}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch promo usage stats');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching promo usage stats:', error);
+ throw error;
+ }
+ }
+};
+
+export default promoService;
\ No newline at end of file
diff --git a/src/store/api/promoSettingsService.js b/src/store/api/promoSettingsService.js
new file mode 100644
index 0000000..4faab7c
--- /dev/null
+++ b/src/store/api/promoSettingsService.js
@@ -0,0 +1,151 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+};
+
+const promoSettingsService = {
+ getAll: async (searchParams = {}) => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ Object.keys(searchParams).forEach(key => {
+ if (searchParams[key]) {
+ queryParams.append(key, searchParams[key]);
+ }
+ });
+
+ const queryString = queryParams.toString();
+ const url = `${BASE_URL}promo-setting/list${queryString ? `?${queryString}` : ''}`;
+
+ console.log('Making request to:', url);
+
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Endpoint not found: ${url}`);
+ }
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: Failed to fetch promo settings`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching promo settings:', error);
+ throw error;
+ }
+ },
+
+ // Get single promo setting by ID
+ getById: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo-setting/${id}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch promo setting');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching promo setting:', error);
+ throw error;
+ }
+ },
+
+ // Create new promo setting
+ create: async (data) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo-setting/create`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(data)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to create promo setting');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error creating promo setting:', error);
+ throw error;
+ }
+ },
+
+ // Update promo setting
+ update: async (id, data) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo-setting/update/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(data)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to update promo setting');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error updating promo setting:', error);
+ throw error;
+ }
+ },
+
+ // Delete promo setting
+ delete: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}promo-setting/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to delete promo setting');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error deleting promo setting:', error);
+ throw error;
+ }
+ }
+};
+export default promoSettingsService;
\ No newline at end of file
diff --git a/src/store/api/reportService.js b/src/store/api/reportService.js
new file mode 100644
index 0000000..bad7f57
--- /dev/null
+++ b/src/store/api/reportService.js
@@ -0,0 +1,169 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+};
+
+const reportService = {
+ getPromoReport: async (searchParams = {}) => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ Object.keys(searchParams).forEach(key => {
+ if (searchParams[key]) {
+ queryParams.append(key, searchParams[key]);
+ }
+ });
+
+ const queryString = queryParams.toString();
+ const url = `${BASE_URL}report/promo${queryString ? `?${queryString}` : ''}`;
+
+ console.log('Making request to:', url);
+
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Endpoint not found: ${url}`);
+ }
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: Failed to fetch promo settings`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching promo settings:', error);
+ throw error;
+ }
+ },
+
+ getSalesReport: async (searchParams = {}) => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ Object.keys(searchParams).forEach(key => {
+ if (searchParams[key]) {
+ queryParams.append(key, searchParams[key]);
+ }
+ });
+
+ const queryString = queryParams.toString();
+ const url = `${BASE_URL}report/sales${queryString ? `?${queryString}` : ''}`;
+
+ console.log('Making request to:', url);
+
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Endpoint not found: ${url}`);
+ }
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: Failed to fetch promo settings`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching promo settings:', error);
+ throw error;
+ }
+ },
+
+ getProductReport: async (searchParams = {}) => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ Object.keys(searchParams).forEach(key => {
+ if (searchParams[key]) {
+ queryParams.append(key, searchParams[key]);
+ }
+ });
+
+ const queryString = queryParams.toString();
+ const url = `${BASE_URL}report/product${queryString ? `?${queryString}` : ''}`;
+
+ console.log('Making request to:', url);
+
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Endpoint not found: ${url}`);
+ }
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: Failed to fetch product report`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching product report:', error);
+ throw error;
+ }
+ },
+
+ getExportExcel: async (searchParams = {}) => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ Object.keys(searchParams).forEach(key => {
+ if (searchParams[key]) {
+ queryParams.append(key, searchParams[key]);
+ }
+ });
+
+ const queryString = queryParams.toString();
+ const url = `${BASE_URL}report/export${queryString ? `?${queryString}` : ''}`;
+
+ console.log('Making request to:', url);
+
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 404) {
+ throw new Error(`Endpoint not found: ${url}`);
+ }
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: Failed to fetch product report`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching product report:', error);
+ throw error;
+ }
+ },
+};
+export default reportService;
\ No newline at end of file
diff --git a/src/store/api/tagService.js b/src/store/api/tagService.js
new file mode 100644
index 0000000..44640cf
--- /dev/null
+++ b/src/store/api/tagService.js
@@ -0,0 +1,195 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+class TagService {
+ constructor() {
+ this.baseURL = `${BASE_URL}tag`;
+ }
+
+ getToken() {
+ return sessionStorage.getItem('token');
+ }
+
+ async handleResponse(response) {
+ if (!response.ok) {
+ const errorData = await response.text();
+ throw new Error(`API Error: ${response.status} - ${errorData}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ return await response.json();
+ }
+ return await response.text();
+ }
+
+ // Get all tags
+ async getTagList() {
+ try {
+ const token = this.getToken();
+ const response = await fetch(`${this.baseURL}/list`, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error fetching tag list:', error);
+ throw error;
+ }
+ }
+
+ // Get single tag by ID
+ async getTag(tagId) {
+ try {
+ const token = this.getToken();
+ const response = await fetch(`${this.baseURL}/${tagId}`, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error(`Error fetching tag ${tagId}:`, error);
+ throw error;
+ }
+ }
+
+ // Create new tag
+ async createTag(tagData) {
+ try {
+ const formData = new FormData();
+ const token = this.getToken();
+
+ if (tagData.title) {
+ formData.append('title', tagData.title);
+ }
+
+ if (tagData.icon) {
+ formData.append('icon', tagData.icon);
+ }
+
+ const response = await fetch(`${this.baseURL}/create`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: formData,
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error('Error creating tag:', error);
+ throw error;
+ }
+ }
+
+ // Update existing tag
+ async updateTag(tagId, tagData) {
+ try {
+ const formData = new FormData();
+ const token = this.getToken();
+
+ if (tagData.title) {
+ formData.append('title', tagData.title);
+ }
+
+ if (tagData.icon) {
+ formData.append('icon', tagData.icon);
+ }
+
+ const response = await fetch(`${this.baseURL}/update/${tagId}`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: formData,
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error(`Error updating tag ${tagId}:`, error);
+ throw error;
+ }
+ }
+
+ // Delete tag
+ async deleteTag(tagId) {
+ try {
+ const formData = new FormData();
+ const token = this.getToken();
+
+ const response = await fetch(`${this.baseURL}/delete/${tagId}`, {
+ method: 'POST',
+ headers : {
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: formData,
+ });
+
+ return await this.handleResponse(response);
+ } catch (error) {
+ console.error(`Error deleting tag ${tagId}:`, error);
+ throw error;
+ }
+ }
+
+ validateTagData(tagData) {
+ const errors = {};
+
+ if (!tagData.title || tagData.title.trim().length === 0) {
+ errors.title = 'Tag title is required';
+ }
+
+ if (tagData.title && tagData.title.trim().length > 100) {
+ errors.title = 'Tag title must be less than 100 characters';
+ }
+
+ if (tagData.icon) {
+ const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
+ const maxSize = 5 * 1024 * 1024;
+
+ if (!validTypes.includes(tagData.icon.type)) {
+ errors.icon = 'Icon must be a valid image file (JPEG, PNG, GIF)';
+ }
+
+ if (tagData.icon.size > maxSize) {
+ errors.icon = 'Icon file size must be less than 5MB';
+ }
+ }
+
+ return {
+ isValid: Object.keys(errors).length === 0,
+ errors
+ };
+ }
+
+ transformApiTagToComponent(apiTag) {
+ return {
+ id: apiTag.id,
+ title: apiTag.title,
+ name: apiTag.title,
+ icon: apiTag.icon,
+ iconUrl: apiTag.icon_url || apiTag.iconUrl,
+ createdAt: apiTag.created_at || apiTag.createdAt,
+ updatedAt: apiTag.updated_at || apiTag.updatedAt,
+ };
+ }
+
+ transformComponentTagToApi(componentTag) {
+ return {
+ title: componentTag.title || componentTag.name,
+ icon: componentTag.icon,
+ };
+ }
+}
+
+const tagService = new TagService();
+export default tagService;
\ No newline at end of file
diff --git a/src/store/api/taxService.js b/src/store/api/taxService.js
new file mode 100644
index 0000000..2fad5ab
--- /dev/null
+++ b/src/store/api/taxService.js
@@ -0,0 +1,146 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+const token = sessionStorage.getItem('token');
+
+class TaxService {
+ async fetchTaxes() {
+ try {
+ const token = sessionStorage.getItem('token');
+ const response = await fetch(`${BASE_URL}settings/tax`, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Authorization': `Bearer ${token}`,
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ console.log("Fetched tax data:", data);
+
+ let taxes = [];
+ if (Array.isArray(data)) {
+ taxes = data;
+ } else if (Array.isArray(data.data)) {
+ taxes = data.data;
+ } else if (Array.isArray(data.result)) {
+ taxes = data.result;
+ } else if (Array.isArray(data.taxes)) {
+ taxes = data.taxes;
+ }
+
+ return taxes;
+ } catch (error) {
+ console.error('Error fetching taxes:', error);
+ throw new Error('Failed to load tax settings. Please try again.');
+ }
+ }
+
+ async createTax(taxData) {
+ try {
+ const formData = new FormData();
+
+ // Handle outlet_id - convert array to comma-separated string if needed
+ if (Array.isArray(taxData.outlet_id)) {
+ formData.append('outlet_id', taxData.outlet_id.join(','));
+ } else {
+ formData.append('outlet_id', taxData.outlet_id);
+ }
+
+ formData.append('tax_type', taxData.tax_type);
+ formData.append('tax_rate', taxData.tax_rate);
+ formData.append('order_type', taxData.order_type);
+
+ const response = await fetch(`${BASE_URL}settings/tax/create`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+ return result;
+ } catch (error) {
+ console.error('Error creating tax:', error);
+ throw new Error('Failed to create tax. Please try again.');
+ }
+ }
+
+ async updateTax(id, taxData) {
+ try {
+ const formData = new FormData();
+
+ // Only append fields that are provided and not undefined/null
+ if (taxData.outlet_id !== undefined && taxData.outlet_id !== null) {
+ // Handle outlet_id - convert array to comma-separated string if needed
+ if (Array.isArray(taxData.outlet_id)) {
+ formData.append('outlet_id', taxData.outlet_id.join(','));
+ } else {
+ formData.append('outlet_id', taxData.outlet_id);
+ }
+ }
+
+ if (taxData.tax_type !== undefined && taxData.tax_type !== null) {
+ formData.append('tax_type', taxData.tax_type);
+ }
+
+ if (taxData.tax_rate !== undefined && taxData.tax_rate !== null) {
+ formData.append('tax_rate', taxData.tax_rate);
+ }
+
+ if (taxData.order_type !== undefined && taxData.order_type !== null) {
+ formData.append('order_type', taxData.order_type);
+ }
+
+ const response = await fetch(`${BASE_URL}settings/tax/update/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+ return result;
+ } catch (error) {
+ console.error('Error updating tax:', error);
+ throw new Error('Failed to update tax. Please try again.');
+ }
+ }
+
+ async deleteTax(id) {
+ try {
+ const response = await fetch(`${BASE_URL}settings/tax/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error deleting tax:', error);
+ throw new Error('Failed to delete tax. Please try again.');
+ }
+ }
+}
+
+const taxService = new TaxService();
+export default taxService;
\ No newline at end of file
diff --git a/src/store/api/topuplistsService.js b/src/store/api/topuplistsService.js
new file mode 100644
index 0000000..66d3267
--- /dev/null
+++ b/src/store/api/topuplistsService.js
@@ -0,0 +1,59 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+const token = sessionStorage.getItem('token');
+
+class TopUpService {
+ async fetchTopupLists(filters = {}) {
+ try {
+ const token = sessionStorage.getItem('token');
+ const params = new URLSearchParams({
+ ...(filters.status ? { status: filters.status } : {}),
+ ...(filters.search ? { search: filters.search } : {}),
+ ...(filters.payment_method ? { payment_method: filters.payment_method } : {}),
+ ...(filters.start_date ? { start_date: filters.start_date } : {}),
+ ...(filters.end_date ? { end_date: filters.end_date } : {}),
+ });
+
+ const response = await fetch(`${BASE_URL}customer/topup/list?${params}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+
+ const data = await response.json();
+ return data.data || [];
+ } catch (error) {
+ console.error('Error fetching topup settings:', error);
+ throw new Error('Failed to load topup settings. Please try again.');
+ }
+ }
+
+
+ async deleteTopupSetting(id) {
+ try {
+ const response = await fetch(`${BASE_URL}customer/topup/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error deleting topup setting:', error);
+ throw new Error('Failed to delete topup setting. Please try again.');
+ }
+ }
+}
+
+const topupService = new TopUpService();
+export default topupService;
\ No newline at end of file
diff --git a/src/store/api/topupsettingService.js b/src/store/api/topupsettingService.js
new file mode 100644
index 0000000..029c38b
--- /dev/null
+++ b/src/store/api/topupsettingService.js
@@ -0,0 +1,183 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+const token = sessionStorage.getItem('token');
+
+class TopUpService {
+ async fetchTopupSettings() {
+ try {
+ const token = sessionStorage.getItem('token');
+ const response = await fetch(`${BASE_URL}topup/list`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ console.log("Fetched topup settings data:", data);
+
+ let topupSettings = [];
+ if (Array.isArray(data)) {
+ topupSettings = data;
+ } else if (Array.isArray(data.data)) {
+ topupSettings = data.data;
+ } else if (Array.isArray(data.result)) {
+ topupSettings = data.result;
+ } else if (Array.isArray(data.topup_settings)) {
+ topupSettings = data.topup_settings;
+ }
+
+ return topupSettings;
+ } catch (error) {
+ console.error('Error fetching topup settings:', error);
+ throw new Error('Failed to load topup settings. Please try again.');
+ }
+ }
+
+ async createTopupSetting(topupData) {
+ try {
+ const response = await fetch(`${BASE_URL}topup/create`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ topup_amount: topupData.topup_amount,
+ credit_amount: topupData.credit_amount,
+ status: topupData.status || 'active'
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+ return result;
+ } catch (error) {
+ console.error('Error creating topup setting:', error);
+ throw new Error('Failed to create topup setting. Please try again.');
+ }
+ }
+
+ async updateTopupSetting(id, topupData) {
+ try {
+ const response = await fetch(`${BASE_URL}topup/update/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ credit_amount: topupData.credit_amount,
+ ...(topupData.topup_amount && { topup_amount: topupData.topup_amount }),
+ ...(topupData.status && { status: topupData.status })
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+ return result;
+ } catch (error) {
+ console.error('Error updating topup setting:', error);
+ throw new Error('Failed to update topup setting. Please try again.');
+ }
+ }
+
+ async deleteTopupSetting(id) {
+ try {
+ const response = await fetch(`${BASE_URL}topup/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error deleting topup setting:', error);
+ throw new Error('Failed to delete topup setting. Please try again.');
+ }
+ }
+
+ async createCustomerTopup(topupData) {
+ try {
+ const token = sessionStorage.getItem('token');
+
+ const response = await fetch(`https://icom.ipsgroup.com.my/api/customer/topup/create`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ customer_id: topupData.customer_id,
+ payment_method: topupData.payment_method,
+ topup_setting_id: topupData.topup_setting_id
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const result = await response.json();
+ return result;
+ } catch (error) {
+ console.error('Error creating customer topup:', error);
+ throw new Error('Failed to create customer topup. Please try again.');
+ }
+ }
+
+ async getTopupSettingById(id) {
+ try {
+ const token = sessionStorage.getItem('token');
+ const response = await fetch(`${BASE_URL}topup/${id}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`,
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ console.error('Error getting topup setting by ID:', error);
+ const settings = await this.fetchTopupSettings();
+ return settings.find(setting =>
+ setting.id == id
+ );
+ }
+}
+
+ async getActiveTopupSettings() {
+ try {
+ const settings = await this.fetchTopupSettings();
+ return settings.filter(setting => setting.status === 'active');
+ } catch (error) {
+ console.error('Error getting active topup settings:', error);
+ throw new Error('Failed to get active topup settings. Please try again.');
+ }
+ }
+}
+
+const topupService = new TopUpService();
+export default topupService;
\ No newline at end of file
diff --git a/src/store/api/userService.js b/src/store/api/userService.js
new file mode 100644
index 0000000..ee6f99e
--- /dev/null
+++ b/src/store/api/userService.js
@@ -0,0 +1,284 @@
+import {VITE_API_BASE_URL} from "../../constant/config";
+import md5 from "blueimp-md5";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+// Helper function to get fresh token
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+};
+
+class UserService {
+ static async createUser(userData) {
+ try {
+ // Prepare the base user data
+ const requestData = {
+ username: userData.username,
+ name: userData.name,
+ password_hash: md5(userData.password),
+ role: userData.userRoles.toLowerCase(),
+ status: userData.activeStatus.toLowerCase(),
+ menuPermissions: userData.menuPermissions // Add permissions data
+ };
+
+ // Add outlet_id only if the role is 'outlet'
+ if (userData.userRoles.toLowerCase() === 'outlet' && userData.outlet) {
+ requestData.outlet_id = userData.outlet;
+ }
+
+ const response = await fetch(`${BASE_URL}users`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(requestData)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+
+ if (!response.ok) {
+ let errorMessage = 'Failed to create user';
+ try {
+ const errorData = await response.json();
+ errorMessage = errorData.message || errorMessage;
+ // Handle specific validation errors if returned by the API
+ if (errorData.errors) {
+ errorMessage = Object.values(errorData.errors).join(', ');
+ }
+ } catch (e) {
+ const errorText = await response.text();
+ errorMessage = errorText || errorMessage;
+ }
+ throw new Error(errorMessage);
+ }
+
+ const responseData = await response.json();
+
+ // Return the created user data with all relevant information
+ return {
+ ...responseData,
+ userRoles: userData.userRoles,
+ activeStatus: userData.activeStatus,
+ menuPermissions: userData.menuPermissions, // Include permissions in response
+ ...(userData.userRoles.toLowerCase() === 'outlet' && { outlet: userData.outlet })
+ };
+
+ } catch (error) {
+ console.error('Error creating user:', error);
+ throw error;
+ }
+}
+
+ // Get all users
+ static async getAllUsers(user_id) {
+ try {
+ const response = await fetch(`${BASE_URL}users?user_id=${user_id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+
+ if (!response.ok) {
+ let errorMessage = 'Failed to fetch users';
+ try {
+ const errorData = await response.json();
+ errorMessage = errorData.message || errorMessage;
+ } catch (e) {
+ const errorText = await response.text();
+ errorMessage = errorText || errorMessage;
+ }
+ throw new Error(errorMessage);
+ }
+
+ const data = await response.json();
+ console.log("API /users response:", data);
+
+ // Updated to handle the actual API response structure
+ const users = Array.isArray(data) ? data
+ : Array.isArray(data.data) ? data.data // <-- Added this line to handle your API structure
+ : Array.isArray(data.result) ? data.result
+ : Array.isArray(data.users) ? data.users
+ : [];
+
+ if (!Array.isArray(users)) throw new Error("API did not return a user array");
+
+ return users.map(user => ({
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ userRoles: this.capitalizeFirst(user.role),
+ activeStatus: this.capitalizeFirst(user.status),
+ createTime: user.created_at || user.createTime || new Date().toISOString().replace('T', ' ').substr(0, 19)
+ }));
+ } catch (error) {
+ console.error('Error fetching users:', error);
+ throw error;
+ }
+ }
+
+ // Get single user
+ static async getUser(userId) {
+ try {
+ const response = await fetch(`${BASE_URL}users/${userId}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+
+ if (!response.ok) {
+ let errorMessage = 'Failed to fetch user';
+ try {
+ const errorData = await response.json();
+ errorMessage = errorData.message || errorMessage;
+ } catch (e) {
+ const errorText = await response.text();
+ errorMessage = errorText || errorMessage;
+ }
+ throw new Error(errorMessage);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching user:', error);
+ throw error;
+ }
+ }
+
+ // Update user
+static async updateUser(userId, userData) {
+ try {
+ // Prepare the update data in the format backend expects
+ const requestData = {};
+
+ // Map frontend field names to backend field names
+ if (userData.username) requestData.username = userData.username;
+ if (userData.name) requestData.name = userData.name;
+
+ // Convert role format - FIXED: Only check userRoles once
+ if (userData.userRoles) {
+ requestData.role = userData.userRoles.toLowerCase();
+
+ // Handle outlet assignment based on role
+ if (userData.userRoles.toLowerCase() === 'outlet') {
+ // If role is outlet, set outlet_id (even if it's empty/null to clear it)
+ requestData.outlet_id = userData.outlet || null;
+ } else {
+ // If role is NOT outlet, explicitly set outlet_id to null
+ requestData.outlet_id = null;
+ }
+ }
+
+ // Convert status format
+ if (userData.activeStatus) requestData.status = userData.activeStatus.toLowerCase();
+
+ // Add password if provided
+ if (userData.password && userData.password.trim()) {
+ requestData.password = userData.password;
+ }
+
+ // Add menu permissions if provided
+ if (userData.menuPermissions !== undefined) {
+ requestData.menuPermissions = userData.menuPermissions;
+ }
+
+ console.log('Sending update data:', requestData); // Debug log
+
+ const response = await fetch(`${BASE_URL}users/update/${userId}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(requestData)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+
+ if (!response.ok) {
+ let errorMessage = 'Failed to update user';
+ try {
+ const errorData = await response.json();
+ errorMessage = errorData.message || errorMessage;
+ if (errorData.errors) {
+ errorMessage = Object.values(errorData.errors).join(', ');
+ }
+ } catch (e) {
+ const errorText = await response.text();
+ errorMessage = errorText || errorMessage;
+ }
+ throw new Error(errorMessage);
+ }
+
+ return await response.json();
+
+ } catch (error) {
+ console.error('Error updating user:', error);
+ throw error;
+ }
+}
+
+ static async deleteUser(userId) {
+ try {
+ const response = await fetch(`${BASE_URL}users/delete/${userId}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+
+ if (!response.ok) {
+ let errorMessage = 'Failed to delete user';
+ try {
+ const errorData = await response.json();
+ errorMessage = errorData.message || errorMessage;
+ } catch (e) {
+ const errorText = await response.text();
+ errorMessage = errorText || errorMessage;
+ }
+ throw new Error(errorMessage);
+ }
+
+ try {
+ const responseData = await response.json();
+ return responseData;
+ } catch (e) {
+ return { success: true, message: 'User deleted successfully' };
+ }
+ } catch (error) {
+ console.error('Error deleting user:', error);
+ throw error;
+ }
+ }
+
+ static capitalizeFirst(str) {
+ if (!str) return '';
+ return str.charAt(0).toUpperCase() + str.slice(1);
+ }
+}
+
+export default UserService;
\ No newline at end of file
diff --git a/src/store/api/voucherScheduleService.js b/src/store/api/voucherScheduleService.js
new file mode 100644
index 0000000..3c0fd39
--- /dev/null
+++ b/src/store/api/voucherScheduleService.js
@@ -0,0 +1,245 @@
+import { VITE_API_BASE_URL } from "../../constant/config";
+import promoSettingsService from "./promoSettingsService";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem('token');
+ return {
+ 'Authorization': `Bearer ${token}`,
+ };
+};
+
+const voucherScheduleService = {
+ getAll: async (searchParams = {}) => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ if (searchParams.voucher_title) {
+ queryParams.append('voucher_title', searchParams.voucher_title);
+ }
+ if (searchParams.voucher_owner) {
+ queryParams.append('voucher_owner', searchParams.voucher_owner);
+ }
+
+ const queryString = queryParams.toString();
+ const url = `${BASE_URL}voucher-schedule${queryString ? `?${queryString}` : ''}`;
+
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch voucher schedules');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching voucher schedules:', error);
+ throw error;
+ }
+ },
+
+ getById: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}voucher-schedule/${id}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch voucher schedule');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching voucher schedule:', error);
+ throw error;
+ }
+ },
+
+ create: async (data) => {
+ try {
+ const payload = {
+ promo_setting_id: data.promo_setting_id ? String(data.promo_setting_id) : "",
+ voucher_schedule_mode: data.voucher_schedule_mode || "",
+ voucher_date_type: data.voucher_date_type || "",
+ filter_membership: data.filter_membership ? String(data.filter_membership) : "",
+ filter_customer_type: data.filter_customer_type ? String(data.filter_customer_type) : "",
+ schedule_date: data.schedule_date || "",
+ schedule_time: data.schedule_time || "",
+ quantity: data.quantity ? String(data.quantity) : "",
+ voucher_expiration: data.voucher_expiration ? String(data.voucher_expiration) : "",
+ };
+
+ console.log('Payload:', payload);
+ const response = await fetch(`${BASE_URL}voucher-schedule/create`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(payload)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to create voucher schedule');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error creating voucher schedule:', error);
+ throw error;
+ }
+ },
+
+ update: async (id, data) => {
+ try {
+ const payload = {
+ promo_setting_id: data.promo_setting_id ? String(data.promo_setting_id) : "",
+ voucher_schedule_mode: data.voucher_schedule_mode || "",
+ voucher_date_type: data.voucher_date_type || "",
+ filter_membership: data.filter_membership ? String(data.filter_membership) : "",
+ filter_customer_type: data.filter_customer_type ? String(data.filter_customer_type) : "",
+ schedule_date: data.schedule_date || "",
+ schedule_time: data.schedule_time || "",
+ quantity: data.quantity ? String(data.quantity) : "",
+ voucher_expiration: data.voucher_expiration ? String(data.voucher_expiration) : "",
+ };
+
+ console.log('Payload:', payload);
+
+ const response = await fetch(`${BASE_URL}voucher-schedule/update/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(payload)
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || 'Failed to update voucher schedule');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error updating voucher schedule:', error);
+ throw error;
+ }
+ },
+
+ delete: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}voucher-schedule/delete/${id}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ },
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to delete voucher schedule');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error deleting voucher schedule:', error);
+ throw error;
+ }
+ },
+
+ getVoucherSettings: async (voucher_setting_id) => {
+ try {
+ const response = await fetch(`${BASE_URL}settings/voucher/${voucher_setting_id}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...getAuthHeaders(),
+ }
+ });
+
+ if (response.status === 401) {
+ throw new Error('Authentication failed. Please login again.');
+ }
+ if (!response.ok) throw new Error('Failed to fetch voucher settings');
+
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching voucher settings:', error);
+ throw error;
+ }
+ },
+
+ // Updated method to get voucher schedule with promo settings data merged
+ getAllWithSettings: async (searchParams = {}) => {
+ try {
+ // First, get all voucher schedules
+ const scheduleResponse = await voucherScheduleService.getAll(searchParams);
+
+ if (!scheduleResponse.data || scheduleResponse.data.length === 0) {
+ return scheduleResponse;
+ }
+
+ // Then, fetch promo settings for each schedule using promoSettingsService
+ const mergedData = await Promise.all(
+ scheduleResponse.data.map(async (schedule) => {
+ try {
+ // Use promoSettingsService to get the promo setting details
+ const promoSettingResponse = await promoSettingsService.getById(schedule.promo_setting_id);
+ const promoSettingDetails = promoSettingResponse.data;
+
+ return {
+ ...schedule,
+ // Add promo setting details to the schedule
+ voucher_title: promoSettingDetails.voucher_title || '-',
+ voucher_type: promoSettingDetails.voucher_type || '-',
+ amount: promoSettingDetails.amount || '-',
+ voucher_minimum_purchase: promoSettingDetails.voucher_minimum_purchase || '-',
+ scheduleId: schedule.id,
+ promoSettingId: promoSettingDetails.id,
+ id: `schedule-${schedule.id}-promo-${promoSettingDetails.id}`,
+ };
+ } catch (error) {
+ console.error(`Error fetching promo setting for schedule ${schedule.id}:`, error);
+ return {
+ ...schedule,
+ voucher_title: '-',
+ voucher_type: '-',
+ amount: '-',
+ voucher_minimum_purchase: '-',
+ scheduleId: schedule.id,
+ id: `schedule-${schedule.id}`,
+ };
+ }
+ })
+ );
+
+ return {
+ ...scheduleResponse,
+ data: mergedData
+ };
+ } catch (error) {
+ console.error('Error fetching voucher schedules with promo settings:', error);
+ throw error;
+ }
+ }
+};
+
+export default voucherScheduleService;
\ No newline at end of file
diff --git a/src/store/api/voucherService.js b/src/store/api/voucherService.js
new file mode 100644
index 0000000..78b5442
--- /dev/null
+++ b/src/store/api/voucherService.js
@@ -0,0 +1,415 @@
+import { VITE_API_BASE_URL } from "../../constant/config";
+
+const BASE_URL = VITE_API_BASE_URL;
+
+const getAuthHeaders = () => {
+ const token = sessionStorage.getItem("token");
+ return {
+ Authorization: `Bearer ${token}`,
+ };
+};
+
+const voucherService = {
+ getAll: async (searchParams = {}) => {
+ try {
+ const queryParams = new URLSearchParams();
+
+ if (searchParams.date_from) {
+ queryParams.append("date_from", searchParams.date_from);
+ }
+ if (searchParams.date_to) {
+ queryParams.append("date_to", searchParams.date_to);
+ }
+
+ const queryString = queryParams.toString();
+ const url = `${BASE_URL}voucher-point/list${
+ queryString ? `?${queryString}` : ""
+ }`;
+
+ const response = await fetch(url, {
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ });
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to fetch vouchers");
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error fetching vouchers:", error);
+ throw error;
+ }
+ },
+
+ getById: async (id) => {
+ try {
+ // Try the voucher-point endpoint first
+ let response = await fetch(`${BASE_URL}voucher-point/${id}`, {
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ });
+
+ // If voucher-point fails, try the settings/voucher endpoint
+ if (!response.ok) {
+ response = await fetch(`${BASE_URL}settings/voucher/${id}`, {
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ });
+ }
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to fetch voucher");
+
+ const data = await response.json();
+
+ // Handle different response formats
+ if (data.data && Array.isArray(data.data) && data.data.length > 0) {
+ return { success: true, data: data.data[0] };
+ } else if (data.data && !Array.isArray(data.data)) {
+ return { success: true, data: data.data };
+ } else {
+ return data;
+ }
+ } catch (error) {
+ console.error("Error fetching voucher:", error);
+ throw error;
+ }
+ },
+
+ create: async (data) => {
+ try {
+ const formData = new FormData();
+
+ // Add all the form fields
+ formData.append("voucher_name", data.voucher_name || "");
+ formData.append(
+ "voucher_minimum_purchase",
+ data.voucher_minimum_purchase || ""
+ );
+ formData.append(
+ "voucher_total_count",
+ data.voucher_total_count ? String(data.voucher_total_count) : ""
+ );
+ formData.append(
+ "voucher_redeem_count",
+ data.voucher_redeem_count ? String(data.voucher_redeem_count) : ""
+ );
+ formData.append(
+ "voucher_count_customer",
+ data.voucher_count_customer ? String(data.voucher_count_customer) : ""
+ );
+ formData.append("voucher_expiry_type", data.voucher_expiry_type || "");
+ formData.append("voucher_expiry_value", data.voucher_expiry_value || "");
+ formData.append("voucher_expired_date", data.voucher_expired_date || "");
+ formData.append(
+ "voucher_point_redeem",
+ data.voucher_point_redeem ? String(data.voucher_point_redeem) : ""
+ );
+ formData.append("voucher_type", data.voucher_type || "");
+ formData.append("voucher_setting", data.voucher_setting || "");
+ formData.append("voucher_details", data.voucher_details || "");
+ formData.append("voucher_tnc", data.voucher_tnc || "");
+ formData.append("voucher_status", data.voucher_status || "");
+ formData.append(
+ "promo_setting_id",
+ data.promo_setting_id ? String(data.promo_setting_id) : ""
+ );
+
+ if (data.voucher_image && data.voucher_image instanceof File) {
+ formData.append("voucher_image", data.voucher_image);
+ }
+
+ console.log("Creating voucher with data:", Object.fromEntries(formData));
+
+ const response = await fetch(`${BASE_URL}voucher-point/create`, {
+ method: "POST",
+ headers: {
+ ...getAuthHeaders(),
+ // Don't set Content-Type header for FormData, let the browser set it
+ },
+ body: formData,
+ });
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || "Failed to create voucher");
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error creating voucher:", error);
+ throw error;
+ }
+ },
+
+ update: async (id, data) => {
+ try {
+ // Check if we have a file to upload
+ const hasFile = data.voucher_image && data.voucher_image instanceof File;
+
+ let response;
+
+ if (hasFile) {
+ // Use FormData for file uploads
+ const formData = new FormData();
+
+ // Add all form fields to FormData
+ Object.keys(data).forEach((key) => {
+ if (data[key] !== null && data[key] !== undefined) {
+ if (key === "voucher_image" && data[key] instanceof File) {
+ formData.append(key, data[key]);
+ } else {
+ formData.append(key, String(data[key]));
+ }
+ }
+ });
+
+ console.log(
+ "Updating voucher with FormData:",
+ Object.fromEntries(formData)
+ );
+
+ response = await fetch(`${BASE_URL}settings/voucher/update/${id}`, {
+ method: "POST",
+ headers: {
+ ...getAuthHeaders(),
+ },
+ body: formData,
+ });
+
+ if (!response.ok) {
+ response = await fetch(`${BASE_URL}voucher-point/update/${id}`, {
+ method: "POST",
+ headers: {
+ ...getAuthHeaders(),
+ },
+ body: formData,
+ });
+ }
+ } else {
+ // Use JSON for updates without file
+ const payload = {
+ voucher_name: data.voucher_name || "",
+ voucher_minimum_purchase: data.voucher_minimum_purchase || 0,
+ voucher_total_count: data.voucher_total_count || 0,
+ voucher_redeem_count: data.voucher_redeem_count || 0,
+ voucher_count_customer: data.voucher_count_customer || 0,
+ voucher_expiry_type: data.voucher_expiry_type || "",
+ voucher_expiry_value: data.voucher_expiry_value || "",
+ voucher_expired_date: data.voucher_expired_date || "",
+ voucher_point_redeem: data.voucher_point_redeem || 0,
+ voucher_type: data.voucher_type || "",
+ voucher_setting: data.voucher_setting || "",
+ voucher_details: data.voucher_details || "",
+ voucher_tnc: data.voucher_tnc || "",
+ voucher_status: data.voucher_status || "",
+ promo_setting_id: data.promo_setting_id || 0,
+ };
+
+ console.log("Updating voucher with JSON:", payload);
+
+ // Try settings/voucher/update endpoint first (original code path)
+ response = await fetch(`${BASE_URL}settings/voucher/update/${id}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(payload),
+ });
+
+ // If settings endpoint fails, try voucher-point endpoint
+ if (!response.ok) {
+ response = await fetch(`${BASE_URL}voucher-point/update/${id}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(payload),
+ });
+ }
+ }
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || "Failed to update voucher");
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error updating voucher:", error);
+ throw error;
+ }
+ },
+
+ delete: async (id) => {
+ try {
+ const response = await fetch(`${BASE_URL}voucher-point/delete/${id}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ });
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || "Failed to delete voucher");
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error deleting voucher:", error);
+ throw error;
+ }
+ },
+
+ // Additional utility methods
+ getVouchersByStatus: async (status) => {
+ try {
+ const response = await fetch(
+ `${BASE_URL}voucher-point/list?status=${status}`,
+ {
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ }
+ );
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to fetch vouchers by status");
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error fetching vouchers by status:", error);
+ throw error;
+ }
+ },
+
+ validateVoucher: async (voucherId) => {
+ try {
+ const response = await fetch(
+ `${BASE_URL}voucher-point/validate/${voucherId}`,
+ {
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ }
+ );
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) throw new Error("Failed to validate voucher");
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error validating voucher:", error);
+ throw error;
+ }
+ },
+
+ redeemVoucher: async (voucherId, customerId) => {
+ try {
+ const response = await fetch(`${BASE_URL}voucher-point/redeem`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify({
+ voucher_id: voucherId,
+ customer_id: customerId,
+ }),
+ });
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || "Failed to redeem voucher");
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error redeeming voucher:", error);
+ throw error;
+ }
+ },
+
+ searchMemberList: async (filters) => {
+ const queryString = new URLSearchParams(filters).toString();
+
+ try {
+ const response = await fetch(
+ `${BASE_URL}get/member-list?${queryString}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ }
+ );
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error searching member list:", error);
+ throw error;
+ }
+ },
+
+ sendVoucher: async (data) => {
+ console.log(data);
+ try {
+ const response = await fetch(`${BASE_URL}send-voucher`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeaders(),
+ },
+ body: JSON.stringify(data),
+ });
+
+ if (response.status === 401) {
+ throw new Error("Authentication failed. Please login again.");
+ }
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.message || "Failed to send voucher");
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error("Error sending voucher:", error);
+ throw error;
+ }
+ },
+};
+
+export default voucherService;
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000..865e6d8
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,17 @@
+import { configureStore } from "@reduxjs/toolkit";
+import rootReducer from "./rootReducer";
+import { apiSlice } from "./api/apiSlice";
+
+const store = configureStore({
+ reducer: {
+ ...rootReducer,
+ [apiSlice.reducerPath]: apiSlice.reducer,
+ },
+ //devTools: false,
+ middleware: (getDefaultMiddleware) => {
+ const middleware = [...getDefaultMiddleware(), apiSlice.middleware];
+ return middleware;
+ },
+});
+
+export default store;
diff --git a/src/store/layout.js b/src/store/layout.js
new file mode 100644
index 0000000..6351c87
--- /dev/null
+++ b/src/store/layout.js
@@ -0,0 +1,136 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+// theme config import
+import themeConfig from "@/configs/themeConfig";
+
+const initialDarkMode = () => {
+ const item = window.localStorage.getItem("darkMode");
+ return item ? JSON.parse(item) : themeConfig.layout.darkMode;
+};
+
+const initialSidebarCollapsed = () => {
+ const item = window.localStorage.getItem("sidebarCollapsed");
+ return item ? JSON.parse(item) : themeConfig.layout.menu.isCollapsed;
+};
+
+const initialSemiDarkMode = () => {
+ const item = window.localStorage.getItem("semiDarkMode");
+ return item ? JSON.parse(item) : themeConfig.layout.semiDarkMode;
+};
+
+const initialRtl = () => {
+ const item = window.localStorage.getItem("direction");
+ return item ? JSON.parse(item) : themeConfig.layout.isRTL;
+};
+
+const initialSkin = () => {
+ const item = window.localStorage.getItem("skin");
+ return item ? JSON.parse(item) : themeConfig.layout.skin;
+};
+
+const initialType = () => {
+ const item = window.localStorage.getItem("type");
+ return item ? JSON.parse(item) : themeConfig.layout.type;
+};
+
+const initialMonochrome = () => {
+ const item = window.localStorage.getItem("monochrome");
+ return item ? JSON.parse(item) : themeConfig.layout.isMonochrome;
+};
+const initialState = {
+ isRTL: initialRtl(),
+ darkMode: initialDarkMode(),
+ isCollapsed: initialSidebarCollapsed(),
+ customizer: themeConfig.layout.customizer,
+ semiDarkMode: initialSemiDarkMode(),
+ skin: initialSkin(),
+ contentWidth: themeConfig.layout.contentWidth,
+ type: initialType(),
+ menuHidden: themeConfig.layout.menu.isHidden,
+ navBarType: themeConfig.layout.navBarType,
+ footerType: themeConfig.layout.footerType,
+ mobileMenu: themeConfig.layout.mobileMenu,
+ isMonochrome: initialMonochrome(),
+};
+
+export const layoutSlice = createSlice({
+ name: "layout",
+ initialState,
+ reducers: {
+ // handle dark mode
+ handleDarkMode: (state, action) => {
+ state.darkMode = action.payload;
+ window.localStorage.setItem("darkMode", action.payload);
+ },
+ // handle sidebar collapsed
+ handleSidebarCollapsed: (state, action) => {
+ state.isCollapsed = action.payload;
+ window.localStorage.setItem("sidebarCollapsed", action.payload);
+ },
+ // handle customizer
+ handleCustomizer: (state, action) => {
+ state.customizer = action.payload;
+ },
+ // handle semiDark
+ handleSemiDarkMode: (state, action) => {
+ state.semiDarkMode = action.payload;
+ window.localStorage.setItem("semiDarkMode", action.payload);
+ },
+ // handle rtl
+ handleRtl: (state, action) => {
+ state.isRTL = action.payload;
+ window.localStorage.setItem("direction", JSON.stringify(action.payload));
+ },
+ // handle skin
+ handleSkin: (state, action) => {
+ state.skin = action.payload;
+ window.localStorage.setItem("skin", JSON.stringify(action.payload));
+ },
+ // handle content width
+ handleContentWidth: (state, action) => {
+ state.contentWidth = action.payload;
+ },
+ // handle type
+ handleType: (state, action) => {
+ state.type = action.payload;
+ window.localStorage.setItem("type", JSON.stringify(action.payload));
+ },
+ // handle menu hidden
+ handleMenuHidden: (state, action) => {
+ state.menuHidden = action.payload;
+ },
+ // handle navbar type
+ handleNavBarType: (state, action) => {
+ state.navBarType = action.payload;
+ },
+ // handle footer type
+ handleFooterType: (state, action) => {
+ state.footerType = action.payload;
+ },
+ handleMobileMenu: (state, action) => {
+ state.mobileMenu = action.payload;
+ },
+ handleMonoChrome: (state, action) => {
+ state.isMonochrome = action.payload;
+ window.localStorage.setItem("monochrome", JSON.stringify(action.payload));
+ },
+ },
+});
+
+export const {
+ handleDarkMode,
+ handleSidebarCollapsed,
+ handleCustomizer,
+ handleSemiDarkMode,
+ handleRtl,
+ handleSkin,
+ handleContentWidth,
+ handleType,
+ handleMenuHidden,
+ handleNavBarType,
+ handleFooterType,
+ handleMobileMenu,
+ handleMonoChrome,
+} = layoutSlice.actions;
+
+export default layoutSlice.reducer;
diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js
new file mode 100644
index 0000000..8ec348f
--- /dev/null
+++ b/src/store/rootReducer.js
@@ -0,0 +1,8 @@
+import layout from "./layout";
+import auth from "./api/auth/authSlice";
+
+const rootReducer = {
+ layout,
+ auth,
+};
+export default rootReducer;
diff --git a/src/utils/dataTableStyles.js b/src/utils/dataTableStyles.js
new file mode 100644
index 0000000..e01b268
--- /dev/null
+++ b/src/utils/dataTableStyles.js
@@ -0,0 +1,54 @@
+const customStyles = {
+ headRow: {
+ style: {
+ backgroundColor: '#312e81',
+ color: 'white',
+ minHeight: '50px',
+ fontSize: '16px',
+ justifyContent: 'center',
+ },
+ },
+ headCells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ fontWeight: '500',
+ justifyContent: 'center',
+ textAlign: 'center',
+ subHeaderWrap: true,
+ },
+ },
+ rows: {
+ style: {
+ minHeight: '60px',
+ fontSize: '15px',
+ '&:hover': {
+ backgroundColor: '#f9fafb',
+ },
+ justifyContent: 'center',
+ center: true,
+ },
+ highlightOnHoverStyle: {
+ backgroundColor: '#f9fafb',
+ },
+ },
+ cells: {
+ style: {
+ paddingLeft: '16px',
+ paddingRight: '16px',
+ justifyContent: 'center',
+ textAlign: 'center',
+ alignItems: 'center',
+ center: true,
+ },
+ },
+ pagination: {
+ style: {
+ borderTopStyle: 'solid',
+ borderTopWidth: '1px',
+ borderTopColor: '#e5e7eb',
+ },
+ },
+};
+
+export default customStyles;
\ No newline at end of file
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
new file mode 100644
index 0000000..bd5e09b
--- /dev/null
+++ b/tailwind.config.cjs
@@ -0,0 +1,169 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ "./node_modules/react-tailwindcss-datepicker/dist/index.esm.js",
+ ],
+ mode: "jit",
+ darkMode: "class",
+ theme: {
+ container: {
+ center: true,
+ padding: {
+ DEFAULT: "15px",
+ sm: "15px",
+ lg: "15px",
+ xl: "0",
+ "2xl": "0",
+ },
+ screens: {
+ sm: "640px",
+ md: "768px",
+ lg: "1024px",
+ xl: "1280px",
+ "2xl": "1280px",
+ },
+ },
+ extend: {
+ colors: {
+ primary: {
+ 50: "#F6F8FF",
+ 100: "#EDF0FF",
+ 200: "#D1DAFE",
+ 300: "#B4C2FD",
+ 400: "#8092FF",
+ 500: "#4669fa",
+ 600: "#3F5EDF",
+ 700: "#2A3F96",
+ 800: "#203071",
+ 900: "#151F49",
+ },
+ secondary: {
+ 50: "#F9FAFB",
+ 100: "#F4F5F7",
+ 200: "#E5E7EB",
+ 300: "#D2D6DC",
+ 400: "#9FA6B2",
+ 500: "#A0AEC0",
+ 600: "#475569",
+ 700: "#334155",
+ 800: "#1E293B",
+ 900: "#0F172A",
+ },
+ danger: {
+ 50: "#FFF7F7",
+ 100: "#FEEFEF",
+ 200: "#FCD6D7",
+ 300: "#FABBBD",
+ 400: "#F68B8D",
+ 500: "#F1595C",
+ 600: "#D75052",
+ 700: "#913638",
+ 800: "#6D292A",
+ 900: "#461A1B",
+ },
+ black: {
+ 50: "#F9FAFB",
+ 100: "#F4F5F7",
+ 200: "#E5E7EB",
+ 300: "#D2D6DC",
+ 400: "#9FA6B2",
+ 500: "#111112",
+ 600: "#475569",
+ 700: "#334155",
+ 800: "#1E293B",
+ 900: "#0F172A",
+ },
+ warning: {
+ 50: "#FFFAF8",
+ 100: "#FFF4F1",
+ 200: "#FEE4DA",
+ 300: "#FDD2C3",
+ 400: "#FCB298",
+ 500: "#FA916B",
+ 600: "#DF8260",
+ 700: "#965741",
+ 800: "#714231",
+ 900: "#492B20",
+ },
+ info: {
+ 50: "#F3FEFF",
+ 100: "#E7FEFF",
+ 200: "#C5FDFF",
+ 300: "#A3FCFF",
+ 400: "#5FF9FF",
+ 500: "#0CE7FA",
+ 600: "#00B8D4",
+ 700: "#007A8D",
+ 800: "#005E67",
+ 900: "#003F42",
+ },
+ success: {
+ 50: "#F3FEF8",
+ 100: "#E7FDF1",
+ 200: "#C5FBE3",
+ 300: "#A3F9D5",
+ 400: "#5FF5B1",
+ 500: "#50C793",
+ 600: "#3F9A7A",
+ 700: "#2E6D61",
+ 800: "#1F4B47",
+ 900: "#0F2A2E",
+ },
+ gray: {
+ 50: "#F9FAFB",
+ 100: "#F4F5F7",
+ 200: "#E5E7EB",
+ 300: "#D2D6DC",
+ 400: "#9FA6B2",
+ 500: "#68768A",
+ 600: "#475569",
+ 700: "#334155",
+ 800: "#1E293B",
+ 900: "#0F172A",
+ },
+ },
+
+ fontFamily: {
+ inter: ["Inter", "sans-serif"],
+ },
+ boxShadow: {
+ base: "0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16)",
+ base2:
+ "0px 2px 4px rgba(40, 41, 61, 0.04), 0px 8px 16px rgba(96, 97, 112, 0.16)",
+ base3: "16px 10px 40px rgba(15, 23, 42, 0.22)",
+ deep: "-2px 0px 8px rgba(0, 0, 0, 0.16)",
+ dropdown: "0px 4px 8px rgba(0, 0, 0, 0.08)",
+
+ testi: "0px 4px 24px rgba(0, 0, 0, 0.06)",
+ todo: "rgba(235 233 241, 0.6) 0px 3px 10px 0px",
+ },
+ keyframes: {
+ zoom: {
+ "0%, 100%": { transform: "scale(0.5)" },
+ "50%": { transform: "scale(1)" },
+ },
+ tada: {
+ "0%": { transform: "scale3d(1, 1, 1)" },
+ "10%, 20%": {
+ transform: "scale3d(1, 1, 0.95) rotate3d(0, 0, 1, -10deg)",
+ },
+ "30%, 50%, 70%, 90%": {
+ transform: "scale3d(1, 1, 1) rotate3d(0, 0, 1, 10deg)",
+ },
+ "40%, 60%, 80%": {
+ transform: "rotate3d(0, 0, 1, -10deg)",
+ },
+ "100%": { transform: "scale3d(1, 1, 1)" },
+ },
+ },
+ animation: {
+ "spin-slow": "spin 3s linear infinite",
+ zoom: "zoom 1s ease-in-out infinite",
+ tada: "tada 1.5s ease-in-out infinite",
+ },
+ },
+ },
+ plugins: [],
+};
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..6522f0d
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,63 @@
+import { defineConfig } from "vite";
+import reactRefresh from "@vitejs/plugin-react-refresh";
+import react from "@vitejs/plugin-react";
+import path from "path";
+import rollupReplace from "@rollup/plugin-replace";
+// https://vitejs.dev/config/
+export default defineConfig({
+ base: '/cms/',
+ build: {
+ outDir: 'dist/cms',
+ assetsDir: 'assets',
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ vendor: ['react', 'react-dom'],
+ ui: ['@coreui/react', '@coreui/coreui'],
+ charts: ['apexcharts', 'react-apexcharts'],
+ }
+ }
+ }
+ },
+ resolve: {
+ alias: [
+ {
+ // "@": path.resolve(__dirname, "./src"),
+ find: "@",
+ replacement: path.resolve(__dirname, "./src"),
+ },
+ ],
+ },
+
+ plugins: [
+ rollupReplace({
+ preventAssignment: true,
+ values: {
+ __DEV__: JSON.stringify(true),
+ "process.env.NODE_ENV": JSON.stringify("development"),
+ },
+ }),
+ react(),
+ reactRefresh(),
+ ],
+
+ server: {
+ proxy: {
+ '/api': {
+ target: 'https://icom.ipsgroup.com.my/',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ secure: true,
+ configure: (proxy, options) => {
+ proxy.on('proxyReq', (proxyReq, req, res) => {
+ console.log('Proxying request:', req.method, req.url);
+ console.log('Target:', proxyReq.path);
+ });
+ proxy.on('proxyRes', (proxyRes, req, res) => {
+ console.log('Proxy response:', proxyRes.statusCode, req.url);
+ });
+ }
+ }
+ }
+ }
+});
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..63ab102
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,3952 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@alloc/quick-lru@^5.2.0":
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz"
+ integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
+
+"@ampproject/remapping@^2.2.0":
+ version "2.2.1"
+ resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz"
+ integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.0"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@ant-design/colors@^7.0.0", "@ant-design/colors@^7.2.1":
+ version "7.2.1"
+ resolved "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz"
+ integrity sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==
+ dependencies:
+ "@ant-design/fast-color" "^2.0.6"
+
+"@ant-design/cssinjs-utils@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz"
+ integrity sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==
+ dependencies:
+ "@ant-design/cssinjs" "^1.21.0"
+ "@babel/runtime" "^7.23.2"
+ rc-util "^5.38.0"
+
+"@ant-design/cssinjs@^1.21.0", "@ant-design/cssinjs@^1.23.0":
+ version "1.24.0"
+ resolved "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz"
+ integrity sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ "@emotion/hash" "^0.8.0"
+ "@emotion/unitless" "^0.7.5"
+ classnames "^2.3.1"
+ csstype "^3.1.3"
+ rc-util "^5.35.0"
+ stylis "^4.3.4"
+
+"@ant-design/fast-color@^2.0.6":
+ version "2.0.6"
+ resolved "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz"
+ integrity sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==
+ dependencies:
+ "@babel/runtime" "^7.24.7"
+
+"@ant-design/icons-svg@^4.4.0":
+ version "4.4.2"
+ resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
+ integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
+
+"@ant-design/icons@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz"
+ integrity sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==
+ dependencies:
+ "@ant-design/colors" "^7.0.0"
+ "@ant-design/icons-svg" "^4.4.0"
+ "@babel/runtime" "^7.24.8"
+ classnames "^2.2.6"
+ rc-util "^5.31.1"
+
+"@ant-design/react-slick@~1.1.2":
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz"
+ integrity sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==
+ dependencies:
+ "@babel/runtime" "^7.10.4"
+ classnames "^2.2.5"
+ json2mq "^0.2.0"
+ resize-observer-polyfill "^1.5.1"
+ throttle-debounce "^5.0.0"
+
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz"
+ integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.27.1"
+ js-tokens "^4.0.0"
+ picocolors "^1.1.1"
+
+"@babel/compat-data@^7.27.2":
+ version "7.27.2"
+ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz"
+ integrity sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==
+
+"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.14.8", "@babel/core@^7.28.0":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz"
+ integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.27.1"
+ "@babel/generator" "^7.28.3"
+ "@babel/helper-compilation-targets" "^7.27.2"
+ "@babel/helper-module-transforms" "^7.28.3"
+ "@babel/helpers" "^7.28.3"
+ "@babel/parser" "^7.28.3"
+ "@babel/template" "^7.27.2"
+ "@babel/traverse" "^7.28.3"
+ "@babel/types" "^7.28.2"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
+"@babel/generator@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz"
+ integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==
+ dependencies:
+ "@babel/parser" "^7.28.3"
+ "@babel/types" "^7.28.2"
+ "@jridgewell/gen-mapping" "^0.3.12"
+ "@jridgewell/trace-mapping" "^0.3.28"
+ jsesc "^3.0.2"
+
+"@babel/helper-compilation-targets@^7.27.2":
+ version "7.27.2"
+ resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz"
+ integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==
+ dependencies:
+ "@babel/compat-data" "^7.27.2"
+ "@babel/helper-validator-option" "^7.27.1"
+ browserslist "^4.24.0"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
+"@babel/helper-globals@^7.28.0":
+ version "7.28.0"
+ resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz"
+ integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
+
+"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz"
+ integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==
+ dependencies:
+ "@babel/traverse" "^7.27.1"
+ "@babel/types" "^7.27.1"
+
+"@babel/helper-module-transforms@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz"
+ integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==
+ dependencies:
+ "@babel/helper-module-imports" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.27.1"
+ "@babel/traverse" "^7.28.3"
+
+"@babel/helper-plugin-utils@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz"
+ integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==
+
+"@babel/helper-string-parser@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz"
+ integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
+
+"@babel/helper-validator-identifier@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz"
+ integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
+
+"@babel/helper-validator-option@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz"
+ integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
+
+"@babel/helpers@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz"
+ integrity sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==
+ dependencies:
+ "@babel/template" "^7.27.2"
+ "@babel/types" "^7.28.2"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz"
+ integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==
+ dependencies:
+ "@babel/types" "^7.28.2"
+
+"@babel/plugin-transform-react-jsx-self@^7.14.5", "@babel/plugin-transform-react-jsx-self@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz"
+ integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.27.1"
+
+"@babel/plugin-transform-react-jsx-source@^7.14.5", "@babel/plugin-transform-react-jsx-source@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz"
+ integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.27.1"
+
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz"
+ integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==
+
+"@babel/template@^7.27.2":
+ version "7.27.2"
+ resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz"
+ integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
+ dependencies:
+ "@babel/code-frame" "^7.27.1"
+ "@babel/parser" "^7.27.2"
+ "@babel/types" "^7.27.1"
+
+"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz"
+ integrity sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==
+ dependencies:
+ "@babel/code-frame" "^7.27.1"
+ "@babel/generator" "^7.28.3"
+ "@babel/helper-globals" "^7.28.0"
+ "@babel/parser" "^7.28.3"
+ "@babel/template" "^7.27.2"
+ "@babel/types" "^7.28.2"
+ debug "^4.3.1"
+
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.28.2":
+ version "7.28.2"
+ resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz"
+ integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==
+ dependencies:
+ "@babel/helper-string-parser" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.27.1"
+
+"@coreui/coreui-pro@^5.14.2":
+ version "5.14.2"
+ resolved "https://registry.npmjs.org/@coreui/coreui-pro/-/coreui-pro-5.14.2.tgz"
+ integrity sha512-WjZNLPKGe+VbCk4Jdzorwpd+h1xNaIk95mVHOS8as2ScFe2j7gtphP3SRpxr+n8+R6SsuKSFfp/CUfjk/JqtMA==
+ dependencies:
+ html-entities "^2.6.0"
+ html-to-md "^0.8.8"
+
+"@coreui/coreui@^5.4.0":
+ version "5.4.0"
+ resolved "https://registry.npmjs.org/@coreui/coreui/-/coreui-5.4.0.tgz"
+ integrity sha512-PtLossDHiU8Q7l/MDwaNjvpleDcPKDhkqvYGAhF+/rolZQteQFwjTx/7jD+v5c0YP+PHu9kmhrfTCaHMwK0UPA==
+ dependencies:
+ html-entities "^2.6.0"
+ html-to-md "^0.8.8"
+
+"@coreui/react-pro@^5.17.0":
+ version "5.17.0"
+ resolved "https://registry.npmjs.org/@coreui/react-pro/-/react-pro-5.17.0.tgz"
+ integrity sha512-W3hxa0ZI++A/Uoot+eZKtSjnTXZ+vRaBd4PEfSh4ncAa9eiEuh3jTs362ri5PkB/PzdQNHaeAqhNujWCHlHrFA==
+ dependencies:
+ "@coreui/coreui-pro" "^5.14.2"
+ "@popperjs/core" "^2.11.8"
+ prop-types "^15.8.1"
+
+"@coreui/react@^5.7.0":
+ version "5.7.0"
+ resolved "https://registry.npmjs.org/@coreui/react/-/react-5.7.0.tgz"
+ integrity sha512-iaR9VVCY9jKLhRWvM9evQ8hGaa74u+NuU3fRYfbC9sc2qgGrtUQXtG+IOnn+7C40kOwcswnnnp84cKRaoFZ+CQ==
+ dependencies:
+ "@coreui/coreui" "^5.4.0"
+ "@popperjs/core" "^2.11.8"
+ prop-types "^15.8.1"
+
+"@dnd-kit/accessibility@^3.1.1":
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz"
+ integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==
+ dependencies:
+ tslib "^2.0.0"
+
+"@dnd-kit/core@^6.3.0", "@dnd-kit/core@^6.3.1":
+ version "6.3.1"
+ resolved "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz"
+ integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==
+ dependencies:
+ "@dnd-kit/accessibility" "^3.1.1"
+ "@dnd-kit/utilities" "^3.2.2"
+ tslib "^2.0.0"
+
+"@dnd-kit/modifiers@^9.0.0":
+ version "9.0.0"
+ resolved "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz"
+ integrity sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==
+ dependencies:
+ "@dnd-kit/utilities" "^3.2.2"
+ tslib "^2.0.0"
+
+"@dnd-kit/utilities@^3.2.2":
+ version "3.2.2"
+ resolved "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz"
+ integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
+ dependencies:
+ tslib "^2.0.0"
+
+"@emotion/babel-plugin@^11.11.0":
+ version "11.11.0"
+ resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz"
+ integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==
+ dependencies:
+ "@babel/helper-module-imports" "^7.16.7"
+ "@babel/runtime" "^7.18.3"
+ "@emotion/hash" "^0.9.1"
+ "@emotion/memoize" "^0.8.1"
+ "@emotion/serialize" "^1.1.2"
+ babel-plugin-macros "^3.1.0"
+ convert-source-map "^1.5.0"
+ escape-string-regexp "^4.0.0"
+ find-root "^1.1.0"
+ source-map "^0.5.7"
+ stylis "4.2.0"
+
+"@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0":
+ version "11.11.0"
+ resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz"
+ integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==
+ dependencies:
+ "@emotion/memoize" "^0.8.1"
+ "@emotion/sheet" "^1.2.2"
+ "@emotion/utils" "^1.2.1"
+ "@emotion/weak-memoize" "^0.3.1"
+ stylis "4.2.0"
+
+"@emotion/hash@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz"
+ integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
+"@emotion/hash@^0.9.1":
+ version "0.9.1"
+ resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz"
+ integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==
+
+"@emotion/is-prop-valid@^0.8.2":
+ version "0.8.8"
+ resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz"
+ integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
+ dependencies:
+ "@emotion/memoize" "0.7.4"
+
+"@emotion/is-prop-valid@1.2.2":
+ version "1.2.2"
+ resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz"
+ integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==
+ dependencies:
+ "@emotion/memoize" "^0.8.1"
+
+"@emotion/memoize@^0.8.1":
+ version "0.8.1"
+ resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz"
+ integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==
+
+"@emotion/memoize@0.7.4":
+ version "0.7.4"
+ resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz"
+ integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
+
+"@emotion/react@^11.8.1":
+ version "11.11.0"
+ resolved "https://registry.npmjs.org/@emotion/react/-/react-11.11.0.tgz"
+ integrity sha512-ZSK3ZJsNkwfjT3JpDAWJZlrGD81Z3ytNDsxw1LKq1o+xkmO5pnWfr6gmCC8gHEFf3nSSX/09YrG67jybNPxSUw==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ "@emotion/babel-plugin" "^11.11.0"
+ "@emotion/cache" "^11.11.0"
+ "@emotion/serialize" "^1.1.2"
+ "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1"
+ "@emotion/utils" "^1.2.1"
+ "@emotion/weak-memoize" "^0.3.1"
+ hoist-non-react-statics "^3.3.1"
+
+"@emotion/serialize@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz"
+ integrity sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==
+ dependencies:
+ "@emotion/hash" "^0.9.1"
+ "@emotion/memoize" "^0.8.1"
+ "@emotion/unitless" "^0.8.1"
+ "@emotion/utils" "^1.2.1"
+ csstype "^3.0.2"
+
+"@emotion/sheet@^1.2.2":
+ version "1.2.2"
+ resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz"
+ integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==
+
+"@emotion/unitless@^0.7.5":
+ version "0.7.5"
+ resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz"
+ integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
+
+"@emotion/unitless@^0.8.1":
+ version "0.8.1"
+ resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz"
+ integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
+
+"@emotion/unitless@0.8.1":
+ version "0.8.1"
+ resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz"
+ integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
+
+"@emotion/use-insertion-effect-with-fallbacks@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz"
+ integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==
+
+"@emotion/utils@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz"
+ integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==
+
+"@emotion/weak-memoize@^0.3.1":
+ version "0.3.1"
+ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz"
+ integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==
+
+"@esbuild/aix-ppc64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz"
+ integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==
+
+"@esbuild/android-arm@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz"
+ integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==
+
+"@esbuild/android-arm64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz"
+ integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==
+
+"@esbuild/android-x64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz"
+ integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==
+
+"@esbuild/darwin-arm64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz"
+ integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==
+
+"@esbuild/darwin-x64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz"
+ integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==
+
+"@esbuild/freebsd-arm64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz"
+ integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==
+
+"@esbuild/freebsd-x64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz"
+ integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==
+
+"@esbuild/linux-arm@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz"
+ integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==
+
+"@esbuild/linux-arm64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz"
+ integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==
+
+"@esbuild/linux-ia32@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz"
+ integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==
+
+"@esbuild/linux-loong64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz"
+ integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==
+
+"@esbuild/linux-mips64el@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz"
+ integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==
+
+"@esbuild/linux-ppc64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz"
+ integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==
+
+"@esbuild/linux-riscv64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz"
+ integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==
+
+"@esbuild/linux-s390x@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz"
+ integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==
+
+"@esbuild/linux-x64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz"
+ integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==
+
+"@esbuild/netbsd-arm64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz"
+ integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==
+
+"@esbuild/netbsd-x64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz"
+ integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==
+
+"@esbuild/openbsd-arm64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz"
+ integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==
+
+"@esbuild/openbsd-x64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz"
+ integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==
+
+"@esbuild/openharmony-arm64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz"
+ integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==
+
+"@esbuild/sunos-x64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz"
+ integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==
+
+"@esbuild/win32-arm64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz"
+ integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==
+
+"@esbuild/win32-ia32@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz"
+ integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==
+
+"@esbuild/win32-x64@0.25.9":
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz"
+ integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
+
+"@faker-js/faker@^8.0.2":
+ version "8.0.2"
+ resolved "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz"
+ integrity sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==
+
+"@floating-ui/core@^1.2.6":
+ version "1.2.6"
+ resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz"
+ integrity sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==
+
+"@floating-ui/dom@^1.0.1":
+ version "1.2.8"
+ resolved "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.8.tgz"
+ integrity sha512-XLwhYV90MxiHDq6S0rzFZj00fnDM+A1R9jhSioZoMsa7G0Q0i+Q4x40ajR8FHSdYDE1bgjG45mIWe6jtv9UPmg==
+ dependencies:
+ "@floating-ui/core" "^1.2.6"
+
+"@fullcalendar/core@^6.1.4", "@fullcalendar/core@~6.1.19", "@fullcalendar/core@~6.1.7":
+ version "6.1.19"
+ resolved "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.19.tgz"
+ integrity sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ==
+ dependencies:
+ preact "~10.12.1"
+
+"@fullcalendar/daygrid@^6.1.19", "@fullcalendar/daygrid@~6.1.19":
+ version "6.1.19"
+ resolved "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.19.tgz"
+ integrity sha512-IAAfnMICnVWPjpT4zi87i3FEw0xxSza0avqY/HedKEz+l5MTBYvCDPOWDATpzXoLut3aACsjktIyw9thvIcRYQ==
+
+"@fullcalendar/interaction@^6.1.19":
+ version "6.1.19"
+ resolved "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.19.tgz"
+ integrity sha512-GOciy79xe8JMVp+1evAU3ytdwN/7tv35t5i1vFkifiuWcQMLC/JnLg/RA2s4sYmQwoYhTw/p4GLcP0gO5B3X5w==
+
+"@fullcalendar/list@^6.1.4":
+ version "6.1.7"
+ resolved "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.7.tgz"
+ integrity sha512-Fl6jGKylhrk+g/RCISsv66vzpzjCFhd4r3nUDeHTAAE375OYGlVPKpH67YaKxpMrofj82JLIzS3JEzyQBZo0Cg==
+
+"@fullcalendar/react@^6.1.19":
+ version "6.1.19"
+ resolved "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.19.tgz"
+ integrity sha512-FP78vnyylaL/btZeHig8LQgfHgfwxLaIG6sKbNkzkPkKEACv11UyyBoTSkaavPsHtXvAkcTED1l7TOunAyPEnA==
+
+"@fullcalendar/timegrid@^6.1.19":
+ version "6.1.19"
+ resolved "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.19.tgz"
+ integrity sha512-OuzpUueyO9wB5OZ8rs7TWIoqvu4v3yEqdDxZ2VcsMldCpYJRiOe7yHWKr4ap5Tb0fs7Rjbserc/b6Nt7ol6BRg==
+ dependencies:
+ "@fullcalendar/daygrid" "~6.1.19"
+
+"@googlemaps/js-api-loader@1.16.8":
+ version "1.16.8"
+ resolved "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz"
+ integrity sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==
+
+"@googlemaps/markerclusterer@2.5.3":
+ version "2.5.3"
+ resolved "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz"
+ integrity sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==
+ dependencies:
+ fast-deep-equal "^3.1.3"
+ supercluster "^8.0.1"
+
+"@headlessui/react@^1.7.4":
+ version "1.7.14"
+ resolved "https://registry.npmjs.org/@headlessui/react/-/react-1.7.14.tgz"
+ integrity sha512-znzdq9PG8rkwcu9oQ2FwIy0ZFtP9Z7ycS+BAqJ3R5EIqC/0bJGvhT7193rFf+45i9nnPsYvCQVW4V/bB9Xc+gA==
+ dependencies:
+ client-only "^0.0.1"
+
+"@hookform/resolvers@^2.9.10":
+ version "2.9.11"
+ resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.11.tgz"
+ integrity sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==
+
+"@iconify/react@^4.0.0":
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/@iconify/react/-/react-4.1.0.tgz"
+ integrity sha512-Mf72i3TNNKpKCKxmo7kzqyrUdCgaoljpqtWmtqpqwyxoV4ukhnDsSRNLhf2yBnqGr3cVZsdj/i0FMpXIY0Qk0g==
+ dependencies:
+ "@iconify/types" "^2.0.0"
+
+"@iconify/types@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz"
+ integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==
+
+"@icons/material@^0.2.4":
+ version "0.2.4"
+ resolved "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz"
+ integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
+
+"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.2":
+ version "0.3.13"
+ resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz"
+ integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz"
+ integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
+ version "1.5.5"
+ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
+ integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
+
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.30"
+ resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz"
+ integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@juggle/resize-observer@^3.3.1":
+ version "3.4.0"
+ resolved "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz"
+ integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
+
+"@kurkle/color@^0.3.0":
+ version "0.3.2"
+ resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz"
+ integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
+
+"@miragejs/pretender-node-polyfill@^0.1.0":
+ version "0.1.2"
+ resolved "https://registry.npmjs.org/@miragejs/pretender-node-polyfill/-/pretender-node-polyfill-0.1.2.tgz"
+ integrity sha512-M/BexG/p05C5lFfMunxo/QcgIJnMT2vDVCd00wNqK2ImZONIlEETZwWJu1QtLxtmYlSHlCFl3JNzp0tLe7OJ5g==
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
+ version "2.0.5"
+ resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@parcel/watcher-android-arm64@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz"
+ integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==
+
+"@parcel/watcher-darwin-arm64@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz"
+ integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==
+
+"@parcel/watcher-darwin-x64@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz"
+ integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==
+
+"@parcel/watcher-freebsd-x64@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz"
+ integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==
+
+"@parcel/watcher-linux-arm-glibc@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz"
+ integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==
+
+"@parcel/watcher-linux-arm-musl@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz"
+ integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==
+
+"@parcel/watcher-linux-arm64-glibc@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz"
+ integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==
+
+"@parcel/watcher-linux-arm64-musl@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz"
+ integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==
+
+"@parcel/watcher-linux-x64-glibc@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz"
+ integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==
+
+"@parcel/watcher-linux-x64-musl@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz"
+ integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
+
+"@parcel/watcher-win32-arm64@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz"
+ integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==
+
+"@parcel/watcher-win32-ia32@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz"
+ integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==
+
+"@parcel/watcher-win32-x64@2.5.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz"
+ integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==
+
+"@parcel/watcher@^2.4.1":
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz"
+ integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==
+ dependencies:
+ detect-libc "^1.0.3"
+ is-glob "^4.0.3"
+ micromatch "^4.0.5"
+ node-addon-api "^7.0.0"
+ optionalDependencies:
+ "@parcel/watcher-android-arm64" "2.5.1"
+ "@parcel/watcher-darwin-arm64" "2.5.1"
+ "@parcel/watcher-darwin-x64" "2.5.1"
+ "@parcel/watcher-freebsd-x64" "2.5.1"
+ "@parcel/watcher-linux-arm-glibc" "2.5.1"
+ "@parcel/watcher-linux-arm-musl" "2.5.1"
+ "@parcel/watcher-linux-arm64-glibc" "2.5.1"
+ "@parcel/watcher-linux-arm64-musl" "2.5.1"
+ "@parcel/watcher-linux-x64-glibc" "2.5.1"
+ "@parcel/watcher-linux-x64-musl" "2.5.1"
+ "@parcel/watcher-win32-arm64" "2.5.1"
+ "@parcel/watcher-win32-ia32" "2.5.1"
+ "@parcel/watcher-win32-x64" "2.5.1"
+
+"@popperjs/core@^2.11.8", "@popperjs/core@^2.9.0":
+ version "2.11.8"
+ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
+ integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
+
+"@rc-component/async-validator@^5.0.3":
+ version "5.0.4"
+ resolved "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz"
+ integrity sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==
+ dependencies:
+ "@babel/runtime" "^7.24.4"
+
+"@rc-component/color-picker@~2.0.1":
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz"
+ integrity sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==
+ dependencies:
+ "@ant-design/fast-color" "^2.0.6"
+ "@babel/runtime" "^7.23.6"
+ classnames "^2.2.6"
+ rc-util "^5.38.1"
+
+"@rc-component/context@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz"
+ integrity sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ rc-util "^5.27.0"
+
+"@rc-component/mini-decimal@^1.0.1":
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz"
+ integrity sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+
+"@rc-component/mutate-observer@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz"
+ integrity sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+ classnames "^2.3.2"
+ rc-util "^5.24.4"
+
+"@rc-component/portal@^1.0.0-8", "@rc-component/portal@^1.0.0-9", "@rc-component/portal@^1.0.2", "@rc-component/portal@^1.1.0", "@rc-component/portal@^1.1.1":
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz"
+ integrity sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+ classnames "^2.3.2"
+ rc-util "^5.24.4"
+
+"@rc-component/qrcode@~1.0.0":
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz"
+ integrity sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==
+ dependencies:
+ "@babel/runtime" "^7.24.7"
+ classnames "^2.3.2"
+ rc-util "^5.38.0"
+
+"@rc-component/tour@~1.15.1":
+ version "1.15.1"
+ resolved "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz"
+ integrity sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+ "@rc-component/portal" "^1.0.0-9"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.3.2"
+ rc-util "^5.24.4"
+
+"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0":
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.0.tgz"
+ integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==
+ dependencies:
+ "@babel/runtime" "^7.23.2"
+ "@rc-component/portal" "^1.1.0"
+ classnames "^2.3.2"
+ rc-motion "^2.0.0"
+ rc-resize-observer "^1.3.1"
+ rc-util "^5.44.0"
+
+"@react-dnd/asap@^5.0.1":
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz"
+ integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==
+
+"@react-dnd/invariant@^4.0.1":
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz"
+ integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==
+
+"@react-dnd/shallowequal@^4.0.1":
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz"
+ integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==
+
+"@react-google-maps/api@^2.20.6":
+ version "2.20.6"
+ resolved "https://registry.npmjs.org/@react-google-maps/api/-/api-2.20.6.tgz"
+ integrity sha512-frxkSHWbd36ayyxrEVopSCDSgJUT1tVKXvQld2IyzU3UnDuqqNA3AZE4/fCdqQb2/zBQx3nrWnZB1wBXDcrjcw==
+ dependencies:
+ "@googlemaps/js-api-loader" "1.16.8"
+ "@googlemaps/markerclusterer" "2.5.3"
+ "@react-google-maps/infobox" "2.20.0"
+ "@react-google-maps/marker-clusterer" "2.20.0"
+ "@types/google.maps" "3.58.1"
+ invariant "2.2.4"
+
+"@react-google-maps/infobox@2.20.0":
+ version "2.20.0"
+ resolved "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.20.0.tgz"
+ integrity sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==
+
+"@react-google-maps/marker-clusterer@2.20.0":
+ version "2.20.0"
+ resolved "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.20.0.tgz"
+ integrity sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==
+
+"@react-leaflet/core@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz"
+ integrity sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==
+
+"@react-spring/animated@~10.0.3":
+ version "10.0.3"
+ resolved "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.3.tgz"
+ integrity sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==
+ dependencies:
+ "@react-spring/shared" "~10.0.3"
+ "@react-spring/types" "~10.0.3"
+
+"@react-spring/core@~10.0.3":
+ version "10.0.3"
+ resolved "https://registry.npmjs.org/@react-spring/core/-/core-10.0.3.tgz"
+ integrity sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==
+ dependencies:
+ "@react-spring/animated" "~10.0.3"
+ "@react-spring/shared" "~10.0.3"
+ "@react-spring/types" "~10.0.3"
+
+"@react-spring/rafz@~10.0.3":
+ version "10.0.3"
+ resolved "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.3.tgz"
+ integrity sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==
+
+"@react-spring/shared@~10.0.3":
+ version "10.0.3"
+ resolved "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.3.tgz"
+ integrity sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==
+ dependencies:
+ "@react-spring/rafz" "~10.0.3"
+ "@react-spring/types" "~10.0.3"
+
+"@react-spring/types@~10.0.3":
+ version "10.0.3"
+ resolved "https://registry.npmjs.org/@react-spring/types/-/types-10.0.3.tgz"
+ integrity sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==
+
+"@react-spring/web@^10.0.3":
+ version "10.0.3"
+ resolved "https://registry.npmjs.org/@react-spring/web/-/web-10.0.3.tgz"
+ integrity sha512-ndU+kWY81rHsT7gTFtCJ6mrVhaJ6grFmgTnENipzmKqot4HGf5smPNK+cZZJqoGeDsj9ZsiWPW4geT/NyD484A==
+ dependencies:
+ "@react-spring/animated" "~10.0.3"
+ "@react-spring/core" "~10.0.3"
+ "@react-spring/shared" "~10.0.3"
+ "@react-spring/types" "~10.0.3"
+
+"@reduxjs/toolkit@^1.9.0":
+ version "1.9.5"
+ resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz"
+ integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==
+ dependencies:
+ immer "^9.0.21"
+ redux "^4.2.1"
+ redux-thunk "^2.4.2"
+ reselect "^4.1.8"
+
+"@remix-run/router@1.6.2":
+ version "1.6.2"
+ resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.6.2.tgz"
+ integrity sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA==
+
+"@rolldown/pluginutils@1.0.0-beta.30":
+ version "1.0.0-beta.30"
+ resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz"
+ integrity sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==
+
+"@rollup/plugin-replace@^5.0.2":
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz"
+ integrity sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==
+ dependencies:
+ "@rollup/pluginutils" "^5.0.1"
+ magic-string "^0.27.0"
+
+"@rollup/pluginutils@^4.1.1":
+ version "4.2.1"
+ resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz"
+ integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
+ dependencies:
+ estree-walker "^2.0.1"
+ picomatch "^2.2.2"
+
+"@rollup/pluginutils@^5.0.1":
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz"
+ integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ estree-walker "^2.0.2"
+ picomatch "^2.3.1"
+
+"@rollup/rollup-android-arm-eabi@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz"
+ integrity sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==
+
+"@rollup/rollup-android-arm64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz"
+ integrity sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==
+
+"@rollup/rollup-darwin-arm64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz"
+ integrity sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==
+
+"@rollup/rollup-darwin-x64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz"
+ integrity sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==
+
+"@rollup/rollup-freebsd-arm64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz"
+ integrity sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==
+
+"@rollup/rollup-freebsd-x64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz"
+ integrity sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz"
+ integrity sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==
+
+"@rollup/rollup-linux-arm-musleabihf@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz"
+ integrity sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==
+
+"@rollup/rollup-linux-arm64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz"
+ integrity sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==
+
+"@rollup/rollup-linux-arm64-musl@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz"
+ integrity sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==
+
+"@rollup/rollup-linux-loong64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz"
+ integrity sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==
+
+"@rollup/rollup-linux-ppc64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz"
+ integrity sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==
+
+"@rollup/rollup-linux-riscv64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz"
+ integrity sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==
+
+"@rollup/rollup-linux-riscv64-musl@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz"
+ integrity sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==
+
+"@rollup/rollup-linux-s390x-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz"
+ integrity sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==
+
+"@rollup/rollup-linux-x64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz"
+ integrity sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==
+
+"@rollup/rollup-linux-x64-musl@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz"
+ integrity sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==
+
+"@rollup/rollup-openharmony-arm64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz"
+ integrity sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==
+
+"@rollup/rollup-win32-arm64-msvc@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz"
+ integrity sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==
+
+"@rollup/rollup-win32-ia32-msvc@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz"
+ integrity sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==
+
+"@rollup/rollup-win32-x64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz"
+ integrity sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==
+
+"@rollup/rollup-win32-x64-msvc@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz"
+ integrity sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==
+
+"@south-paw/react-vector-maps@^3.2.0":
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/@south-paw/react-vector-maps/-/react-vector-maps-3.2.0.tgz"
+ integrity sha512-4Y88ZA8RuXxlBD7hgguVZjiTZsxvnN0Eheip/7YeM22B8hwae058C4Xx7Fi1PyIhOF5yWU/yXxyCMwFE7Awrwg==
+
+"@svg-maps/world@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/@svg-maps/world/-/world-1.0.1.tgz"
+ integrity sha512-Mawh/jEYBBHnug9S17PyePLYKJ+Xd0Bbh96mCePebpbvcbJu5YKpfKhpyMeLFmmdWPrSFxl0f0MTsJfXU0gSaQ==
+
+"@tippyjs/react@^4.2.6":
+ version "4.2.6"
+ resolved "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz"
+ integrity sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==
+ dependencies:
+ tippy.js "^6.3.1"
+
+"@types/babel__core@^7.20.5":
+ version "7.20.5"
+ resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
+ integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
+ dependencies:
+ "@babel/parser" "^7.20.7"
+ "@babel/types" "^7.20.7"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.27.0"
+ resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz"
+ integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.4.4"
+ resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz"
+ integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*":
+ version "7.20.7"
+ resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz"
+ integrity sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==
+ dependencies:
+ "@babel/types" "^7.20.7"
+
+"@types/d3-array@^3.0.3":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.4.tgz"
+ integrity sha512-nwvEkG9vYOc0Ic7G7kwgviY4AQlTfYGIZ0fqB7CQHXGyYM6nO7kJh5EguSNA3jfh4rq7Sb7eMVq8isuvg2/miQ==
+
+"@types/d3-color@*":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz"
+ integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==
+
+"@types/d3-ease@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz"
+ integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==
+
+"@types/d3-interpolate@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
+ integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz"
+ integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==
+
+"@types/d3-scale@^4.0.2":
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.3.tgz"
+ integrity sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-shape@^3.1.0":
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.1.tgz"
+ integrity sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time@*", "@types/d3-time@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz"
+ integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==
+
+"@types/d3-timer@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz"
+ integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==
+
+"@types/estree@^1.0.0", "@types/estree@1.0.8":
+ version "1.0.8"
+ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
+ integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
+
+"@types/google.maps@^3.54.10", "@types/google.maps@3.58.1":
+ version "3.58.1"
+ resolved "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz"
+ integrity sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==
+
+"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1", "@types/hoist-non-react-statics@>= 3.3.1":
+ version "3.3.1"
+ resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
+"@types/lodash.memoize@^4.1.7":
+ version "4.1.7"
+ resolved "https://registry.npmjs.org/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz"
+ integrity sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==
+ dependencies:
+ "@types/lodash" "*"
+
+"@types/lodash@*", "@types/lodash@^4.14.175":
+ version "4.14.194"
+ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz"
+ integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==
+
+"@types/parse-json@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz"
+ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+
+"@types/prop-types@*":
+ version "15.7.5"
+ resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz"
+ integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
+
+"@types/react-dom@^16.8 || ^17.0 || ^18.0", "@types/react-dom@^18.0.8":
+ version "18.2.4"
+ resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz"
+ integrity sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==
+ dependencies:
+ "@types/react" "*"
+
+"@types/react-redux@^7.1.20":
+ version "7.1.25"
+ resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz"
+ integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+ redux "^4.0.0"
+
+"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.1":
+ version "4.4.6"
+ resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz"
+ integrity sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==
+ dependencies:
+ "@types/react" "*"
+
+"@types/react@*", "@types/react@^16.8 || ^17.0 || ^18.0", "@types/react@^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react@^18.0.24", "@types/react@>= 16":
+ version "18.2.6"
+ resolved "https://registry.npmjs.org/@types/react/-/react-18.2.6.tgz"
+ integrity sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ csstype "^3.0.2"
+
+"@types/scheduler@*":
+ version "0.16.3"
+ resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz"
+ integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
+
+"@types/stylis@4.2.5":
+ version "4.2.5"
+ resolved "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz"
+ integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==
+
+"@types/use-sync-external-store@^0.0.3":
+ version "0.0.3"
+ resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz"
+ integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
+
+"@types/uuid@8.3.4":
+ version "8.3.4"
+ resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz"
+ integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
+
+"@vis.gl/react-google-maps@^1.5.2":
+ version "1.5.2"
+ resolved "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.5.2.tgz"
+ integrity sha512-0Ypmde7M73GgV4TgcaUTNKXsbcXWToPVuawMNrVg7htXmhpEfLARHwhtmP6N1da3od195ZKC8ShXzC6Vm+zYHQ==
+ dependencies:
+ "@types/google.maps" "^3.54.10"
+ fast-deep-equal "^3.1.3"
+
+"@vitejs/plugin-react-refresh@^1.3.6":
+ version "1.3.6"
+ resolved "https://registry.npmjs.org/@vitejs/plugin-react-refresh/-/plugin-react-refresh-1.3.6.tgz"
+ integrity sha512-iNR/UqhUOmFFxiezt0em9CgmiJBdWR+5jGxB2FihaoJfqGt76kiwaKoVOJVU5NYcDWMdN06LbyN2VIGIoYdsEA==
+ dependencies:
+ "@babel/core" "^7.14.8"
+ "@babel/plugin-transform-react-jsx-self" "^7.14.5"
+ "@babel/plugin-transform-react-jsx-source" "^7.14.5"
+ "@rollup/pluginutils" "^4.1.1"
+ react-refresh "^0.10.0"
+
+"@vitejs/plugin-react@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.0.tgz"
+ integrity sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==
+ dependencies:
+ "@babel/core" "^7.28.0"
+ "@babel/plugin-transform-react-jsx-self" "^7.27.1"
+ "@babel/plugin-transform-react-jsx-source" "^7.27.1"
+ "@rolldown/pluginutils" "1.0.0-beta.30"
+ "@types/babel__core" "^7.20.5"
+ react-refresh "^0.17.0"
+
+"@wojtekmaj/date-utils@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.1.3.tgz"
+ integrity sha512-rHrDuTl1cx5LYo8F4K4HVauVjwzx4LwrKfEk4br4fj4nK8JjJZ8IG6a6pBHkYmPLBQHCOEDwstb0WNXMGsmdOw==
+
+antd@^5.27.1:
+ version "5.27.1"
+ resolved "https://registry.npmjs.org/antd/-/antd-5.27.1.tgz"
+ integrity sha512-jGMSdBN7hAMvPV27B4RhzZfL6n6yu8yDbo7oXrlJasaOqB7bSDPcjdEy1kXy3JPsny/Qazb1ykzRI4EfcByAPQ==
+ dependencies:
+ "@ant-design/colors" "^7.2.1"
+ "@ant-design/cssinjs" "^1.23.0"
+ "@ant-design/cssinjs-utils" "^1.1.3"
+ "@ant-design/fast-color" "^2.0.6"
+ "@ant-design/icons" "^5.6.1"
+ "@ant-design/react-slick" "~1.1.2"
+ "@babel/runtime" "^7.26.0"
+ "@rc-component/color-picker" "~2.0.1"
+ "@rc-component/mutate-observer" "^1.1.0"
+ "@rc-component/qrcode" "~1.0.0"
+ "@rc-component/tour" "~1.15.1"
+ "@rc-component/trigger" "^2.3.0"
+ classnames "^2.5.1"
+ copy-to-clipboard "^3.3.3"
+ dayjs "^1.11.11"
+ rc-cascader "~3.34.0"
+ rc-checkbox "~3.5.0"
+ rc-collapse "~3.9.0"
+ rc-dialog "~9.6.0"
+ rc-drawer "~7.3.0"
+ rc-dropdown "~4.2.1"
+ rc-field-form "~2.7.0"
+ rc-image "~7.12.0"
+ rc-input "~1.8.0"
+ rc-input-number "~9.5.0"
+ rc-mentions "~2.20.0"
+ rc-menu "~9.16.1"
+ rc-motion "^2.9.5"
+ rc-notification "~5.6.4"
+ rc-pagination "~5.1.0"
+ rc-picker "~4.11.3"
+ rc-progress "~4.0.0"
+ rc-rate "~2.13.1"
+ rc-resize-observer "^1.4.3"
+ rc-segmented "~2.7.0"
+ rc-select "~14.16.8"
+ rc-slider "~11.1.8"
+ rc-steps "~6.0.1"
+ rc-switch "~4.1.0"
+ rc-table "~7.51.1"
+ rc-tabs "~15.7.0"
+ rc-textarea "~1.10.2"
+ rc-tooltip "~6.4.0"
+ rc-tree "~5.13.1"
+ rc-tree-select "~5.27.0"
+ rc-upload "~4.9.2"
+ rc-util "^5.44.4"
+ scroll-into-view-if-needed "^3.1.0"
+ throttle-debounce "^5.0.2"
+
+any-promise@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
+ integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
+
+anymatch@~3.1.2:
+ version "3.1.3"
+ resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+apexcharts@^3.18.0, apexcharts@^3.36.3:
+ version "3.40.0"
+ resolved "https://registry.npmjs.org/apexcharts/-/apexcharts-3.40.0.tgz"
+ integrity sha512-dSi3BUfCJkFd67uFp+xffrJVd3lDT7AAUUyRp0qPYiglJ76CeZLddVhM3FAk1P9GCzf8VewqGYUPCYQvXm+b9A==
+ dependencies:
+ svg.draggable.js "^2.2.2"
+ svg.easing.js "^2.0.0"
+ svg.filter.js "^2.0.2"
+ svg.pathmorphing.js "^0.1.3"
+ svg.resize.js "^1.4.3"
+ svg.select.js "^3.0.1"
+
+arg@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz"
+ integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+attr-accept@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz"
+ integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
+
+autoprefixer@^10.4.13:
+ version "10.4.14"
+ resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz"
+ integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==
+ dependencies:
+ browserslist "^4.21.5"
+ caniuse-lite "^1.0.30001464"
+ fraction.js "^4.2.0"
+ normalize-range "^0.1.2"
+ picocolors "^1.0.0"
+ postcss-value-parser "^4.2.0"
+
+axios@^1.10.0:
+ version "1.12.2"
+ resolved "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz"
+ integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
+ dependencies:
+ follow-redirects "^1.15.6"
+ form-data "^4.0.4"
+ proxy-from-env "^1.1.0"
+
+babel-plugin-macros@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz"
+ integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ cosmiconfig "^7.0.0"
+ resolve "^1.19.0"
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
+ integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+blueimp-md5@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz"
+ integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==
+
+brace-expansion@^1.1.7:
+ version "1.1.12"
+ resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
+ integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^3.0.3, braces@~3.0.2:
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+browserslist@^4.21.5, browserslist@^4.24.0, "browserslist@>= 4.21.0":
+ version "4.24.5"
+ resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz"
+ integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==
+ dependencies:
+ caniuse-lite "^1.0.30001716"
+ electron-to-chromium "^1.5.149"
+ node-releases "^2.0.19"
+ update-browserslist-db "^1.1.3"
+
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase-css@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
+ integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+camelize@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz"
+ integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==
+
+can-use-dom@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.npmjs.org/can-use-dom/-/can-use-dom-0.1.0.tgz"
+ integrity sha512-ceOhN1DL7Y4O6M0j9ICgmTYziV89WMd96SvSl0REd8PMgrY0B/WBOPoed5S1KUmJqXgUXh8gzSe6E3ae27upsQ==
+
+caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001716:
+ version "1.0.30001717"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz"
+ integrity sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==
+
+charenc@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz"
+ integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==
+
+chart.js@^4.1.1, chart.js@^4.2.0:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/chart.js/-/chart.js-4.3.0.tgz"
+ integrity sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==
+ dependencies:
+ "@kurkle/color" "^0.3.0"
+
+chokidar@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+chokidar@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
+ integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
+ dependencies:
+ readdirp "^4.0.1"
+
+classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2, classnames@^2.5.1, classnames@2.x:
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz"
+ integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
+
+cleave.js@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/cleave.js/-/cleave.js-1.6.0.tgz"
+ integrity sha512-ivqesy3j5hQVG3gywPfwKPbi/7ZSftY/UNp5uphnqjr25yI2CP8FS2ODQPzuLXXnNLi29e2+PgPkkiKUXLs/Nw==
+
+client-only@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
+ integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
+
+clsx@^1.1.1, clsx@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz"
+ integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
+ integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+
+compute-scroll-into-view@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz"
+ integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+convert-source-map@^1.5.0:
+ version "1.9.0"
+ resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
+ integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+
+convert-source-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
+ integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
+
+copy-to-clipboard@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz"
+ integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
+ dependencies:
+ toggle-selection "^1.0.6"
+
+core-js@^3.0.1:
+ version "3.30.2"
+ resolved "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz"
+ integrity sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==
+
+cosmiconfig@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz"
+ integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==
+ dependencies:
+ "@types/parse-json" "^4.0.0"
+ import-fresh "^3.2.1"
+ parse-json "^5.0.0"
+ path-type "^4.0.0"
+ yaml "^1.10.0"
+
+crypt@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz"
+ integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
+
+crypto-js@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz"
+ integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
+
+css-box-model@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz"
+ integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
+ dependencies:
+ tiny-invariant "^1.0.6"
+
+css-color-keywords@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz"
+ integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==
+
+css-to-react-native@3.2.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz"
+ integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==
+ dependencies:
+ camelize "^1.0.0"
+ css-color-keywords "^1.0.0"
+ postcss-value-parser "^4.0.2"
+
+css-unit-converter@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz"
+ integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==
+
+cssesc@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
+ integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+csstype@^3.0.2, csstype@^3.1.3, csstype@3.1.3:
+ version "3.1.3"
+ resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
+ integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+
+d3-array@^3.1.6, d3-array@^3.2.2, "d3-array@2 - 3", "d3-array@2.10.0 - 3":
+ version "3.2.3"
+ resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.3.tgz"
+ integrity sha512-JRHwbQQ84XuAESWhvIPaUV4/1UYTBOLiOPGWqgFDHZS1D5QN9c57FbH3QpEnQMYiOXNzKUQyGTZf+EVO7RT5TQ==
+ dependencies:
+ internmap "1 - 2"
+
+"d3-color@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-ease@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+"d3-format@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz"
+ integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+d3-interpolate@^3.0.1, "d3-interpolate@1.2.0 - 3":
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+d3-path@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-scale@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
+d3-shape@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
+"d3-time-format@2 - 4":
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
+d3-time@^3.0.0, "d3-time@1 - 3", "d3-time@2.1.1 - 3":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
+d3-timer@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+dayjs@^1.11.11, dayjs@^1.11.6, dayjs@^1.11.7, "dayjs@>= 1.x":
+ version "1.11.13"
+ resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz"
+ integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
+
+debug@^4.1.0, debug@^4.3.1:
+ version "4.4.0"
+ resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
+ integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
+ dependencies:
+ ms "^2.1.3"
+
+decimal.js-light@^2.4.1:
+ version "2.5.1"
+ resolved "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz"
+ integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
+
+deepmerge@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
+ integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+detect-libc@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz"
+ integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
+
+didyoumean@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
+ integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
+
+dlv@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz"
+ integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
+
+dnd-core@^16.0.1:
+ version "16.0.1"
+ resolved "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz"
+ integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==
+ dependencies:
+ "@react-dnd/asap" "^5.0.1"
+ "@react-dnd/invariant" "^4.0.1"
+ redux "^4.2.0"
+
+dom-helpers@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz"
+ integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+
+dom-helpers@^5.0.1:
+ version "5.2.1"
+ resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz"
+ integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
+ dependencies:
+ "@babel/runtime" "^7.8.7"
+ csstype "^3.0.2"
+
+dom7@^4.0.4:
+ version "4.0.6"
+ resolved "https://registry.npmjs.org/dom7/-/dom7-4.0.6.tgz"
+ integrity sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==
+ dependencies:
+ ssr-window "^4.0.0"
+
+dunder-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz"
+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-errors "^1.3.0"
+ gopd "^1.2.0"
+
+electron-to-chromium@^1.5.149:
+ version "1.5.151"
+ resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz"
+ integrity sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==
+
+error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-define-property@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz"
+ integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+ dependencies:
+ es-errors "^1.3.0"
+
+es-set-tostringtag@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz"
+ integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
+ dependencies:
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.6"
+ has-tostringtag "^1.0.2"
+ hasown "^2.0.2"
+
+esbuild@^0.25.0:
+ version "0.25.9"
+ resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz"
+ integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.25.9"
+ "@esbuild/android-arm" "0.25.9"
+ "@esbuild/android-arm64" "0.25.9"
+ "@esbuild/android-x64" "0.25.9"
+ "@esbuild/darwin-arm64" "0.25.9"
+ "@esbuild/darwin-x64" "0.25.9"
+ "@esbuild/freebsd-arm64" "0.25.9"
+ "@esbuild/freebsd-x64" "0.25.9"
+ "@esbuild/linux-arm" "0.25.9"
+ "@esbuild/linux-arm64" "0.25.9"
+ "@esbuild/linux-ia32" "0.25.9"
+ "@esbuild/linux-loong64" "0.25.9"
+ "@esbuild/linux-mips64el" "0.25.9"
+ "@esbuild/linux-ppc64" "0.25.9"
+ "@esbuild/linux-riscv64" "0.25.9"
+ "@esbuild/linux-s390x" "0.25.9"
+ "@esbuild/linux-x64" "0.25.9"
+ "@esbuild/netbsd-arm64" "0.25.9"
+ "@esbuild/netbsd-x64" "0.25.9"
+ "@esbuild/openbsd-arm64" "0.25.9"
+ "@esbuild/openbsd-x64" "0.25.9"
+ "@esbuild/openharmony-arm64" "0.25.9"
+ "@esbuild/sunos-x64" "0.25.9"
+ "@esbuild/win32-arm64" "0.25.9"
+ "@esbuild/win32-ia32" "0.25.9"
+ "@esbuild/win32-x64" "0.25.9"
+
+escalade@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
+ integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+
+escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+estree-walker@^2.0.1, estree-walker@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
+ integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+eventemitter3@^4.0.1:
+ version "4.0.7"
+ resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz"
+ integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+eventemitter3@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz"
+ integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==
+
+fake-xml-http-request@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/fake-xml-http-request/-/fake-xml-http-request-2.1.2.tgz"
+ integrity sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==
+
+fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-diff@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz"
+ integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
+
+fast-equals@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz"
+ integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==
+
+fast-glob@^3.2.12:
+ version "3.2.12"
+ resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz"
+ integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.4"
+
+fastq@^1.6.0:
+ version "1.15.0"
+ resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz"
+ integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+ dependencies:
+ reusify "^1.0.4"
+
+fdir@^6.5.0:
+ version "6.5.0"
+ resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
+ integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
+
+file-selector@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz"
+ integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==
+ dependencies:
+ tslib "^2.4.0"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-root@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz"
+ integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
+flatpickr@^4.6.2:
+ version "4.6.13"
+ resolved "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz"
+ integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==
+
+follow-redirects@^1.15.6:
+ version "1.15.9"
+ resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz"
+ integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
+
+form-data@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz"
+ integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ es-set-tostringtag "^2.1.0"
+ hasown "^2.0.2"
+ mime-types "^2.1.12"
+
+fraction.js@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz"
+ integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
+
+framer-motion@^10.12.12:
+ version "10.12.12"
+ resolved "https://registry.npmjs.org/framer-motion/-/framer-motion-10.12.12.tgz"
+ integrity sha512-DDCqp60U6hR7aUrXj/BXc/t0Sd/U4ep6w/NZQkw898K+u7s+Vv/P8yxq4WTNA86kU9QCsqOgn1Qhz2DpYK0Oag==
+ dependencies:
+ tslib "^2.4.0"
+ optionalDependencies:
+ "@emotion/is-prop-valid" "^0.8.2"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.1, function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+gensync@^1.0.0-beta.2:
+ version "1.0.0-beta.2"
+ resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
+ integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-intrinsic@^1.2.6:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ es-define-property "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
+ function-bind "^1.1.2"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ math-intrinsics "^1.1.0"
+
+get-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz"
+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+ dependencies:
+ dunder-proto "^1.0.1"
+ es-object-atoms "^1.0.0"
+
+get-user-locale@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.2.1.tgz"
+ integrity sha512-3814zipTZ2MvczOcppEXB3jXu+0HWwj5WmPI6//SeCnUIUaRXu7W4S54eQZTEPadlMZefE+jAlPOn+zY3tD4Qw==
+ dependencies:
+ "@types/lodash.memoize" "^4.1.7"
+ lodash.memoize "^4.1.1"
+
+glob-parent@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz"
+ integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+ dependencies:
+ is-glob "^4.0.3"
+
+glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob@7.1.6:
+ version "7.1.6"
+ resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
+ integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+gopd@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz"
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz"
+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
+ integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+ dependencies:
+ has-symbols "^1.0.3"
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
+html-entities@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz"
+ integrity sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==
+
+html-to-md@^0.8.8:
+ version "0.8.8"
+ resolved "https://registry.npmjs.org/html-to-md/-/html-to-md-0.8.8.tgz"
+ integrity sha512-lgK3KKagobOguNi1XOfNaTtFSsjySir1CPfzewzVUjFM4x0RASnyZu47Hoe9nStpWFwpOwIrdxXzhxLIRbWllQ==
+
+immediate@~3.0.5:
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
+ integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
+immer@^9.0.21:
+ version "9.0.21"
+ resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz"
+ integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
+
+immutable@^5.0.2:
+ version "5.1.3"
+ resolved "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz"
+ integrity sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==
+
+import-fresh@^3.2.1:
+ version "3.3.0"
+ resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"
+ integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+ dependencies:
+ parent-module "^1.0.0"
+ resolve-from "^4.0.0"
+
+inflected@^2.0.4:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/inflected/-/inflected-2.1.0.tgz"
+ integrity sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2:
+ version "2.0.4"
+ resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
+invariant@2.2.4:
+ version "2.2.4"
+ resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
+ integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-buffer@~1.1.6:
+ version "1.1.6"
+ resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+is-core-module@^2.11.0:
+ version "2.12.1"
+ resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz"
+ integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==
+ dependencies:
+ has "^1.0.3"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+jiti@^1.18.2, jiti@>=1.21.0:
+ version "1.21.7"
+ resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz"
+ integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+jsesc@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz"
+ integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
+
+json-parse-even-better-errors@^2.3.0:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz"
+ integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json2mq@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz"
+ integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==
+ dependencies:
+ string-convert "^0.2.0"
+
+json5@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+kdbush@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz"
+ integrity sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==
+
+leaflet@^1.9.0, leaflet@^1.9.3:
+ version "1.9.4"
+ resolved "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz"
+ integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
+
+lie@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz"
+ integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==
+ dependencies:
+ immediate "~3.0.5"
+
+lilconfig@^2.0.5, lilconfig@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz"
+ integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
+
+lines-and-columns@^1.1.6:
+ version "1.2.4"
+ resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
+ integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+localforage@^1.10.0:
+ version "1.10.0"
+ resolved "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz"
+ integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
+ dependencies:
+ lie "3.1.1"
+
+lodash-es@^4.17.15, lodash-es@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
+ integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
+lodash.clonedeep@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz"
+ integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
+
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz"
+ integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
+
+lodash.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz"
+ integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
+
+lodash.memoize@^4.1.1, lodash.memoize@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz"
+ integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
+
+lodash.throttle@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz"
+ integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
+
+lodash@^4.0.0, lodash@^4.0.1, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+lucide-react@^0.507.0:
+ version "0.507.0"
+ resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.507.0.tgz"
+ integrity sha512-XfgE6gvAHwAtnbUvWiTTHx4S3VGR+cUJHEc0vrh9Ogu672I1Tue2+Cp/8JJqpytgcBHAB1FVI297W4XGNwc2dQ==
+
+magic-string@^0.27.0:
+ version "0.27.0"
+ resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz"
+ integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.4.13"
+
+match-sorter@^6.3.1:
+ version "6.3.1"
+ resolved "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz"
+ integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ remove-accents "0.4.2"
+
+material-colors@^1.2.1:
+ version "1.2.6"
+ resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz"
+ integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
+
+math-intrinsics@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz"
+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+md5@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz"
+ integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
+ dependencies:
+ charenc "0.0.2"
+ crypt "0.0.2"
+ is-buffer "~1.1.6"
+
+memoize-one@^5.1.1:
+ version "5.2.1"
+ resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz"
+ integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
+
+memoize-one@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz"
+ integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
+
+merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4, micromatch@^4.0.5:
+ version "4.0.8"
+ resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+ dependencies:
+ braces "^3.0.3"
+ picomatch "^2.3.1"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+minimatch@^3.0.4:
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+miragejs@^0.1.47:
+ version "0.1.48"
+ resolved "https://registry.npmjs.org/miragejs/-/miragejs-0.1.48.tgz"
+ integrity sha512-MGZAq0Q3OuRYgZKvlB69z4gLN4G3PvgC4A2zhkCXCXrLD5wm2cCnwNB59xOBVA+srZ0zEes6u+VylcPIkB4SqA==
+ dependencies:
+ "@miragejs/pretender-node-polyfill" "^0.1.0"
+ inflected "^2.0.4"
+ lodash "^4.0.0"
+ pretender "^3.4.7"
+
+ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+mz@^2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
+ integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
+ dependencies:
+ any-promise "^1.0.0"
+ object-assign "^4.0.1"
+ thenify-all "^1.0.0"
+
+nanoclone@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz"
+ integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
+
+nanoid@^3.3.11, nanoid@^3.3.7:
+ version "3.3.11"
+ resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
+ integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
+
+node-addon-api@^7.0.0:
+ version "7.1.1"
+ resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz"
+ integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
+
+node-releases@^2.0.19:
+ version "2.0.19"
+ resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz"
+ integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz"
+ integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
+
+object-assign@^4.0.1, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-hash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz"
+ integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+organization-chart-react@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/organization-chart-react/-/organization-chart-react-1.1.2.tgz"
+ integrity sha512-oq8LbPpYrqRFmtIOXSwfSEHTS5LB+Ya+UiEWNKkoaESH2EsCivPWIH3qX/bvf4d9qW58CMYpfUF2dWYace0KGg==
+ dependencies:
+ react "^18.2.0"
+ react-dom "^18.2.0"
+
+parchment@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz"
+ integrity sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==
+
+parent-module@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
+ integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+ dependencies:
+ callsites "^3.0.0"
+
+parse-json@^5.0.0:
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz"
+ integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ error-ex "^1.3.1"
+ json-parse-even-better-errors "^2.3.0"
+ lines-and-columns "^1.1.6"
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-type@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
+ integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+picocolors@^1.0.0, picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+"picomatch@^3 || ^4", picomatch@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
+ integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+
+pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
+ integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
+
+pirates@^4.0.1:
+ version "4.0.5"
+ resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz"
+ integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
+
+postcss-import@^15.1.0:
+ version "15.1.0"
+ resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz"
+ integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
+ dependencies:
+ postcss-value-parser "^4.0.0"
+ read-cache "^1.0.0"
+ resolve "^1.1.7"
+
+postcss-js@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz"
+ integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
+ dependencies:
+ camelcase-css "^2.0.1"
+
+postcss-load-config@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz"
+ integrity sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==
+ dependencies:
+ lilconfig "^2.0.5"
+ yaml "^2.1.1"
+
+postcss-nested@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz"
+ integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==
+ dependencies:
+ postcss-selector-parser "^6.0.11"
+
+postcss-selector-parser@^6.0.11:
+ version "6.0.13"
+ resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz"
+ integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==
+ dependencies:
+ cssesc "^3.0.0"
+ util-deprecate "^1.0.2"
+
+postcss-value-parser@^3.3.0:
+ version "3.3.1"
+ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz"
+ integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+
+postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
+ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.23, postcss@>=8.0.9, postcss@8.4.49:
+ version "8.4.49"
+ resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz"
+ integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==
+ dependencies:
+ nanoid "^3.3.7"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
+postcss@^8.5.6:
+ version "8.5.6"
+ resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
+ integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
+ dependencies:
+ nanoid "^3.3.11"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
+preact@~10.12.1:
+ version "10.12.1"
+ resolved "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz"
+ integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==
+
+pretender@^3.4.7:
+ version "3.4.7"
+ resolved "https://registry.npmjs.org/pretender/-/pretender-3.4.7.tgz"
+ integrity sha512-jkPAvt1BfRi0RKamweJdEcnjkeu7Es8yix3bJ+KgBC5VpG/Ln4JE3hYN6vJym4qprm8Xo5adhWpm3HCoft1dOw==
+ dependencies:
+ fake-xml-http-request "^2.1.2"
+ route-recognizer "^0.3.3"
+
+primereact@^10.9.6:
+ version "10.9.6"
+ resolved "https://registry.npmjs.org/primereact/-/primereact-10.9.6.tgz"
+ integrity sha512-0Jjz/KzfUURSHaPTXJwjL2Dc7CDPnbO17MivyJz7T5smGAMLY5d+IqpQhV61R22G/rDmhMh3+32LCNva2M8fRw==
+ dependencies:
+ "@types/react-transition-group" "^4.4.1"
+ react-transition-group "^4.4.1"
+
+prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
+ version "15.8.1"
+ resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.13.1"
+
+property-expr@^2.0.4:
+ version "2.0.5"
+ resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz"
+ integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
+
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+quill-delta@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz"
+ integrity sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==
+ dependencies:
+ fast-diff "^1.3.0"
+ lodash.clonedeep "^4.5.0"
+ lodash.isequal "^4.5.0"
+
+quill@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz"
+ integrity sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==
+ dependencies:
+ eventemitter3 "^5.0.1"
+ lodash-es "^4.17.21"
+ parchment "^3.0.0"
+ quill-delta "^5.1.0"
+
+raf-schd@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz"
+ integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
+
+rc-cascader@~3.34.0:
+ version "3.34.0"
+ resolved "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz"
+ integrity sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==
+ dependencies:
+ "@babel/runtime" "^7.25.7"
+ classnames "^2.3.1"
+ rc-select "~14.16.2"
+ rc-tree "~5.13.0"
+ rc-util "^5.43.0"
+
+rc-checkbox@~3.5.0:
+ version "3.5.0"
+ resolved "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz"
+ integrity sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.3.2"
+ rc-util "^5.25.2"
+
+rc-collapse@~3.9.0:
+ version "3.9.0"
+ resolved "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz"
+ integrity sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "2.x"
+ rc-motion "^2.3.4"
+ rc-util "^5.27.0"
+
+rc-dialog@~9.6.0:
+ version "9.6.0"
+ resolved "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz"
+ integrity sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/portal" "^1.0.0-8"
+ classnames "^2.2.6"
+ rc-motion "^2.3.0"
+ rc-util "^5.21.0"
+
+rc-drawer@~7.3.0:
+ version "7.3.0"
+ resolved "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz"
+ integrity sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==
+ dependencies:
+ "@babel/runtime" "^7.23.9"
+ "@rc-component/portal" "^1.1.1"
+ classnames "^2.2.6"
+ rc-motion "^2.6.1"
+ rc-util "^5.38.1"
+
+rc-dropdown@~4.2.0, rc-dropdown@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz"
+ integrity sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.2.6"
+ rc-util "^5.44.1"
+
+rc-field-form@~2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz"
+ integrity sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+ "@rc-component/async-validator" "^5.0.3"
+ rc-util "^5.32.2"
+
+rc-image@~7.12.0:
+ version "7.12.0"
+ resolved "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz"
+ integrity sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ "@rc-component/portal" "^1.0.2"
+ classnames "^2.2.6"
+ rc-dialog "~9.6.0"
+ rc-motion "^2.6.2"
+ rc-util "^5.34.1"
+
+rc-input-number@~9.5.0:
+ version "9.5.0"
+ resolved "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz"
+ integrity sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/mini-decimal" "^1.0.1"
+ classnames "^2.2.5"
+ rc-input "~1.8.0"
+ rc-util "^5.40.1"
+
+rc-input@~1.8.0:
+ version "1.8.0"
+ resolved "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz"
+ integrity sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-util "^5.18.1"
+
+rc-mentions@~2.20.0:
+ version "2.20.0"
+ resolved "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz"
+ integrity sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==
+ dependencies:
+ "@babel/runtime" "^7.22.5"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.2.6"
+ rc-input "~1.8.0"
+ rc-menu "~9.16.0"
+ rc-textarea "~1.10.0"
+ rc-util "^5.34.1"
+
+rc-menu@~9.16.0, rc-menu@~9.16.1:
+ version "9.16.1"
+ resolved "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz"
+ integrity sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "2.x"
+ rc-motion "^2.4.3"
+ rc-overflow "^1.3.1"
+ rc-util "^5.27.0"
+
+rc-motion@^2.0.0, rc-motion@^2.0.1, rc-motion@^2.3.0, rc-motion@^2.3.4, rc-motion@^2.4.3, rc-motion@^2.4.4, rc-motion@^2.6.1, rc-motion@^2.6.2, rc-motion@^2.9.0, rc-motion@^2.9.5:
+ version "2.9.5"
+ resolved "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz"
+ integrity sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-util "^5.44.0"
+
+rc-notification@~5.6.4:
+ version "5.6.4"
+ resolved "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz"
+ integrity sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "2.x"
+ rc-motion "^2.9.0"
+ rc-util "^5.20.1"
+
+rc-overflow@^1.3.1, rc-overflow@^1.3.2:
+ version "1.4.1"
+ resolved "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz"
+ integrity sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-resize-observer "^1.0.0"
+ rc-util "^5.37.0"
+
+rc-pagination@~5.1.0:
+ version "5.1.0"
+ resolved "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz"
+ integrity sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.3.2"
+ rc-util "^5.38.0"
+
+rc-picker@~4.11.3:
+ version "4.11.3"
+ resolved "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz"
+ integrity sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==
+ dependencies:
+ "@babel/runtime" "^7.24.7"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.2.1"
+ rc-overflow "^1.3.2"
+ rc-resize-observer "^1.4.0"
+ rc-util "^5.43.0"
+
+rc-progress@~4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz"
+ integrity sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.6"
+ rc-util "^5.16.1"
+
+rc-rate@~2.13.1:
+ version "2.13.1"
+ resolved "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz"
+ integrity sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.5"
+ rc-util "^5.0.1"
+
+rc-resize-observer@^1.0.0, rc-resize-observer@^1.1.0, rc-resize-observer@^1.3.1, rc-resize-observer@^1.4.0, rc-resize-observer@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz"
+ integrity sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==
+ dependencies:
+ "@babel/runtime" "^7.20.7"
+ classnames "^2.2.1"
+ rc-util "^5.44.1"
+ resize-observer-polyfill "^1.5.1"
+
+rc-segmented@~2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz"
+ integrity sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-motion "^2.4.4"
+ rc-util "^5.17.0"
+
+rc-select@~14.16.2, rc-select@~14.16.8:
+ version "14.16.8"
+ resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz"
+ integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/trigger" "^2.1.1"
+ classnames "2.x"
+ rc-motion "^2.0.1"
+ rc-overflow "^1.3.1"
+ rc-util "^5.16.1"
+ rc-virtual-list "^3.5.2"
+
+rc-slider@~11.1.8:
+ version "11.1.8"
+ resolved "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz"
+ integrity sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.5"
+ rc-util "^5.36.0"
+
+rc-steps@~6.0.1:
+ version "6.0.1"
+ resolved "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz"
+ integrity sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==
+ dependencies:
+ "@babel/runtime" "^7.16.7"
+ classnames "^2.2.3"
+ rc-util "^5.16.1"
+
+rc-switch@~4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz"
+ integrity sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ classnames "^2.2.1"
+ rc-util "^5.30.0"
+
+rc-table@~7.51.1:
+ version "7.51.1"
+ resolved "https://registry.npmjs.org/rc-table/-/rc-table-7.51.1.tgz"
+ integrity sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/context" "^1.4.0"
+ classnames "^2.2.5"
+ rc-resize-observer "^1.1.0"
+ rc-util "^5.44.3"
+ rc-virtual-list "^3.14.2"
+
+rc-tabs@~15.7.0:
+ version "15.7.0"
+ resolved "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz"
+ integrity sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ classnames "2.x"
+ rc-dropdown "~4.2.0"
+ rc-menu "~9.16.0"
+ rc-motion "^2.6.2"
+ rc-resize-observer "^1.0.0"
+ rc-util "^5.34.1"
+
+rc-textarea@~1.10.0, rc-textarea@~1.10.2:
+ version "1.10.2"
+ resolved "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz"
+ integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.1"
+ rc-input "~1.8.0"
+ rc-resize-observer "^1.0.0"
+ rc-util "^5.27.0"
+
+rc-tooltip@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz"
+ integrity sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.3.1"
+ rc-util "^5.44.3"
+
+rc-tree-select@~5.27.0:
+ version "5.27.0"
+ resolved "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz"
+ integrity sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==
+ dependencies:
+ "@babel/runtime" "^7.25.7"
+ classnames "2.x"
+ rc-select "~14.16.2"
+ rc-tree "~5.13.0"
+ rc-util "^5.43.0"
+
+rc-tree@~5.13.0, rc-tree@~5.13.1:
+ version "5.13.1"
+ resolved "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz"
+ integrity sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "2.x"
+ rc-motion "^2.0.1"
+ rc-util "^5.16.1"
+ rc-virtual-list "^3.5.1"
+
+rc-upload@~4.9.2:
+ version "4.9.2"
+ resolved "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz"
+ integrity sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ classnames "^2.2.5"
+ rc-util "^5.2.0"
+
+rc-util@^5.0.1, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.2.0, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.24.4, rc-util@^5.25.2, rc-util@^5.27.0, rc-util@^5.30.0, rc-util@^5.31.1, rc-util@^5.32.2, rc-util@^5.34.1, rc-util@^5.35.0, rc-util@^5.36.0, rc-util@^5.37.0, rc-util@^5.38.0, rc-util@^5.38.1, rc-util@^5.40.1, rc-util@^5.43.0, rc-util@^5.44.0, rc-util@^5.44.1, rc-util@^5.44.3, rc-util@^5.44.4:
+ version "5.44.4"
+ resolved "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz"
+ integrity sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ react-is "^18.2.0"
+
+rc-virtual-list@^3.14.2, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
+ version "3.19.1"
+ resolved "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.1.tgz"
+ integrity sha512-DCapO2oyPqmooGhxBuXHM4lFuX+sshQwWqqkuyFA+4rShLe//+GEPVwiDgO+jKtKHtbeYwZoNvetwfHdOf+iUQ==
+ dependencies:
+ "@babel/runtime" "^7.20.0"
+ classnames "^2.2.6"
+ rc-resize-observer "^1.0.0"
+ rc-util "^5.36.0"
+
+react-apexcharts@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.0.tgz"
+ integrity sha512-DrcMV4aAMrUG+n6412yzyATWEyCDWlpPBBhVbpzBC4PDeuYU6iF84SmExbck+jx5MUm4U5PM3/T307Mc3kzc9Q==
+ dependencies:
+ prop-types "^15.5.7"
+
+react-beautiful-dnd@^13.1.1:
+ version "13.1.1"
+ resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz"
+ integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+ css-box-model "^1.2.0"
+ memoize-one "^5.1.1"
+ raf-schd "^4.0.2"
+ react-redux "^7.2.0"
+ redux "^4.0.4"
+ use-memo-one "^1.1.1"
+
+react-calendar@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.npmjs.org/react-calendar/-/react-calendar-4.2.1.tgz"
+ integrity sha512-T5oKXD+KLy/g6bmJJkZ7E9wj0iRMesWMZcrC7q2kI6ybOsu9NlPQx8uXJzG4A4C3Sh5Xi0deznyzWIVsUpF8tA==
+ dependencies:
+ "@types/react" "*"
+ "@wojtekmaj/date-utils" "^1.1.3"
+ clsx "^1.2.1"
+ get-user-locale "^2.2.1"
+ prop-types "^15.6.0"
+
+react-chartjs-2@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz"
+ integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==
+
+react-collapse@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/react-collapse/-/react-collapse-5.1.1.tgz"
+ integrity sha512-k6cd7csF1o9LBhQ4AGBIdxB60SUEUMQDAnL2z1YvYNr9KoKr+nDkhN6FK7uGaBd/rYrYfrMpzpmJEIeHRYogBw==
+
+react-color@^2.19.3:
+ version "2.19.3"
+ resolved "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz"
+ integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==
+ dependencies:
+ "@icons/material" "^0.2.4"
+ lodash "^4.17.15"
+ lodash-es "^4.17.15"
+ material-colors "^1.2.1"
+ prop-types "^15.5.10"
+ reactcss "^1.2.0"
+ tinycolor2 "^1.4.1"
+
+react-colorful@^5.6.1:
+ version "5.6.1"
+ resolved "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz"
+ integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==
+
+react-csv@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz"
+ integrity sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==
+
+react-data-table-component@^7.7.0:
+ version "7.7.0"
+ resolved "https://registry.npmjs.org/react-data-table-component/-/react-data-table-component-7.7.0.tgz"
+ integrity sha512-5knL6zMSKlbvzu9P04KM5Lx8/EyQujb4I9z3rWeoVX++IDJadQ7aR4X5J6EeS90wjK0Xoa6btaVeglnCAqD2ag==
+ dependencies:
+ deepmerge "^4.3.1"
+
+react-dnd-html5-backend@^16.0.1:
+ version "16.0.1"
+ resolved "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz"
+ integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==
+ dependencies:
+ dnd-core "^16.0.1"
+
+react-dnd@^16.0.1:
+ version "16.0.1"
+ resolved "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz"
+ integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==
+ dependencies:
+ "@react-dnd/invariant" "^4.0.1"
+ "@react-dnd/shallowequal" "^4.0.1"
+ dnd-core "^16.0.1"
+ fast-deep-equal "^3.1.3"
+ hoist-non-react-statics "^3.3.2"
+
+react-dom@*, "react-dom@^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0 || ^17.0 || ^18.0.0", "react-dom@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16 || ^17 || ^18", "react-dom@^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.7.0 || ^17 || ^18 || ^19", "react-dom@^16.8 || ^17 || ^18 || ^19", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.8.5 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.0 || ^18.0.0 || ^19.0.0", react-dom@^18.0.0, react-dom@^18.2.0, "react-dom@>= 16.8.0", react-dom@>=15.0.0, react-dom@>=16, react-dom@>=16.0.0, react-dom@>=16.11.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0, "react-dom@>=16.8.0 || ^19.0 || ^19.0.0-rc", react-dom@>=16.9.0, react-dom@>=17:
+ version "18.2.0"
+ resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
+ integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
+ dependencies:
+ loose-envify "^1.1.0"
+ scheduler "^0.23.0"
+
+react-dropzone@^14.2.3:
+ version "14.2.3"
+ resolved "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz"
+ integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==
+ dependencies:
+ attr-accept "^2.2.2"
+ file-selector "^0.6.0"
+ prop-types "^15.8.1"
+
+react-flatpickr@^3.10.13:
+ version "3.10.13"
+ resolved "https://registry.npmjs.org/react-flatpickr/-/react-flatpickr-3.10.13.tgz"
+ integrity sha512-4m+K1K8jhvRFI8J/AHmQfA5hLALzhebEtEK8mLevXjX24MV3u502crzBn+EGFIBOfNUtrL5PId9FsGwgtuz/og==
+ dependencies:
+ flatpickr "^4.6.2"
+ prop-types "^15.5.10"
+
+react-hook-form@^7.0.0, react-hook-form@^7.39.5:
+ version "7.43.9"
+ resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.9.tgz"
+ integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==
+
+react-icons@^5.5.0:
+ version "5.5.0"
+ resolved "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz"
+ integrity sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==
+
+react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0:
+ version "16.13.1"
+ resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
+react-is@^17.0.2:
+ version "17.0.2"
+ resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
+react-is@^18.0.0:
+ version "18.3.1"
+ resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz"
+ integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
+
+react-is@^18.2.0:
+ version "18.3.1"
+ resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz"
+ integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
+
+react-leaflet@^4.2.0:
+ version "4.2.1"
+ resolved "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz"
+ integrity sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==
+ dependencies:
+ "@react-leaflet/core" "^2.1.0"
+
+react-lifecycles-compat@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz"
+ integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
+
+react-redux@^7.2.0:
+ version "7.2.9"
+ resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz"
+ integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==
+ dependencies:
+ "@babel/runtime" "^7.15.4"
+ "@types/react-redux" "^7.1.20"
+ hoist-non-react-statics "^3.3.2"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-is "^17.0.2"
+
+"react-redux@^7.2.1 || ^8.0.2", react-redux@^8.0.5:
+ version "8.0.5"
+ resolved "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz"
+ integrity sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==
+ dependencies:
+ "@babel/runtime" "^7.12.1"
+ "@types/hoist-non-react-statics" "^3.3.1"
+ "@types/use-sync-external-store" "^0.0.3"
+ hoist-non-react-statics "^3.3.2"
+ react-is "^18.0.0"
+ use-sync-external-store "^1.0.0"
+
+react-refresh@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz"
+ integrity sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==
+
+react-refresh@^0.17.0:
+ version "0.17.0"
+ resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz"
+ integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==
+
+react-resize-detector@^8.0.4:
+ version "8.1.0"
+ resolved "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz"
+ integrity sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==
+ dependencies:
+ lodash "^4.17.21"
+
+react-router-dom@^6.4.3:
+ version "6.11.2"
+ resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.11.2.tgz"
+ integrity sha512-JNbKtAeh1VSJQnH6RvBDNhxNwemRj7KxCzc5jb7zvDSKRnPWIFj9pO+eXqjM69gQJ0r46hSz1x4l9y0651DKWw==
+ dependencies:
+ "@remix-run/router" "1.6.2"
+ react-router "6.11.2"
+
+react-router@6.11.2:
+ version "6.11.2"
+ resolved "https://registry.npmjs.org/react-router/-/react-router-6.11.2.tgz"
+ integrity sha512-74z9xUSaSX07t3LM+pS6Un0T55ibUE/79CzfZpy5wsPDZaea1F8QkrsiyRnA2YQ7LwE/umaydzXZV80iDCPkMg==
+ dependencies:
+ "@remix-run/router" "1.6.2"
+
+react-select@^5.7.0:
+ version "5.7.3"
+ resolved "https://registry.npmjs.org/react-select/-/react-select-5.7.3.tgz"
+ integrity sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg==
+ dependencies:
+ "@babel/runtime" "^7.12.0"
+ "@emotion/cache" "^11.4.0"
+ "@emotion/react" "^11.8.1"
+ "@floating-ui/dom" "^1.0.1"
+ "@types/react-transition-group" "^4.4.0"
+ memoize-one "^6.0.0"
+ prop-types "^15.6.0"
+ react-transition-group "^4.3.0"
+ use-isomorphic-layout-effect "^1.1.2"
+
+react-smooth@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.3.tgz"
+ integrity sha512-yl4y3XiMorss7ayF5QnBiSprig0+qFHui8uh7Hgg46QX5O+aRMRKlfGGNGLHno35JkQSvSYY8eCWkBfHfrSHfg==
+ dependencies:
+ fast-equals "^5.0.0"
+ react-transition-group "2.9.0"
+
+react-table@^7.8.0:
+ version "7.8.0"
+ resolved "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz"
+ integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==
+
+react-tailwindcss-datepicker@^1.4.2:
+ version "1.6.1"
+ resolved "https://registry.npmjs.org/react-tailwindcss-datepicker/-/react-tailwindcss-datepicker-1.6.1.tgz"
+ integrity sha512-MJlwOquIeuQCS5QEHyOIRTCKabgBKZinJrglE+nTtrw45GzY+80mbR0zzM9FUBbWyTNCWg2/IsDzxYyeG9tNNA==
+
+react-toastify@^9.1.3:
+ version "9.1.3"
+ resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz"
+ integrity sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==
+ dependencies:
+ clsx "^1.1.1"
+
+react-transition-group@^4.3.0, react-transition-group@^4.4.1, react-transition-group@^4.4.5:
+ version "4.4.5"
+ resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz"
+ integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ dom-helpers "^5.0.1"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+
+react-transition-group@2.9.0:
+ version "2.9.0"
+ resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz"
+ integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
+ dependencies:
+ dom-helpers "^3.4.0"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+ react-lifecycles-compat "^3.0.4"
+
+react@*, "react@^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0 || ^17.0 || ^18.0.0", "react@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16 || ^17 || ^18", "react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.7.0 || ^17 || ^18 || ^19", "react@^16.8 || ^17 || ^18 || ^19", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17 || ^18", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.3 || ^17 || ^18", "react@^16.8.3 || ^17.0.0-0 || ^18.0.0", "react@^16.8.5 || ^17.0.0 || ^18.0.0", "react@^16.9.0 || ^17.0.0 || ^18", "react@^17.0.0 || ^18.0.0 || ^19.0.0", "react@^17.0.2 || ^18.2.0", react@^18.0.0, react@^18.2.0, "react@>= 16.14", "react@>= 16.8 || 18.0.0", "react@>= 16.8.0", "react@>= 17.0.0", react@>=0.13, react@>=15.0.0, react@>=16, "react@>=16, <=18", react@>=16.0.0, react@>=16.11.0, react@>=16.3.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0, "react@>=16.8.0 || ^19.0 || ^19.0.0-rc", react@>=16.9.0, react@>=17:
+ version "18.2.0"
+ resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
+ integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
+reactcss@^1.2.0:
+ version "1.2.3"
+ resolved "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz"
+ integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==
+ dependencies:
+ lodash "^4.0.1"
+
+read-cache@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz"
+ integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
+ dependencies:
+ pify "^2.3.0"
+
+readdirp@^4.0.1:
+ version "4.1.2"
+ resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
+ integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+recharts-scale@^0.4.4:
+ version "0.4.5"
+ resolved "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz"
+ integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==
+ dependencies:
+ decimal.js-light "^2.4.1"
+
+recharts@^2.3.2:
+ version "2.6.2"
+ resolved "https://registry.npmjs.org/recharts/-/recharts-2.6.2.tgz"
+ integrity sha512-dVhNfgI21LlF+4AesO3mj+i+9YdAAjoGaDWIctUgH/G2iy14YVtb/DSUeic77xr19rbKCiq+pQGfeg2kJQDHig==
+ dependencies:
+ classnames "^2.2.5"
+ eventemitter3 "^4.0.1"
+ lodash "^4.17.19"
+ react-is "^16.10.2"
+ react-resize-detector "^8.0.4"
+ react-smooth "^2.0.2"
+ recharts-scale "^0.4.4"
+ reduce-css-calc "^2.1.8"
+ victory-vendor "^36.6.8"
+
+reduce-css-calc@^2.1.8:
+ version "2.1.8"
+ resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz"
+ integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==
+ dependencies:
+ css-unit-converter "^1.1.1"
+ postcss-value-parser "^3.3.0"
+
+redux-thunk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz"
+ integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==
+
+redux@^4, redux@^4.0.0, redux@^4.0.4, redux@^4.2.0, redux@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz"
+ integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+
+remove-accents@0.4.2:
+ version "0.4.2"
+ resolved "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz"
+ integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
+
+reselect@^4.1.8:
+ version "4.1.8"
+ resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz"
+ integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==
+
+resize-observer-polyfill@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
+ integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
+resolve-from@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
+ integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve@^1.1.7, resolve@^1.19.0, resolve@^1.22.2:
+ version "1.22.2"
+ resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz"
+ integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
+ dependencies:
+ is-core-module "^2.11.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz"
+ integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rollup@^4.43.0:
+ version "4.52.4"
+ resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz"
+ integrity sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==
+ dependencies:
+ "@types/estree" "1.0.8"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.52.4"
+ "@rollup/rollup-android-arm64" "4.52.4"
+ "@rollup/rollup-darwin-arm64" "4.52.4"
+ "@rollup/rollup-darwin-x64" "4.52.4"
+ "@rollup/rollup-freebsd-arm64" "4.52.4"
+ "@rollup/rollup-freebsd-x64" "4.52.4"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.52.4"
+ "@rollup/rollup-linux-arm-musleabihf" "4.52.4"
+ "@rollup/rollup-linux-arm64-gnu" "4.52.4"
+ "@rollup/rollup-linux-arm64-musl" "4.52.4"
+ "@rollup/rollup-linux-loong64-gnu" "4.52.4"
+ "@rollup/rollup-linux-ppc64-gnu" "4.52.4"
+ "@rollup/rollup-linux-riscv64-gnu" "4.52.4"
+ "@rollup/rollup-linux-riscv64-musl" "4.52.4"
+ "@rollup/rollup-linux-s390x-gnu" "4.52.4"
+ "@rollup/rollup-linux-x64-gnu" "4.52.4"
+ "@rollup/rollup-linux-x64-musl" "4.52.4"
+ "@rollup/rollup-openharmony-arm64" "4.52.4"
+ "@rollup/rollup-win32-arm64-msvc" "4.52.4"
+ "@rollup/rollup-win32-ia32-msvc" "4.52.4"
+ "@rollup/rollup-win32-x64-gnu" "4.52.4"
+ "@rollup/rollup-win32-x64-msvc" "4.52.4"
+ fsevents "~2.3.2"
+
+route-recognizer@^0.3.3:
+ version "0.3.4"
+ resolved "https://registry.npmjs.org/route-recognizer/-/route-recognizer-0.3.4.tgz"
+ integrity sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+sass@^1.56.1, sass@^1.70.0:
+ version "1.90.0"
+ resolved "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz"
+ integrity sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==
+ dependencies:
+ chokidar "^4.0.0"
+ immutable "^5.0.2"
+ source-map-js ">=0.6.2 <2.0.0"
+ optionalDependencies:
+ "@parcel/watcher" "^2.4.1"
+
+scheduler@^0.23.0:
+ version "0.23.0"
+ resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz"
+ integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
+ dependencies:
+ loose-envify "^1.1.0"
+
+scroll-into-view-if-needed@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz"
+ integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==
+ dependencies:
+ compute-scroll-into-view "^3.0.2"
+
+semver@^6.3.1:
+ version "6.3.1"
+ resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
+shallowequal@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz"
+ integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
+simplebar-react@^2.4.3:
+ version "2.4.3"
+ resolved "https://registry.npmjs.org/simplebar-react/-/simplebar-react-2.4.3.tgz"
+ integrity sha512-Ep8gqAUZAS5IC2lT5RE4t1ZFUIVACqbrSRQvFV9a6NbVUzXzOMnc4P82Hl8Ak77AnPQvmgUwZS7aUKLyBoMAcg==
+ dependencies:
+ prop-types "^15.6.1"
+ simplebar "^5.3.9"
+
+simplebar@^5.3.9:
+ version "5.3.9"
+ resolved "https://registry.npmjs.org/simplebar/-/simplebar-5.3.9.tgz"
+ integrity sha512-1vIIpjDvY9sVH14e0LGeiCiTFU3ILqAghzO6OI9axeG+mvU/vMSrvXeAXkBolqFFz3XYaY8n5ahH9MeP3sp2Ag==
+ dependencies:
+ "@juggle/resize-observer" "^3.3.1"
+ can-use-dom "^0.1.0"
+ core-js "^3.0.1"
+ lodash.debounce "^4.0.8"
+ lodash.memoize "^4.1.2"
+ lodash.throttle "^4.1.1"
+
+sort-by@^0.0.2:
+ version "0.0.2"
+ resolved "https://registry.npmjs.org/sort-by/-/sort-by-0.0.2.tgz"
+ integrity sha512-iOX5oHA4a0eqTMFiWrHYqv924UeRKFBLhym7iwSVG37Egg2wApgZKAjyzM9WZjMwKv6+8Zi+nIaJ7FYsO9EkoA==
+
+source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0":
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+source-map@^0.5.7:
+ version "0.5.7"
+ resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz"
+ integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
+
+ssr-window@^4.0.0, ssr-window@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz"
+ integrity sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==
+
+string-convert@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz"
+ integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==
+
+"styled-components@>= 5.0.0":
+ version "6.1.18"
+ resolved "https://registry.npmjs.org/styled-components/-/styled-components-6.1.18.tgz"
+ integrity sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==
+ dependencies:
+ "@emotion/is-prop-valid" "1.2.2"
+ "@emotion/unitless" "0.8.1"
+ "@types/stylis" "4.2.5"
+ css-to-react-native "3.2.0"
+ csstype "3.1.3"
+ postcss "8.4.49"
+ shallowequal "1.1.0"
+ stylis "4.3.2"
+ tslib "2.6.2"
+
+stylis@^4.3.4:
+ version "4.3.6"
+ resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz"
+ integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
+
+stylis@4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz"
+ integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
+
+stylis@4.3.2:
+ version "4.3.2"
+ resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz"
+ integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==
+
+sucrase@^3.32.0:
+ version "3.32.0"
+ resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz"
+ integrity sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.2"
+ commander "^4.0.0"
+ glob "7.1.6"
+ lines-and-columns "^1.1.6"
+ mz "^2.7.0"
+ pirates "^4.0.1"
+ ts-interface-checker "^0.1.9"
+
+supercluster@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz"
+ integrity sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==
+ dependencies:
+ kdbush "^4.0.2"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+svg.draggable.js@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz"
+ integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==
+ dependencies:
+ svg.js "^2.0.1"
+
+svg.easing.js@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz"
+ integrity sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==
+ dependencies:
+ svg.js ">=2.3.x"
+
+svg.filter.js@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz"
+ integrity sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==
+ dependencies:
+ svg.js "^2.2.5"
+
+svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5, svg.js@>=2.3.x:
+ version "2.7.1"
+ resolved "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz"
+ integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==
+
+svg.pathmorphing.js@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz"
+ integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==
+ dependencies:
+ svg.js "^2.4.0"
+
+svg.resize.js@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz"
+ integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==
+ dependencies:
+ svg.js "^2.6.5"
+ svg.select.js "^2.1.2"
+
+svg.select.js@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz"
+ integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==
+ dependencies:
+ svg.js "^2.2.5"
+
+svg.select.js@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz"
+ integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==
+ dependencies:
+ svg.js "^2.6.5"
+
+sweetalert2@^11.4.8:
+ version "11.22.4"
+ resolved "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.4.tgz"
+ integrity sha512-JwcRODfozxiKmspFp+xctZ2izAmLAKbRPcoLMEW7LdugN/YmNrX1LT7hdBW87qsgupEO1ukBBuB17KzKFKW0tg==
+
+swiper@^8.4.5:
+ version "8.4.7"
+ resolved "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz"
+ integrity sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==
+ dependencies:
+ dom7 "^4.0.4"
+ ssr-window "^4.0.2"
+
+tailwindcss@^3.2.4:
+ version "3.3.2"
+ resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz"
+ integrity sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==
+ dependencies:
+ "@alloc/quick-lru" "^5.2.0"
+ arg "^5.0.2"
+ chokidar "^3.5.3"
+ didyoumean "^1.2.2"
+ dlv "^1.1.3"
+ fast-glob "^3.2.12"
+ glob-parent "^6.0.2"
+ is-glob "^4.0.3"
+ jiti "^1.18.2"
+ lilconfig "^2.1.0"
+ micromatch "^4.0.5"
+ normalize-path "^3.0.0"
+ object-hash "^3.0.0"
+ picocolors "^1.0.0"
+ postcss "^8.4.23"
+ postcss-import "^15.1.0"
+ postcss-js "^4.0.1"
+ postcss-load-config "^4.0.1"
+ postcss-nested "^6.0.1"
+ postcss-selector-parser "^6.0.11"
+ postcss-value-parser "^4.2.0"
+ resolve "^1.22.2"
+ sucrase "^3.32.0"
+
+thenify-all@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz"
+ integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
+ dependencies:
+ thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+ version "3.3.1"
+ resolved "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz"
+ integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
+ dependencies:
+ any-promise "^1.0.0"
+
+throttle-debounce@^5.0.0, throttle-debounce@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz"
+ integrity sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==
+
+tiny-invariant@^1.0.6:
+ version "1.3.1"
+ resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz"
+ integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
+
+tinycolor2@^1.4.1:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz"
+ integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
+
+tinyglobby@^0.2.15:
+ version "0.2.15"
+ resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz"
+ integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
+ dependencies:
+ fdir "^6.5.0"
+ picomatch "^4.0.3"
+
+tippy.js@^6.3.1:
+ version "6.3.7"
+ resolved "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz"
+ integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
+ dependencies:
+ "@popperjs/core" "^2.9.0"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+toggle-selection@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
+ integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
+
+toposort@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz"
+ integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==
+
+ts-interface-checker@^0.1.9:
+ version "0.1.13"
+ resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
+ integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
+
+tslib@^2.0.0, tslib@^2.4.0, tslib@2.6.2:
+ version "2.6.2"
+ resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
+ integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
+
+update-browserslist-db@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz"
+ integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
+ dependencies:
+ escalade "^3.2.0"
+ picocolors "^1.1.1"
+
+use-isomorphic-layout-effect@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz"
+ integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
+
+use-memo-one@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz"
+ integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==
+
+use-sync-external-store@^1.0.0:
+ version "1.5.0"
+ resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
+ integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
+
+util-deprecate@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+uuid@8.3.2:
+ version "8.3.2"
+ resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+uuidv4@^6.2.13:
+ version "6.2.13"
+ resolved "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz"
+ integrity sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==
+ dependencies:
+ "@types/uuid" "8.3.4"
+ uuid "8.3.2"
+
+victory-vendor@^36.6.8:
+ version "36.6.10"
+ resolved "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.6.10.tgz"
+ integrity sha512-7YqYGtsA4mByokBhCjk+ewwPhUfzhR1I3Da6/ZsZUv/31ceT77RKoaqrxRq5Ki+9we4uzf7+A+7aG2sfYhm7nA==
+ dependencies:
+ "@types/d3-array" "^3.0.3"
+ "@types/d3-ease" "^3.0.0"
+ "@types/d3-interpolate" "^3.0.1"
+ "@types/d3-scale" "^4.0.2"
+ "@types/d3-shape" "^3.1.0"
+ "@types/d3-time" "^3.0.0"
+ "@types/d3-timer" "^3.0.0"
+ d3-array "^3.1.6"
+ d3-ease "^3.0.1"
+ d3-interpolate "^3.0.1"
+ d3-scale "^4.0.2"
+ d3-shape "^3.1.0"
+ d3-time "^3.0.0"
+ d3-timer "^3.0.1"
+
+"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", vite@^7.1.1:
+ version "7.1.10"
+ resolved "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz"
+ integrity sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==
+ dependencies:
+ esbuild "^0.25.0"
+ fdir "^6.5.0"
+ picomatch "^4.0.3"
+ postcss "^8.5.6"
+ rollup "^4.43.0"
+ tinyglobby "^0.2.15"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yaml@^1.10.0:
+ version "1.10.2"
+ resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
+ integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
+yaml@^2.1.1, yaml@^2.4.2:
+ version "2.8.1"
+ resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
+ integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
+
+yarn@^1.22.19:
+ version "1.22.19"
+ resolved "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz"
+ integrity sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==
+
+yup@^0.32.11:
+ version "0.32.11"
+ resolved "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz"
+ integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==
+ dependencies:
+ "@babel/runtime" "^7.15.4"
+ "@types/lodash" "^4.14.175"
+ lodash "^4.17.21"
+ lodash-es "^4.17.21"
+ nanoclone "^0.2.1"
+ property-expr "^2.0.4"
+ toposort "^2.0.2"