img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
+ className
+ )}
+ {...props} />
+ );
+}
+
+function CardHeader({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function CardTitle({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function CardDescription({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function CardAction({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function CardContent({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function CardFooter({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/client/src/components/ui/dialog.jsx b/client/src/components/ui/dialog.jsx
new file mode 100644
index 0000000000..b30f2e4efd
--- /dev/null
+++ b/client/src/components/ui/dialog.jsx
@@ -0,0 +1,148 @@
+import * as React from "react"
+import { Dialog as DialogPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { XIcon } from "lucide-react"
+
+function Dialog({
+ ...props
+}) {
+ return
;
+}
+
+function DialogTrigger({
+ ...props
+}) {
+ return
;
+}
+
+function DialogPortal({
+ ...props
+}) {
+ return
;
+}
+
+function DialogClose({
+ ...props
+}) {
+ return
;
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+
+ )}
+
+
+ );
+}
+
+function DialogHeader({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function DialogFooter({
+ className,
+ showCloseButton = false,
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+ {showCloseButton && (
+
+
+
+ )}
+
+ );
+}
+
+function DialogTitle({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function DialogDescription({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/client/src/components/ui/input.jsx b/client/src/components/ui/input.jsx
new file mode 100644
index 0000000000..26b14f1514
--- /dev/null
+++ b/client/src/components/ui/input.jsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({
+ className,
+ type,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export { Input }
diff --git a/client/src/components/ui/separator.jsx b/client/src/components/ui/separator.jsx
new file mode 100644
index 0000000000..3116fdd656
--- /dev/null
+++ b/client/src/components/ui/separator.jsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+import { Separator as SeparatorPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export { Separator }
diff --git a/client/src/components/ui/switch.jsx b/client/src/components/ui/switch.jsx
new file mode 100644
index 0000000000..1d40ffaf19
--- /dev/null
+++ b/client/src/components/ui/switch.jsx
@@ -0,0 +1,27 @@
+import * as React from "react"
+import { Switch as SwitchPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Switch({
+ className,
+ size = "default",
+ ...props
+}) {
+ return (
+
+
+
+ );
+}
+
+export { Switch }
diff --git a/client/src/components/ui/table.jsx b/client/src/components/ui/table.jsx
new file mode 100644
index 0000000000..a4a7971993
--- /dev/null
+++ b/client/src/components/ui/table.jsx
@@ -0,0 +1,146 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+function Table({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function TableHeader({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function TableBody({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function TableFooter({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function TableRow({
+ className,
+ ...props
+}) {
+ return (
+
|
+ );
+}
+
+function TableHead({
+ className,
+ ...props
+}) {
+ return (
+
|
+ );
+}
+
+function TableCell({
+ className,
+ ...props
+}) {
+ return (
+
|
+ );
+}
+
+function TableCaption({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+};
\ No newline at end of file
diff --git a/client/src/components/ui/textarea.jsx b/client/src/components/ui/textarea.jsx
new file mode 100644
index 0000000000..568976901c
--- /dev/null
+++ b/client/src/components/ui/textarea.jsx
@@ -0,0 +1,20 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+export { Textarea }
diff --git a/client/src/constants/.gitkeep b/client/src/constants/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/constants/Roles.js b/client/src/constants/Roles.js
new file mode 100644
index 0000000000..5cbc92a1e2
--- /dev/null
+++ b/client/src/constants/Roles.js
@@ -0,0 +1,6 @@
+export const ROLES ={
+ ADMIN:"admin",
+ USER:"user",
+ VENDOR:"vendor"
+
+}
\ No newline at end of file
diff --git a/client/src/constants/Venue.js b/client/src/constants/Venue.js
new file mode 100644
index 0000000000..0231ddccc1
--- /dev/null
+++ b/client/src/constants/Venue.js
@@ -0,0 +1,44 @@
+export const Amenities = [
+ "Wifi",
+ "Parking",
+ "Air Conditioning",
+ "Catering Kitchen",
+ "Sound System",
+ "Projector",
+ "Stage",
+ "Dance Floor",
+ "Outdoor Area",
+ "Valet Parking",
+ "Generator Backup",
+ "CCTV Security",
+ "Green Room",
+ "Bridal Suite",
+ "Swimming Pool",
+ "Elevator",
+ "Bar Counter",
+ "Photo Booth"
+]
+
+
+export const VenueCategory = [
+ "Beach Side",
+ "Conference Hall",
+ "Auditorium",
+ "Banquet Hall",
+ "Party Hall",
+ "Rooftop",
+ "Cafe",
+ "Farm House",
+ "Palace",
+ "Studio",
+ "Outdoor Garden",
+ "Resort",
+ "Hotel"
+]
+
+export const Ratings = [
+ {label: "4+ ⭐", value: 4},
+ {label: "3+ ⭐", value: 3},
+ {label: "2+ ⭐", value: 2}
+]
+
diff --git a/client/src/constants/adminMenu.js b/client/src/constants/adminMenu.js
new file mode 100644
index 0000000000..87c490312c
--- /dev/null
+++ b/client/src/constants/adminMenu.js
@@ -0,0 +1,49 @@
+import {
+ LayoutDashboard,
+ Users,
+ Store,
+ Building2,
+ CalendarDays,
+ CreditCard,
+ Tags,
+} from "lucide-react";
+
+import { ROUTES } from "./routes";
+
+export const ADMIN_MENU = [
+ {
+ title: "Dashboard",
+ path: ROUTES.ADMIN.DASHBOARD,
+ icon: LayoutDashboard,
+ },
+ {
+ title: "User Management",
+ path: ROUTES.ADMIN.USERS,
+ icon: Users,
+ },
+ {
+ title: "Vendor Management",
+ path: ROUTES.ADMIN.VENDORS,
+ icon: Store,
+ },
+ {
+ title: "Venue Management",
+ path: ROUTES.ADMIN.VENUES,
+ icon: Building2,
+ },
+ {
+ title: "Booking Management",
+ path: ROUTES.ADMIN.BOOKINGS,
+ icon: CalendarDays,
+ },
+ {
+ title: "Payment Management",
+ path: ROUTES.ADMIN.PAYMENTS,
+ icon: CreditCard,
+ },
+ {
+ title: "Category Management",
+ path: ROUTES.ADMIN.CATEGORIES,
+ icon: Tags,
+ },
+];
\ No newline at end of file
diff --git a/client/src/constants/apiRoutes.js b/client/src/constants/apiRoutes.js
new file mode 100644
index 0000000000..410d804b92
--- /dev/null
+++ b/client/src/constants/apiRoutes.js
@@ -0,0 +1,104 @@
+export const API_ROUTES = {
+AUTH:{
+ REGISTER:(role)=> `/auth/${role}/register`,
+ VERIFY_OTP:(role)=>`/auth/${role}/verifyotp`,
+ RESEND_OTP:(role)=>`/auth/${role}/resendotp`,
+ LOGIN:(role)=>`/auth/${role}/login`,
+ FORGOT_PASSWORD: (role) => `/auth/${role}/forgotpassword`,
+ RESET_PASSWORD: (role) => `/auth/${role}/resetpassword`,
+
+
+ LOGOUT: (role) => `/auth/${role}/logout`,
+ REFRESH:(role)=>`/auth/${role}/refresh`,
+ GETME: '/auth/getme'
+
+},
+
+ USER: {
+ VENUE:{
+ VENUES: '/user/venues',
+ GET_BY_ID:(venueId)=>`/user/venue/${venueId}`,
+ TOP_VENUES: '/user/top-venues'
+ },
+ PROFILE: {
+ PROFILE: "/user/profile",
+ PROFILE_IMAGE: "/user/profile/image",
+ REQUEST_EMAIL_CHANGE_OTP: "/user/profile/email/request-otp",
+ VERIFY_EMAIL_CHANGE_OTP: "/user/profile/email/verify-otp",
+ RESEND_EMAIL_CHANGE_OTP: "/user/profile/email/resend-otp",
+ CHANGE_PASSWORD: "/user/profile/change-password"
+ },
+ CHANGE_PASSWORD: {
+ CHANGE_PASSWORD: "user/changepassword",
+ },
+ WISHLIST: {
+ GET: "/user/wishlist",
+ ADD: (venueId) => `/user/wishlist/${venueId}`,
+ REMOVE: (venueId) => `/user/wishlist/${venueId}`,
+ },
+
+ BOOKINGS: {
+ RESERVE: "/user/booking/reserve",
+ CONFIRM: "/user/booking/confirm",
+ GET_BY_ID: (bookingId) => `/user/booking/${bookingId}`,
+ GET_ALL: "/user/booking",
+ CANCEL: (bookingId) => `/user/bookings/${bookingId}/cancel`,
+ AVAILABILITY: (venueId) => `/user/booking/${venueId}/availability`,
+ }
+ },
+ ADMIN: {
+ DASHBOARD:{
+ STATISTICS: "/admin/dashboard/statistics",
+
+ },
+ USER: {
+ USERS: "/admin/users",
+ UPDATE_STATUS: (userId) => `/admin/users/${userId}/status`,
+ },
+
+ VENDOR: {
+ VENDORS: "/admin/vendors",
+ GET_BY_ID: (vendorId) => `/admin/vendors/${vendorId}`,
+ APPROVE: (vendorId) => `/admin/vendors/${vendorId}/approve`,
+ REJECT: (vendorId) => `/admin/vendors/${vendorId}/reject`,
+ UPDATE_STATUS: (vendorId) => `/admin/vendors/${vendorId}/status`,
+ },
+
+ VENUE: {
+ VENUES: "/admin/venues",
+ GET_BY_ID: (venueId) => `/admin/venues/${venueId}`,
+ APPROVE: (venueId) => `/admin/venues/${venueId}/approve`,
+ REJECT: (venueId) => `/admin/venues/${venueId}/reject`,
+ UPDATE_STATUS: (venueId) => `/admin/venues/${venueId}/status`,
+ },
+
+ BOOKING: {
+ BOOKINGS: "/admin/bookings",
+ GET_BY_ID: (bookingId) => `/admin/bookings/${bookingId}`,
+ STATISTICS: "/admin/bookings/statistics",
+ },
+
+ PAYMENT: {
+ PAYMENTS: "/admin/payments",
+ GET_BY_ID: (paymentId) => `/admin/payments/${paymentId}`,
+ STATISTICS: "/admin/payments/statistics",
+ },
+ },
+ VENDOR: {
+ DASHBOARD: "/vendor/dashboard",
+ PROFILE: "/vendor/profile",
+ BOOKINGS: "/vendor/bookings",
+ BOOKING_BY_ID: (bookingId) =>
+ `/vendor/bookings/${bookingId}`,
+
+ VENUES: "/vendor/venues",
+ CREATE_VENUE: "/vendor/venue",
+ UPDATE_VENUE:(venueId)=>`/vendor/venue/${venueId}`,
+ DELETE_VENUE:(venueId)=>`/vendor/venue/${venueId}`,
+ VENUE_BY_ID: (venueId) =>
+ `/vendor/venues/${venueId}`,
+ UPDATE_VENUE_STATUS: (venueId) =>
+ `/vendor/venues/${venueId}/status`,
+},
+
+}
\ No newline at end of file
diff --git a/client/src/constants/mockVenues.js b/client/src/constants/mockVenues.js
new file mode 100644
index 0000000000..2d516149bb
--- /dev/null
+++ b/client/src/constants/mockVenues.js
@@ -0,0 +1,40 @@
+export const similarVenues = [
+ {
+ id: "similar-1",
+ name: "Grand Celebration Hall",
+ category: "Wedding Hall",
+ rating: 4.5,
+ pricePerDay: 25000,
+ seatingCapacity: 300,
+ standingCapacity: 500,
+ description: "A beautiful venue for weddings and celebrations.",
+ address: {
+ city: "Kochi",
+ state: "Kerala",
+ },
+ images: [
+ {
+ url: "https://images.unsplash.com/photo-1519167758481-83f550bb49b3",
+ },
+ ],
+ },
+ {
+ id: "similar-2",
+ name: "Royal Convention Center",
+ category: "Convention Hall",
+ rating: 4.7,
+ pricePerDay: 35000,
+ seatingCapacity: 500,
+ standingCapacity: 800,
+ description: "Spacious venue suitable for large events.",
+ address: {
+ city: "Kochi",
+ state: "Kerala",
+ },
+ images: [
+ {
+ url: "https://images.unsplash.com/photo-1507504031003-b417219a0fde",
+ },
+ ],
+ },
+];
\ No newline at end of file
diff --git a/client/src/constants/routes.js b/client/src/constants/routes.js
new file mode 100644
index 0000000000..638e7e820e
--- /dev/null
+++ b/client/src/constants/routes.js
@@ -0,0 +1,53 @@
+export const ROUTES = {
+ PUBLIC: {
+ HOME: '/',
+ SIGNUP: '/signup',
+ LOGIN: '/login',
+ REGISTER:'/register',
+ VERIFY_OTP:'/verify-otp',
+ FORGOT_PASSWORD: "/forgot-password",
+ RESET_PASSWORD: "/reset-password",
+
+
+
+ },
+ USER: {
+ PROFILE: '/user/profile',
+ BROWSE_VENUES: '/user/venues',
+ VENUE_DETAILS: '/user/venue/:id',
+ CHANGE_PASSWORD: '/user/changepassword',
+ WISHLIST: '/user/wishlist',
+ BOOKINGS: '/user/bookings',
+ BOOKING_DETAIL: '/user/bookings/:bookingId',
+ BOOKING_SUMMARY:`/user/booking-summary`,
+ PAYMENT:`/user/payment`,
+ PAYMENT_GATEWAY:`/user/payment-gateway`,
+ PAYMENT_SUCCESS:`/user/payment-success`,
+ PAYMENT_FAILURE:`/user/payment-failure`
+
+ },
+ VENDOR: {
+ DASHBOARD: '/vendor/dashboard',
+ VENUES: '/vendor/venues',
+ VENUE_DETAILS: '/vendor/venues/:venueId',
+ BOOKINGS: '/vendor/bookings',
+ ADD_VENUE: '/vendor/add-venue',
+ EDIT_VENUE: '/vendor/edit-venue/:venueId',
+ PROFILE: '/vendor/profile',
+ SETTINGS: '/vendor/settings',
+ },
+ ADMIN: {
+ ROOT: "/admin",
+ LOGIN: "/admin/login",
+ DASHBOARD: "dashboard",
+ USERS: "users",
+ VENDORS: "vendors",
+ VENUES: "venues",
+ VENUE_DETAILS: "venues/:venueId",
+ BOOKINGS: "bookings",
+ BOOKING_DETAIL: "bookings/:bookingId",
+ PAYMENTS: "payments",
+ PAYMENT_DETAILS:"payments/:paymentId",
+ CATEGORIES: "categories",
+ }
+}
\ No newline at end of file
diff --git a/client/src/hooks/.gitkeep b/client/src/hooks/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/hooks/useDebounce.js b/client/src/hooks/useDebounce.js
new file mode 100644
index 0000000000..1b82fb9a0f
--- /dev/null
+++ b/client/src/hooks/useDebounce.js
@@ -0,0 +1,23 @@
+import { useEffect, useState } from "react";
+
+const useDebounce = (value, delay = 500) => {
+
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+
+ const timer = setTimeout(() => {
+
+ setDebouncedValue(value);
+
+ }, delay);
+
+ return () => clearTimeout(timer);
+
+ }, [value, delay]);
+
+ return debouncedValue;
+
+};
+
+export default useDebounce;
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
new file mode 100644
index 0000000000..f2a87746d8
--- /dev/null
+++ b/client/src/index.css
@@ -0,0 +1,130 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+@import "shadcn/tailwind.css";
+@import "@fontsource-variable/inter";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --font-heading: var(--font-sans);
+ --font-sans: 'Inter Variable', sans-serif;
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --color-foreground: var(--foreground);
+ --color-background: var(--background);
+ --radius-sm: calc(var(--radius) * 0.6);
+ --radius-md: calc(var(--radius) * 0.8);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) * 1.4);
+ --radius-2xl: calc(var(--radius) * 1.8);
+ --radius-3xl: calc(var(--radius) * 2.2);
+ --radius-4xl: calc(var(--radius) * 2.6);
+}
+
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ html {
+ @apply font-sans;
+ }
+}
\ No newline at end of file
diff --git a/client/src/lib/axios.js b/client/src/lib/axios.js
new file mode 100644
index 0000000000..723513a480
--- /dev/null
+++ b/client/src/lib/axios.js
@@ -0,0 +1,110 @@
+import axios from "axios";
+import { setAccessToken } from '@/redux/slices/AuthSlice'
+
+const BACKEND_URL = import.meta.env.VITE_BACKEND_URL ? `${import.meta.env.VITE_BACKEND_URL}/api/v1` : "http://localhost:4000/api/v1";
+
+const api = axios.create({
+ baseURL: BACKEND_URL,
+ withCredentials: true,
+});
+
+let isRefreshing = false;
+
+let failedQueue = [];
+
+const processQueue = (error) => {
+ failedQueue.forEach((promise) => {
+ if (error) {
+ promise.reject(error);
+ } else {
+ promise.resolve(null);
+ }
+ });
+
+ failedQueue = [];
+};
+
+let store;
+
+export const injectStore = (_store) => {
+ store = _store;
+};
+
+api.interceptors.request.use((config) => {
+ const token = store?.getState().auth.accessToken;
+
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+
+ return config;
+});
+
+api.interceptors.response.use(
+ (response) => response,
+ async (error) => {
+ const originalRequest = error.config;
+
+ if (!error.response) {
+ console.log("Network error or server not reachable");
+ return Promise.reject(error);
+ }
+
+ if (
+ error.response.status === 401 &&
+ !originalRequest._retry &&
+ !originalRequest.url?.includes("/refresh") &&
+ !originalRequest.url?.includes("/login") &&
+ !originalRequest.url?.includes("/register")
+ ) {
+ if (isRefreshing) {
+ return new Promise((resolve, reject) => {
+ failedQueue.push({ resolve, reject });
+ }).then(() => api(originalRequest));
+ }
+
+
+ originalRequest._retry = true;
+ isRefreshing = true;
+
+ const role = originalRequest.url?.includes("user") ? "user" : originalRequest.url?.includes("vendor") ? "vendor" : "admin"
+
+ try {
+ const response = await api.post(
+ `/auth/${role}/refresh`,
+ {},
+ { withCredentials: true }
+ );
+
+ const newAccessToken = response.data.data.accessToken;
+
+ if (!newAccessToken) {
+ console.log("Token not found", response.data.data);
+ throw new Error("Access token not in refresh response");
+ }
+
+ if (store) {
+ store.dispatch(setAccessToken(newAccessToken));
+ }
+
+ processQueue(null);
+
+ return api(originalRequest);
+ } catch (refreshError) {
+ processQueue(refreshError);
+
+ if (store) {
+ store.dispatch(setAccessToken(null));
+ }
+
+ return Promise.reject(refreshError);
+ } finally {
+ isRefreshing = false;
+ }
+ }
+
+ return Promise.reject(error);
+ }
+);
+
+export default api;
\ No newline at end of file
diff --git a/client/src/lib/getInitilas.js b/client/src/lib/getInitilas.js
new file mode 100644
index 0000000000..a349853fdb
--- /dev/null
+++ b/client/src/lib/getInitilas.js
@@ -0,0 +1,14 @@
+export const getInitials = (fullName = "") => {
+ const words = fullName.trim().split(" ").filter(Boolean);
+
+ if(words.length === 0) return "";
+
+ if(words.length === 1){
+ return words[0][0].toUpperCase();
+ }
+
+ return(
+ words[0][0] +
+ words[words.length - 1][0]
+ ).toUpperCase();
+};
\ No newline at end of file
diff --git a/client/src/lib/utils.js b/client/src/lib/utils.js
new file mode 100644
index 0000000000..0041ae67ae
--- /dev/null
+++ b/client/src/lib/utils.js
@@ -0,0 +1,16 @@
+// src/utils/utils.js
+
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs) {
+ return twMerge(clsx(inputs));
+}
+
+export function formatDateToDDMMYYYY(date) {
+ if (!date) return "";
+
+ const [year, month, day] = date.split("-");
+
+ return `${day}-${month}-${year}`;
+}
\ No newline at end of file
diff --git a/client/src/lib/validation/VendorProfileValidator.js b/client/src/lib/validation/VendorProfileValidator.js
new file mode 100644
index 0000000000..ba6d717e22
--- /dev/null
+++ b/client/src/lib/validation/VendorProfileValidator.js
@@ -0,0 +1,46 @@
+import { z } from "zod";
+
+export const UpdateVendorProfileSchema = z.object({
+ fullName: z
+ .string()
+ .trim()
+ .min(3, "Full name must contain at least 3 characters")
+ .optional(),
+
+ phone: z
+ .string()
+ .regex(/^[6-9]\d{9}$/, "Enter a valid 10-digit phone number")
+ .optional(),
+
+ companyName: z
+ .string()
+ .trim()
+ .min(2, "Company name must contain at least 2 characters")
+ .optional(),
+
+ address: z
+ .object({
+ addressLine1: z
+ .string()
+ .min(1, "Address is required"),
+
+ city: z
+ .string()
+ .min(1, "City is required"),
+
+ state: z
+ .string()
+ .min(1, "State is required"),
+
+ pincode: z
+ .string()
+ .regex(/^\d{6}$/, "Pincode must be 6 digits"),
+ })
+ .optional(),
+
+ bio: z
+ .string()
+ .min(10, "Bio must contain at least 10 characters")
+ .max(300, "Bio cannot exceed 300 characters")
+ .optional(),
+});
\ No newline at end of file
diff --git a/client/src/lib/validation/adminVendorValidation.js b/client/src/lib/validation/adminVendorValidation.js
new file mode 100644
index 0000000000..67447b2700
--- /dev/null
+++ b/client/src/lib/validation/adminVendorValidation.js
@@ -0,0 +1,8 @@
+import { z } from "zod";
+
+export const rejectReasonSchema = z.object({
+ reason: z
+ .string()
+ .trim()
+ .min(1, "Rejection reason is required")
+});
\ No newline at end of file
diff --git a/client/src/lib/validation/authValidation.js b/client/src/lib/validation/authValidation.js
new file mode 100644
index 0000000000..384843b944
--- /dev/null
+++ b/client/src/lib/validation/authValidation.js
@@ -0,0 +1,69 @@
+import { z } from "zod";
+
+export const loginSchema = z.object({
+ email: z
+ .string()
+ .trim()
+ .email("Invalid email address")
+ .min(1, "Email is required"),
+ password: z
+ .string()
+ .min(1, "Password is required")
+ .min(6, "Password must be at least 6 characters"),
+});
+
+export const registerSchema = z.object({
+ fullName: z
+ .string()
+ .trim()
+ .min(3, "Full name must be at least 3 characters")
+ .max(50, "Full name cannot exceed 50 characters"),
+ email: z
+ .string()
+ .trim()
+ .email("Invalid email address"),
+ phone: z
+ .string()
+ .trim()
+ .min(10, "Phone number must be at least 10 digits"),
+ password: z
+ .string()
+ .min(6, "Password must be at least 6 characters"),
+ role: z
+ .enum(["user", "vendor"])
+ .default("user"),
+});
+
+export const verifyOtpSchema = z.object({
+ otp: z
+ .string()
+ .trim()
+ .regex(/^\d{6}$/, "OTP must be 6 digits"),
+ email: z
+ .string()
+ .trim()
+ .email("Invalid email address"),
+});
+
+export const forgotPasswordSchema = z.object({
+ email: z
+ .string()
+ .trim()
+ .email("Invalid email address")
+ .min(1, "Email is required"),
+});
+
+export const resetPasswordSchema = z
+ .object({
+ password: z
+ .string()
+ .min(6, "Password must be at least 6 characters"),
+ confirmPassword: z
+ .string()
+ .min(1, "Please confirm your password"),
+ token: z.string(),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: "Passwords do not match",
+ path: ["confirmPassword"],
+ });
diff --git a/client/src/lib/validation/bookingValidation.js b/client/src/lib/validation/bookingValidation.js
new file mode 100644
index 0000000000..b724283181
--- /dev/null
+++ b/client/src/lib/validation/bookingValidation.js
@@ -0,0 +1,47 @@
+export const validateBookingDetails = ({
+ eventDate,
+ startTime,
+ endTime,
+ venue,
+}) => {
+ const errors = {};
+
+ if (!eventDate) {
+ errors.eventDate = "Event date is required";
+ }
+
+ if (!startTime) {
+ errors.startTime = "Start time is required";
+ }
+
+ if (!endTime) {
+ errors.endTime = "End time is required";
+ }
+
+ if (startTime && endTime) {
+ if (endTime <= startTime) {
+ errors.endTime =
+ "End time must be after start time";
+ }
+ }
+
+ if (startTime && endTime && venue?.minimumBookingHours) {
+ const start = convertTimeToMinutes(startTime);
+ const end = convertTimeToMinutes(endTime);
+
+ const duration = (end - start) / 60;
+
+ if (duration < venue.minimumBookingHours) {
+ errors.endTime =
+ `Minimum booking duration is ${venue.minimumBookingHours} hours`;
+ }
+ }
+
+ return errors;
+};
+
+const convertTimeToMinutes = (time) => {
+ const [hours, minutes] = time.split(":").map(Number);
+
+ return hours * 60 + minutes;
+};
\ No newline at end of file
diff --git a/client/src/lib/validation/userProfileValidation.js b/client/src/lib/validation/userProfileValidation.js
new file mode 100644
index 0000000000..1c6d1b36b8
--- /dev/null
+++ b/client/src/lib/validation/userProfileValidation.js
@@ -0,0 +1,39 @@
+import { z } from "zod";
+
+export const updateProfileSchema = z
+ .object({
+ fullName: z
+ .string()
+ .trim()
+ .min(3, "Full nam emust be 3 charecters")
+ .max(50, "Full name cannot exceed 50 charecters")
+ .optional(),
+
+ phone: z
+ .string()
+ .trim()
+ .regex(/^[0-9]{10}$/, "phone number must be 10 digits")
+ .optional(),
+ })
+ .refine(
+ (data) =>
+ data.fullName?.trim() || data.phone?.trim(),
+ {
+ message: "At least one field must be provided",
+ }
+ );
+
+export const UserProfileParamsSchema = z.object({
+ userId: z.string().regex(/^[0-9a-fA-F]{24}$/, "invalid user ID"),
+});
+
+export const RequestEmailChangeOtpSchema = z.object({
+ newEmail: z.string().trim().email("invalid email address"),
+});
+
+export const verifyEmailOtpSchema = z.object({
+ otp: z
+ .string()
+ .trim()
+ .regex(/^\d{6}$/, "OTP must be 6 digits"),
+});
diff --git a/client/src/lib/validation/venueValidation.js b/client/src/lib/validation/venueValidation.js
new file mode 100644
index 0000000000..39f8960fe4
--- /dev/null
+++ b/client/src/lib/validation/venueValidation.js
@@ -0,0 +1,146 @@
+import { z } from "zod";
+
+// Keep these values exactly the same as your backend VenueCategory enum
+export const VenueCategory = {
+ BANQUET_HALL: "BANQUET_HALL",
+ CONFERENCE_HALL: "CONFERENCE_HALL",
+ AUDITORIUM: "AUDITORIUM",
+ OUTDOOR: "OUTDOOR",
+ RESTAURANT: "RESTAURANT",
+ HOTEL: "HOTEL",
+};
+
+// ==============================
+// CREATE VENUE SCHEMA
+// ==============================
+
+export const createVenueSchema = z.object({
+ name: z
+ .string()
+ .trim()
+ .min(3, "Venue name must be at least 3 characters")
+ .max(100, "Venue name cannot exceed 100 characters"),
+
+ description: z
+ .string()
+ .trim()
+ .min(10, "Description must be at least 10 characters")
+ .max(2000, "Description cannot exceed 2000 characters"),
+
+ category: z.string().min(1, "Category is required"),
+
+ websiteUrl: z
+ .string()
+ .url("Invalid website URL")
+ .optional()
+ .or(z.literal("")),
+
+ addressLine1: z
+ .string()
+ .trim()
+ .min(5, "Address is required"),
+
+ city: z
+ .string()
+ .trim()
+ .min(2, "City is required"),
+
+ state: z
+ .string()
+ .trim()
+ .min(2, "State is required"),
+
+ country: z
+ .string()
+ .trim()
+ .min(2, "Country is required"),
+
+ phone: z
+ .string()
+ .trim()
+ .regex(/^[0-9]{10,15}$/, "Invalid phone number"),
+
+ pincode: z
+ .string()
+ .trim()
+ .regex(/^[0-9]{4,10}$/, "Invalid pincode"),
+
+ googleMapLink: z
+ .string()
+ .url("Invalid Google Map URL")
+ .optional()
+ .or(z.literal("")),
+
+ seatingCapacity: z.coerce
+ .number()
+ .min(0, "Seating capacity cannot be negative"),
+
+ standingCapacity: z.coerce
+ .number()
+ .min(0, "Standing capacity cannot be negative"),
+
+ pricePerHour: z.coerce
+ .number()
+ .min(0, "Price per hour cannot be negative"),
+
+ pricePerDay: z.coerce
+ .number()
+ .min(0, "Price per day cannot be negative"),
+
+ securityDeposit: z.coerce
+ .number()
+ .min(0, "Security deposit cannot be negative"),
+
+ weekendSurcharge: z.coerce
+ .number()
+ .min(0, "Weekend surcharge cannot be negative"),
+
+ minimumBookingHours: z.coerce
+ .number()
+ .min(0, "Minimum booking hours cannot be negative"),
+
+ amenities: z.preprocess(
+ (value) => {
+ if (typeof value === "string") {
+ try {
+ return JSON.parse(value);
+ } catch {
+ return value;
+ }
+ }
+
+ return value;
+ },
+ z.array(z.string()).optional()
+ ),
+
+ images: z
+ .array(z.instanceof(File))
+ .min(3, "Upload at least 3 images"),
+
+ license: z
+ .instanceof(File)
+ .refine(
+ (file) => file.type === "application/pdf",
+ "Only PDF files are allowed"
+ )
+ .nullable()
+ .optional(),
+});
+
+// ==============================
+// EDIT VENUE SCHEMA
+// ==============================
+
+export const editVenueSchema = createVenueSchema
+ .omit({
+ images: true,
+ license: true,
+ })
+ .extend({
+ vendorId: z.string().min(1, "Vendor ID is required"),
+
+ deletedImages: z.string().optional(),
+
+ deletedLicense: z.string().optional(),
+ });
\ No newline at end of file
diff --git a/client/src/main.jsx b/client/src/main.jsx
new file mode 100644
index 0000000000..7b4b8f6d60
--- /dev/null
+++ b/client/src/main.jsx
@@ -0,0 +1,16 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import "./index.css";
+import App from "./app/App";
+import { Provider } from "react-redux";
+import { store } from "./redux/store";
+import { Toaster } from "react-hot-toast";
+
+createRoot(document.getElementById("root")).render(
+
+
+
+
+
+
+);
diff --git a/client/src/presentation/.gitkeep b/client/src/presentation/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/components/admin/.gitkeep b/client/src/presentation/components/admin/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/components/admin/AdminHeader.jsx b/client/src/presentation/components/admin/AdminHeader.jsx
new file mode 100644
index 0000000000..fb68803909
--- /dev/null
+++ b/client/src/presentation/components/admin/AdminHeader.jsx
@@ -0,0 +1,79 @@
+import { useState } from "react";
+import { ChevronDown, User, LogOut } from "lucide-react";
+import toast from "react-hot-toast";
+import { useDispatch } from "react-redux";
+import { logout } from "@/redux/slices/AuthSlice";
+import { ROLES } from "@/constants/Roles";
+import { useNavigate } from "react-router-dom";
+import { ROUTES } from "@/constants/routes";
+
+const AdminHeader = () => {
+ const dispatch = useDispatch()
+ const navigate = useNavigate()
+ const [showDropdown, setShowDropdown] = useState(false);
+
+ const handleLogout = async () => {
+ try {
+ await dispatch(logout({role: ROLES.ADMIN})).unwrap()
+ toast('Admin logged out successfully')
+ navigate(ROUTES.ADMIN.LOGIN)
+ } catch (error) {
+ toast.error(error)
+ }
+ }
+
+ return (
+
+
+
+ );
+
+};
+
+export default AdminHeader;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/AdminSidebar.jsx b/client/src/presentation/components/admin/AdminSidebar.jsx
new file mode 100644
index 0000000000..ec76aea34d
--- /dev/null
+++ b/client/src/presentation/components/admin/AdminSidebar.jsx
@@ -0,0 +1,89 @@
+import { LogOut } from "lucide-react";
+import { ADMIN_MENU } from "@/constants/adminMenu";
+import SidebarItem from "./SidebarItem";
+import logo from "@/assets/images/logo.jpeg";
+import toast from "react-hot-toast";
+import { useDispatch } from "react-redux";
+import { logout } from "@/redux/slices/AuthSlice";
+import { ROLES } from "@/constants/Roles";
+import { useNavigate } from "react-router-dom";
+import { ROUTES } from "@/constants/routes";
+
+const AdminSidebar = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const handleLogout = async () => {
+ try {
+ await dispatch(
+ logout({ role: ROLES.ADMIN })
+ ).unwrap();
+
+ toast("Admin logged out successfully");
+
+ navigate(ROUTES.ADMIN.LOGIN);
+
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default AdminSidebar;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/SidebarItem.jsx b/client/src/presentation/components/admin/SidebarItem.jsx
new file mode 100644
index 0000000000..97ed25e89e
--- /dev/null
+++ b/client/src/presentation/components/admin/SidebarItem.jsx
@@ -0,0 +1,41 @@
+import { NavLink } from "react-router-dom";
+
+const SidebarItem = ({ menu }) => {
+
+ const Icon = menu.icon;
+
+ return (
+
+
+
+ `flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200
+
+ ${
+
+ isActive
+
+ ? "bg-orange-500 text-white"
+
+ : "text-gray-300 hover:bg-gray-800 hover:text-white"
+
+ }`
+
+ }
+
+ >
+
+
+
+ {menu.title}
+
+
+
+ );
+
+};
+
+export default SidebarItem;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/BookingActionCard.jsx b/client/src/presentation/components/admin/bookingManagement/BookingActionCard.jsx
new file mode 100644
index 0000000000..6606d89d15
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/BookingActionCard.jsx
@@ -0,0 +1,136 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+import { Badge } from "@/components/ui/badge";
+
+const BookingActionCard = ({ booking }) => {
+
+ const getBadgeClass = (status) => {
+
+ switch (status?.toLowerCase()) {
+
+ // Booking Status
+ case "pending":
+ return "bg-yellow-100 text-yellow-800 border-yellow-300";
+
+ case "confirmed":
+ return "bg-green-100 text-green-800 border-green-300";
+
+ case "completed":
+ return "bg-blue-100 text-blue-800 border-blue-300";
+
+ case "cancelled":
+ return "bg-red-100 text-red-800 border-red-300";
+
+ // Payment Status
+ case "partial":
+ return "bg-sky-100 text-sky-800 border-sky-300";
+
+ case "paid":
+ case "success":
+ return "bg-green-100 text-green-800 border-green-300";
+
+ case "failed":
+ return "bg-red-100 text-red-800 border-red-300";
+
+ case "refunded":
+ return "bg-purple-100 text-purple-800 border-purple-300";
+
+ default:
+ return "";
+ }
+
+ };
+console.log("Booking:", booking);
+
+ return (
+
+
+
+
+
+
+
+ Booking Status
+
+
+
+
+
+
+
+ {/* Booking Status */}
+
+
+
+
+
+ Booking Status
+
+
+
+
+ {booking?.status}
+
+
+
+
+ {/* Payment Status */}
+
+
+
+
+
+ Payment Status
+
+
+
+
+ {booking?.paymentStatus}
+
+
+
+
+ {/* Cancellation Reason */}
+
+ {booking?.cancellationReason && (
+
+
+
+
+
+ Cancellation Reason
+
+
+
+
+
+ {booking.cancellationReason}
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+
+};
+
+export default BookingActionCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/BookingFilters.jsx b/client/src/presentation/components/admin/bookingManagement/BookingFilters.jsx
new file mode 100644
index 0000000000..a8124f2486
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/BookingFilters.jsx
@@ -0,0 +1,97 @@
+import SearchBar from "../common/SearchBar";
+import FilterTabs from "../common/FilterTabs";
+import FilterDropdown from "../common/FilterDropdown";
+
+const BookingFilters = ({
+ search,
+ onSearchChange,
+ status,
+ onStatusChange,
+ paymentStatus,
+ onPaymentStatusChange,
+}) => {
+
+ const tabs = [
+ {
+ label: "All",
+ value: "",
+ },
+ {
+ label: "Pending",
+ value: "pending",
+ },
+ {
+ label: "Confirmed",
+ value: "confirmed",
+ },
+ {
+ label: "Completed",
+ value: "completed",
+ },
+ {
+ label: "Cancelled",
+ value: "cancelled",
+ },
+
+ ];
+
+ const paymentOptions = [
+ {
+ label: "All Payments",
+ value: "",
+ },
+ {
+ label: "Pending",
+ value: "pending",
+ },
+ {
+ label: "Partial",
+ value: "partial",
+ },
+ {
+ label: "Paid",
+ value: "paid",
+ },
+ {
+ label: "Failed",
+ value: "failed",
+ },
+ {
+ label: "Refunded",
+ value: "refunded",
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
+
+export default BookingFilters;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/BookingStats.jsx b/client/src/presentation/components/admin/bookingManagement/BookingStats.jsx
new file mode 100644
index 0000000000..289cd3796f
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/BookingStats.jsx
@@ -0,0 +1,95 @@
+import {
+ CalendarCheck,
+ Clock3,
+ CheckCircle,
+ XCircle,
+ CalendarDays,
+} from "lucide-react";
+
+import {
+ Card,
+ CardContent,
+} from "@/components/ui/card";
+
+const BookingStats = ({ stats }) => {
+
+ const cards = [
+ {
+ title: "TotalBookings",
+ value: stats?.totalBookings || 0,
+ icon: CalendarDays,
+ color: "text-blue-600",
+ },
+ {
+ title: "Pending",
+ value: stats?.pendingBookings || 0,
+ icon: Clock3,
+ color: "text-yellow-600",
+ },
+ {
+ title: "Confirmed",
+ value: stats?.confirmedBookings || 0,
+ icon: CheckCircle,
+ color: "text-green-600",
+ },
+
+ {
+ title: "Cancelled",
+ value: stats?.cancelledBookings || 0,
+ icon: XCircle,
+ color: "text-red-600",
+ },
+ {
+ title: "Completed",
+ value: stats?.completedBookings || 0,
+ icon: CalendarCheck,
+ color: "text-indigo-600",
+ },
+ ];
+
+ return (
+
+
+ {cards.map((card) => {
+
+ const Icon = card.icon;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {card.title}
+
+
+
+
+
+ {card.value}
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ })}
+
+
+ );
+};
+
+export default BookingStats;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/BookingStatusCard.jsx b/client/src/presentation/components/admin/bookingManagement/BookingStatusCard.jsx
new file mode 100644
index 0000000000..c07ee76781
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/BookingStatusCard.jsx
@@ -0,0 +1,143 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+import { Badge } from "@/components/ui/badge";
+
+const BookingStatusCard = ({ booking }) => {
+
+ const getStatusVariant = (status) => {
+
+ switch (status) {
+
+ case "confirmed":
+ return "default";
+
+ case "pending":
+ return "secondary";
+
+ case "cancelled":
+ return "destructive";
+
+ case "completed":
+ return "outline";
+
+ default:
+ return "secondary";
+
+ }
+
+ };
+
+ const formatDate = (date) => {
+
+ if (!date) return "-";
+
+ return new Date(date).toLocaleDateString("en-IN", {
+
+ day: "2-digit",
+
+ month: "long",
+
+ year: "numeric",
+
+ });
+
+ };
+
+ return (
+
+
+
+
+
+
+
+ Booking Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Status
+
+
+
+
+
+ {booking.status}
+
+
+
+
+
+
+
+
+
+ Payment Status
+
+
+
+
+
+ {booking.paymentStatus}
+
+
+
+
+
+
+
+
+
+ Booking Date
+
+
+
+
+
+ {formatDate(booking.bookingDate)}
+
+
+
+
+
+
+
+
+
+ Created At
+
+
+
+
+
+ {formatDate(booking.createdAt)}
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default BookingStatusCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/BookingTable.jsx b/client/src/presentation/components/admin/bookingManagement/BookingTable.jsx
new file mode 100644
index 0000000000..f67cd07704
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/BookingTable.jsx
@@ -0,0 +1,89 @@
+import {
+ Table,
+ TableBody,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+import BookingTableRow from "./BookingTableRow";
+
+const BookingTable = ({
+ bookings,
+ onView,
+}) => {
+
+ return (
+
+
+
+
+
+
+
+
+
+ Booking ID
+
+ User
+
+ Venue
+
+ Vendor
+
+ Event Date
+
+ Total
+
+ Status
+
+ Payment
+
+
+ Actions
+
+
+
+
+
+
+
+
+ {bookings?.length > 0 ? (
+
+ bookings.map((booking) => (
+
+
+
+ ))
+
+ ) : (
+
+
+
+ |
+ No bookings found
+ |
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+
+};
+
+export default BookingTable;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/BookingTableRow.jsx b/client/src/presentation/components/admin/bookingManagement/BookingTableRow.jsx
new file mode 100644
index 0000000000..67bfa231f7
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/BookingTableRow.jsx
@@ -0,0 +1,168 @@
+import { Eye } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+
+import {
+ TableCell,
+ TableRow,
+} from "@/components/ui/table";
+
+const BookingTableRow = ({
+ booking,
+ onView,
+}) => {
+
+ const bookingStatus = booking.status;
+
+ const paymentStatus = booking.paymentStatus;
+
+ const bookingStatusClass = {
+
+ pending: "bg-yellow-100 text-yellow-700",
+
+ confirmed: "bg-green-100 text-green-700",
+
+ cancelled: "bg-red-100 text-red-700",
+
+ completed: "bg-blue-100 text-blue-700",
+
+ };
+
+ const paymentStatusClass = {
+
+ pending: "bg-yellow-100 text-yellow-700",
+
+ partial: "bg-orange-100 text-orange-700",
+
+ paid: "bg-green-100 text-green-700",
+
+ refunded: "bg-purple-100 text-purple-700",
+
+ failed: "bg-red-100 text-red-700",
+
+ success: "bg-green-100 text-green-700",
+
+ };
+
+ return (
+
+
+
+ {/* Booking ID */}
+
+
+
+ {booking._id?.slice(-6)}
+
+
+
+ {/* User */}
+
+
+
+ {booking.user?.fullName || "-"}
+
+
+
+ {/* Venue */}
+
+
+
+ {booking.venue?.name || "-"}
+
+
+
+ {/* Vendor */}
+
+
+
+ {
+ booking.vendor?.fullName ||
+ "-"}
+
+
+
+ {/* Booking Date */}
+
+
+
+ {new Date(
+ booking.bookingDate
+ ).toLocaleDateString()}
+
+
+
+ {/* Total Amount */}
+
+
+
+ ₹{booking.totalAmount?.toLocaleString()}
+
+
+
+ {/* Booking Status */}
+
+
+
+
+
+ {bookingStatus}
+
+
+
+
+
+ {/* Payment Status */}
+
+
+
+
+
+ {paymentStatus}
+
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default BookingTableRow;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/BookingTimelineCard.jsx b/client/src/presentation/components/admin/bookingManagement/BookingTimelineCard.jsx
new file mode 100644
index 0000000000..da4d0cf0ae
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/BookingTimelineCard.jsx
@@ -0,0 +1,164 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+const BookingTimelineCard = ({ booking }) => {
+
+ const formatDate = (date) => {
+
+ if (!date) return "-";
+
+ return new Date(date).toLocaleDateString("en-IN", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ });
+
+ };
+
+ const formatDateTime = (date) => {
+
+ if (!date) return "-";
+
+ return new Date(date).toLocaleString("en-IN", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+
+ };
+
+ return (
+
+
+
+
+
+
+
+ Booking Timeline
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Booking Date
+
+
+
+
+
+ {formatDate(booking?.bookingDate)}
+
+
+
+
+
+
+
+
+
+ Guest Count
+
+
+
+
+
+ {booking?.guestCount}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Start Time
+
+
+
+
+
+ {booking?.startTime}
+
+
+
+
+
+
+
+
+
+ End Time
+
+
+
+
+
+ {booking?.endTime}
+
+
+
+
+
+
+
+
+
+
+
+ Created At
+
+
+
+
+
+ {formatDateTime(booking?.createdAt)}
+
+
+
+
+
+
+
+
+
+ Last Updated
+
+
+
+
+
+ {formatDateTime(booking?.updatedAt)}
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default BookingTimelineCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/BookingpaymentCard.jsx b/client/src/presentation/components/admin/bookingManagement/BookingpaymentCard.jsx
new file mode 100644
index 0000000000..d319af946f
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/BookingpaymentCard.jsx
@@ -0,0 +1,140 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+const BookingPaymentCard = ({ booking }) => {
+
+ const paymentStatusColor = {
+
+ pending: "text-yellow-600",
+
+ partial: "text-orange-600",
+
+ paid: "text-green-600",
+
+ failed: "text-red-600",
+
+ refunded: "text-blue-600",
+
+ success: "text-green-600",
+
+ };
+
+ return (
+
+
+
+
+
+
+
+ Payment Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Amount
+
+
+
+
+
+ ₹ {booking?.totalAmount ?? 0}
+
+
+
+
+
+
+
+
+
+ Advance Amount
+
+
+
+
+
+ ₹ {booking?.advanceAmount ?? 0}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Paid Amount
+
+
+
+
+
+ ₹ {booking?.paidAmount ?? 0}
+
+
+
+
+
+
+
+
+
+ Remaining Amount
+
+
+
+
+
+ ₹ {booking?.remainingAmount ?? 0}
+
+
+
+
+
+
+
+
+
+
+
+ Payment Status
+
+
+
+
+
+ {booking?.paymentStatus || "-"}
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default BookingPaymentCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/UserInfoCard.jsx b/client/src/presentation/components/admin/bookingManagement/UserInfoCard.jsx
new file mode 100644
index 0000000000..f35be4bc9c
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/UserInfoCard.jsx
@@ -0,0 +1,82 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+const UserInfoCard = ({ user }) => {
+
+ return (
+
+
+
+
+
+
+
+ User Information
+
+
+
+
+
+
+
+
+
+
+
+ Full Name
+
+
+
+
+
+ {user?.fullName || "-"}
+
+
+
+
+
+
+
+
+
+ Email
+
+
+
+
+
+ {user?.email || "-"}
+
+
+
+
+
+
+
+
+
+ Phone
+
+
+
+
+
+ {user?.phone || "-"}
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default UserInfoCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/VendorInfoCard.jsx b/client/src/presentation/components/admin/bookingManagement/VendorInfoCard.jsx
new file mode 100644
index 0000000000..efecc81c6d
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/VendorInfoCard.jsx
@@ -0,0 +1,98 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+const VendorInfoCard = ({ vendor }) => {
+
+ return (
+
+
+
+
+
+
+
+ Vendor Information
+
+
+
+
+
+
+
+
+
+
+
+ Full Name
+
+
+
+
+
+ {vendor?.fullName || "-"}
+
+
+
+
+
+
+
+
+
+ Company Name
+
+
+
+
+
+ {vendor?.companyName || "-"}
+
+
+
+
+
+
+
+
+
+ Email
+
+
+
+
+
+ {vendor?.email || "-"}
+
+
+
+
+
+
+
+
+
+ Phone
+
+
+
+
+
+ {vendor?.phone || "-"}
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default VendorInfoCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/bookingManagement/VenueInfoCard.jsx b/client/src/presentation/components/admin/bookingManagement/VenueInfoCard.jsx
new file mode 100644
index 0000000000..06dff9def3
--- /dev/null
+++ b/client/src/presentation/components/admin/bookingManagement/VenueInfoCard.jsx
@@ -0,0 +1,120 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+const VenueInfoCard = ({ venue }) => {
+
+ return (
+
+
+
+
+
+
+
+ Venue Information
+
+
+
+
+
+
+
+
+
+
+
+ Venue Name
+
+
+
+
+
+ {venue?.name || "-"}
+
+
+
+
+
+
+
+
+
+ Category
+
+
+
+
+
+ {venue?.category || "-"}
+
+
+
+
+
+
+
+
+
+ Address
+
+
+
+
+
+ {venue?.address
+ ? `${venue.address.addressLine1}, ${venue.address.city}, ${venue.address.state}`
+ : "-"}
+
+
+
+
+
+
+
+
+
+
+
+ Seating Capacity
+
+
+
+
+
+ {venue?.seatingCapacity ?? "-"}
+
+
+
+
+
+
+
+
+
+ Standing Capacity
+
+
+
+
+
+ {venue?.standingCapacity ?? "-"}
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default VenueInfoCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/common/FilterDropdown.jsx b/client/src/presentation/components/admin/common/FilterDropdown.jsx
new file mode 100644
index 0000000000..66f49b7993
--- /dev/null
+++ b/client/src/presentation/components/admin/common/FilterDropdown.jsx
@@ -0,0 +1,34 @@
+const FilterDropdown = ({
+ label,
+ value,
+ onChange,
+ options = [],
+ className = "",
+}) => {
+ return (
+
+ {label && (
+
+ )}
+
+
+
+ );
+};
+
+export default FilterDropdown;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/common/FilterTabs.jsx b/client/src/presentation/components/admin/common/FilterTabs.jsx
new file mode 100644
index 0000000000..6df93821f4
--- /dev/null
+++ b/client/src/presentation/components/admin/common/FilterTabs.jsx
@@ -0,0 +1,42 @@
+const FilterTabs = ({
+ tabs,
+ activeTab,
+ onTabChange
+}) => {
+console.log("activeTab",activeTab)
+console.log("tabs",tabs)
+ return (
+
+
+
+ {tabs.map((tab) => (
+
+
+
+ ))}
+
+
+
+ );
+
+};
+
+export default FilterTabs;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/common/PageHeader.jsx b/client/src/presentation/components/admin/common/PageHeader.jsx
new file mode 100644
index 0000000000..3c431579a4
--- /dev/null
+++ b/client/src/presentation/components/admin/common/PageHeader.jsx
@@ -0,0 +1,25 @@
+const PageHeader = ({ title, subtitle, children }) => {
+ return (
+
+
+
+ {title}
+
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+ {children && (
+
+ {children}
+
+ )}
+
+ );
+};
+
+export default PageHeader;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/common/SearchBar.jsx b/client/src/presentation/components/admin/common/SearchBar.jsx
new file mode 100644
index 0000000000..27d253bea1
--- /dev/null
+++ b/client/src/presentation/components/admin/common/SearchBar.jsx
@@ -0,0 +1,46 @@
+import { Search } from "lucide-react";
+
+const SearchBar = ({
+ value,
+ onChange,
+ placeholder = "Search..."
+}) => {
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default SearchBar;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/dashboard/BookingOverviewChart.jsx b/client/src/presentation/components/admin/dashboard/BookingOverviewChart.jsx
new file mode 100644
index 0000000000..082e918c7a
--- /dev/null
+++ b/client/src/presentation/components/admin/dashboard/BookingOverviewChart.jsx
@@ -0,0 +1,94 @@
+import {
+ ResponsiveContainer,
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+} from "recharts";
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+const BookingOverviewChart = ({ data }) => {
+ const getMonthName = (monthNumber) => {
+ return new Date(
+ 2000,
+ monthNumber - 1
+ ).toLocaleString("en-US", {
+ month: "short",
+ });
+ };
+
+ const chartData = data.map((item) => ({
+ ...item,
+ month: getMonthName(item.month),
+ }));
+
+ return (
+
+
+
+
+
+
+
+ Booking Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default BookingOverviewChart;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/dashboard/DashboardStats.jsx b/client/src/presentation/components/admin/dashboard/DashboardStats.jsx
new file mode 100644
index 0000000000..987857f9ba
--- /dev/null
+++ b/client/src/presentation/components/admin/dashboard/DashboardStats.jsx
@@ -0,0 +1,119 @@
+import {
+ Users,
+ UserCheck,
+ Building2,
+ CalendarDays,
+ IndianRupee,
+ Clock3,
+ BadgeCheck,
+} from "lucide-react";
+
+import {
+ Card,
+ CardContent,
+} from "@/components/ui/card";
+
+const DashboardStats = ({ stats }) => {
+
+ const cards = [
+
+ {
+ title: "Total Users",
+ value: stats?.summary?.totalUsers || 0,
+ icon: Users,
+ color: "text-blue-600",
+ },
+
+ {
+ title: "Total Vendors",
+ value: stats?.summary?.totalVendors || 0,
+ icon: UserCheck,
+ color: "text-green-600",
+ },
+
+ {
+ title: "Total Venues",
+ value: stats?.summary?.totalVenues || 0,
+ icon: Building2,
+ color: "text-purple-600",
+ },
+
+ {
+ title: "Total Bookings",
+ value: stats?.summary?.totalBookings || 0,
+ icon: CalendarDays,
+ color: "text-orange-600",
+ },
+
+ {
+ title: "Revenue",
+ value: `₹${(stats?.summary?.totalRevenue || 0).toLocaleString()}`,
+ icon: IndianRupee,
+ color: "text-emerald-600",
+ },
+
+ {
+ title: "Pending Vendors",
+ value: stats?.summary?.pendingVendorApprovals || 0,
+ icon: Clock3,
+ color: "text-yellow-600",
+ },
+
+ {
+ title: "Pending Venues",
+ value: stats?.summary?.pendingVenueApprovals || 0,
+ icon: BadgeCheck,
+ color: "text-red-600",
+ },
+
+ ];
+
+ return (
+
+
+
+ {cards.map((card) => {
+
+ const Icon = card.icon;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {card.title}
+
+
+
+
+
+ {card.value}
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ })}
+
+
+
+ );
+
+};
+
+export default DashboardStats;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/dashboard/RevenueChart.jsx b/client/src/presentation/components/admin/dashboard/RevenueChart.jsx
new file mode 100644
index 0000000000..b06f373951
--- /dev/null
+++ b/client/src/presentation/components/admin/dashboard/RevenueChart.jsx
@@ -0,0 +1,103 @@
+import {
+ ResponsiveContainer,
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+} from "recharts";
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+const RevenueChart = ({ data }) => {
+ const getMonthName = (monthNumber) => {
+
+ return new Date(
+ 2000,
+ monthNumber - 1
+ ).toLocaleString("en-US", {
+ month: "short",
+ });
+
+ };
+
+ const chartData = data.map((item) => ({
+
+ ...item,
+
+ month: getMonthName(item.month),
+
+ }));
+
+ return (
+
+
+
+
+
+
+
+ Revenue Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `₹${value.toLocaleString()}`
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default RevenueChart;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/paymentManagement/BookingInfoCard.jsx b/client/src/presentation/components/admin/paymentManagement/BookingInfoCard.jsx
new file mode 100644
index 0000000000..e410374fcc
--- /dev/null
+++ b/client/src/presentation/components/admin/paymentManagement/BookingInfoCard.jsx
@@ -0,0 +1,137 @@
+import {
+ CalendarDays,
+ Clock3,
+ ClipboardList,
+ CreditCard,
+} from "lucide-react";
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+import { Badge } from "@/components/ui/badge";
+
+const BookingInfoCard = ({ booking }) => {
+ if (!booking) return null;
+
+ const bookingStatusClass = {
+ pending: "bg-yellow-100 text-yellow-700",
+ confirmed: "bg-green-100 text-green-700",
+ cancelled: "bg-red-100 text-red-700",
+ completed: "bg-blue-100 text-blue-700",
+ rejected: "bg-red-100 text-red-700",
+ };
+
+ const paymentStatusClass = {
+ pending: "bg-yellow-100 text-yellow-700",
+ partial: "bg-orange-100 text-orange-700",
+ paid: "bg-green-100 text-green-700",
+ refunded: "bg-purple-100 text-purple-700",
+ failed: "bg-red-100 text-red-700",
+ success: "bg-green-100 text-green-700",
+ };
+
+ return (
+
+
+ Booking Information
+
+
+
+
+ {/* Booking ID */}
+
+
+
+
+
+ Booking ID
+
+
+
+ {booking.id?.slice(-8)}
+
+
+
+
+ {/* Booking Date */}
+
+
+
+
+
+ Booking Date
+
+
+
+ {new Date(
+ booking.bookingDate
+ ).toLocaleDateString()}
+
+
+
+
+ {/* Event Time */}
+
+
+
+
+
+ Event Time
+
+
+
+ {booking.startTime} - {booking.endTime}
+
+
+
+
+ {/* Booking Status */}
+
+
+
+
+
+ Booking Status
+
+
+
+ {booking.status}
+
+
+
+
+ {/* Booking Payment Status */}
+
+
+
+
+
+ Booking Payment Status
+
+
+
+ {booking.paymentStatus}
+
+
+
+
+
+
+ );
+};
+
+export default BookingInfoCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/paymentManagement/PaymentFilters.jsx b/client/src/presentation/components/admin/paymentManagement/PaymentFilters.jsx
new file mode 100644
index 0000000000..4be7103c77
--- /dev/null
+++ b/client/src/presentation/components/admin/paymentManagement/PaymentFilters.jsx
@@ -0,0 +1,95 @@
+import SearchBar from "../common/SearchBar";
+import FilterTabs from "../common/FilterTabs";
+import FilterDropdown from "../common/FilterDropdown";
+
+const PaymentFilters = ({
+ search,
+ onSearchChange,
+ paymentStatus,
+ onPaymentStatusChange,
+ paymentType,
+ onPaymentTypeChange,
+}) => {
+
+const tabs = [
+ {
+ label: "All",
+ value: "",
+ },
+ {
+ label: "Advance",
+ value: "advance",
+ },
+ {
+ label: "Balance",
+ value: "balance",
+ },
+ {
+ label: "Full",
+ value: "full",
+ },
+];
+
+ const paymentStatusOptions = [
+ {
+ label: "All Payments",
+ value: "",
+ },
+ {
+ label: "Pending",
+ value: "pending",
+ },
+ {
+ label: "Success",
+ value: "success",
+ },
+ {
+ label: "Failed",
+ value: "failed",
+ },
+ {
+ label: "Refunded",
+ value: "refunded",
+ },
+ ];
+
+
+ return (
+
+
+
+ );
+
+};
+
+export default PaymentFilters;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/paymentManagement/PaymentInfoCard.jsx b/client/src/presentation/components/admin/paymentManagement/PaymentInfoCard.jsx
new file mode 100644
index 0000000000..b5b96a5408
--- /dev/null
+++ b/client/src/presentation/components/admin/paymentManagement/PaymentInfoCard.jsx
@@ -0,0 +1,159 @@
+import {
+ IndianRupee,
+ CreditCard,
+ Wallet,
+ BadgeCheck,
+ RotateCcw,
+ CalendarClock,
+} from "lucide-react";
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+import { Badge } from "@/components/ui/badge";
+
+const PaymentInfoCard = ({ payment }) => {
+ if (!payment) return null;
+
+ const statusClass = {
+ success: "bg-green-100 text-green-700",
+ pending: "bg-yellow-100 text-yellow-700",
+ failed: "bg-red-100 text-red-700",
+ refunded: "bg-purple-100 text-purple-700",
+ partial: "bg-orange-100 text-orange-700",
+ paid: "bg-green-100 text-green-700",
+ };
+
+ return (
+
+
+ Payment Information
+
+
+
+
+ {/* Amount */}
+
+
+
+
+
+ Amount Paid
+
+
+
+ ₹{payment.amount?.toLocaleString()}
+
+
+
+
+ {/* Payment Type */}
+
+
+
+
+
+ Payment Type
+
+
+
+ {payment.paymentType}
+
+
+
+
+ {/* Payment Method */}
+
+
+
+
+
+ Payment Method
+
+
+
+ {payment.paymentMethod}
+
+
+
+
+ {/* Payment Status */}
+
+
+
+
+
+ Payment Status
+
+
+
+ {payment.paymentStatus}
+
+
+
+
+ {/* Refund Amount */}
+
+
+
+
+
+ Refund Amount
+
+
+
+ {payment.refundAmount
+ ? `₹${payment.refundAmount.toLocaleString()}`
+ : "-"}
+
+
+
+
+ {/* Refund Reason */}
+
+
+
+
+
+ Refund Reason
+
+
+
+ {payment.refundReason || "-"}
+
+
+
+
+ {/* Refunded At */}
+
+
+
+
+
+ Refunded At
+
+
+
+ {payment.refundedAt
+ ? new Date(
+ payment.refundedAt
+ ).toLocaleString()
+ : "-"}
+
+
+
+
+
+
+ );
+};
+
+export default PaymentInfoCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/paymentManagement/PaymentStats.jsx b/client/src/presentation/components/admin/paymentManagement/PaymentStats.jsx
new file mode 100644
index 0000000000..7eab0a61d9
--- /dev/null
+++ b/client/src/presentation/components/admin/paymentManagement/PaymentStats.jsx
@@ -0,0 +1,111 @@
+import {
+ CreditCard,
+ CheckCircle,
+ Clock3,
+ XCircle,
+ RotateCcw,
+ IndianRupee,
+} from "lucide-react";
+
+import {
+ Card,
+ CardContent,
+} from "@/components/ui/card";
+
+const PaymentStats = ({ stats }) => {
+
+ const cards = [
+
+ {
+ title: "Total Payments",
+ value: stats?.totalPayments || 0,
+ icon: CreditCard,
+ color: "text-blue-600",
+ },
+
+ {
+ title: "Successful",
+ value: stats?.successfulPayments || 0,
+ icon: CheckCircle,
+ color: "text-green-600",
+ },
+
+ {
+ title: "Pending",
+ value: stats?.pendingPayments || 0,
+ icon: Clock3,
+ color: "text-yellow-600",
+ },
+
+ {
+ title: "Failed",
+ value: stats?.failedPayments || 0,
+ icon: XCircle,
+ color: "text-red-600",
+ },
+
+ {
+ title: "Refunded",
+ value: stats?.refundedPayments || 0,
+ icon: RotateCcw,
+ color: "text-purple-600",
+ },
+
+ {
+ title: "Revenue",
+ value: `₹${(stats?.totalRevenue || 0).toLocaleString()}`,
+ icon: IndianRupee,
+ color: "text-emerald-600",
+ },
+
+ ];
+
+ return (
+
+
+
+ {cards.map((card) => {
+
+ const Icon = card.icon;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {card.title}
+
+
+
+
+
+ {card.value}
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ })}
+
+
+
+ );
+
+};
+
+export default PaymentStats;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/paymentManagement/PaymentSummaryCard.jsx b/client/src/presentation/components/admin/paymentManagement/PaymentSummaryCard.jsx
new file mode 100644
index 0000000000..9015a9538c
--- /dev/null
+++ b/client/src/presentation/components/admin/paymentManagement/PaymentSummaryCard.jsx
@@ -0,0 +1,142 @@
+import {
+ CreditCard,
+ BadgeCheck,
+ Calendar,
+ Wallet,
+} from "lucide-react";
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+import { Badge } from "@/components/ui/badge";
+
+const PaymentSummaryCard = ({ payment }) => {
+ const statusClass = {
+ success: "bg-green-100 text-green-700",
+ pending: "bg-yellow-100 text-yellow-700",
+ failed: "bg-red-100 text-red-700",
+ refunded: "bg-purple-100 text-purple-700",
+ };
+
+ return (
+
+
+
+ Payment Summary
+
+
+
+
+
+
+
+ {/* Payment ID */}
+
+
+
+
+
+ Payment ID
+
+
+
+ {payment.id?.slice(-8)}
+
+
+
+
+ {/* Status */}
+
+
+
+
+
+ Payment Status
+
+
+
+ {payment.paymentStatus}
+
+
+
+
+ {/* Payment Type */}
+
+
+
+
+
+ Payment Type
+
+
+
+ {payment.paymentType}
+
+
+
+
+ {/* Payment Method */}
+
+
+
+
+
+ Payment Method
+
+
+
+ {payment.paymentMethod}
+
+
+
+
+ {/* Created */}
+
+
+
+
+
+ Created At
+
+
+
+ {new Date(
+ payment.createdAt
+ ).toLocaleString()}
+
+
+
+
+ {/* Updated */}
+
+
+
+
+
+ Updated At
+
+
+
+ {new Date(
+ payment.updatedAt
+ ).toLocaleString()}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PaymentSummaryCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/paymentManagement/PaymentTable.jsx b/client/src/presentation/components/admin/paymentManagement/PaymentTable.jsx
new file mode 100644
index 0000000000..4e34e680a8
--- /dev/null
+++ b/client/src/presentation/components/admin/paymentManagement/PaymentTable.jsx
@@ -0,0 +1,91 @@
+import {
+ Table,
+ TableBody,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+import PaymentTableRow from "./PaymentTableRow";
+
+const PaymentTable = ({
+ payments,
+ onView,
+}) => {
+
+ return (
+
+
+
+
+
+
+
+
+
+ Payment ID
+
+ Booking ID
+
+ User
+
+ Vendor
+
+ Amount
+
+ Payment Type
+
+ Payment Status
+
+ Date
+
+
+ Actions
+
+
+
+
+
+
+
+
+ {payments?.length > 0 ? (
+
+ payments.map((payment) => (
+
+
+
+ ))
+
+ ) : (
+
+
+
+ |
+
+ No payments found
+
+ |
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+
+};
+
+export default PaymentTable;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/paymentManagement/PaymentTableRow.jsx b/client/src/presentation/components/admin/paymentManagement/PaymentTableRow.jsx
new file mode 100644
index 0000000000..589d89be64
--- /dev/null
+++ b/client/src/presentation/components/admin/paymentManagement/PaymentTableRow.jsx
@@ -0,0 +1,158 @@
+import { Eye } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+
+import {
+ TableCell,
+ TableRow,
+} from "@/components/ui/table";
+
+const PaymentTableRow = ({
+ payment,
+ onView,
+}) => {
+
+ const paymentStatusClass = {
+
+ pending: "bg-yellow-100 text-yellow-700",
+
+ success: "bg-green-100 text-green-700",
+
+ failed: "bg-red-100 text-red-700",
+
+ refunded: "bg-purple-100 text-purple-700",
+
+ };
+
+ const paymentTypeClass = {
+
+ advance: "bg-blue-100 text-blue-700",
+
+ balance: "bg-orange-100 text-orange-700",
+
+ full: "bg-purple-100 text-purple-700",
+
+ };
+
+ return (
+
+
+
+ {/* Payment ID */}
+
+
+
+ {payment._id?.slice(-6)}
+
+
+
+ {/* Booking ID */}
+
+
+
+ {payment.bookingId?._id?.slice(-6) || "-"}
+
+
+
+ {/* User */}
+
+
+
+ {payment.userId?.fullName || "-"}
+
+
+
+ {/* Vendor */}
+
+
+
+ {payment.vendorId?.fullName ||
+ payment.vendorId?.companyName ||
+ "-"}
+
+
+
+ {/* Amount */}
+
+
+
+ ₹{payment.amount?.toLocaleString()}
+
+
+
+ {/* Payment Type */}
+
+
+
+
+
+ {payment.paymentType}
+
+
+
+
+
+ {/* Payment Status */}
+
+
+
+
+
+ {payment.paymentStatus}
+
+
+
+
+
+ {/* Date */}
+
+
+
+ {new Date(
+ payment.createdAt
+ ).toLocaleDateString()}
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default PaymentTableRow;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/userManagement/UserFilters.jsx b/client/src/presentation/components/admin/userManagement/UserFilters.jsx
new file mode 100644
index 0000000000..dfe6d76b04
--- /dev/null
+++ b/client/src/presentation/components/admin/userManagement/UserFilters.jsx
@@ -0,0 +1,61 @@
+
+import SearchBar from "../common/SearchBar";
+import FilterTabs from "../common/FilterTabs";
+
+const UserFilters = ({
+ search,
+ onSearchChange,
+ status,
+ onStatusChange
+}) => {
+
+ const tabs = [
+
+ {
+ label: "All",
+ value: "all"
+ },
+
+ {
+ label: "Active",
+ value: "active"
+ },
+
+ {
+ label: "Blocked",
+ value: "blocked"
+ }
+
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default UserFilters;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/userManagement/UserTable.jsx b/client/src/presentation/components/admin/userManagement/UserTable.jsx
new file mode 100644
index 0000000000..557f65052b
--- /dev/null
+++ b/client/src/presentation/components/admin/userManagement/UserTable.jsx
@@ -0,0 +1,97 @@
+import UserTableRow from "./UserTableRow";
+
+import {
+ Table,
+ TableBody,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+
+const UserTable = ({
+ users,
+ onView,
+ onBlock,
+}) => {
+
+ return (
+
+
+
+
+
+
+
+ User ID
+
+ Name
+
+ Email
+
+ Phone
+
+ Status
+
+
+
+ Actions
+
+
+
+
+
+
+
+
+
+ {
+
+ users.length > 0 ? (
+
+ users.map((user) => (
+
+
+
+ ))
+
+ ) : (
+
+
+
+ |
+
+ No users found
+
+ |
+
+
+
+ )
+
+ }
+
+
+
+
+
+ );
+
+};
+
+export default UserTable;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/userManagement/UserTableRow.jsx b/client/src/presentation/components/admin/userManagement/UserTableRow.jsx
new file mode 100644
index 0000000000..a29d2f9bff
--- /dev/null
+++ b/client/src/presentation/components/admin/userManagement/UserTableRow.jsx
@@ -0,0 +1,135 @@
+import { Eye, Ban, CheckCircle } from "lucide-react";
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+
+import {
+ TableCell,
+ TableRow,
+} from "@/components/ui/table"
+
+const UserTableRow = ({
+ user,
+ onView,
+ onBlock,
+}) => {
+ console.log(user)
+
+ const isBlocked = user.isBlocked;
+
+
+ return (
+
+
+
+
+
+ {user.id.slice(0,8)}
+
+
+
+
+
+ {user.fullName}
+
+
+
+
+
+ {user.email}
+
+
+
+
+
+ {user.phone}
+
+
+
+
+
+
+ {isBlocked ? "Blocked" : "Active"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default UserTableRow;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/vendorManagement/VendorFilters.jsx b/client/src/presentation/components/admin/vendorManagement/VendorFilters.jsx
new file mode 100644
index 0000000000..7d79ef5227
--- /dev/null
+++ b/client/src/presentation/components/admin/vendorManagement/VendorFilters.jsx
@@ -0,0 +1,62 @@
+import SearchBar from "../common/SearchBar";
+import FilterTabs from "../common/FilterTabs";
+
+const VendorFilters = ({
+ search,
+ onSearchChange,
+ status,
+ onStatusChange,
+}) => {
+
+ const tabs = [
+
+ {
+ label: "All",
+ value: "all",
+ },
+
+ {
+ label: "Pending",
+ value: "pending",
+ },
+
+ {
+ label: "Approved",
+ value: "approved",
+ },
+
+ {
+ label: "Rejected",
+ value: "rejected",
+ },
+
+ {
+ label: "Blocked",
+ value: "blocked",
+ },
+
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default VendorFilters;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/vendorManagement/VendorTable.jsx b/client/src/presentation/components/admin/vendorManagement/VendorTable.jsx
new file mode 100644
index 0000000000..8c8ade6e7c
--- /dev/null
+++ b/client/src/presentation/components/admin/vendorManagement/VendorTable.jsx
@@ -0,0 +1,107 @@
+import VendorTableRow from "./VendorTableRow";
+
+import {
+ Table,
+ TableHeader,
+ TableBody,
+ TableHead,
+ TableRow,
+} from "@/components/ui/table";
+
+const VendorTable = ({
+ vendors,
+ onView,
+ onApprove,
+ onReject,
+ onBlock,
+ onUnblock,
+}) => {
+
+ return (
+
+
+
+
+
+
+
+
+
+ Vendor ID
+
+ Company
+
+ Owner
+
+ Email
+
+ Phone
+
+ Status
+
+
+
+ Actions
+
+
+
+
+
+
+
+
+
+ {
+
+ vendors.length > 0 ? (
+
+ vendors.map((vendor) => (
+
+
+
+ ))
+
+ ) : (
+
+
+
+ |
+ No vendors found.
+ |
+
+
+
+ )
+
+ }
+
+
+
+
+
+
+
+ );
+
+};
+
+export default VendorTable;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/vendorManagement/VendorTableRow.jsx b/client/src/presentation/components/admin/vendorManagement/VendorTableRow.jsx
new file mode 100644
index 0000000000..4c92ca994e
--- /dev/null
+++ b/client/src/presentation/components/admin/vendorManagement/VendorTableRow.jsx
@@ -0,0 +1,233 @@
+import {
+ Eye,
+ CheckCircle,
+ XCircle,
+ Ban,
+ ShieldCheck,
+} from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+
+import {
+ TableCell,
+ TableRow,
+} from "@/components/ui/table";
+
+const VendorTableRow = ({
+ vendor,
+ onView,
+ onApprove,
+ onReject,
+ onBlock,
+ onUnblock,
+}) => {
+
+ const isBlocked = vendor.isBlocked;
+
+ const approvalStatus = vendor.approvalStatus;
+
+ // Display Status
+ let displayStatus = "";
+
+ if (isBlocked) {
+
+ displayStatus = "Blocked";
+
+ } else {
+
+ displayStatus =
+ approvalStatus.charAt(0) +
+ approvalStatus.slice(1).toLowerCase();
+
+ }
+
+ // Badge Variant
+ let badgeVariant = "secondary";
+
+ if (isBlocked) {
+
+ badgeVariant = "destructive";
+
+ } else if (approvalStatus === "APPROVED") {
+
+ badgeVariant = "default";
+
+ } else if (approvalStatus === "REJECTED") {
+
+ badgeVariant = "outline";
+
+ }
+
+ return (
+
+
+
+ {/* Vendor ID */}
+
+
+
+ {vendor.id.slice(-6)}
+
+
+
+ {/* Company */}
+
+
+
+ {vendor.companyName}
+
+
+
+ {/* Owner */}
+
+
+
+ {vendor.fullName}
+
+
+
+ {/* Email */}
+
+
+
+ {vendor.email}
+
+
+
+ {/* Phone */}
+
+
+
+ {vendor.phone}
+
+
+
+ {/* Status */}
+
+
+
+
+ {displayStatus}
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+ {/* View */}
+
+
+
+ {/* Pending */}
+
+ {
+ !isBlocked &&
+ approvalStatus === "PENDING" && (
+
+ <>
+
+
+
+
+ >
+
+ )
+ }
+
+ {/* Approved */}
+
+ {
+ !isBlocked &&
+ approvalStatus === "APPROVED" && (
+
+
+
+ )
+ }
+
+ {/* Blocked */}
+
+ {
+ isBlocked && (
+
+
+
+ )
+ }
+
+
+
+
+
+
+
+ );
+
+};
+
+export default VendorTableRow;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/venueManagement/VenueFilters.jsx b/client/src/presentation/components/admin/venueManagement/VenueFilters.jsx
new file mode 100644
index 0000000000..a654aee935
--- /dev/null
+++ b/client/src/presentation/components/admin/venueManagement/VenueFilters.jsx
@@ -0,0 +1,128 @@
+import SearchBar from "../common/SearchBar";
+import FilterTabs from "../common/FilterTabs";
+import FilterDropdown from "../common/FilterDropdown";
+
+const VenueFilters = ({
+ search,
+ onSearchChange,
+ status,
+ onStatusChange,
+ category,
+ onCategoryChange,
+}) => {
+
+ const tabs = [
+ {
+ label: "All",
+ value: "all",
+ },
+ {
+ label: "Pending",
+ value: "pending",
+ },
+ {
+ label: "Approved",
+ value: "approved",
+ },
+ {
+ label: "Rejected",
+ value: "rejected",
+ },
+ {
+ label: "Blocked",
+ value: "blocked",
+ },
+ ];
+
+const categoryOptions = [
+ {
+ label: "All Categories",
+ value: "",
+ },
+ {
+ label: "Beach Side",
+ value: "Beach Side",
+ },
+ {
+ label: "Conference Hall",
+ value: "Conference Hall",
+ },
+ {
+ label: "Auditorium",
+ value: "Auditorium",
+ },
+ {
+ label: "Banquet Hall",
+ value: "Banquet Hall",
+ },
+ {
+ label: "Party Hall",
+ value: "Party Hall",
+ },
+ {
+ label: "Rooftop",
+ value: "Rooftop",
+ },
+ {
+ label: "Cafe",
+ value: "Cafe",
+ },
+ {
+ label: "Farm House",
+ value: "Farm House",
+ },
+ {
+ label: "Palace",
+ value: "Palace",
+ },
+ {
+ label: "Studio",
+ value: "Studio",
+ },
+ {
+ label: "Outdoor Garden",
+ value: "Outdoor Garden",
+ },
+ {
+ label: "Resort",
+ value: "Resort",
+ },
+ {
+ label: "Hotel",
+ value: "Hotel",
+ },
+];
+
+ return (
+
+
+
+ );
+};
+
+export default VenueFilters;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/venueManagement/VenueTable.jsx b/client/src/presentation/components/admin/venueManagement/VenueTable.jsx
new file mode 100644
index 0000000000..7edc5a7d5b
--- /dev/null
+++ b/client/src/presentation/components/admin/venueManagement/VenueTable.jsx
@@ -0,0 +1,99 @@
+import VenueTableRow from "./VenueTableRow";
+
+import {
+ Table,
+ TableHeader,
+ TableBody,
+ TableHead,
+ TableRow,
+} from "@/components/ui/table";
+
+const VenueTable = ({
+ venues,
+ onView,
+ onApprove,
+ onReject,
+ onBlock,
+ onUnblock,
+}) => {
+
+ return (
+
+
+
+
+
+
+
+
+
+ Venue ID
+ Venue Name
+ Category
+ City
+ Price/Day
+ Status
+
+ Actions
+
+
+
+
+
+
+
+
+ {
+
+ venues.length > 0 ? (
+
+ venues.map((venue) => (
+
+
+
+ ))
+
+ ) : (
+
+
+
+ |
+ No venues found.
+ |
+
+
+
+ )
+
+ }
+
+
+
+
+
+
+
+ );
+
+};
+
+export default VenueTable;
\ No newline at end of file
diff --git a/client/src/presentation/components/admin/venueManagement/VenueTableRow.jsx b/client/src/presentation/components/admin/venueManagement/VenueTableRow.jsx
new file mode 100644
index 0000000000..2caa133285
--- /dev/null
+++ b/client/src/presentation/components/admin/venueManagement/VenueTableRow.jsx
@@ -0,0 +1,233 @@
+import {
+ Eye,
+ CheckCircle,
+ XCircle,
+ Ban,
+ ShieldCheck,
+} from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+
+import {
+ TableCell,
+ TableRow,
+} from "@/components/ui/table";
+
+const VenueTableRow = ({
+ venue,
+ onView,
+ onApprove,
+ onReject,
+ onBlock,
+ onUnblock,
+}) => {
+
+ const isBlocked = venue.isBlocked;
+
+ const approvalStatus = venue.approvalStatus;
+
+ // Display Status
+ let displayStatus = "";
+
+ if (isBlocked) {
+
+ displayStatus = "Blocked";
+
+ } else {
+
+ displayStatus =
+ approvalStatus.charAt(0) +
+ approvalStatus.slice(1).toLowerCase();
+
+ }
+
+ //Badge Variant
+ let badgeVariant = "secondary";
+
+ if (isBlocked) {
+
+ badgeVariant = "destructive";
+
+ } else if (approvalStatus === "APPROVED") {
+
+ badgeVariant = "default";
+
+ } else if (approvalStatus === "REJECTED") {
+
+ badgeVariant = "outline";
+
+ }
+
+ return (
+
+
+
+ {/* Venue ID */}
+
+
+
+ {venue.id.slice(-6)}
+
+
+
+ {/* name */}
+
+
+
+ {venue.name}
+
+
+
+ {/* category */}
+
+
+
+ {venue.category}
+
+
+
+ {/* Address */}
+
+
+
+ {venue.address?.city}
+
+
+
+ {/* Price */}
+
+
+
+ ₹{venue.pricePerDay.toLocaleString()}
+
+
+
+ {/* Status */}
+
+
+
+
+ {displayStatus}
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+ {/* View */}
+
+
+
+ {/* Pending */}
+
+ {
+ !isBlocked &&
+ approvalStatus === "PENDING" && (
+
+ <>
+
+
+
+
+ >
+
+ )
+ }
+
+ {/* Approved */}
+
+ {
+ !isBlocked &&
+ approvalStatus === "ACTIVE" && (
+
+
+
+ )
+ }
+
+ {/* Blocked */}
+
+ {
+ isBlocked && (
+
+
+
+ )
+ }
+
+
+
+
+
+
+
+ );
+
+};
+
+export default VenueTableRow;
\ No newline at end of file
diff --git a/client/src/presentation/components/auth/.gitkeep b/client/src/presentation/components/auth/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/components/auth/AuthBanner.jsx b/client/src/presentation/components/auth/AuthBanner.jsx
new file mode 100644
index 0000000000..f0901d0674
--- /dev/null
+++ b/client/src/presentation/components/auth/AuthBanner.jsx
@@ -0,0 +1,40 @@
+const features = [
+ "5,000+ Verified Venues",
+ "Secure Booking Process",
+ "24/7 Customer Support"
+]
+
+const AuthBanner = () => {
+ return (
+
+
+ {/* Background image overlay */}
+
+
+
+
+ Find Your Perfect Event Venue
+
+
+
+ Access thousands of verified venues across India. Book with confidence for your special occasions.
+
+
+
+ {features.map((feature) => (
+ -
+
+ {feature}
+
+ ))}
+
+
+
+ )
+}
+
+export default AuthBanner
diff --git a/client/src/presentation/components/auth/ForgotPasswordForm.jsx b/client/src/presentation/components/auth/ForgotPasswordForm.jsx
new file mode 100644
index 0000000000..fe9366ca5c
--- /dev/null
+++ b/client/src/presentation/components/auth/ForgotPasswordForm.jsx
@@ -0,0 +1,140 @@
+import { useState } from 'react'
+import { Link, useLocation } from 'react-router-dom'
+import { Mail, ArrowLeft } from 'lucide-react'
+import api from '@/lib/axios'
+// import { ROLES } from "@/constants/Roles";
+import { API_ROUTES } from "@/constants/apiRoutes"
+
+import { ROUTES } from '@/constants/routes'
+
+const ForgotPasswordForm = () => {
+ const location = useLocation()
+ const [email, setEmail] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [success, setSuccess] = useState(false)
+ const role = location.state?.role
+
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+ setLoading(true)
+ setError(null)
+
+ try {
+ await api.post(API_ROUTES.AUTH.FORGOT_PASSWORD(role), { email })
+ setSuccess(true)
+ } catch (err) {
+ console.log(err)
+ setError(err.response?.data?.message || 'Something went wrong. Please try again.')
+ } finally {
+
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ {/* Logo */}
+
+
+
+ 🏛️
+
+
Book My Venue
+
+
+
+ {/* Card */}
+
+
+
+ {success ? (
+ /* Success state */
+
+
+
Check your inbox
+
+ We've sent a password reset link to {email}
+
+
+
+ Back to Sign In
+
+
+ ) : (
+ /* Form state */
+ <>
+
Forgot Password?
+
+ No worries! Enter your email and we'll send you a reset link.
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+ Back to Sign In
+
+
+ >
+ )}
+
+
+
+ {/* Help footer */}
+
+
+ )
+}
+
+export default ForgotPasswordForm
diff --git a/client/src/presentation/components/auth/LoginForm.jsx b/client/src/presentation/components/auth/LoginForm.jsx
new file mode 100644
index 0000000000..0ead8c3a47
--- /dev/null
+++ b/client/src/presentation/components/auth/LoginForm.jsx
@@ -0,0 +1,201 @@
+import { useState } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { useNavigate, Link, useLocation } from 'react-router-dom'
+import { User, Mail, Lock, Eye, EyeOff } from 'lucide-react'
+import { login } from "@/redux/slices/AuthSlice"
+import { ROUTES } from "@/constants/routes"
+import { ROLES } from '@/constants/Roles'
+import toast from "react-hot-toast"
+
+const LoginForm = () => {
+ const dispatch = useDispatch()
+ const navigate = useNavigate()
+ const location = useLocation()
+
+ const initialRole = location.state?.role || ROLES.USER
+ const initialEmail = location.state?.email || ""
+
+ const [role, setRole] = useState(initialRole)
+ const [formData, setFormData] = useState({ email: initialEmail, password: "" })
+ const [showPassword, setShowPassword] = useState(false)
+
+ const { loading } = useSelector((state) => state.auth)
+
+ const handleChange = (e) => {
+ setFormData({ ...formData, [e.target.name]: e.target.value })
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ try {
+ // 1. Dispatch login action passing selected role (USER or VENDOR)
+ const result = await dispatch(
+ login({
+ role,
+ data: formData,
+ })
+ ).unwrap()
+
+ console.log("Login Result:", result);
+
+ toast.success("Login successful!")
+
+
+ const userRole = result?.data?.user?.role || result?.data?.vendor?.role || result?.user?.role || result?.role || role
+
+
+ if (userRole.toLowerCase() === ROLES.VENDOR.toLowerCase()) {
+ navigate(ROUTES.VENDOR.DASHBOARD)
+
+ } else if (userRole === "customer") {
+
+ navigate(ROUTES.PUBLIC.HOME)
+ }
+
+ } catch (err) {
+ console.error("Login failed:", err)
+ toast.error(err?.message || err || "Invalid email or password")
+ }
+ }
+
+ return (
+
+
Welcome Back
+
Sign in to your account to continue
+
+ {/* Role Selector Buttons */}
+
+
+
+
+
+ {/* Error Message Banner */}
+
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+ )
+}
+
+export default LoginForm
\ No newline at end of file
diff --git a/client/src/presentation/components/auth/RegisterForm.jsx b/client/src/presentation/components/auth/RegisterForm.jsx
new file mode 100644
index 0000000000..32485b5956
--- /dev/null
+++ b/client/src/presentation/components/auth/RegisterForm.jsx
@@ -0,0 +1,176 @@
+import { useState } from 'react'
+import { useNavigate, Link } from 'react-router-dom'
+import { User, Mail, Phone, Lock, Eye, EyeOff } from 'lucide-react'
+import { useDispatch, useSelector } from "react-redux"
+import toast from "react-hot-toast"
+
+import { ROUTES } from '@/constants/routes'
+import { registerSchema } from '@/lib/validation/authValidation'
+import { ROLES } from '@/constants/Roles'
+import { registerUser } from "@/redux/slices/AuthSlice"
+
+const RegisterForm = () => {
+ const navigate = useNavigate()
+ const dispatch = useDispatch()
+
+ const [formData, setFormData] = useState({
+ fullName: '',
+ email: '',
+ phone: '',
+ password: '',
+ confirmPassword: '',
+ })
+ const [role, setRole] = useState(ROLES.USER)
+ const [showPassword, setShowPassword] = useState(false)
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false)
+
+ const { loading } = useSelector((state) => state.auth)
+
+ const handleChange = (e) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value,
+ })
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+
+ if (formData.password !== formData.confirmPassword) {
+ toast.error('Passwords do not match')
+ return
+ }
+
+ // 2. Schema validation (Zod)
+ const validation = registerSchema.safeParse({
+ ...formData,
+ role,
+ })
+
+ if (!validation.success) {
+ const errorMessage = validation.error.issues[0]?.message || 'Validation failed'
+ toast.error(errorMessage)
+ return
+ }
+
+
+ try {
+ const result = await dispatch(
+ registerUser({
+ role,
+ userData: validation.data,
+ })
+ ).unwrap()
+
+ console.log("success")
+ toast.success(result?.message || "Account created successfully! Please verify your OTP.")
+
+ navigate(ROUTES.PUBLIC.VERIFY_OTP, {
+ state: {
+ email: formData.email,
+ role,
+
+ },
+ })
+ } catch (err) {
+ const apiError = err?.message || err || "Registration failed"
+ toast.error(apiError)
+ }
+
+ }
+
+ return (
+
+
Create Account
+
Join thousands of happy customers
+
+ {/* Role toggle */}
+
+
+
+
+
+
+
+
+ Already have an account?{' '}
+ Sign in
+
+
+ )
+}
+
+export default RegisterForm
\ No newline at end of file
diff --git a/client/src/presentation/components/auth/ResetPasswordForm.jsx b/client/src/presentation/components/auth/ResetPasswordForm.jsx
new file mode 100644
index 0000000000..8b3643aeec
--- /dev/null
+++ b/client/src/presentation/components/auth/ResetPasswordForm.jsx
@@ -0,0 +1,114 @@
+import { useState } from "react";
+import { useDispatch } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import toast from "react-hot-toast";
+import { ROUTES } from "@/constants/routes";
+import { ROLES } from "@/constants/Roles";
+import {useSearchParams } from "react-router-dom";
+import { resetPassword } from "@/redux/slices/AuthSlice";
+
+const ResetPasswordForm = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const [searchParams] = useSearchParams();
+ const token = searchParams.get("token");
+ const role = searchParams.get("role");
+ console.timeLog("role", role)
+ const originalRole = role === "customer" ? ROLES.USER : ROLES.VENDOR
+ console.log("original role", originalRole)
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+
+ if (!token || !originalRole) {
+ toast.error("Invalid reset password link.");
+ navigate(ROUTES.PUBLIC.LOGIN);
+ return;
+}
+
+
+
+ if (password !== confirmPassword) {
+ toast.error("Passwords do not match.");
+ return;
+ }
+
+ try {
+ setLoading(true);
+
+ await dispatch(
+ resetPassword({
+ role: originalRole,
+ token,
+ password,
+ confirmPassword,
+
+ })
+ ).unwrap();
+
+ toast.success("Password reset successfully.");
+
+ if (role === ROLES.ADMIN) {
+ navigate(ROUTES.ADMIN.LOGIN);
+ } else if (role === ROLES.VENDOR) {
+ navigate(ROUTES.PUBLIC.LOGIN);
+ } else {
+ navigate(ROUTES.PUBLIC.LOGIN);
+ }
+
+ } catch (error) {
+ toast.error(error || "Failed to reset password.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default ResetPasswordForm;
\ No newline at end of file
diff --git a/client/src/presentation/components/auth/VerifyOtpForm.jsx b/client/src/presentation/components/auth/VerifyOtpForm.jsx
new file mode 100644
index 0000000000..5fa88d0c29
--- /dev/null
+++ b/client/src/presentation/components/auth/VerifyOtpForm.jsx
@@ -0,0 +1,179 @@
+import { useState, useEffect } from "react";
+import { Mail } from "lucide-react";
+import { useNavigate, useLocation } from "react-router-dom";
+import { useDispatch, useSelector } from "react-redux";
+import toast from "react-hot-toast"
+import { resendOtp, verifyOtp } from "@/redux/slices/AuthSlice";
+
+import { ROUTES } from "@/constants/routes";
+
+const VerifyOtpForm = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const dispatch = useDispatch();
+
+ const email = location.state?.email || "";
+ const role = location.state?.role;
+
+ const { loading, error } = useSelector((state) => state.auth);
+
+ const [otpCode, setOtpCode] = useState("");
+ const [resending, setResending] = useState(false);
+
+ const [countdown, setCountdown] = useState(30);
+ const [canResend, setCanResend] = useState(false);
+
+ useEffect(() => {
+ if (countdown <= 0) {
+ setCanResend(true);
+ return;
+ }
+
+ const timer = setTimeout(() => {
+ setCountdown((prev) => prev - 1);
+ }, 1000);
+
+ return () => clearTimeout(timer);
+ }, [countdown]);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!/^\d{6}$/.test(otpCode)) {
+ toast.error("Please enter a valid 6-digit OTP");
+ return;
+ }
+
+ try {
+
+ const response = await dispatch(
+ verifyOtp({
+ role,
+ email,
+ otpCode,
+ })
+ ).unwrap();
+
+ toast.success(response?.message || "OTP verified successfully!");
+
+ navigate(ROUTES.PUBLIC.LOGIN, {
+ state: {
+ role,
+ email,
+ },
+ });
+ } catch (err) {
+ // err contains the payload passed via rejectWithValue in your slice
+ const errorMessage = typeof err === "string" ? err : err?.message || "OTP verification failed";
+ toast.error(errorMessage);
+ }
+ };
+
+ const handleResendOtp = async () => {
+ if (!email || !role) {
+ toast.error("Missing email or role. Please try logging in again.");
+ return;
+ }
+
+ setResending(true);
+
+ try {
+ const response = await dispatch(
+ resendOtp({
+ role,
+ email,
+ })
+ ).unwrap();
+
+ toast.success(response?.message || "New OTP sent to your email!");
+
+ setCountdown(30);
+ setCanResend(false);
+ setOtpCode("");
+ } catch (err) {
+ const errorMessage = typeof err === "string" ? err : err?.message || "Failed to resend OTP";
+ toast.error(errorMessage);
+ } finally {
+ setResending(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+ Verify Your Email
+
+
+
+ We sent a 6-digit OTP to {email}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ {canResend ? (
+
+ ) : (
+
Resend OTP in {countdown}s
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default VerifyOtpForm;
\ No newline at end of file
diff --git a/client/src/presentation/components/common/Footer.jsx b/client/src/presentation/components/common/Footer.jsx
new file mode 100644
index 0000000000..90b955e460
--- /dev/null
+++ b/client/src/presentation/components/common/Footer.jsx
@@ -0,0 +1,89 @@
+
+const Footer = () => {
+ return (
+
+ )
+}
+
+export default Footer
diff --git a/client/src/presentation/components/common/Header.jsx b/client/src/presentation/components/common/Header.jsx
new file mode 100644
index 0000000000..5c4a971d0d
--- /dev/null
+++ b/client/src/presentation/components/common/Header.jsx
@@ -0,0 +1,120 @@
+import { Heart, User, ChevronDown, LogOut } from "lucide-react";
+import { useState } from "react";
+import Logo from "@/assets/images/logo.jpeg";
+import { useNavigate } from "react-router-dom";
+import { ROUTES } from "@/constants/routes";
+import { useSelector, useDispatch } from "react-redux";
+import { logout } from "@/redux/slices/AuthSlice";
+import { ROLES } from "@/constants/Roles";
+import toast from "react-hot-toast";
+// import { logout } from '@/redux/slices/authSlice'
+
+const Header = () => {
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ const { user } = useSelector((state) => state.auth);
+
+ const [open, setOpen] = useState(false);
+ const role = user?.role === "customer" ? ROLES.USER : ROLES.VENDOR;
+
+ const handleLogout = async () => {
+ try {
+ await dispatch(logout({ role })).unwrap();
+ toast.success(`${role} logged out successfully`);
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ return (
+
+
+
+
+

+
+
+
Book My Venue
+
+
+
+ {user && (
+
+ )}
+
+ {!user ? (
+ <>
+
+
+
+ >
+ ) : (
+
+
+
+ {open && (
+
+
+
+
+
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default Header;
diff --git a/client/src/presentation/components/common/Pagination.jsx b/client/src/presentation/components/common/Pagination.jsx
new file mode 100644
index 0000000000..36d00b7b38
--- /dev/null
+++ b/client/src/presentation/components/common/Pagination.jsx
@@ -0,0 +1,72 @@
+import { ChevronLeft, ChevronRight } from "lucide-react";
+
+const Pagination = ({
+ currentPage,
+ totalPages,
+ onPageChange,
+}) => {
+
+ if (totalPages <= 1) return null;
+
+ const pages = [];
+
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+
+ return (
+
+
+
+ {/* Previous Button */}
+
+
+
+ {/* Page Numbers */}
+
+
+
+ {pages.map((page) => (
+
+
+
+ ))}
+
+
+
+ {/* Next Button */}
+
+
+
+
+
+ );
+
+};
+
+export default Pagination;
\ No newline at end of file
diff --git a/client/src/presentation/components/common/VenueCard.jsx b/client/src/presentation/components/common/VenueCard.jsx
new file mode 100644
index 0000000000..90e6be06d9
--- /dev/null
+++ b/client/src/presentation/components/common/VenueCard.jsx
@@ -0,0 +1,126 @@
+import { useNavigate } from "react-router-dom";
+
+export default function VenueCard({
+ venue,
+ variant = "default",
+ isWishlisted = false,
+ onWishlistToggle,
+}) {
+ const navigate = useNavigate();
+
+ const handleVenueClick = () => {
+ navigate(`/user/venue/${venue.id}`);
+ };
+
+ const handleWishlistClick = (event) => {
+ event.stopPropagation();
+ onWishlistToggle?.(venue.id);
+ };
+
+ return (
+
+ {/* Image */}
+
+

+
+
+ {venue.category}
+
+
+
+
+
+
+ {/* Rating */}
+
+ ⭐
+ {venue.rating || 0}
+
+
+ {/* Name */}
+
{venue.name}
+
+ {/* Location */}
+
+ 📍 {venue.address?.city}, {venue.address?.state}
+
+
+ {/* Description */}
+ {variant === "default" && (
+
+ {venue.description}
+
+ )}
+
+ {/* Price + Capacity */}
+
+
+
Starting from
+
+
+ ₹{venue.pricePerDay}
+ /day
+
+
+
+
+
Capacity
+
+
+ {venue.seatingCapacity} Seating
+
+
+
+ {venue.standingCapacity} Standing
+
+
+
+
+ {/* Browse Extra Details */}
+ {variant === "default" && (
+ <>
+
+
Available
+
+
+ {venue.availabilityRules?.openTime}-
+ {venue.availabilityRules?.closeTime}
+
+
+
+
+ {venue.amenities?.slice(0, 3).map((item, index) => (
+
+ {item}
+
+ ))}
+
+ >
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/modal/.gitkeep b/client/src/presentation/components/modal/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/components/modal/ConfirmationModal.jsx b/client/src/presentation/components/modal/ConfirmationModal.jsx
new file mode 100644
index 0000000000..37bf6aa230
--- /dev/null
+++ b/client/src/presentation/components/modal/ConfirmationModal.jsx
@@ -0,0 +1,46 @@
+import { Button } from "@/components/ui/button"
+import Modal from "./Modal";
+
+const ConfirmationModal = ({
+ isOpen,
+ onClose,
+ onConfirm,
+ title,
+ message,
+ confirmText = "Confirm",
+ cancelText = "Cancel",
+ confirmVariant = "destructive",
+}) => {
+ return (
+
+
+
+ {message}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ConfirmationModal;
\ No newline at end of file
diff --git a/client/src/presentation/components/modal/Modal.jsx b/client/src/presentation/components/modal/Modal.jsx
new file mode 100644
index 0000000000..7ff64d06fb
--- /dev/null
+++ b/client/src/presentation/components/modal/Modal.jsx
@@ -0,0 +1,44 @@
+const Modal = ({
+ isOpen,
+ onClose,
+ title,
+ children,
+}) => {
+
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
+
+ {/* Header */}
+
+
+
+
{title}
+
+
+
+
+
+ {/* Body */}
+
+
+
+ {children}
+
+
+
+
+
+
+
+ );
+
+};
+
+export default Modal;
\ No newline at end of file
diff --git a/client/src/presentation/components/modal/RejectReasonModal.jsx b/client/src/presentation/components/modal/RejectReasonModal.jsx
new file mode 100644
index 0000000000..9f80dee905
--- /dev/null
+++ b/client/src/presentation/components/modal/RejectReasonModal.jsx
@@ -0,0 +1,98 @@
+import { useState, useEffect } from "react";
+import { rejectReasonSchema } from "@/lib/validation/adminVendorValidation";
+
+const RejectReasonModal = ({
+ isOpen,
+ onClose,
+ onSubmit,
+ title = "Reject",
+}) => {
+
+ const [reason, setReason] = useState("");
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ if (!isOpen) {
+ setReason("");
+ setError("");
+ }
+ }, [isOpen]);
+
+ if (!isOpen) return null;
+
+ /* const handleReject = () => {
+
+ if (!reason.trim()) {
+ alert("Please enter rejection reason.");
+ return;
+ }
+
+ onSubmit(reason);
+ };*/
+ const handleReject = () => {
+
+ const result = rejectReasonSchema.safeParse({
+ reason,
+ });
+
+ if (!result.success) {
+ setError(result.error.issues[0].message);
+ return;
+ }
+
+ setError("");
+ onSubmit(reason);
+ };
+
+ return (
+
+ );
+};
+
+export default RejectReasonModal;
\ No newline at end of file
diff --git a/client/src/presentation/components/modal/ViewUserModal.jsx b/client/src/presentation/components/modal/ViewUserModal.jsx
new file mode 100644
index 0000000000..4c53c2c8e6
--- /dev/null
+++ b/client/src/presentation/components/modal/ViewUserModal.jsx
@@ -0,0 +1,114 @@
+
+import Modal from "./Modal";
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+
+const ViewUserModal = ({
+ isOpen,
+ onClose,
+ user,
+}) => {
+
+ if (!user) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+ User ID
+
+
+
+ {user.id}
+
+
+
+
+
+ Full Name
+
+
+
+ {user.fullName}
+
+
+
+
+
+ Email
+
+
+
+ {user.email}
+
+
+
+
+
+ Phone
+
+
+
+ {user.phone}
+
+
+
+
+
+ Status
+
+
+
+ {user.isBlocked ? "Blocked" : "Active"}
+
+
+
+
+
+ Joined On
+
+
+
+ {new Date(user.createdAt).toLocaleDateString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+export default ViewUserModal;
\ No newline at end of file
diff --git a/client/src/presentation/components/modal/ViewVendorModal.jsx b/client/src/presentation/components/modal/ViewVendorModal.jsx
new file mode 100644
index 0000000000..4b8059b88b
--- /dev/null
+++ b/client/src/presentation/components/modal/ViewVendorModal.jsx
@@ -0,0 +1,238 @@
+import Modal from "./Modal";
+import { Badge } from "@/components/ui/badge";
+import { UserCircle } from "lucide-react";
+
+const ViewVendorModal = ({
+ isOpen,
+ onClose,
+ vendor,
+}) => {
+
+ if (!vendor) return null;
+
+ return (
+
+
+
+
+
+ {/* Profile */}
+
+
+
+ {vendor.profileImage?.url ? (
+

+ ) : (
+
+
+
+ )}
+
+
+
+
+ {vendor.fullName}
+
+
+
+
+
+ {vendor.companyName}
+
+
+
+
+
+
+
+ {/* Basic Information */}
+
+
+
+
+
+
+
+ Email
+
+
+
+
{vendor.email}
+
+
+
+
+
+
+
+ Phone
+
+
+
+
{vendor.phone}
+
+
+
+
+
+ {/* Bio */}
+
+
+
+
+
+ Bio
+
+
+
+
+
+ {vendor.bio || "-"}
+
+
+
+
+
+ {/* Address */}
+
+
+
+
+
+ Address
+
+
+
+
+
+ {vendor.address?.addressLine1}
+
+
+
+
+
+ {vendor.address?.city},{" "}
+ {vendor.address?.state}
+
+
+
+
+
+ {vendor.address?.pincode}
+
+
+
+
+
+ {/* Status */}
+
+
+
+
+
+
+
+ Approval Status
+
+
+
+
+
+ {vendor.approvalStatus}
+
+
+
+
+
+
+
+
+
+ Account Status
+
+
+
+
+
+ {
+ vendor.isBlocked
+ ? "Blocked"
+ : "Active"
+ }
+
+
+
+
+
+
+
+ {/* Joined */}
+
+
+
+
+
+ Joined On
+
+
+
+
+
+ {new Date(
+ vendor.createdAt
+ ).toLocaleDateString()}
+
+
+
+
+
+ {/* Rejection Reason */}
+
+ {
+
+ vendor.approvalStatus === "REJECTED" &&
+ vendor.rejectionReason && (
+
+
+
+
+
+ Rejection Reason
+
+
+
+
+
+ {vendor.rejectionReason}
+
+
+
+
+
+ )
+
+ }
+
+
+
+
+
+ );
+
+};
+
+export default ViewVendorModal;
\ No newline at end of file
diff --git a/client/src/presentation/components/user/.gitkeep b/client/src/presentation/components/user/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/components/user/BookingHistoryCard.jsx b/client/src/presentation/components/user/BookingHistoryCard.jsx
new file mode 100644
index 0000000000..f69320afbf
--- /dev/null
+++ b/client/src/presentation/components/user/BookingHistoryCard.jsx
@@ -0,0 +1,75 @@
+import { ChevronRight, Calendar, Users, MapPin } from "lucide-react";
+import { useNavigate } from "react-router-dom";
+
+const statusStyles = {
+ confirmed: "bg-green-100 text-green-700",
+ cancelled: "bg-red-100 text-red-600",
+ pending: "bg-yellow-100 text-yellow-700",
+};
+
+const BookingHistoryCard = ({ booking }) => {
+ const navigate = useNavigate();
+
+ const formatDate = (date) =>
+ new Date(date).toLocaleDateString("en-IN", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ });
+
+ return (
+
navigate(`/user/bookings/${booking.id}`)}
+ className="cursor-pointer border rounded-2xl p-4 flex justify-between hover:shadow-lg transition"
+ >
+
+

+
+
+
{booking.venueId.name}
+
+
+
+ {booking.venueId.address.city}, {booking.venueId.address.state}
+
+
+
+
+
+ {formatDate(booking.bookingDate)}
+
+
+
+
+ {booking.guestCount} Guests
+
+
+
+
+
+
+
+
+ {booking.status.charAt(0).toUpperCase() + booking.status.slice(1)}
+
+
+
+ ₹{booking.totalAmount.toLocaleString()}
+
+
+
+
+
+
+ );
+};
+
+export default BookingHistoryCard;
diff --git a/client/src/presentation/components/user/ChangePasswordForm.jsx b/client/src/presentation/components/user/ChangePasswordForm.jsx
new file mode 100644
index 0000000000..14422f6c76
--- /dev/null
+++ b/client/src/presentation/components/user/ChangePasswordForm.jsx
@@ -0,0 +1,120 @@
+import { useState } from "react";
+import { Eye, EyeOff, Lock } from "lucide-react";
+import { useDispatch, useSelector } from "react-redux";
+import { changePassword } from "@/redux/slices/UserProfileSlice";
+import { toast } from "react-hot-toast";
+import PasswordStrength from "./PasswordStrength";
+
+const PasswordField = ({ label, name, value, show, setShow, onChange }) => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const ChangePasswordForm = () => {
+ const [showCurrent, setShowCurrent] = useState(false);
+ const [showNew, setShowNew] = useState(false);
+ const [showConfirm, setShowConfirm] = useState(false);
+
+ const dispatch = useDispatch();
+
+ const { loading } = useSelector((state) => state.userProfile);
+
+ const [formData, setFormData] = useState({
+ currentPassword: "",
+ newPassword: "",
+ confirmPassword: "",
+ });
+
+ const handleChange = (e) => {
+ setFormData((prev) => ({
+ ...prev,
+ [e.target.name]: e.target.value,
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ try {
+ const message = await dispatch(changePassword(formData)).unwrap();
+
+ toast.success(message);
+
+ setFormData({
+ currentPassword: "",
+ newPassword: "",
+ confirmPassword: "",
+ });
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default ChangePasswordForm;
diff --git a/client/src/presentation/components/user/PasswordRequirements.jsx b/client/src/presentation/components/user/PasswordRequirements.jsx
new file mode 100644
index 0000000000..b51e9fb261
--- /dev/null
+++ b/client/src/presentation/components/user/PasswordRequirements.jsx
@@ -0,0 +1,71 @@
+import { CheckCircle, ShieldCheck } from "lucide-react";
+
+const requirements = [
+ "At least 8 characters",
+ "One uppercase letter",
+ "One lowercase letter",
+ "One number",
+ "One special character",
+];
+
+const PasswordRequirements = () => {
+ return (
+
+
+
+
+
+ Password Requirements
+
+
+
+ Your password must contain:
+
+
+
+ {requirements.map((item) => (
+
+
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ Keep Your Account Safe
+
+
+
+ Choose a strong password that you don't use on
+ other websites.
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PasswordRequirements;
\ No newline at end of file
diff --git a/client/src/presentation/components/user/PasswordStrength.jsx b/client/src/presentation/components/user/PasswordStrength.jsx
new file mode 100644
index 0000000000..db6bf44ee1
--- /dev/null
+++ b/client/src/presentation/components/user/PasswordStrength.jsx
@@ -0,0 +1,56 @@
+const PasswordStrength = ({ password }) => {
+ if (!password) return null;
+
+ const checks = {
+ length: password.length >= 8,
+ uppercase: /[A-Z]/.test(password),
+ lowercase: /[a-z]/.test(password),
+ number: /\d/.test(password),
+ special: /[@$!%*?&]/.test(password),
+ };
+
+ const passed = Object.values(checks).filter(Boolean).length;
+
+ let strength;
+ let color;
+
+ if (passed <= 2) {
+ strength = "Weak";
+ color = "bg-red-500";
+ } else if (passed <= 4) {
+ strength = "Medium";
+ color = "bg-yellow-500";
+ } else {
+ strength = "Strong";
+ color = "bg-green-500";
+ }
+
+ return (
+
+ {/* Progress Bar */}
+
+
+
+ Password Strength:{" "}
+
+ {strength}
+
+
+
+ );
+ };
+
+ export default PasswordStrength;
\ No newline at end of file
diff --git a/client/src/presentation/components/user/UserEditProfileForm.jsx b/client/src/presentation/components/user/UserEditProfileForm.jsx
new file mode 100644
index 0000000000..07158e0645
--- /dev/null
+++ b/client/src/presentation/components/user/UserEditProfileForm.jsx
@@ -0,0 +1,302 @@
+import { useState } from "react";
+import {
+ updateProfileSchema,
+ RequestEmailChangeOtpSchema,
+ verifyEmailOtpSchema,
+} from "@/lib/validation/userProfileValidation";
+
+const UserEditProfileForm = ({
+ user,
+ onSave,
+ onCancel,
+ onRequestEmailOtp,
+ onVerifyOtp,
+ onResendOtp,
+}) => {
+ const [formData, setFormData] = useState({
+ name: user.fullName || "",
+ email: user.email || "",
+ phone: user.phone || "",
+ });
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ setErrors((prev) => ({
+ ...prev,
+ [name === "name" ? "fullName" : name]: "",
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ const result = updateProfileSchema.safeParse({
+ fullName: formData.name,
+ phone: formData.phone,
+ });
+
+ if (!result.success) {
+ const fieldErrors = {};
+
+ result.error.issues.forEach((issue) => {
+ fieldErrors[issue.path[0]] = issue.message;
+ });
+
+ setErrors(fieldErrors);
+ return;
+ }
+
+ setErrors({});
+
+ try {
+ await onSave(formData);
+ } catch (error) {}
+ };
+
+ const [showEmailSection, setShowEmailSection] = useState(false);
+ const [newEmail, setNewEmail] = useState("");
+ const [otpLoading, setOtpLoading] = useState(false);
+ const [otpSent, setOtpSent] = useState(false);
+
+ const handleSendOtp = async () => {
+ const result = RequestEmailChangeOtpSchema.safeParse({
+ newEmail,
+ });
+
+ if (!result.success) {
+ setErrors((prev) => ({
+ ...prev,
+ newEmail: result.error.issues[0].message,
+ }));
+
+ return;
+ }
+
+ setErrors((prev) => ({
+ ...prev,
+ newEmail: "",
+ }));
+
+ try {
+ setOtpLoading(true);
+
+ await onRequestEmailOtp(newEmail);
+
+ setOtpSent(true);
+ } catch (error) {
+ // Parent already shows toast
+ } finally {
+ setOtpLoading(false);
+ }
+ };
+
+ const [otp, setOtp] = useState("");
+
+ const handleVerifyOtp = async () => {
+ const result = verifyEmailOtpSchema.safeParse({
+ otp,
+ });
+
+ if (!result.success) {
+ setOtpError(result.error.issues[0].message);
+ return;
+ }
+
+ try {
+ await onVerifyOtp(otp);
+
+ setOtpError("");
+ } catch (error) {
+ setOtpError(error);
+ }
+ };
+
+ const handleResendOtp = async () => {
+ try {
+ setOtpLoading(true);
+
+ await onResendOtp();
+ } catch (error) {
+ // Parent already shows the toast
+ } finally {
+ setOtpLoading(false);
+ }
+ };
+
+ const [otpError, setOtpError] = useState("");
+ const [errors, setErrors] = useState({});
+
+ return (
+
+ );
+};
+
+export default UserEditProfileForm;
diff --git a/client/src/presentation/components/user/UserProfileImage.jsx b/client/src/presentation/components/user/UserProfileImage.jsx
new file mode 100644
index 0000000000..774d569073
--- /dev/null
+++ b/client/src/presentation/components/user/UserProfileImage.jsx
@@ -0,0 +1,103 @@
+import { getInitials } from "@/lib/getInitilas";
+import { Pencil, Camera, Trash2 } from "lucide-react";
+import { useRef, useState } from "react";
+
+const UserProfileImage = ({
+ image,
+ name,
+ email,
+ memberSince,
+ onImageChange,
+ onRemoveImage,
+}) => {
+ const [showMenu, setShowMenu] = useState(false);
+
+ const fileInputRef = useRef(null);
+ return (
+
+ {/* Profile Image */}
+
+
+ {image ? (
+

+ ) : (
+ getInitials(name)
+ )}
+
+
+ {/* Pencil Button */}
+
+
+ {showMenu && (
+
+ {/* Upload */}
+
+
+ {/* Remove */}
+ {image && (
+
+ )}
+
+ )}
+
+
{
+ if (e.target.files[0]) {
+ onImageChange(e.target.files[0]);
+ }
+ }}
+ />
+
+
+ {/* Name */}
+
{name}
+
+ {/* Email */}
+
{email}
+
+ {/* Member Since */}
+
+
Member Since
+
+ {memberSince || "-"}
+
+
+
+ );
+};
+
+export default UserProfileImage;
diff --git a/client/src/presentation/components/user/UserProfileInformation.jsx b/client/src/presentation/components/user/UserProfileInformation.jsx
new file mode 100644
index 0000000000..b593e999b3
--- /dev/null
+++ b/client/src/presentation/components/user/UserProfileInformation.jsx
@@ -0,0 +1,94 @@
+import {
+ User,
+ Mail,
+ Phone,
+ CalendarDays,
+ MapPin,
+ Pencil,
+ Settings,
+ } from "lucide-react";
+
+ const UserProfileInformation = ({
+ user,
+ onEditProfile,
+ onAccountSettings,
+ }) => {
+ const profileItems = [
+ {
+ icon: User,
+ label: "Full Name",
+ value: user?.fullName || "-",
+ },
+ {
+ icon: Mail,
+ label: "Email",
+ value: user?.email || "-",
+ },
+ {
+ icon: Phone,
+ label: "Phone",
+ value: user?.phone || "-",
+ },
+ // {
+ // icon: CalendarDays,
+ // label: "Date of Birth",
+ // value: user.dob,
+ // },
+ // {
+ // icon: MapPin,
+ // label: "Location",
+ // value: user.location,
+ // },
+ ];
+
+ return (
+
+
+ Profile Information
+
+
+
+ {profileItems.map((item) => {
+ const Icon = item.icon;
+
+ return (
+
+
+
+
+ {item.label}
+
+
+
+ {item.value}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ );
+ };
+
+ export default UserProfileInformation;
\ No newline at end of file
diff --git a/client/src/presentation/components/user/UserSidebar.jsx b/client/src/presentation/components/user/UserSidebar.jsx
new file mode 100644
index 0000000000..ffb48d8898
--- /dev/null
+++ b/client/src/presentation/components/user/UserSidebar.jsx
@@ -0,0 +1,66 @@
+import { ROUTES } from "@/constants/routes";
+import { CalendarDays, Heart, Key, MapPin, Settings, User } from "lucide-react";
+import React from "react";
+import { NavLink } from "react-router-dom";
+
+const UserSidebar = () => {
+ const menuItems = [
+ {
+ name: "Explore Venues",
+ path: ROUTES.USER.BROWSE_VENUES,
+ icon: MapPin ,
+ },
+ {
+ name: "Profile",
+ path: ROUTES.USER.PROFILE,
+ icon: User,
+ },
+ {
+ name: "Wishlist",
+ path: ROUTES.USER.WISHLIST,
+ icon: Heart,
+ },
+ {
+ name: "Bookings",
+ path: ROUTES.USER.BOOKINGS,
+ icon: CalendarDays,
+ },
+ {
+ name: "Change Password",
+ path: ROUTES.USER.CHANGE_PASSWORD,
+ icon: Key,
+ },
+ ];
+
+ return (
+
+ );
+};
+
+export default UserSidebar;
diff --git a/client/src/presentation/components/user/venueDetails/BookingCard.jsx b/client/src/presentation/components/user/venueDetails/BookingCard.jsx
new file mode 100644
index 0000000000..5bb27eb454
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/BookingCard.jsx
@@ -0,0 +1,159 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { ROUTES } from "@/constants/routes";
+
+export default function BookingCard({ venue, availability }) {
+ const [bookingType, setBookingType] = useState("daily");
+ const [startTime, setStartTime] = useState("");
+ const [endTime, setEndTime] = useState("");
+ const [guestCount, setGuestCount] = useState("");
+
+ const navigate = useNavigate();
+
+ // ======================================
+ // MAXIMUM GUEST CAPACITY
+ // ======================================
+ const maxGuestCount = Math.max(
+ venue.seatingCapacity || 0,
+ venue.standingCapacity || 0
+ );
+
+ // ======================================
+ // SELECTED DATE (normalize to YYYY-MM-DD)
+ // ======================================
+ let bookingDate = "";
+ if (availability?.eventDate) {
+ const parsed = new Date(availability.eventDate);
+ bookingDate = parsed.toISOString().split("T")[0]; // ✅ normalized
+ }
+
+ // ======================================
+ // BOOKING TYPE CHANGE
+ // ======================================
+ const handleBookingTypeChange = (event) => {
+ const type = event.target.value;
+ setBookingType(type);
+
+ if (type === "daily") {
+ setStartTime("");
+ setEndTime("");
+ }
+ };
+
+ // ======================================
+ // CONTINUE TO BOOKING SUMMARY
+ // ======================================
+ const venueOpenTime = venue.availabilityRules?.openTime || "00:00";
+ const venueCloseTime = venue.availabilityRules?.closeTime || "23:59";
+
+ const handleContinue = () => {
+ navigate(ROUTES.USER.BOOKING_SUMMARY, {
+ state: {
+ venue,
+ bookingType,
+ bookingDate, // ✅ normalized date passed forward
+ startTime: bookingType === "hourly" ? startTime : venueOpenTime,
+ endTime: bookingType === "hourly" ? endTime : venueCloseTime,
+ guestCount: Number(guestCount),
+ },
+ });
+ };
+
+ // ======================================
+ // VALIDATION
+ // ======================================
+ const isHourlyValid =
+ bookingType === "hourly" && startTime && endTime && startTime < endTime;
+
+ const isFormValid =
+ bookingDate &&
+ Number(guestCount) > 0 &&
+ Number(guestCount) <= maxGuestCount &&
+ (bookingType === "daily" || isHourlyValid);
+
+ return (
+
+ );
+}
diff --git a/client/src/presentation/components/user/venueDetails/CancellationPolicy.jsx b/client/src/presentation/components/user/venueDetails/CancellationPolicy.jsx
new file mode 100644
index 0000000000..1f6d03c3d8
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/CancellationPolicy.jsx
@@ -0,0 +1,23 @@
+export default function CancellationPolicy() {
+ return (
+
+
+ ✓
+
+
+
+
+ Flexible cancellation policy
+
+
+
+ Cancel up to 72 hours before the event and receive a full refund of your 20% advance payment.
+
+
+
+ An advance payment of 20% of the total booking amount is required.
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/user/venueDetails/HostedBy.jsx b/client/src/presentation/components/user/venueDetails/HostedBy.jsx
new file mode 100644
index 0000000000..9c6ef85c18
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/HostedBy.jsx
@@ -0,0 +1,46 @@
+export default function HostedBy({ vendor }) {
+ if (!vendor) {
+ return null;
+ }
+
+ const hostName =
+ vendor.companyName || vendor.fullName || "Venue Host";
+
+ const hostInitial =
+ vendor.companyName?.charAt(0) ||
+ vendor.fullName?.charAt(0) ||
+ "V";
+
+ return (
+
+ Hosted By
+
+
+
+
+ {hostInitial}
+
+
+
+
{hostName}
+
+
+ Professional venue host
+
+
+
+ {vendor.email}
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/user/venueDetails/SimilarVenues.jsx b/client/src/presentation/components/user/venueDetails/SimilarVenues.jsx
new file mode 100644
index 0000000000..3c3c2be005
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/SimilarVenues.jsx
@@ -0,0 +1,25 @@
+import VenueCard from "@/presentation/components/common/VenueCard";
+
+export default function SimilarVenues({ venues = [] }) {
+ if (!venues.length) {
+ return null;
+ }
+
+ return (
+
+
+ Similar Venues
+
+
+
+ {venues.map((venue) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/user/venueDetails/VenueAbout.jsx b/client/src/presentation/components/user/venueDetails/VenueAbout.jsx
new file mode 100644
index 0000000000..612a70ab85
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/VenueAbout.jsx
@@ -0,0 +1,13 @@
+export default function VenueAbout({ description }) {
+ return (
+
+
+ About this venue
+
+
+
+ {description}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/user/venueDetails/VenueAmenities.jsx b/client/src/presentation/components/user/venueDetails/VenueAmenities.jsx
new file mode 100644
index 0000000000..4acfc6d11a
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/VenueAmenities.jsx
@@ -0,0 +1,30 @@
+export default function VenueAmenities({ amenities = [] }) {
+ return (
+
+
+ Amenities & Features
+
+
+ {amenities.length === 0 ? (
+
+ No amenities listed for this venue.
+
+ ) : (
+
+ {amenities.map((amenity) => (
+
+ ✓
+
+
+ {amenity}
+
+
+ ))}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/user/venueDetails/VenueAvailability.jsx b/client/src/presentation/components/user/venueDetails/VenueAvailability.jsx
new file mode 100644
index 0000000000..4a3dec530a
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/VenueAvailability.jsx
@@ -0,0 +1,101 @@
+import { useState } from "react";
+import { Calendar } from "@/components/ui/calendar";
+
+export default function VenueAvailability({
+ venue,
+ onAvailabilityChange,
+}) {
+ const [selectedDate, setSelectedDate] =
+ useState(null);
+
+ const formatDateKey = (date) => {
+ const year = date.getFullYear();
+
+ const month = String(
+ date.getMonth() + 1
+ ).padStart(2, "0");
+
+ const day = String(
+ date.getDate()
+ ).padStart(2, "0");
+
+ return `${year}-${month}-${day}`;
+ };
+
+ const handleDateSelect = (date) => {
+ if (!date) return;
+
+ const dateKey =
+ formatDateKey(date);
+
+ setSelectedDate(date);
+
+ onAvailabilityChange?.({
+ eventDate: dateKey,
+ });
+ };
+
+ return (
+
+
+ {/* ======================================
+ HEADER
+ ====================================== */}
+
+
+ Select Event Date
+
+
+
+ Choose the date for your event. The exact
+ availability will be validated when you continue
+ with the booking.
+
+
+
+
+ {/* ======================================
+ CALENDAR
+ ====================================== */}
+
+
+
+
+
+
+
+ {/* ======================================
+ SELECTED DATE
+ ====================================== */}
+
+ {selectedDate && (
+
+
+
+
+ Selected Event Date
+
+
+
+ {selectedDate.toLocaleDateString(
+ "en-GB"
+ )}
+
+
+
+
+ )}
+
+
+
+
+ );
+}
+
diff --git a/client/src/presentation/components/user/venueDetails/VenueGallery.jsx b/client/src/presentation/components/user/venueDetails/VenueGallery.jsx
new file mode 100644
index 0000000000..73351a90a2
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/VenueGallery.jsx
@@ -0,0 +1,14 @@
+export default function VenueGallery({ venue }) {
+ return (
+
+ {venue.images?.map((image, index) => (
+

+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/user/venueDetails/VenueHeader.jsx b/client/src/presentation/components/user/venueDetails/VenueHeader.jsx
new file mode 100644
index 0000000000..c247faaa20
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/VenueHeader.jsx
@@ -0,0 +1,42 @@
+export default function VenueHeader({ venue }) {
+ return (
+
+
+
+
+ {venue.category}
+
+
+
+ {venue.name}
+
+
+
+ 📍 {venue.address?.addressLine1},{" "}
+ {venue.address?.city},{" "}
+ {venue.address?.state}
+
+
+
+
+
+
+
+ ⭐ {venue.rating || 0}
+
+
+ 🪑 {venue.seatingCapacity} Seating
+
+
+
+ 🧍 {venue.standingCapacity} Standing
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/user/venueDetails/VenueReviews.jsx b/client/src/presentation/components/user/venueDetails/VenueReviews.jsx
new file mode 100644
index 0000000000..afcc6dfcd3
--- /dev/null
+++ b/client/src/presentation/components/user/venueDetails/VenueReviews.jsx
@@ -0,0 +1,79 @@
+export default function VenueReviews({
+ rating = 0,
+ reviews = [],
+}) {
+ return (
+
+ {/* Header */}
+
+
+ Guest Reviews
+
+
+
+
+ ★
+
+
+
+ {rating || "0.0"}
+
+
+
+ ({reviews.length} reviews)
+
+
+
+
+ {/* Reviews */}
+ {reviews.length === 0 ? (
+
+ No reviews yet.
+
+ ) : (
+
+ {reviews.slice(0, 3).map((review, index) => (
+
+
+
+
+ {review.user?.fullName || "Anonymous User"}
+
+
+
+ {review.createdAt
+ ? new Date(
+ review.createdAt
+ ).toLocaleDateString()
+ : ""}
+
+
+
+
+ ★ {review.rating}
+
+
+
+
+ {review.comment}
+
+
+ ))}
+
+ )}
+
+ {/* View All */}
+ {reviews.length > 3 && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/.gitkeep b/client/src/presentation/components/vendor/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/components/vendor/SidebarProfileCard.jsx b/client/src/presentation/components/vendor/SidebarProfileCard.jsx
new file mode 100644
index 0000000000..fa3da1dd2e
--- /dev/null
+++ b/client/src/presentation/components/vendor/SidebarProfileCard.jsx
@@ -0,0 +1,54 @@
+import { useSelector } from "react-redux";
+
+const SidebarProfileCard = () => {
+ const { profile } = useSelector((state) => state.vendorProfile);
+ const { user } = useSelector((state) => state.auth)
+
+ const initials =
+ user?.name
+ ?.split(" ")
+ .map((word) => word[0])
+ .join("")
+ .slice(0, 2)
+ .toUpperCase() || "--";
+
+ return (
+
+
+ {profile?.profileImage?.url ? (
+

+ ) : (
+
+ {initials}
+
+ )}
+
+
+
+ {user?.name || "Vendor"}
+
+
+
+ Venue Owner
+
+
+
+
+
+
+ );
+};
+
+export default SidebarProfileCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/VendorNavbar.jsx b/client/src/presentation/components/vendor/VendorNavbar.jsx
new file mode 100644
index 0000000000..3dcaa44081
--- /dev/null
+++ b/client/src/presentation/components/vendor/VendorNavbar.jsx
@@ -0,0 +1,235 @@
+import { useState, useRef, useEffect } from "react";
+import { Input } from "@/components/ui/input";
+import {
+ Bell,
+ Search,
+ ChevronDown,
+ LogOut,
+ User,
+ Settings,
+} from "lucide-react";
+
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import toast from "react-hot-toast";
+
+import { logout } from "@/redux/slices/AuthSlice";
+import { ROUTES } from "@/constants/routes";
+import { ROLES } from "@/constants/Roles";
+
+const VendorNavbar = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+
+ const dropdownRef = useRef(null);
+
+ const { profile } = useSelector(
+ (state) => state.vendorProfile
+ );
+
+ const { user } = useSelector(
+ (state) => state.auth
+ );
+
+ const initials =
+ user?.name
+ ?.split(" ")
+ .map((word) => word[0])
+ .join("")
+ .slice(0, 2)
+ .toUpperCase() || "--";
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target)
+ ) {
+ setIsDropdownOpen(false);
+ }
+ };
+
+ document.addEventListener(
+ "mousedown",
+ handleClickOutside
+ );
+
+ return () => {
+ document.removeEventListener(
+ "mousedown",
+ handleClickOutside
+ );
+ };
+ }, []);
+
+ const handleLogout = async () => {
+ try {
+ await dispatch(
+ logout({
+ role: ROLES.VENDOR,
+ })
+ ).unwrap();
+
+ toast.success(
+ "Vendor logged out successfully"
+ );
+
+ navigate(ROUTES.PUBLIC.LOGIN);
+ } catch (error) {
+ toast.error(
+ error?.message ||
+ "Logout failed. Please try again."
+ );
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default VendorNavbar;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/VendorSidebar.jsx b/client/src/presentation/components/vendor/VendorSidebar.jsx
new file mode 100644
index 0000000000..9a34837605
--- /dev/null
+++ b/client/src/presentation/components/vendor/VendorSidebar.jsx
@@ -0,0 +1,150 @@
+import { NavLink, useNavigate } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import toast from "react-hot-toast";
+
+import {
+ LayoutDashboard,
+ Building2,
+ PlusSquare,
+ CalendarDays,
+ User,
+ Settings,
+ LogOut,
+} from "lucide-react";
+
+import SidebarProfileCard from "./SidebarProfileCard";
+
+import { ROUTES } from "@/constants/routes";
+import { ROLES } from "@/constants/Roles";
+import { logout } from "@/redux/slices/AuthSlice";
+
+import logo from "@/assets/images/logo.jpeg";
+
+const VendorSidebar = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const menuItems = [
+ {
+ name: "Dashboard",
+ path: ROUTES.VENDOR.DASHBOARD,
+ icon: LayoutDashboard,
+ },
+ {
+ name: "My Venues",
+ path: ROUTES.VENDOR.VENUES,
+ icon: Building2,
+ },
+ {
+ name: "Add Venue",
+ path: ROUTES.VENDOR.ADD_VENUE,
+ icon: PlusSquare,
+ },
+ {
+ name: "Bookings",
+ path: ROUTES.VENDOR.BOOKINGS,
+ icon: CalendarDays,
+ },
+ {
+ name: "Profile",
+ path: ROUTES.VENDOR.PROFILE,
+ icon: User,
+ },
+ {
+ name: "Settings",
+ path: ROUTES.VENDOR.SETTINGS,
+ icon: Settings,
+ },
+ ];
+
+ const handleLogout = async () => {
+ try {
+ await dispatch(
+ logout({
+ role: ROLES.VENDOR,
+ })
+ ).unwrap();
+
+ toast.success("Vendor logged out successfully");
+
+ navigate(ROUTES.PUBLIC.LOGIN);
+ } catch (error) {
+ toast.error(
+ error?.message || "Logout failed. Please try again."
+ );
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default VendorSidebar;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/addVenue/AddVenueHeader.jsx b/client/src/presentation/components/vendor/addVenue/AddVenueHeader.jsx
new file mode 100644
index 0000000000..7bd25ef826
--- /dev/null
+++ b/client/src/presentation/components/vendor/addVenue/AddVenueHeader.jsx
@@ -0,0 +1,16 @@
+const AddVenueHeader = ({
+ title = "Add New Venue",
+ subtitle = "Fill in the details to list your venue",
+}) => (
+
+
{title}
+
+
+ {subtitle}
+
+
+
+
+);
+
+export default AddVenueHeader;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/addVenue/AmenitiesForm.jsx b/client/src/presentation/components/vendor/addVenue/AmenitiesForm.jsx
new file mode 100644
index 0000000000..acbc5c7d3d
--- /dev/null
+++ b/client/src/presentation/components/vendor/addVenue/AmenitiesForm.jsx
@@ -0,0 +1,39 @@
+const amenitiesList = [
+ "Parking","WiFi","Air Conditioning","Catering","Decoration",
+ "Sound System","Stage","Projector","Power Backup","Security",
+ "Valet Parking","DJ Setup"
+];
+
+const AmenitiesForm = ({ amenities, setAmenities }) => {
+ const toggleAmenity = (item) => {
+ setAmenities(
+ amenities.includes(item)
+ ? amenities.filter((a) => a !== item)
+ : [...amenities, item]
+ );
+ };
+
+ return (
+
+
Venue Amenities
+
+ {amenitiesList.map((item) => (
+
+ ))}
+
+
+ );
+};
+
+export default AmenitiesForm;
diff --git a/client/src/presentation/components/vendor/addVenue/FormActions.jsx b/client/src/presentation/components/vendor/addVenue/FormActions.jsx
new file mode 100644
index 0000000000..414e0095d1
--- /dev/null
+++ b/client/src/presentation/components/vendor/addVenue/FormActions.jsx
@@ -0,0 +1,13 @@
+import { Button } from "@/components/ui/button";
+
+const FormActions = ({ onPublish }) => {
+ return (
+
+
+
+ );
+};
+
+export default FormActions;
diff --git a/client/src/presentation/components/vendor/addVenue/PricingForm.jsx b/client/src/presentation/components/vendor/addVenue/PricingForm.jsx
new file mode 100644
index 0000000000..a51ee51c38
--- /dev/null
+++ b/client/src/presentation/components/vendor/addVenue/PricingForm.jsx
@@ -0,0 +1,78 @@
+import { Input } from "@/components/ui/input";
+
+const pricingFields = [
+ ["seatingCapacity", "Seating Capacity", "500"],
+ ["standingCapacity", "Standing Capacity", "700"],
+ ["pricePerHour", "Price Per Hour (₹)", "2000"],
+ ["pricePerDay", "Price Per Day (₹)", "50000"],
+ ["securityDeposit", "Security Deposit (₹)", "10000"],
+ ["weekendSurcharge", "Weekend Surcharge (%)", "15"],
+ ["minimumBookingHours", "Minimum Booking Hours", "4"],
+];
+
+const PricingForm = ({
+ pricing,
+ setPricing,
+ errors = {},
+}) => {
+ const handleChange = (field, value) => {
+ setPricing({
+ ...pricing,
+ [field]: value,
+ });
+ };
+
+ return (
+
+
+
+ Capacity & Pricing
+
+
+
+ {pricingFields.map(([field, label, placeholder]) => (
+
+
+
+
+ handleChange(field, event.target.value)
+ }
+ placeholder={placeholder}
+ aria-invalid={Boolean(errors[field])}
+ />
+
+ {errors[field] && (
+
+ {errors[field]}
+
+ )}
+
+ ))}
+
+
+
+
+
+ Pricing Tips
+
+
+
+ - • Competitive pricing gets more bookings.
+ - • Weekend pricing can be 10-20% higher.
+ - • Include deposits for venue protection.
+ - • Keep cancellation policy clear.
+
+
+
+ );
+};
+
+export default PricingForm;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/addVenue/ReviewForm.jsx b/client/src/presentation/components/vendor/addVenue/ReviewForm.jsx
new file mode 100644
index 0000000000..af0476ad0c
--- /dev/null
+++ b/client/src/presentation/components/vendor/addVenue/ReviewForm.jsx
@@ -0,0 +1,104 @@
+import { Button } from "@/components/ui/button";
+
+const ReviewForm = ({
+ venueName,
+ category,
+ description,
+ addressLine1,
+ city,
+ state,
+ country,
+ pricing,
+ amenities,
+ onPublish,
+ loading,
+ submitLabel = "Publish Venue",
+}) => {
+ return (
+
+
+ Review Venue Details
+
+
+
+
+
Venue Name
+
+
+ {venueName || "Not provided"}
+
+
+
+
+
Category
+
+
+ {category || "Not provided"}
+
+
+
+
+
Description
+
+
+ {description || "Not provided"}
+
+
+
+
+
Address
+
+
+ {addressLine1 || ""}
+ {addressLine1 && city ? ", " : ""}
+ {city || ""}
+ {city && state ? ", " : ""}
+ {state || ""}
+ {(city || state) && country ? ", " : ""}
+ {country || ""}
+
+
+
+
+
Capacity
+
+
+ {pricing.seatingCapacity || "0"} seated /{" "}
+ {pricing.standingCapacity || "0"} standing
+
+
+
+
+
Price
+
+
+ ₹{pricing.pricePerDay || "0"} / day
+
+
+
+
+
Amenities
+
+
+ {amenities.length > 0
+ ? amenities.join(", ")
+ : "No amenities selected"}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ReviewForm;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/addVenue/VenueDetailsForm.jsx b/client/src/presentation/components/vendor/addVenue/VenueDetailsForm.jsx
new file mode 100644
index 0000000000..434d61f859
--- /dev/null
+++ b/client/src/presentation/components/vendor/addVenue/VenueDetailsForm.jsx
@@ -0,0 +1,480 @@
+import { useEffect, useMemo } from "react";
+import { toast } from "react-hot-toast";
+
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { VenueCategory } from "@/constants/Venue";
+
+const VenueDetailsForm = ({
+ venueName,
+ setVenueName,
+ category,
+ setCategory,
+ description,
+ setDescription,
+ addressLine1,
+ setAddressLine1,
+ city,
+ setCity,
+ state,
+ setState,
+ country,
+ setCountry,
+ phone,
+ setPhone,
+ pincode,
+ setPincode,
+ websiteUrl,
+ setWebsiteUrl,
+ googleMapLink,
+ setGoogleMapLink,
+ images,
+ setImages,
+ license,
+ setLicense,
+ existingImages = [],
+ onRemoveExistingImage,
+ errors = {},
+}) => {
+ const handleImageChange = (event) => {
+ const files = Array.from(event.target.files || []);
+
+ setImages(files);
+ };
+
+ const handleLicenseChange = (event) => {
+ const file = event.target.files?.[0];
+
+ if (!file) return;
+
+ if (file.type !== "application/pdf") {
+ toast.error("Only PDF files are allowed.");
+
+ event.target.value = "";
+ setLicense(null);
+
+ return;
+ }
+
+ setLicense(file);
+ };
+
+ const newImagePreviews = useMemo(() => {
+ return images.map((file) => ({
+ url: URL.createObjectURL(file),
+ name: file.name,
+ }));
+ }, [images]);
+
+ useEffect(() => {
+ return () => {
+ newImagePreviews.forEach((preview) => {
+ URL.revokeObjectURL(preview.url);
+ });
+ };
+ }, [newImagePreviews]);
+
+ return (
+
+
+ Venue Details
+
+
+
+ {/* Venue Name */}
+
+
+
+
+ setVenueName(event.target.value)
+ }
+ placeholder="Enter venue name"
+ aria-invalid={Boolean(errors.venueName)}
+ />
+
+ {errors.venueName && (
+
+ {errors.venueName}
+
+ )}
+
+
+ {/* Category */}
+
+
+
+
+
+ {errors.category && (
+
+ {errors.category}
+
+ )}
+
+
+ {/* Address */}
+
+
+
+
+ setAddressLine1(event.target.value)
+ }
+ placeholder="Street address"
+ aria-invalid={Boolean(errors.addressLine1)}
+ />
+
+ {errors.addressLine1 && (
+
+ {errors.addressLine1}
+
+ )}
+
+
+ {/* City */}
+
+
+
+
setCity(event.target.value)}
+ placeholder="City"
+ aria-invalid={Boolean(errors.city)}
+ />
+
+ {errors.city && (
+
+ {errors.city}
+
+ )}
+
+
+ {/* State */}
+
+
+
+
setState(event.target.value)}
+ placeholder="State"
+ aria-invalid={Boolean(errors.state)}
+ />
+
+ {errors.state && (
+
+ {errors.state}
+
+ )}
+
+
+ {/* Country */}
+
+
+
+
+ setCountry(event.target.value)
+ }
+ placeholder="Country"
+ aria-invalid={Boolean(errors.country)}
+ />
+
+ {errors.country && (
+
+ {errors.country}
+
+ )}
+
+
+ {/* Phone */}
+
+
+
+
setPhone(event.target.value)}
+ placeholder="Enter phone number"
+ aria-invalid={Boolean(errors.phone)}
+ />
+
+ {errors.phone && (
+
+ {errors.phone}
+
+ )}
+
+
+ {/* Pincode */}
+
+
+
+
+ setPincode(event.target.value)
+ }
+ placeholder="Postal code"
+ aria-invalid={Boolean(errors.pincode)}
+ />
+
+ {errors.pincode && (
+
+ {errors.pincode}
+
+ )}
+
+
+ {/* Website */}
+
+
+
+
+ setWebsiteUrl(event.target.value)
+ }
+ placeholder="https://example.com"
+ aria-invalid={Boolean(errors.websiteUrl)}
+ />
+
+ {errors.websiteUrl && (
+
+ {errors.websiteUrl}
+
+ )}
+
+
+ {/* Google Map */}
+
+
+
+
+ setGoogleMapLink(event.target.value)
+ }
+ placeholder="https://goo.gl/maps/..."
+ aria-invalid={Boolean(errors.googleMapLink)}
+ />
+
+ {errors.googleMapLink && (
+
+ {errors.googleMapLink}
+
+ )}
+
+
+ {/* Images */}
+
+
+
+ {existingImages.length > 0 && (
+
+ {existingImages.map((image) => (
+
+

+
+
+
+ ))}
+
+ )}
+
+ {newImagePreviews.length > 0 && (
+
+ {newImagePreviews.map((preview) => (
+
+

+
+ ))}
+
+ )}
+
+
+
+ {errors.images && (
+
+ {errors.images}
+
+ )}
+
+
+ {/* Business License */}
+
+
+
+
+
+ {license && (
+
+ )}
+
+ {errors.license && (
+
+ {errors.license}
+
+ )}
+
+
+
+ {/* Description */}
+
+
+ );
+};
+
+export default VenueDetailsForm;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingActions.jsx b/client/src/presentation/components/vendor/booking/BookingActions.jsx
new file mode 100644
index 0000000000..253f8e21cf
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingActions.jsx
@@ -0,0 +1,45 @@
+import { Eye, Check, X } from "lucide-react";
+
+const BookingActions = ({
+ bookingId,
+ status,
+ onView,
+ onApprove,
+ onReject,
+}) => {
+ return (
+
+ {/* View */}
+
+
+ {/* Pending Actions */}
+ {status === "Pending" && (
+ <>
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default BookingActions;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingDetailsModal.jsx b/client/src/presentation/components/vendor/booking/BookingDetailsModal.jsx
new file mode 100644
index 0000000000..a4f1ca6927
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingDetailsModal.jsx
@@ -0,0 +1,294 @@
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+
+const BookingDetailsModal = ({
+ open,
+ onClose,
+ booking,
+ loading,
+}) => {
+ const formatAmount = (amount) => {
+ return `₹${Number(amount || 0).toLocaleString("en-IN")}`;
+ };
+
+ const formatDate = (date) => {
+ if (!date) return "-";
+
+ return new Date(date).toLocaleDateString("en-US");
+ };
+
+ return (
+
+ );
+};
+
+export default BookingDetailsModal;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingFilters.jsx b/client/src/presentation/components/vendor/booking/BookingFilters.jsx
new file mode 100644
index 0000000000..e50a058f92
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingFilters.jsx
@@ -0,0 +1,101 @@
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+
+import {
+ Search,
+ Download,
+ Filter,
+} from "lucide-react";
+
+const BookingFilters = ({
+ search,
+ setSearch,
+ status,
+ setStatus,
+ onExport,
+}) => {
+ return (
+
+
+ {/* =========================
+ SEARCH
+ ========================= */}
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search bookings, customers, venues..."
+ className="pl-10"
+ />
+
+
+
+
+ {/* =========================
+ STATUS FILTER
+ ========================= */}
+
+
+
+
+
+
+
+
+
+ {/* =========================
+ EXPORT
+ ========================= */}
+
+
+
+ );
+};
+
+export default BookingFilters;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingHeader.jsx b/client/src/presentation/components/vendor/booking/BookingHeader.jsx
new file mode 100644
index 0000000000..295ae12429
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingHeader.jsx
@@ -0,0 +1,15 @@
+const BookingHeader = () => {
+ return (
+
+
+ Booking Management
+
+
+
+ View and manage customer bookings
+
+
+ );
+};
+
+export default BookingHeader;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingPagination.jsx b/client/src/presentation/components/vendor/booking/BookingPagination.jsx
new file mode 100644
index 0000000000..d1817c25e7
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingPagination.jsx
@@ -0,0 +1,105 @@
+const BookingPagination = ({
+ currentPage,
+ setCurrentPage,
+ totalPages,
+ totalCount,
+}) => {
+
+ if (totalPages <= 1) {
+ return null;
+ }
+
+
+ const handlePrevious = () => {
+
+ if (currentPage > 1) {
+
+ setCurrentPage(
+ currentPage - 1
+ );
+
+ }
+
+ };
+
+
+ const handleNext = () => {
+
+ if (currentPage < totalPages) {
+
+ setCurrentPage(
+ currentPage + 1
+ );
+
+ }
+
+ };
+
+
+ return (
+
+
+
+
+
+ Showing page {currentPage} of {totalPages}
+
+ {" "}({totalCount} bookings)
+
+
+
+
+
+
+
+
+
+
+
+ {currentPage}
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+
+export default BookingPagination;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingRow.jsx b/client/src/presentation/components/vendor/booking/BookingRow.jsx
new file mode 100644
index 0000000000..aafbe12635
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingRow.jsx
@@ -0,0 +1,149 @@
+import { Eye } from "lucide-react";
+
+const BookingRow = ({ booking, onView }) => {
+ const formatDate = (date) => {
+ if (!date) return "-";
+
+ return new Date(date).toLocaleDateString("en-US");
+ };
+
+ const formatAmount = (amount) => {
+ if (
+ amount === undefined ||
+ amount === null
+ ) {
+ return "-";
+ }
+
+ return `₹${Number(amount).toLocaleString("en-IN")}`;
+ };
+
+ const getStatusClass = (status) => {
+ switch (status?.toLowerCase()) {
+ case "confirmed":
+ return "bg-green-100 text-green-600";
+
+ case "pending":
+ return "bg-yellow-100 text-yellow-600";
+
+ case "completed":
+ return "bg-blue-100 text-blue-600";
+
+ case "cancelled":
+ return "bg-red-100 text-red-600";
+
+ default:
+ return "bg-gray-100 text-gray-600";
+ }
+ };
+
+ const getPaymentClass = (status) => {
+ switch (status?.toLowerCase()) {
+ case "paid":
+ return "bg-green-100 text-green-600";
+
+ case "partial":
+ return "bg-orange-100 text-orange-600";
+
+ case "pending":
+ return "bg-yellow-100 text-yellow-600";
+
+ case "failed":
+ return "bg-red-100 text-red-600";
+
+ default:
+ return "bg-gray-100 text-gray-600";
+ }
+ };
+
+ // ==============================
+ // PAYMENT CALCULATIONS
+ // ==============================
+
+ const totalAmount = Number(
+ booking.totalAmount || 0
+ );
+
+ const paidAmount = Number(
+ booking.advanceAmount || 0
+ );
+
+ const remainingAmount =
+ totalAmount - paidAmount;
+
+ return (
+
+ {/* Booking ID */}
+ |
+
+ {booking.id?.slice(-6)}
+
+ |
+
+ {/* Customer */}
+
+
+ {booking.userId?.fullName || "-"}
+
+ |
+
+ {/* Venue */}
+
+
+ {booking.venueId?.name || "-"}
+
+ |
+
+ {/* Event Date */}
+
+
+ {formatDate(booking.bookingDate)}
+
+ |
+
+ {/* Total */}
+
+
+ {formatAmount(totalAmount)}
+
+ |
+
+ {/* Status */}
+
+
+ {booking.status || "-"}
+
+ |
+
+ {/* Payment */}
+
+
+ {booking.paymentStatus || "-"}
+
+ |
+
+ {/* Actions */}
+
+
+ |
+
+ );
+};
+
+export default BookingRow;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingStatCard.jsx b/client/src/presentation/components/vendor/booking/BookingStatCard.jsx
new file mode 100644
index 0000000000..c5e685cb95
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingStatCard.jsx
@@ -0,0 +1,15 @@
+const BookingStatCard = ({ title, value, color }) => {
+ return (
+
+
+ {value}
+
+
+
+ {title}
+
+
+ );
+};
+
+export default BookingStatCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingStats.jsx b/client/src/presentation/components/vendor/booking/BookingStats.jsx
new file mode 100644
index 0000000000..e1cc6dc1e1
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingStats.jsx
@@ -0,0 +1,63 @@
+import BookingStatCard from "./BookingStatCard";
+
+const BookingStats = ({ bookings = [] }) => {
+ const total = bookings.length;
+
+ const pending = bookings.filter(
+ (booking) =>
+ booking.status?.toLowerCase() === "pending"
+ ).length;
+
+ const confirmed = bookings.filter(
+ (booking) =>
+ booking.status?.toLowerCase() === "confirmed"
+ ).length;
+
+ const completed = bookings.filter(
+ (booking) =>
+ booking.status?.toLowerCase() === "completed"
+ ).length;
+
+ const cancelled = bookings.filter(
+ (booking) =>
+ booking.status?.toLowerCase() === "cancelled"
+ ).length;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default BookingStats;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingStatusBadge.jsx b/client/src/presentation/components/vendor/booking/BookingStatusBadge.jsx
new file mode 100644
index 0000000000..9db20ebdda
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingStatusBadge.jsx
@@ -0,0 +1,20 @@
+const BookingStatusBadge = ({ status }) => {
+ const styles = {
+ Confirmed: "bg-green-100 text-green-700",
+ Pending: "bg-yellow-100 text-yellow-700",
+ Cancelled: "bg-red-100 text-red-600",
+ Completed: "bg-blue-100 text-blue-600",
+ };
+
+ return (
+
+ ● {status}
+
+ );
+};
+
+export default BookingStatusBadge;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/booking/BookingTable.jsx b/client/src/presentation/components/vendor/booking/BookingTable.jsx
new file mode 100644
index 0000000000..68d9060f63
--- /dev/null
+++ b/client/src/presentation/components/vendor/booking/BookingTable.jsx
@@ -0,0 +1,92 @@
+import BookingRow from "./BookingRow";
+
+const BookingTable = ({
+ bookings = [],
+ loading,
+ error,
+ onView,
+}) => {
+ if (loading) {
+ return (
+
+
+ Loading bookings...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!bookings.length) {
+ return (
+
+
+ No bookings found
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ |
+ Booking ID
+ |
+
+
+ Customer
+ |
+
+
+ Venue
+ |
+
+
+ Event Date
+ |
+
+
+ Total
+ |
+
+
+ Status
+ |
+
+
+ Payment
+ |
+
+
+ Actions
+ |
+
+
+
+
+ {bookings.map((booking) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default BookingTable;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/dashboard/BookingTrends.jsx b/client/src/presentation/components/vendor/dashboard/BookingTrends.jsx
new file mode 100644
index 0000000000..470bebd8b1
--- /dev/null
+++ b/client/src/presentation/components/vendor/dashboard/BookingTrends.jsx
@@ -0,0 +1,49 @@
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardContent,
+} from "@/components/ui/card";
+
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+} from "recharts";
+
+const BookingTrends = ({ data = [] }) => {
+ return (
+
+
+ Booking Trends
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default BookingTrends;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/dashboard/DashboardCard.jsx b/client/src/presentation/components/vendor/dashboard/DashboardCard.jsx
new file mode 100644
index 0000000000..5b4582fba6
--- /dev/null
+++ b/client/src/presentation/components/vendor/dashboard/DashboardCard.jsx
@@ -0,0 +1,34 @@
+import { Card, CardContent } from "@/components/ui/card";
+
+const DashboardCard = ({
+ title,
+ value,
+ icon: Icon,
+ color = "text-amber-600",
+}) => {
+ return (
+
+
+
+
+
+ {title}
+
+
+
+ {value}
+
+
+
+ {Icon && (
+
+ )}
+
+
+
+ );
+};
+
+export default DashboardCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/dashboard/RecentBookings.jsx b/client/src/presentation/components/vendor/dashboard/RecentBookings.jsx
new file mode 100644
index 0000000000..0d227d6c7c
--- /dev/null
+++ b/client/src/presentation/components/vendor/dashboard/RecentBookings.jsx
@@ -0,0 +1,56 @@
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+const RecentBookings = ({ bookings = [] }) => {
+ return (
+
+
+ Recent Bookings
+
+
+ {bookings.length === 0 ? (
+
+ No recent bookings yet.
+
+ ) : (
+
+
+
+ Customer
+ Venue
+ Status
+
+
+
+
+ {bookings.map((booking) => (
+
+
+ {booking.customer || "-"}
+
+
+
+ {booking.venue || "-"}
+
+
+
+ {booking.status || "-"}
+
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default RecentBookings;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/dashboard/RevenueChart.jsx b/client/src/presentation/components/vendor/dashboard/RevenueChart.jsx
new file mode 100644
index 0000000000..9324be4865
--- /dev/null
+++ b/client/src/presentation/components/vendor/dashboard/RevenueChart.jsx
@@ -0,0 +1,51 @@
+import {
+Card,
+CardHeader,
+CardTitle,
+CardContent,
+} from "@/components/ui/card";
+
+import {
+BarChart,
+Bar,
+XAxis,
+YAxis,
+CartesianGrid,
+Tooltip,
+ResponsiveContainer,
+} from "recharts";
+
+const RevenueChart = ({ data = [] }) => {
+return (
+
+
+ Revenue (₹)
+
+
+
+
+
+
+
+
+
+
+
+ [`₹${value}`, "Revenue"]}
+ />
+
+
+
+
+
+
+
+
+);
+};
+
+export default RevenueChart;
diff --git a/client/src/presentation/components/vendor/dashboard/TopVenues.jsx b/client/src/presentation/components/vendor/dashboard/TopVenues.jsx
new file mode 100644
index 0000000000..a17212d8fe
--- /dev/null
+++ b/client/src/presentation/components/vendor/dashboard/TopVenues.jsx
@@ -0,0 +1,25 @@
+const TopVenues = ({ venues = [] }) => {
+ return (
+
+
+ Top Venues
+
+
+ {venues.length === 0 ? (
+
+ No venue activity yet.
+
+ ) : (
+
+ {venues.map((venue) => (
+ -
+ {venue.name} ({venue.bookings} bookings)
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default TopVenues;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/dashboard/WelcomeBanner.jsx b/client/src/presentation/components/vendor/dashboard/WelcomeBanner.jsx
new file mode 100644
index 0000000000..1191e8b869
--- /dev/null
+++ b/client/src/presentation/components/vendor/dashboard/WelcomeBanner.jsx
@@ -0,0 +1,54 @@
+import { useNavigate } from "react-router-dom";
+import { useSelector } from "react-redux";
+
+
+const WelcomeBanner = ({ dashboard }) => {
+ const navigate = useNavigate();
+ const { user } = useSelector((state) => state.auth)
+
+ const vendorName = user?.name || "Vendor";
+
+
+ const pendingBookings =
+ dashboard?.pendingBookings || 0;
+
+ return (
+
+
+
+
+ Welcome, {vendorName}! 👋
+
+
+
+ You have{" "}
+
+ {pendingBookings} pending bookings
+ {" "}
+ awaiting your approval today.
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WelcomeBanner;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/profile/BusinessInformation.jsx b/client/src/presentation/components/vendor/profile/BusinessInformation.jsx
new file mode 100644
index 0000000000..873c4bc39a
--- /dev/null
+++ b/client/src/presentation/components/vendor/profile/BusinessInformation.jsx
@@ -0,0 +1,150 @@
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+
+const BusinessInformation = ({
+ isEditing,
+ profile,
+ setProfile,
+}) => {
+ const updateField = (field, value) => {
+ setProfile((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const updateAddress = (field, value) => {
+ setProfile((prev) => ({
+ ...prev,
+ address: {
+ ...prev.address,
+ [field]: value,
+ },
+ }));
+ };
+
+ return (
+
+
+ Business Information
+
+
+ {!isEditing ? (
+
+
+
+ Company Name
+
+
+
+ {profile.companyName || "-"}
+
+
+
+
+
+ Address Line
+
+
+
+ {profile.address?.addressLine1 || "-"}
+
+
+
+
+
+ City
+
+
+
+ {profile.address?.city || "-"}
+
+
+
+
+
+ State
+
+
+
+ {profile.address?.state || "-"}
+
+
+
+
+
+ Pincode
+
+
+
+ {profile.address?.pincode || "-"}
+
+
+
+
+
+ Bio
+
+
+
+ {profile.bio || "-"}
+
+
+
+ ) : (
+
+
+ updateField("companyName", e.target.value)
+ }
+ />
+
+
+ updateAddress("addressLine1", e.target.value)
+ }
+ />
+
+
+ updateAddress("city", e.target.value)
+ }
+ />
+
+
+ updateAddress("state", e.target.value)
+ }
+ />
+
+
+ updateAddress("pincode", e.target.value)
+ }
+ />
+
+
+ )}
+
+ );
+};
+
+export default BusinessInformation;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/profile/ChangePassword.jsx b/client/src/presentation/components/vendor/profile/ChangePassword.jsx
new file mode 100644
index 0000000000..ad5621b994
--- /dev/null
+++ b/client/src/presentation/components/vendor/profile/ChangePassword.jsx
@@ -0,0 +1,36 @@
+import { Input } from "@/components/ui/input";
+
+const ChangePassword = ({ isEditing }) => {
+ if (!isEditing) return null;
+
+ return (
+
+ );
+};
+
+export default ChangePassword;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/profile/NotificationSettings.jsx b/client/src/presentation/components/vendor/profile/NotificationSettings.jsx
new file mode 100644
index 0000000000..8f776c0461
--- /dev/null
+++ b/client/src/presentation/components/vendor/profile/NotificationSettings.jsx
@@ -0,0 +1,50 @@
+import { Switch } from "@/components/ui/switch";
+
+const NotificationSettings = ({ isEditing }) => {
+ const settings = [
+ "New Bookings",
+ "Booking Cancellations",
+ "Payment Received",
+ "Weekly Reports",
+ "New Reviews",
+ "Marketing Promotions",
+ ];
+
+ return (
+
+
+
+ Notification Settings
+
+
+
+
+ {settings.map((setting) => (
+
+ ))}
+
+
+
+ {!isEditing && (
+
+ Click "Edit Profile" to modify notification preferences.
+
+ )}
+
+
+ );
+};
+
+export default NotificationSettings;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/profile/PersonalInformation.jsx b/client/src/presentation/components/vendor/profile/PersonalInformation.jsx
new file mode 100644
index 0000000000..ef453953c6
--- /dev/null
+++ b/client/src/presentation/components/vendor/profile/PersonalInformation.jsx
@@ -0,0 +1,101 @@
+import React from "react";
+import { Input } from "@/components/ui/input";
+
+const PersonalInformation = ({
+ isEditing,
+ profile,
+ setProfile,
+}) => {
+ const updateField = (field, value) => {
+ setProfile((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ return (
+
+
+ Personal Information
+
+
+ {!isEditing ? (
+
+
+
+ Full Name
+
+
+
+ {profile.fullName || "-"}
+
+
+
+
+
+ Email Address
+
+
+
+ {profile.email || "-"}
+
+
+
+
+
+ Phone Number
+
+
+
+ {profile.phone || "-"}
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default PersonalInformation;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/profile/ProfileActions.jsx b/client/src/presentation/components/vendor/profile/ProfileActions.jsx
new file mode 100644
index 0000000000..342f54e41b
--- /dev/null
+++ b/client/src/presentation/components/vendor/profile/ProfileActions.jsx
@@ -0,0 +1,31 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+
+const ProfileActions = ({
+ onSave,
+ onCancel,
+ updating,
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default ProfileActions;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/profile/ProfileHeader.jsx b/client/src/presentation/components/vendor/profile/ProfileHeader.jsx
new file mode 100644
index 0000000000..dadc38c736
--- /dev/null
+++ b/client/src/presentation/components/vendor/profile/ProfileHeader.jsx
@@ -0,0 +1,54 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+
+const ProfileHeader = ({
+ profile,
+ isEditing,
+ setIsEditing,
+}) => {
+ const initials =
+ profile?.fullName
+ ?.trim()
+ .split(/\s+/)
+ .filter(Boolean)
+ .map((word) => word[0])
+ .join("")
+ .slice(0, 2)
+ .toUpperCase() || "--";
+
+ return (
+
+
+
+
+ {initials}
+
+
+
+
+ {profile?.fullName || "-"}
+
+
+
+ Venue Owner
+
+
+
+ {profile?.email || "-"}
+
+
+
+
+
+
+
+ );
+};
+
+export default ProfileHeader;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/profile/ProfileStatCard.jsx b/client/src/presentation/components/vendor/profile/ProfileStatCard.jsx
new file mode 100644
index 0000000000..856cfbe5d2
--- /dev/null
+++ b/client/src/presentation/components/vendor/profile/ProfileStatCard.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+
+const ProfileStatCard = ({ title, value }) => {
+ return (
+
+
+ {value}
+
+
+
+ {title}
+
+
+ );
+};
+
+export default ProfileStatCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/profile/ProfileStats.jsx b/client/src/presentation/components/vendor/profile/ProfileStats.jsx
new file mode 100644
index 0000000000..962f6b5607
--- /dev/null
+++ b/client/src/presentation/components/vendor/profile/ProfileStats.jsx
@@ -0,0 +1,59 @@
+import { useSelector } from "react-redux";
+import ProfileStatCard from "./ProfileStatCard";
+
+const ProfileStats = ({ profile }) => {
+ const {
+ loading,
+ error,
+ } = useSelector(
+ (state) => state.vendorProfile
+ );
+
+ return (
+
+
+ {/* ERROR */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* LOADING */}
+ {loading && (
+
+ Loading profile stats...
+
+ )}
+
+ {/* TOTAL VENUES */}
+
+
+ {/* BOOKINGS */}
+
+
+ {/* REVENUE */}
+
+
+ {/* RATING */}
+
+
+
+ );
+};
+
+export default ProfileStats;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/settings/AccountSettingsCard.jsx b/client/src/presentation/components/vendor/settings/AccountSettingsCard.jsx
new file mode 100644
index 0000000000..7a9ee9a7b0
--- /dev/null
+++ b/client/src/presentation/components/vendor/settings/AccountSettingsCard.jsx
@@ -0,0 +1,79 @@
+import React from "react";
+import { Input } from "@/components/ui/input";
+
+const AccountSettingsCard = ({ settings, setSettings }) => {
+const handleChange = (field, value) => {
+setSettings((prev) => ({
+...prev,
+[field]: value,
+}));
+};
+
+return (
+Account Settings
+
+```
+
+
+
+
+ handleChange("email", e.target.value)}
+ placeholder="Enter your email"
+ />
+
+
+
+
+
+ handleChange("phone", e.target.value)}
+ placeholder="Enter your phone number"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+};
+
+export default AccountSettingsCard;
diff --git a/client/src/presentation/components/vendor/settings/DangerzoneCard.jsx b/client/src/presentation/components/vendor/settings/DangerzoneCard.jsx
new file mode 100644
index 0000000000..82f0779c81
--- /dev/null
+++ b/client/src/presentation/components/vendor/settings/DangerzoneCard.jsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+
+const DangerZoneCard = () => {
+ return (
+
+
+
+ Danger Zone
+
+
+
+ Permanently delete your account and all associated data.
+
+
+
+
+
+ );
+};
+
+export default DangerZoneCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/settings/NotificationPreferencesCard.jsx b/client/src/presentation/components/vendor/settings/NotificationPreferencesCard.jsx
new file mode 100644
index 0000000000..70563971d3
--- /dev/null
+++ b/client/src/presentation/components/vendor/settings/NotificationPreferencesCard.jsx
@@ -0,0 +1,41 @@
+import React from "react";
+import { Switch } from "@/components/ui/switch";
+
+const NotificationPreferencesCard = ({ settings, setSettings }) => {
+ const notifications = [
+ { key: "email", label: "Email Notifications" },
+ { key: "sms", label: "SMS Notifications" },
+ { key: "marketing", label: "Marketing Emails" },
+ { key: "bookingUpdates", label: "Booking Updates" },
+ ];
+
+ const toggleNotification = (key) => {
+ setSettings((prev) => ({
+ ...prev,
+ notifications: {
+ ...prev.notifications,
+ [key]: !prev.notifications[key],
+ },
+ }));
+ };
+
+ return (
+
+
Notification Preferences
+
+
+ {notifications.map((item) => (
+
+
{item.label}
+
toggleNotification(item.key)}
+ />
+
+ ))}
+
+
+ );
+};
+
+export default NotificationPreferencesCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/settings/SecuritySettingsCard.jsx b/client/src/presentation/components/vendor/settings/SecuritySettingsCard.jsx
new file mode 100644
index 0000000000..2df4d8f119
--- /dev/null
+++ b/client/src/presentation/components/vendor/settings/SecuritySettingsCard.jsx
@@ -0,0 +1,52 @@
+import React from "react";
+import { Switch } from "@/components/ui/switch";
+
+const SecuritySettingsCard = () => {
+ return (
+
+
+
+ Security Settings
+
+
+
+
+
+
+
+
+ Two-Factor Authentication
+
+
+
+ Add extra security to your account
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SecuritySettingsCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/settings/SessionManagementCard.jsx b/client/src/presentation/components/vendor/settings/SessionManagementCard.jsx
new file mode 100644
index 0000000000..53aaa045d6
--- /dev/null
+++ b/client/src/presentation/components/vendor/settings/SessionManagementCard.jsx
@@ -0,0 +1,67 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+
+const SessionManagementCard = () => {
+ return (
+
+
+
+ Session Management
+
+
+
+
+
+
+
+
+ Current Session
+
+
+
+ Chrome • Windows • Active Now
+
+
+
+
+ Active
+
+
+
+
+
+
+
+
+ Mobile Device
+
+
+
+ Android • Last active 2 hours ago
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SessionManagementCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/settings/SettingsActions.jsx b/client/src/presentation/components/vendor/settings/SettingsActions.jsx
new file mode 100644
index 0000000000..c87639a4c4
--- /dev/null
+++ b/client/src/presentation/components/vendor/settings/SettingsActions.jsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+
+const SettingsActions = ({ onReset, onSave }) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default SettingsActions;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/settings/SettingsHeader.jsx b/client/src/presentation/components/vendor/settings/SettingsHeader.jsx
new file mode 100644
index 0000000000..bcd7c40dc5
--- /dev/null
+++ b/client/src/presentation/components/vendor/settings/SettingsHeader.jsx
@@ -0,0 +1,19 @@
+import React from "react";
+
+const SettingsHeader = () => {
+ return (
+
+
+
+ Settings
+
+
+
+ Manage your account preferences and security settings
+
+
+
+ );
+};
+
+export default SettingsHeader;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/venues/DeleteVenueDialog.jsx b/client/src/presentation/components/vendor/venues/DeleteVenueDialog.jsx
new file mode 100644
index 0000000000..753e3902e0
--- /dev/null
+++ b/client/src/presentation/components/vendor/venues/DeleteVenueDialog.jsx
@@ -0,0 +1,52 @@
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+
+const DeleteVenueDialog = ({
+ open,
+ onOpenChange,
+ onConfirm,
+ loading,
+}) => {
+ return (
+
+
+
+
+
+ Delete Venue?
+
+
+
+ This action cannot be undone.
+ The venue and all associated information will be permanently deleted.
+
+
+
+
+
+ Cancel
+
+
+
+ {loading ? "Deleting..." : "Delete"}
+
+
+
+
+
+
+ );
+};
+
+export default DeleteVenueDialog;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/venues/VenueCard.jsx b/client/src/presentation/components/vendor/venues/VenueCard.jsx
new file mode 100644
index 0000000000..fa839d8afa
--- /dev/null
+++ b/client/src/presentation/components/vendor/venues/VenueCard.jsx
@@ -0,0 +1,128 @@
+import React from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import {
+ MapPin,
+ Users,
+ Calendar,
+ Pencil,
+ Trash2,
+ Star,
+ Eye,
+} from "lucide-react";
+
+const VenueCard = ({
+ image,
+ name,
+ location,
+ guests,
+ price,
+ bookings,
+ rating,
+ category,
+ status,
+ onView,
+ onEdit,
+ onDelete,
+}) => {
+ return (
+
+
+ {/* Image */}
+
+

+
+ {/* Category */}
+
+ {category || "Venue"}
+
+
+ {/* Status */}
+
+ ● {status || "Active"}
+
+
+ {/* Rating */}
+
+
+ {rating}
+
+
+
+
+
+ {/* Title */}
+
+ {name}
+
+
+ {/* Location */}
+
+
+ {location}
+
+
+ {/* Stats */}
+
+
+
+
+ {guests}
+
+
+
+ ₹{price}/day
+
+
+
+
+ {bookings}
+
+
+
+
+ {/* Actions */}
+
+
+ {/* View Details */}
+
+
+ {/* Edit */}
+
+
+ {/* Delete */}
+
+
+
+
+
+
+ );
+};
+
+export default VenueCard;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/venues/VenueFilters.jsx b/client/src/presentation/components/vendor/venues/VenueFilters.jsx
new file mode 100644
index 0000000000..be409595a0
--- /dev/null
+++ b/client/src/presentation/components/vendor/venues/VenueFilters.jsx
@@ -0,0 +1,135 @@
+import React from "react";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Search, Plus } from "lucide-react";
+import ViewToggle from "./ViewToggle";
+import { VenueCategory } from "@/constants/Venue";
+
+const statusOptions = [
+ { label: "All Status", value: "" },
+ { label: "Active", value: "ACTIVE" },
+ { label: "Inactive", value: "INACTIVE" },
+ { label: "Pending", value: "PENDING" },
+ { label: "Rejected", value: "REJECTED" },
+ { label: "Suspended", value: "SUSPENDED" },
+];
+
+const ratingOptions = [
+ { label: "All Ratings", value: "" },
+ { label: "4+ stars", value: 4 },
+ { label: "3+ stars", value: 3 },
+ { label: "2+ stars", value: 2 },
+];
+
+const VenueFilters = ({ filters, onChange, onAddVenue, onToggleView, viewMode }) => {
+ return (
+
+
+ {/* Search */}
+
+
+
+ onChange({ ...filters, search: e.target.value, page: 1 })}
+ placeholder="Search venues..."
+ className="pl-10"
+ />
+
+
+
+
+
+
+
+
+
onChange({ ...filters, minPrice: e.target.value, page: 1 })}
+ placeholder="Min Price"
+ className="h-10 w-32 rounded-lg border border-slate-200 bg-white px-3 text-sm"
+ />
+
+
onChange({ ...filters, maxPrice: e.target.value, page: 1 })}
+ placeholder="Max Price"
+ className="h-10 w-32 rounded-lg border border-slate-200 bg-white px-3 text-sm"
+ />
+
+
+
+
onChange({ ...filters, capacity: e.target.value, page: 1 })}
+ placeholder="Capacity"
+ className="h-10 w-32 rounded-lg border border-slate-200 bg-white px-3 text-sm"
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default VenueFilters;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/venues/VenueForm.jsx b/client/src/presentation/components/vendor/venues/VenueForm.jsx
new file mode 100644
index 0000000000..04b6938147
--- /dev/null
+++ b/client/src/presentation/components/vendor/venues/VenueForm.jsx
@@ -0,0 +1,12 @@
+import React from 'react'
+
+const VenueForm = () => {
+ return (
+
+ venueform
+
+
+ )
+}
+
+export default VenueForm
diff --git a/client/src/presentation/components/vendor/venues/VenueGrid.jsx b/client/src/presentation/components/vendor/venues/VenueGrid.jsx
new file mode 100644
index 0000000000..247a038056
--- /dev/null
+++ b/client/src/presentation/components/vendor/venues/VenueGrid.jsx
@@ -0,0 +1,166 @@
+import React from "react";
+import VenueCard from "./VenueCard";
+import { Button } from "@/components/ui/button";
+import { Trash2 } from "lucide-react";
+
+const VenueGrid = ({
+ venues = [],
+ loading = false,
+ error = "",
+ viewMode = "grid",
+ onView,
+ onEdit,
+ onDelete,
+}) => {
+ if (loading || error) {
+ return null;
+ }
+
+ if (!venues.length) {
+ return
No venues found.
;
+ }
+
+ // =========================
+ // LIST VIEW
+ // =========================
+
+ if (viewMode === "list") {
+ return (
+
+ {venues.map((venue) => {
+ const image =
+ venue.images?.[0]?.url ||
+ "https://images.unsplash.com/photo-1519167758481-83f550bb49b3";
+
+ const location = venue.address?.city
+ ? `${venue.address.city}, ${venue.address.state || ""}`.trim()
+ : "Location not available";
+
+ return (
+
+ {/* Image */}
+
+

+
+
+ {/* Details */}
+
+
+
+ {venue.name}
+
+
+
+ {venue.approvalStatus}
+
+
+
+
+ {location}
+
+
+
+ Category: {venue.category}
+
+
+
+
+ {venue.seatingCapacity || 0} seats
+
+
+
+ ₹
+ {venue.pricePerDay ||
+ venue.pricePerHour ||
+ 0}
+
+
+
+ {venue.rating?.toFixed(1) || 0} ★
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+
+
+
+ );
+ })}
+
+ );
+ }
+
+ // =========================
+ // GRID VIEW
+ // =========================
+
+ return (
+
+ {venues.map((venue) => {
+ const image =
+ venue.images?.[0]?.url ||
+ "https://images.unsplash.com/photo-1519167758481-83f550bb49b3";
+
+ const location = venue.address?.city
+ ? `${venue.address.city}, ${venue.address.state || ""}`.trim()
+ : "Location not available";
+
+ return (
+ onView?.(venue.id)}
+ onEdit={() => onEdit?.(venue.id)}
+ onDelete={() => onDelete?.(venue.id)}
+ />
+ );
+ })}
+
+ );
+};
+
+export default VenueGrid;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/venues/VenueHeader.jsx b/client/src/presentation/components/vendor/venues/VenueHeader.jsx
new file mode 100644
index 0000000000..72f1aff8e6
--- /dev/null
+++ b/client/src/presentation/components/vendor/venues/VenueHeader.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+
+const VenueHeader = () => {
+ return (
+
+
+ Venue Management
+
+
+
+ Manage your listed venues
+
+
+ );
+};
+
+export default VenueHeader;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/venues/VenueStats.jsx b/client/src/presentation/components/vendor/venues/VenueStats.jsx
new file mode 100644
index 0000000000..6797ea5585
--- /dev/null
+++ b/client/src/presentation/components/vendor/venues/VenueStats.jsx
@@ -0,0 +1,13 @@
+import React from "react";
+
+const VenueStats = ({ guests, price, bookings }) => {
+ return (
+
+ {guests} Guests
+ ₹{price}/day
+ {bookings} Bookings
+
+ );
+};
+
+export default VenueStats;
\ No newline at end of file
diff --git a/client/src/presentation/components/vendor/venues/ViewToggle.jsx b/client/src/presentation/components/vendor/venues/ViewToggle.jsx
new file mode 100644
index 0000000000..0846a41b53
--- /dev/null
+++ b/client/src/presentation/components/vendor/venues/ViewToggle.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { LayoutGrid, List } from "lucide-react";
+
+const ViewToggle = ({ viewMode = "grid", onToggle }) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default ViewToggle;
\ No newline at end of file
diff --git a/client/src/presentation/layouts/AdminLayout.jsx b/client/src/presentation/layouts/AdminLayout.jsx
new file mode 100644
index 0000000000..e2e4a4fe3a
--- /dev/null
+++ b/client/src/presentation/layouts/AdminLayout.jsx
@@ -0,0 +1,35 @@
+import { Outlet } from "react-router-dom";
+import AdminSidebar from "@/presentation/components/admin/AdminSidebar";
+import AdminHeader from "@/presentation/components/admin/AdminHeader";
+
+const AdminLayout = () => {
+
+ return (
+
+
+
+ {/* Sidebar */}
+
+
+
+ {/* Main Content */}
+
+
+
+
+
+ );
+
+};
+
+export default AdminLayout;
\ No newline at end of file
diff --git a/client/src/presentation/pages/Home.jsx b/client/src/presentation/pages/Home.jsx
new file mode 100644
index 0000000000..934d8c93f3
--- /dev/null
+++ b/client/src/presentation/pages/Home.jsx
@@ -0,0 +1,319 @@
+import Header from "../components/common/Header";
+import Footer from "../components/common/Footer";
+import { useNavigate } from "react-router-dom";
+import { ROUTES } from "@/constants/routes";
+import { VenueCategory } from "@/constants/Venue";
+import { useDispatch, useSelector } from "react-redux";
+import { useEffect } from "react";
+import { getTopVenues } from "@/redux/slices/UserVenueSlice";
+import VenueCard from "../components/common/VenueCard";
+import HeroImage from '@/assets/images/Hero.jpg'
+import {
+ getWishlist,
+ addToWishlist,
+ removeWishlist,
+} from "@/redux/slices/UserWishlistSlice";
+import { toast } from "react-hot-toast";
+
+export default function Home() {
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const { venues } = useSelector((state) => state.userVenue);
+
+ useEffect(() => {
+ dispatch(getTopVenues());
+ }, [dispatch]);
+
+ const { wishlist } = useSelector((state) => state.userWishlist);
+
+ useEffect(() => {
+ dispatch(getWishlist());
+ }, [dispatch]);
+
+ const handleAddWishlist = async (venueId) => {
+ try {
+ await dispatch(addToWishlist(venueId)).unwrap();
+ dispatch(getWishlist());
+ toast.success("Added to wishlist");
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ const handleRemoveWishlist = async (venueId) => {
+ try {
+ await dispatch(removeWishlist(venueId)).unwrap();
+ dispatch(getWishlist());
+ toast.success("Removed from wishlist");
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ const isWishlisted = (venueId) => {
+ return wishlist?.some((item) => item.id === venueId);
+ };
+
+ return (
+
+
+
+ {/* Dark Overlay */}
+
+
+ {/* Content */}
+
+
+ ✨ India's #1 Venue Booking Platform
+
+
+
+ Discover Your Perfect
+
+ Event Venue
+
+
+
+ From intimate gatherings to grand celebrations, find and book
+ the ideal venue for your special moments.
+
+
+
+
+
+
+
+
+ ✓
+ Verified Venues
+
+
+
+ ⭐
+ 4.8 Rating
+
+
+
+ 🏛️
+ 5000+ Venues
+
+
+
+
+
+
+
+ {/* Section Heading */}
+
+
+ Browse by Category
+
+
+
+ Find the Perfect Venue
+
+ For Every Celebration
+
+
+
+
+ Whether you're planning a wedding, corporate event, birthday party,
+ or family gathering, explore venues tailored to every occasion.
+
+
+
+ {/* Categories */}
+
+ {VenueCategory.map((category) => (
+
+
+
+ {category}
+
+
+
+ Discover premium venues for your {category.toLowerCase()} events.
+
+
+ ))}
+
+
+ {/* Button */}
+
+
+
+
+
+
+
+
+ {/* Section Heading */}
+
+
+
+ Featured Collection
+
+
+
+ Featured
+ Venues
+
+
+
+ Explore our handpicked collection of premium venues, carefully
+ selected to make every celebration truly unforgettable.
+
+
+
+
+
+
+ {/* Venue Cards */}
+
+ {venues.map((venue) => (
+
{
+ if (isWishlisted(venue.id)) {
+ handleRemoveWishlist(venue.id);
+ } else {
+ handleAddWishlist(venue.id);
+ }
+ }}
+ />
+ // navigate(`/user/venue/${venue._id}`)}
+ // className=" cursor-pointer bg-white rounded-3xl overflow-hidden border hover:shadow-xl transition">
+ //
+ //

+ //
+ //
+ // {venue.category}
+ //
+ //
+ //
+ //
+ //
+ // ⭐
+ //
+ //
+ // {venue.rating}
+ //
+ // {/*
+ // ({venue.reviews})
+ // */}
+ //
+ //
{venue.name}
+ //
+ // 📍 {venue.address.city}, {venue.address.state}
+ //
+ //
+ //
+ //
+ // Starting from
+ //
+
+ //
+ // ₹{venue.pricePerDay}
+ // /day
+ //
+ //
+
+ //
+ //
+ // Capacity
+ //
+
+ //
+ // {venue.seatingCapacity}
+ // Seating
+ //
+
+ //
+ // {venue.standingCapacity}
+ // Standing
+ //
+ //
+ //
+ //
+ //
+ ))}
+
+
+
+
+
+
+
+
+ Start Your Journey
+
+
+
+ Ready to Host Your
+ Perfect Event?
+
+
+
+ Discover thousands of verified venues across India or showcase your own
+ venue to reach more customers.
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/presentation/pages/admin/.gitkeep b/client/src/presentation/pages/admin/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/pages/admin/AdminLogin.jsx b/client/src/presentation/pages/admin/AdminLogin.jsx
new file mode 100644
index 0000000000..817c4e9c23
--- /dev/null
+++ b/client/src/presentation/pages/admin/AdminLogin.jsx
@@ -0,0 +1,128 @@
+import { useState } from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { useNavigate } from 'react-router-dom'
+import { Mail, Lock, Eye, EyeOff } from 'lucide-react'
+import { login } from "@/redux/slices/AuthSlice"
+import { ROUTES } from '@/constants/routes'
+import { ROLES } from "@/constants/Roles"
+import toast from "react-hot-toast"
+
+const AdminLoginForm = () => {
+ const dispatch = useDispatch()
+ const navigate = useNavigate()
+
+ const [formData, setFormData] = useState({ email: "", password: "" })
+ const [showPassword, setShowPassword] = useState(false)
+
+ const { loading } = useSelector((state) => state.auth)
+
+ const handleChange = (e) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value,
+ })
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+
+ try {
+ const result = await dispatch(
+ login({
+ role: ROLES.ADMIN,
+ data: formData,
+ })
+ ).unwrap()
+
+ console.log("UNWRAPPED RESULT OBJECT:", result)
+
+ // 1. Check if 'admin' object exists in response data, or read role directly
+ const loggedInRole =
+ result?.user?.role ||
+ (result?.data?.admin ? ROLES.ADMIN : undefined) ||
+ result?.data?.role ||
+ result?.role;
+
+ // 2. Validate role against ROLES enum
+ if (loggedInRole === ROLES.ADMIN) {
+ toast.success("Admin login successful!");
+ navigate("/admin/dashboard");
+ } else {
+ toast.error("Access denied: You are not an admin!");
+ navigate(ROUTES.ADMIN.LOGIN);
+ }
+
+ } catch (err) {
+ console.error("Admin Login failed:", err)
+ toast.error(err?.message || err || "Invalid email or password")
+ }
+}
+
+ return (
+
+
+
+
Admin Login
+
Sign in to access the admin dashboard
+
+
+
+
+ )
+}
+
+export default AdminLoginForm
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/BookingDetails.jsx b/client/src/presentation/pages/admin/BookingDetails.jsx
new file mode 100644
index 0000000000..0288a926b0
--- /dev/null
+++ b/client/src/presentation/pages/admin/BookingDetails.jsx
@@ -0,0 +1,151 @@
+
+
+import { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate, useParams } from "react-router-dom";
+
+import { ArrowLeft } from "lucide-react";
+
+import { getBookingById } from "@/redux/slices/AdminBookingSlice";
+
+import { Button } from "@/components/ui/button";
+
+import BookingStatusCard from "@/presentation/components/admin/bookingManagement/BookingStatusCard";
+import UserInfoCard from "@/presentation/components/admin/bookingManagement/UserInfoCard";
+import VendorInfoCard from "@/presentation/components/admin/bookingManagement/VendorInfoCard";
+import VenueInfoCard from "@/presentation/components/admin/bookingManagement/VenueInfoCard";
+import BookingPaymentCard from "@/presentation/components/admin/bookingManagement/BookingPaymentCard";
+import BookingTimelineCard from "@/presentation/components/admin/bookingManagement/BookingTimelineCard";
+import BookingActionCard from "@/presentation/components/admin/bookingManagement/BookingActionCard";
+
+const BookingDetails = () => {
+
+ const { bookingId } = useParams();
+
+ const dispatch = useDispatch();
+
+ const navigate = useNavigate();
+
+ const {
+ selectedBooking,
+ loading,
+ error,
+ } = useSelector((state) => state.adminBooking);
+
+ useEffect(() => {
+
+ if (bookingId) {
+
+ dispatch(getBookingById(bookingId));
+
+ }
+
+ }, [dispatch, bookingId]);
+
+ if (loading) {
+
+ return (
+
+ Loading...
+
+ );
+
+ }
+
+ if (error) {
+
+ return (
+
+ {error}
+
+ );
+
+ }
+
+ if (!selectedBooking) {
+
+ return null;
+
+ }
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+ Booking Details
+
+
+
+ Booking ID : {selectedBooking.id}
+
+
+
+
+
+
+
+
+ {/* Status */}
+
+
+
+ {/* User + Vendor */}
+
+
+
+
+
+
+
+
+
+ {/* Venue */}
+
+
+
+ {/* Payment */}
+
+
+
+ {/* Timeline */}
+
+
+
+ {/* Actions / Reason */}
+
+
+
+
+
+ );
+
+};
+
+export default BookingDetails;
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/BookingManagement.jsx b/client/src/presentation/pages/admin/BookingManagement.jsx
new file mode 100644
index 0000000000..a1f5fa9f17
--- /dev/null
+++ b/client/src/presentation/pages/admin/BookingManagement.jsx
@@ -0,0 +1,120 @@
+import { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate } from "react-router-dom";
+
+import PageHeader from "@/presentation/components/admin/common/PageHeader";
+import BookingFilters from "@/presentation/components/admin/bookingManagement/BookingFilters";
+import BookingStats from "@/presentation/components/admin/bookingManagement/BookingStats";
+import BookingTable from "@/presentation/components/admin/bookingManagement/BookingTable";
+
+import Pagination from "@/presentation/components/common/Pagination";
+
+import useDebounce from "@/hooks/useDebounce";
+
+import {
+ getBookings,
+ getBookingStats,
+} from "@/redux/slices/AdminBookingSlice";
+
+const BookingManagement = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const {
+ bookings,
+ statistics,
+ loading,
+ error,
+ pagination,
+ } = useSelector((state) => state.adminBooking);
+
+ const [search, setSearch] = useState("");
+ const [page, setPage] = useState(1);
+ const [status, setStatus] = useState("");
+ const [paymentStatus, setPaymentStatus] = useState("");
+
+ const limit = 10;
+
+ const debouncedSearch = useDebounce(search, 500);
+
+ useEffect(() => {
+ dispatch(
+ getBookings({
+ search: debouncedSearch,
+ status,
+ paymentStatus,
+ page,
+ limit,
+ })
+ );
+ }, [
+ dispatch,
+ debouncedSearch,
+ status,
+ paymentStatus,
+ page,
+ ]);
+
+ useEffect(() => {
+ dispatch(getBookingStats());
+ }, [dispatch]);
+
+ const handleView = (booking) => {
+ navigate(`/admin/bookings/${booking._id}`);
+ };
+
+ return (
+
+
+
+
+
+
+
+
setSearch(e.target.value)}
+ status={status}
+ onStatusChange={(value) => {
+ setStatus(value);
+ setPage(1);
+ }}
+ paymentStatus={paymentStatus}
+ onPaymentStatusChange={(value) => {
+ setPaymentStatus(value);
+ setPage(1);
+ }}
+ />
+
+ {loading ? (
+
+ Loading...
+
+ ) : error ? (
+
+ {error}
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default BookingManagement;
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/CategoryManagement.jsx b/client/src/presentation/pages/admin/CategoryManagement.jsx
new file mode 100644
index 0000000000..8f3f3897c9
--- /dev/null
+++ b/client/src/presentation/pages/admin/CategoryManagement.jsx
@@ -0,0 +1,11 @@
+import PageHeader from "@/presentation/components/admin/common/PageHeader";
+const CategoryManagement = () => {
+ return (
+
+ )
+
+}
+export default CategoryManagement
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/Dashboard.jsx b/client/src/presentation/pages/admin/Dashboard.jsx
new file mode 100644
index 0000000000..02b23882ad
--- /dev/null
+++ b/client/src/presentation/pages/admin/Dashboard.jsx
@@ -0,0 +1,72 @@
+import { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import PageHeader from "@/presentation/components/admin/common/PageHeader";
+
+import DashboardStats from "@/presentation/components/admin/dashboard/DashboardStats";
+import BookingOverviewChart from "@/presentation/components/admin/dashboard/BookingOverviewChart";
+import RevenueChart from "@/presentation/components/admin/dashboard/RevenueChart";
+
+import { getDashboardStatistics } from "@/redux/slices/AdminDashboardSlice";
+
+const Dashboard = () => {
+ const dispatch = useDispatch();
+
+ const {
+ statistics,
+ loading,
+ error,
+ } = useSelector((state) => state.adminDashboard);
+
+ useEffect(() => {
+ dispatch(getDashboardStatistics());
+ }, [dispatch]);
+
+ if (loading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Dashboard;
+
diff --git a/client/src/presentation/pages/admin/Login.jsx b/client/src/presentation/pages/admin/Login.jsx
new file mode 100644
index 0000000000..b236af0ab4
--- /dev/null
+++ b/client/src/presentation/pages/admin/Login.jsx
@@ -0,0 +1,7 @@
+import AdminLoginForm from "@/presentation/components/admin/auth/AdminLoginForm";
+
+const Login = () => {
+ return
;
+};
+
+export default Login;
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/PaymentDetails.jsx b/client/src/presentation/pages/admin/PaymentDetails.jsx
new file mode 100644
index 0000000000..30a5438f96
--- /dev/null
+++ b/client/src/presentation/pages/admin/PaymentDetails.jsx
@@ -0,0 +1,107 @@
+import { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { ArrowLeft } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useNavigate, useParams } from "react-router-dom";
+import PageHeader from "@/presentation/components/admin/common/PageHeader";
+
+import PaymentSummaryCard from "@/presentation/components/admin/paymentManagement/PaymentSummaryCard";
+import BookingInfoCard from "@/presentation/components/admin/paymentManagement/BookingInfoCard";
+import PaymentInfoCard from "@/presentation/components/admin/paymentManagement/PaymentInfoCard";
+
+import UserInfoCard from "@/presentation/components/admin/bookingManagement/UserInfoCard";
+import VendorInfoCard from "@/presentation/components/admin/bookingManagement/VendorInfoCard";
+
+import { getPaymentById } from "@/redux/slices/AdminPaymentSlice";
+
+const PaymentDetails = () => {
+ const dispatch = useDispatch();
+
+ const { paymentId } = useParams();
+ const navigate = useNavigate();
+
+ const {
+ loading,
+ error,
+ selectedPayment,
+ } = useSelector((state) => state.adminPayment);
+
+ useEffect(() => {
+ if (paymentId) {
+ dispatch(getPaymentById(paymentId));
+ }
+ }, [dispatch, paymentId]);
+
+ if (loading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ if (!selectedPayment) {
+ return (
+
+ Payment not found.
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {/* Payment Summary */}
+
+
+ {/* User & Vendor */}
+
+
+
+
+
+
+
+
+ {/* Booking & Payment */}
+
+
+
+ );
+};
+
+export default PaymentDetails;
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/PaymentManagement.jsx b/client/src/presentation/pages/admin/PaymentManagement.jsx
new file mode 100644
index 0000000000..53a6eb7208
--- /dev/null
+++ b/client/src/presentation/pages/admin/PaymentManagement.jsx
@@ -0,0 +1,121 @@
+import { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate } from "react-router-dom";
+
+import PageHeader from "@/presentation/components/admin/common/PageHeader";
+
+import PaymentStats from "@/presentation/components/admin/paymentManagement/PaymentStats";
+import PaymentFilters from "@/presentation/components/admin/paymentManagement/PaymentFilters";
+import PaymentTable from "@/presentation/components/admin/paymentManagement/PaymentTable";
+
+import Pagination from "@/presentation/components/common/Pagination";
+
+import useDebounce from "@/hooks/useDebounce";
+
+import {
+ getPayments,
+ getPaymentStats,
+} from "@/redux/slices/AdminPaymentSlice";
+
+const PaymentManagement = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const {
+ payments,
+ statistics,
+ loading,
+ error,
+ pagination,
+ } = useSelector((state) => state.adminPayment);
+
+ const [search, setSearch] = useState("");
+ const [page, setPage] = useState(1);
+ const [paymentStatus, setPaymentStatus] = useState("");
+ const [paymentType, setPaymentType] = useState("");
+
+ const limit = 10;
+
+ const debouncedSearch = useDebounce(search, 500);
+
+ useEffect(() => {
+ dispatch(
+ getPayments({
+ search: debouncedSearch,
+ paymentStatus,
+ paymentType,
+ page,
+ limit,
+ })
+ );
+ }, [
+ dispatch,
+ debouncedSearch,
+ paymentStatus,
+ paymentType,
+ page,
+ ]);
+
+ useEffect(() => {
+ dispatch(getPaymentStats());
+ }, [dispatch]);
+
+ const handleView = (payment) => {
+ navigate(`/admin/payments/${payment._id}`);
+ };
+
+ return (
+
+
+
+
+
+
setSearch(e.target.value)}
+ paymentStatus={paymentStatus}
+ onPaymentStatusChange={(value) => {
+ setPaymentStatus(value);
+ setPage(1);
+ }}
+ paymentType={paymentType}
+ onPaymentTypeChange={(value) => {
+ setPaymentType(value);
+ setPage(1);
+ }}
+ />
+
+ {loading ? (
+
+ Loading...
+
+ ) : error ? (
+
+ {error}
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default PaymentManagement;
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/UserManagement.jsx b/client/src/presentation/pages/admin/UserManagement.jsx
new file mode 100644
index 0000000000..636e653114
--- /dev/null
+++ b/client/src/presentation/pages/admin/UserManagement.jsx
@@ -0,0 +1,341 @@
+import { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import PageHeader from "@/presentation/components/admin/common/PageHeader";
+import UserFilters from "@/presentation/components/admin/userManagement/UserFilters";
+import UserTable from "@/presentation/components/admin/userManagement/UserTable";
+
+import Pagination from "@/presentation/components/common/Pagination"
+
+import ViewUserModal from "@/presentation/components/modal/ViewUserModal";
+import ConfirmationModal from "@/presentation/components/modal/ConfirmationModal";
+
+import useDebounce from "@/hooks/useDebounce";
+
+import {
+ getUsers,
+ updateUserStatus,
+} from "@/redux/slices/AdminUserSlice";
+
+const UserManagement = () => {
+
+ const dispatch = useDispatch();
+
+ const {
+ users,
+ loading,
+ error,
+ pagination,
+ } = useSelector((state) => state.adminUser);
+
+ // -----------------------
+ // States
+ // -----------------------
+
+ const [search, setSearch] = useState("");
+
+ const [isBlocked, setIsBlocked] = useState(undefined);
+
+ const [page, setPage] = useState(1);
+
+ const [activeTab, setActiveTab] = useState("all")
+
+ const limit = 10;
+
+ // -----------------------
+ // Debounce
+ // -----------------------
+
+ const debouncedSearch = useDebounce(search, 500);
+
+ // -----------------------
+ // View Modal
+ // -----------------------
+
+ const [selectedUser, setSelectedUser] = useState(null);
+
+ const [isViewModalOpen, setIsViewModalOpen] =
+ useState(false);
+
+ // -----------------------
+ // Confirmation Modal
+ // -----------------------
+
+ const [
+ isConfirmationOpen,
+ setIsConfirmationOpen,
+ ] = useState(false);
+
+ // -----------------------
+ // Fetch Users
+ // -----------------------
+
+ useEffect(() => {
+
+ dispatch(
+
+ getUsers({
+
+ search: debouncedSearch,
+
+ isBlocked,
+
+ page,
+
+ limit,
+
+ })
+
+ );
+
+ }, [
+
+ dispatch,
+
+ debouncedSearch,
+
+ isBlocked,
+
+ page,
+
+ ]);
+
+ // -----------------------
+ // Handlers
+ // -----------------------
+
+ const handleView = (user) => {
+
+ setSelectedUser(user);
+
+ setIsViewModalOpen(true);
+
+ };
+
+ const handleBlock = (user) => {
+
+ setSelectedUser(user);
+
+ setIsConfirmationOpen(true);
+
+ };
+
+ const handleConfirm = async () => {
+
+ await dispatch(
+
+ updateUserStatus({
+
+ userId: selectedUser.id,
+
+ isBlocked: !selectedUser.isBlocked,
+
+ })
+
+ );
+
+ setIsConfirmationOpen(false);
+
+ dispatch(
+
+ getUsers({
+
+ search: debouncedSearch,
+
+ isBlocked,
+
+ page,
+
+ limit,
+
+ })
+
+ );
+
+ };
+console.log("curr click",activeTab)
+ return (
+
+
+
+
+
+
setSearch(e.target.value)}
+
+ status={activeTab}
+
+ onStatusChange={(value) => {
+ console.log("clicked",value)
+
+ setPage(1);
+
+ setActiveTab(value);
+
+ if (value === "all") {
+
+ setIsBlocked(undefined);
+
+ } else if (value === "active") {
+
+ setIsBlocked(false);
+
+ } else {
+
+ setIsBlocked(true);
+
+ }
+
+ }}
+/>
+
+ {
+
+ loading ?
+
+ (
+
+
+
+ Loading...
+
+
+
+ )
+
+ :
+
+ error ?
+
+ (
+
+
+
+ {error}
+
+
+
+ )
+
+ :
+
+ (
+
+ <>
+
+
+
+
+
+ >
+
+ )
+
+ }
+
+ {/* View Modal */}
+
+
+
+ setIsViewModalOpen(false)
+
+ }
+
+ user={selectedUser}
+
+ />
+
+ {/* Confirmation Modal */}
+
+
+
+ setIsConfirmationOpen(false)
+
+ }
+
+ onConfirm={handleConfirm}
+
+ title={
+
+ selectedUser?.isBlocked
+
+ ? "Unblock User"
+
+ : "Block User"
+
+ }
+
+ message={
+
+ selectedUser?.isBlocked
+
+ ? "Are you sure you want to unblock this user?"
+
+ : "Are you sure you want to block this user?"
+
+ }
+
+ confirmText={
+
+ selectedUser?.isBlocked
+
+ ? "Unblock"
+
+ : "Block"
+
+ }
+
+ confirmVariant={
+
+ selectedUser?.isBlocked
+
+ ? "secondary"
+
+ : "destructive"
+
+ }
+
+ />
+
+
+
+ );
+
+};
+
+export default UserManagement;
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/VendorManagement.jsx b/client/src/presentation/pages/admin/VendorManagement.jsx
new file mode 100644
index 0000000000..1b5ec7e3a2
--- /dev/null
+++ b/client/src/presentation/pages/admin/VendorManagement.jsx
@@ -0,0 +1,479 @@
+import { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import PageHeader from "@/presentation/components/admin/common/PageHeader";
+import VendorFilters from "@/presentation/components/admin/vendorManagement/VendorFilters";
+import VendorTable from "@/presentation/components/admin/vendorManagement/VendorTable";
+
+import Pagination from "@/presentation/components/common/Pagination";
+
+import ViewVendorModal from "@/presentation/components/modal/ViewVendorModal";
+import ConfirmationModal from "@/presentation/components/modal/ConfirmationModal";
+
+import useDebounce from "@/hooks/useDebounce";
+
+import {
+ getVendors,
+ approveVendor,
+ rejectVendor,
+ updateVendorStatus,
+} from "@/redux/slices/AdminvendorSlice"
+
+import RejectReasonModal from "@/presentation/components/modal/RejectReasonModal";
+
+const VendorManagement = () => {
+
+ const dispatch = useDispatch();
+
+ const {
+ vendors,
+ loading,
+ error,
+ pagination,
+ } = useSelector((state) => state.adminVendor);
+
+ // ===========================
+ // States
+ // ===========================
+
+ const [search, setSearch] = useState("");
+
+ const [page, setPage] = useState(1);
+
+ const limit = 10;
+
+ const [activeTab, setActiveTab] = useState("all");
+
+ const [approvalStatus, setApprovalStatus] = useState(undefined);
+
+ const [isBlocked, setIsBlocked] = useState(undefined);
+ //rejectmodal
+ const [isRejectModalOpen, setIsRejectModalOpen] = useState(false);
+ const [rejectionReason, setRejectionReason] = useState("");
+
+
+ // ===========================
+ // Debounce
+ // ===========================
+
+ const debouncedSearch = useDebounce(search, 500);
+
+ // ===========================
+ // View Modal
+ // ===========================
+
+ const [selectedVendor, setSelectedVendor] = useState(null);
+
+ const [isViewModalOpen, setIsViewModalOpen] = useState(false);
+
+ // ===========================
+ // Confirmation Modal
+ // ===========================
+
+ const [isConfirmationOpen, setIsConfirmationOpen] =
+ useState(false);
+
+ const [actionType, setActionType] =
+ useState(null);
+
+ // ===========================
+ // Fetch Vendors
+ // ===========================
+
+ useEffect(() => {
+
+ dispatch(
+
+ getVendors({
+
+ search: debouncedSearch,
+
+ approvalStatus,
+
+ isBlocked,
+
+ page,
+
+ limit,
+
+ })
+
+ );
+
+ }, [
+
+ dispatch,
+
+ debouncedSearch,
+
+ approvalStatus,
+
+ isBlocked,
+
+ page,
+
+ ]);
+
+ // ===========================
+ // Handlers
+ // ===========================
+
+ const handleView = (vendor) => {
+
+ setSelectedVendor(vendor);
+
+ setIsViewModalOpen(true);
+
+ };
+
+ const handleApprove = (vendor) => {
+
+ setSelectedVendor(vendor);
+
+ setActionType("approve");
+
+ setIsConfirmationOpen(true);
+
+ };
+
+ const handleReject = (vendor) => {
+
+ setSelectedVendor(vendor);
+
+ setActionType("reject");
+ setRejectionReason("");
+
+ setIsConfirmationOpen(true);
+
+ };
+
+ const handleBlock = (vendor) => {
+
+ setSelectedVendor(vendor);
+
+ setActionType("block");
+
+ setIsConfirmationOpen(true);
+
+ };
+
+ const handleUnblock = (vendor) => {
+console.log("seleVen",selectedVendor);
+ setSelectedVendor(vendor);
+
+ setActionType("unblock");
+
+ setIsConfirmationOpen(true);
+
+ };
+ // ===========================
+ // Confirm Action
+ // ===========================
+
+ const handleConfirm = async () => {
+
+ switch (actionType) {
+
+ case "approve":
+
+ await dispatch(
+ approveVendor(selectedVendor.id)
+ );
+
+ break;
+
+ case "reject":
+ setIsConfirmationOpen(false);
+ setIsRejectModalOpen(true);
+ return;
+
+
+ case "block":
+
+ await dispatch(
+ updateVendorStatus({
+ vendorId: selectedVendor.id,
+ isBlocked: true,
+ })
+ );
+
+ break;
+
+ case "unblock":
+
+ await dispatch(
+ updateVendorStatus({
+
+ vendorId: selectedVendor.id,
+ isBlocked: false,
+ })
+ );
+
+ break;
+
+ default:
+
+ break;
+
+ }
+
+ setIsConfirmationOpen(false);
+
+ dispatch(
+
+ getVendors({
+
+ search: debouncedSearch,
+
+ approvalStatus,
+
+ isBlocked,
+
+ page,
+
+ limit,
+
+ })
+
+ );
+
+
+ };
+const handleRejectSubmit = async (reason) => {
+
+ await dispatch(
+ rejectVendor({
+ vendorId: selectedVendor.id,
+ rejectionReason: reason,
+ })
+ );
+
+ setIsRejectModalOpen(false);
+
+ dispatch(
+ getVendors({
+ search: debouncedSearch,
+ approvalStatus,
+ isBlocked,
+ page,
+ limit,
+ })
+ );
+};
+ console.log("curr click",activeTab)
+ return (
+
+
+
+
+
+
+ setSearch(e.target.value)
+ }
+
+ status={activeTab}
+
+ onStatusChange={(value) => {
+
+ setPage(1);
+
+ setActiveTab(value);
+
+ switch (value) {
+
+ case "all":
+
+ setApprovalStatus(undefined);
+ setIsBlocked(undefined);
+
+ break;
+
+ case "pending":
+
+ setApprovalStatus("PENDING");
+ setIsBlocked(false);
+
+ break;
+
+ case "approved":
+
+ setApprovalStatus("APPROVED");
+ setIsBlocked(false);
+
+ break;
+
+ case "rejected":
+
+ setApprovalStatus("REJECTED");
+ setIsBlocked(false);
+
+ break;
+
+ case "blocked":
+
+ setApprovalStatus("APPROVED");
+ setIsBlocked(true);
+
+ break;
+
+ default:
+
+ break;
+
+ }
+
+ }}
+
+ />
+
+ {
+
+ loading ? (
+
+
+
+ Loading...
+
+
+
+ ) : error ? (
+
+
+
+ {error}
+
+
+
+ ) : (
+
+ <>
+
+
+
+
+
+ >
+
+ )
+
+ }
+ {/* View Vendor Modal */}
+
+ setIsViewModalOpen(false)}
+
+ vendor={selectedVendor}
+
+ />
+
+ {/* Confirmation Modal */}
+
+ setIsConfirmationOpen(false)}
+
+ onConfirm={handleConfirm}
+
+ title={
+ actionType === "approve"
+ ? "Approve Vendor"
+ : actionType === "reject"
+ ? "Reject Vendor"
+ : actionType === "block"
+ ? "Block Vendor"
+ : "Unblock Vendor"
+ }
+
+ message={
+ actionType === "approve"
+ ? "Are you sure you want to approve this vendor?"
+ : actionType === "reject"
+ ? "Are you sure you want to reject this vendor?"
+ : actionType === "block"
+ ? "Are you sure you want to block this vendor?"
+ : "Are you sure you want to unblock this vendor?"
+ }
+
+ confirmText={
+ actionType === "approve"
+ ? "Approve"
+ : actionType === "reject"
+ ? "Reject"
+ : actionType === "block"
+ ? "Block"
+ : "Unblock"
+ }
+
+ confirmVariant={
+ actionType === "approve"
+ ? "default"
+ : actionType === "reject"
+ ? "destructive"
+ : actionType === "block"
+ ? "destructive"
+ : "secondary"
+ }
+
+ />
+ {
+ setIsRejectModalOpen(false);
+ setRejectionReason("");
+ }}
+ reason={rejectionReason}
+ onReasonChange={(e) =>
+ setRejectionReason(e.target.value)
+ }
+ onSubmit={handleRejectSubmit}
+/>
+
+
+
+
+ );
+
+};
+
+export default VendorManagement;
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/VenueDetails.jsx b/client/src/presentation/pages/admin/VenueDetails.jsx
new file mode 100644
index 0000000000..4667b76fb2
--- /dev/null
+++ b/client/src/presentation/pages/admin/VenueDetails.jsx
@@ -0,0 +1,618 @@
+import { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate, useParams } from "react-router-dom";
+
+import { ArrowLeft } from "lucide-react";
+import { isValidElement } from "react";
+import { Button } from "@/components/ui/button";
+
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+
+import { Badge } from "@/components/ui/badge";
+
+import {
+ getVenueById,
+} from "@/redux/slices/AdminVenueSlice";
+
+const AdminVenueDetails = () => {
+console.log("AdminVenueDetails rendered");
+ const dispatch = useDispatch();
+
+ const navigate = useNavigate();
+
+ const { venueId } = useParams();
+console.log("venueId =", venueId);
+ const {
+
+ selectedVenue,
+
+ loading,
+
+ } = useSelector(
+ state => state.adminVenue
+ );
+
+ useEffect(() => {
+console.log("dispatching getVenueById");
+ dispatch(
+ getVenueById(venueId)
+ );
+
+ }, [dispatch, venueId]);
+
+ if (loading) {
+
+ return (
+
+
+
+ Loading...
+
+
+
+ );
+
+}
+
+if (!selectedVenue) {
+
+ return (
+
+
+
+ Venue not found.
+
+
+
+ );
+
+}
+const DetailRow = ({ label, value }) => {
+ let displayValue;
+
+ if (value === null || value === undefined) {
+ displayValue = "-";
+ } else if (isValidElement(value)) {
+ displayValue = value;
+ } else if (typeof value === "object") {
+ displayValue = JSON.stringify(value, null, 2);
+ } else {
+ displayValue = value;
+ }
+
+ return (
+
+
+ {label}
+
+
+
+ {displayValue}
+
+
+ );
+};
+console.log("selectedVenue", selectedVenue);
+return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {selectedVenue.name}
+
+
+
+
+
+ {selectedVenue.category}
+
+
+
+
+
+
+
+ {
+
+ selectedVenue.isBlocked
+
+ ? "Blocked"
+
+ : selectedVenue.approvalStatus
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Venue Images
+
+
+
+
+
+
+
+
+
+ {
+
+ selectedVenue.images?.length > 0 ? (
+
+ selectedVenue.images.map((image, index) => (
+
+

+
+ ))
+
+ ) : (
+
+
+
+ No images available
+
+
+
+ )
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ Basic Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {selectedVenue.websiteUrl}
+
+ ) : (
+ "-"
+ )
+ }
+/>
+
+
+
+
+
+
+
+
+
+
+
+
+ Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{/*
+ Open Location
+
+ ) : (
+ "-"
+ )
+ }
+/>*/}
+
+
+
+
+
+
+ Vendor Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pricing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Capacity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Availability
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Amenities
+
+
+
+
+
+
+
+
+
+ {
+
+ selectedVenue.amenities?.length > 0
+
+ ? selectedVenue.amenities.map((amenity, index) => (
+
+
+ {amenity}
+
+
+ ))
+
+ : (
+
+
+
+ No amenities available
+
+
+
+ )
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ License
+
+
+
+
+
+
+
+ {
+
+ selectedVenue.license?.length > 0 ? (
+
+
+
+ ) : (
+
+
+
+ No License Uploaded
+
+
+
+ )
+
+ }
+
+
+
+
+{
+ selectedVenue.approvalStatus === "REJECTED" && (
+
+
+
+
+
+
+
+ Rejection Reason
+
+
+
+
+
+
+
+
+
+ {selectedVenue.rejectionReason}
+
+
+
+
+
+
+
+ )
+}
+
+
+);
+};
+
+export default AdminVenueDetails;
\ No newline at end of file
diff --git a/client/src/presentation/pages/admin/VenueManagement.jsx b/client/src/presentation/pages/admin/VenueManagement.jsx
new file mode 100644
index 0000000000..647d2b4f58
--- /dev/null
+++ b/client/src/presentation/pages/admin/VenueManagement.jsx
@@ -0,0 +1,487 @@
+import { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useNavigate } from "react-router-dom";
+
+import PageHeader from "@/presentation/components/admin/common/PageHeader";
+import VenueFilters from "@/presentation/components/admin/venueManagement/VenueFilters";
+import VenueTable from "@/presentation/components/admin/venueManagement/VenueTable";
+
+import Pagination from "@/presentation/components/common/Pagination";
+import ConfirmationModal from "@/presentation/components/modal/ConfirmationModal";
+import RejectReasonModal from "@/presentation/components/modal/RejectReasonModal";
+
+import useDebounce from "@/hooks/useDebounce";
+
+import {
+ getVenues,
+ approveVenue,
+ rejectVenue,
+ updateVenueStatus,
+} from "@/redux/slices/AdminVenueSlice";
+console.log("getVenues =", getVenues);
+
+const VenueManagement = () => {
+
+ const dispatch = useDispatch();
+
+ const navigate = useNavigate();
+
+ const {
+
+ venues,
+
+ loading,
+
+ error,
+
+ pagination,
+
+ } = useSelector(
+ (state) => state.adminVenue
+ );
+
+ const [search, setSearch] = useState("");
+
+ const [page, setPage] = useState(1);
+
+ const limit = 10;
+
+ const [activeTab, setActiveTab] = useState("all");
+
+ const [category, setCategory] = useState("");
+
+ const [approvalStatus, setApprovalStatus] =
+ useState(undefined);
+
+ const [isBlocked, setIsBlocked] =
+ useState(undefined);
+
+ const [selectedVenue, setSelectedVenue] =
+ useState(null);
+
+ const [actionType, setActionType] =
+ useState(null);
+
+ const [isConfirmationOpen, setIsConfirmationOpen] =
+ useState(false);
+
+ const [isRejectModalOpen, setIsRejectModalOpen] =
+ useState(false);
+
+ const debouncedSearch =
+ useDebounce(search, 500);
+
+ useEffect(() => {
+console.log("useEffect executed");
+ dispatch(
+
+ getVenues({
+
+ search: debouncedSearch,
+
+ category,
+
+ approvalStatus,
+
+ isBlocked,
+
+ page,
+
+ limit,
+
+ })
+
+ );
+
+ }, [
+
+ dispatch,
+
+ debouncedSearch,
+
+ category,
+
+ approvalStatus,
+
+ isBlocked,
+
+ page,
+
+ ]);
+
+ const handleView = (venue) => {
+
+ navigate(`/admin/venues/${venue.id}`);
+
+};
+
+const handleApprove = (venue) => {
+
+ setSelectedVenue(venue);
+
+ setActionType("approve");
+
+ setIsConfirmationOpen(true);
+
+};
+
+const handleReject = (venue) => {
+
+ setSelectedVenue(venue);
+
+ setActionType("reject");
+
+ setIsConfirmationOpen(true);
+
+};
+
+const handleBlock = (venue) => {
+
+ setSelectedVenue(venue);
+
+ setActionType("block");
+
+ setIsConfirmationOpen(true);
+
+};
+
+const handleUnblock = (venue) => {
+
+ setSelectedVenue(venue);
+
+ setActionType("unblock");
+
+ setIsConfirmationOpen(true);
+
+};
+
+const handleConfirm = async () => {
+
+ switch (actionType) {
+
+ case "approve":
+
+ await dispatch(
+ approveVenue(selectedVenue.id)
+ );
+
+ break;
+
+ case "reject":
+
+ setIsConfirmationOpen(false);
+
+ setIsRejectModalOpen(true);
+
+ return;
+
+ case "block":
+
+ await dispatch(
+ updateVenueStatus({
+ venueId: selectedVenue.id,
+ isBlocked: true,
+ })
+ );
+
+ break;
+
+ case "unblock":
+
+ await dispatch(
+ updateVenueStatus({
+ venueId: selectedVenue.id,
+ isBlocked: false,
+ })
+ );
+
+ break;
+
+ default:
+
+ break;
+ }
+
+ setIsConfirmationOpen(false);
+
+ dispatch(
+ getVenues({
+ search: debouncedSearch,
+ category,
+ approvalStatus,
+ isBlocked,
+ page,
+ limit,
+ })
+ );
+};
+
+const handleRejectSubmit = async (reason) => {
+
+ await dispatch(
+
+ rejectVenue({
+
+ venueId: selectedVenue.id,
+
+ rejectionReason: reason,
+
+ })
+
+ );
+
+ setIsRejectModalOpen(false);
+
+ dispatch(
+
+ getVenues({
+
+ search: debouncedSearch,
+
+ category,
+
+ approvalStatus,
+
+ isBlocked,
+
+ page,
+
+ limit,
+
+ })
+
+ );
+
+};
+return (
+
+
+
+
+
+
setSearch(e.target.value)}
+ status={activeTab}
+ onStatusChange={(value) => {
+
+ setPage(1);
+
+ setActiveTab(value);
+
+ switch (value) {
+
+ case "all":
+
+ setApprovalStatus(undefined);
+ setIsBlocked(undefined);
+
+ break;
+
+ case "pending":
+
+ setApprovalStatus("PENDING");
+ setIsBlocked(false);
+
+ break;
+
+ case "approved":
+
+ setApprovalStatus("ACTIVE");
+ setIsBlocked(false);
+
+ break;
+
+ case "rejected":
+
+ setApprovalStatus("REJECTED");
+ setIsBlocked(false);
+
+ break;
+
+ case "blocked":
+
+ setApprovalStatus("ACTIVE");
+ setIsBlocked(true);
+
+ break;
+
+ default:
+
+ break;
+ }
+
+ }}
+
+ category={category}
+
+ onCategoryChange={(value) => {
+
+ setCategory(value);
+
+ setPage(1);
+
+ }}
+
+ />
+
+ {
+
+ loading ? (
+
+
+
+ Loading...
+
+
+
+ ) : error ? (
+
+
+
+ {error}
+
+
+
+ ) : (
+
+ <>
+
+
+
+
+
+ >
+
+ )
+
+ }
+
+ setIsConfirmationOpen(false)}
+
+ onConfirm={handleConfirm}
+
+ title={
+
+ actionType === "approve"
+
+ ? "Approve Venue"
+
+ : actionType === "reject"
+
+ ? "Reject Venue"
+
+ : actionType === "block"
+
+ ? "Block Venue"
+
+ : "Unblock Venue"
+
+ }
+
+ message={
+
+ actionType === "approve"
+
+ ? "Are you sure you want to approve this venue?"
+
+ : actionType === "reject"
+
+ ? "Are you sure you want to reject this venue?"
+
+ : actionType === "block"
+
+ ? "Are you sure you want to block this venue?"
+
+ : "Are you sure you want to unblock this venue?"
+
+ }
+
+ confirmText={
+
+ actionType === "approve"
+
+ ? "Approve"
+
+ : actionType === "reject"
+
+ ? "Reject"
+
+ : actionType === "block"
+
+ ? "Block"
+
+ : "Unblock"
+
+ }
+
+ confirmVariant={
+
+ actionType === "approve"
+
+ ? "default"
+
+ : actionType === "reject"
+
+ ? "destructive"
+
+ : actionType === "block"
+
+ ? "destructive"
+
+ : "secondary"
+
+ }
+
+ />
+
+ setIsRejectModalOpen(false)}
+
+ onSubmit={handleRejectSubmit}
+
+ title="Reject Venue"
+
+ />
+
+
+
+);
+}
+export default VenueManagement;
\ No newline at end of file
diff --git a/client/src/presentation/pages/auth/ForgotPassword.jsx b/client/src/presentation/pages/auth/ForgotPassword.jsx
new file mode 100644
index 0000000000..47a4a3c77d
--- /dev/null
+++ b/client/src/presentation/pages/auth/ForgotPassword.jsx
@@ -0,0 +1,7 @@
+import ForgotPasswordForm from '@/presentation/components/auth/ForgotPasswordForm'
+// import { ROLES } from "@/constants/Roles";
+
+
+export default function ForgotPassword() {
+ return
+}
diff --git a/client/src/presentation/pages/auth/Login.jsx b/client/src/presentation/pages/auth/Login.jsx
new file mode 100644
index 0000000000..d2c511ce1f
--- /dev/null
+++ b/client/src/presentation/pages/auth/Login.jsx
@@ -0,0 +1,21 @@
+// import Header from "@/presentation/components/common/Header";
+// import Footer from "@/presentation/components/common/Footer";
+import LoginForm from "@/presentation/components/auth/LoginForm";
+import AuthBanner from "@/presentation/components/auth/AuthBanner";
+
+export default function Login() {
+ return (
+ <>
+ {/*
*/}
+
+
+
+
+
+ {/*
*/}
+ >
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/pages/auth/Register.jsx b/client/src/presentation/pages/auth/Register.jsx
new file mode 100644
index 0000000000..d02bf504a4
--- /dev/null
+++ b/client/src/presentation/pages/auth/Register.jsx
@@ -0,0 +1,19 @@
+// import Header from '@/presentation/components/common/Header'
+// import Footer from '@/presentation/components/common/Footer'
+import RegisterForm from '@/presentation/components/auth/RegisterForm'
+import AuthBanner from '@/presentation/components/auth/AuthBanner'
+
+export default function Register() {
+ return (
+ <>
+ {/*
*/}
+
+
+
+ {/*
*/}
+ >
+ )
+}
diff --git a/client/src/presentation/pages/auth/ResetPassword.jsx b/client/src/presentation/pages/auth/ResetPassword.jsx
new file mode 100644
index 0000000000..bea3f061e5
--- /dev/null
+++ b/client/src/presentation/pages/auth/ResetPassword.jsx
@@ -0,0 +1,5 @@
+import ResetPasswordForm from "@/presentation/components/auth/ResetPasswordForm";
+
+export default function ResetPassword() {
+ return
;
+}
\ No newline at end of file
diff --git a/client/src/presentation/pages/auth/VerifyOtp.jsx b/client/src/presentation/pages/auth/VerifyOtp.jsx
new file mode 100644
index 0000000000..b7126f942e
--- /dev/null
+++ b/client/src/presentation/pages/auth/VerifyOtp.jsx
@@ -0,0 +1,11 @@
+import { useLocation } from "react-router-dom";
+import VerifyOtpForm from "@/presentation/components/auth/VerifyOtpForm";
+
+export default function VerifyOtp() {
+ const location = useLocation();
+
+ const email = location.state?.email;
+ const role = location.state?.role;
+
+ return
;
+}
diff --git a/client/src/presentation/pages/user/.gitkeep b/client/src/presentation/pages/user/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/pages/user/BookingDetail.jsx b/client/src/presentation/pages/user/BookingDetail.jsx
new file mode 100644
index 0000000000..16700c33ed
--- /dev/null
+++ b/client/src/presentation/pages/user/BookingDetail.jsx
@@ -0,0 +1,332 @@
+import Header from "@/presentation/components/common/Header";
+import UserSidebar from "@/presentation/components/user/UserSidebar";
+import { ArrowLeft, Calendar, Users, CreditCard, MapPin } from "lucide-react";
+import { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { useParams } from "react-router-dom";
+import { getBookingById, cancelBooking } from "@/redux/slices/UserBookingSlice";
+import Swal from "sweetalert2";
+import toast from "react-hot-toast";
+import { useNavigate } from "react-router-dom";
+import { XCircle } from "lucide-react";
+
+const BookingDetails = () => {
+ const navigate = useNavigate();
+ const { bookingId } = useParams();
+
+ const dispatch = useDispatch();
+
+ const { booking, loading } = useSelector((state) => state.userBooking);
+
+ useEffect(() => {
+ dispatch(getBookingById(bookingId));
+ }, [dispatch, bookingId]);
+
+ const handleCancelBooking = async () => {
+ const result = await Swal.fire({
+ title: "Cancel Booking",
+ text: "Are you sure you want to cancel this booking?",
+ input: "textarea",
+ inputLabel: "Cancellation Reason",
+ inputPlaceholder: "Enter the reason for cancellation...",
+ inputAttributes: {
+ "aria-label": "Cancellation reason",
+ },
+ showCancelButton: true,
+ confirmButtonText: "Cancel Booking",
+ cancelButtonText: "Keep Booking",
+ confirmButtonColor: "#dc2626",
+ cancelButtonColor: "#6b7280",
+ reverseButtons: true,
+
+ inputValidator: (value) => {
+ if (!value) {
+ return "Cancellation reason is required";
+ }
+
+ if (value.trim().length < 10) {
+ return "Please enter at least 10 characters";
+ }
+
+ return null;
+ },
+ });
+
+ if (!result.isConfirmed) return;
+
+ try {
+ await dispatch(
+ cancelBooking({
+ bookingId,
+ cancellationReason: result.value,
+ })
+ ).unwrap();
+
+ toast.success(
+ "Your booking has been cancelled successfully. The refund amount will be credited to your account within 24 hours."
+ );
+
+ dispatch(getBookingById(bookingId));
+ } catch (error) {
+ console.log(error);
+
+ toast.error(
+ typeof error === "string"
+ ? error
+ : error.message || "Failed to cancel booking"
+ );
+ }
+ };
+
+ if (loading) {
+ return (
+ <>
+
+
+
+
+ Loading booking...
+
+
+ >
+ );
+ }
+
+ if (!booking) {
+ return (
+ <>
+
+
+
+
+ Booking not found.
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* Header */}
+
+
+
+
Booking Details
+
+
+ View your booking information and details
+
+
+
+
+ {booking?.status}
+
+
+
+ {/* Card */}
+
+
+ {/* Image */}
+
+

+
+ {/* Booking */}
+
+
+
Booking Information
+
+
+ }
+ label="Booking ID"
+ value={`BMV-${booking.id
+ .slice(0, 6)
+ .toUpperCase()}-${booking.id.slice(-4).toUpperCase()}`}
+ />
+
+ }
+ label="Booking Date"
+ value={new Date(booking.createdAt).toLocaleDateString(
+ "en-GB"
+ )}
+ />
+
+ }
+ label="Event Date"
+ value={new Date(booking.bookingDate).toLocaleDateString(
+ "en-GB"
+ )}
+ />
+
+ }
+ label="Guests"
+ value={`${booking.guestCount} Guests`}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Venue */}
+
+
+
Venue Details
+
+
+
+
+
+
{booking.venueId.name}
+
+
+ {booking.venueId.address.city},{" "}
+ {booking.venueId.address.state}
+
+
+
+
+

+
+
+
About Venue
+
+
{booking?.venue?.description}
+
+
+
+
+ {/* Buttons */}
+
+
+ {booking.status?.toLowerCase() === "cancelled" ? (
+
+
+
+
+
+
+
+
+ Booking Cancelled
+
+
+
+ This booking has been cancelled successfully.
+
+
+ {booking.cancellationReason && (
+
+
+ Cancellation Reason
+
+
+
+ "{booking.cancellationReason}"
+
+
+ )}
+
+
+ If you'd like to reserve this venue again, you can
+ create a new booking anytime.
+
+
+
+
+
+
+ ) : (
+
+
+
+
+ {booking.paymentStatus?.toLowerCase() === "partial" && (
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+function InfoRow({ icon, label, value }) {
+ return (
+
+
+ {icon}
+
+ {label}
+
+
+
{value}
+
+ );
+}
+
+export default BookingDetails;
diff --git a/client/src/presentation/pages/user/BookingHistory.jsx b/client/src/presentation/pages/user/BookingHistory.jsx
new file mode 100644
index 0000000000..439329baf6
--- /dev/null
+++ b/client/src/presentation/pages/user/BookingHistory.jsx
@@ -0,0 +1,120 @@
+import Header from "@/presentation/components/common/Header";
+import UserSidebar from "@/presentation/components/user/UserSidebar";
+import BookingHistoryCard from "@/presentation/components/user/BookingHistoryCard";
+import { useDispatch, useSelector } from "react-redux";
+import { useEffect, useState } from "react";
+import { getBookings } from "@/redux/slices/UserBookingSlice";
+
+const BookingHistory = () => {
+ const dispatch = useDispatch();
+ const [status, setStatus] = useState("");
+ const [page, setPage] = useState(1);
+
+ const { bookings, loading, pagination } = useSelector(
+ (state) => state.userBooking
+ );
+
+ useEffect(() => {
+ dispatch(
+ getBookings({
+ page,
+ limit: 5,
+ status,
+ })
+ );
+ }, [dispatch, page, status]);
+
+ const start = pagination ? (pagination.page - 1) * pagination.limit + 1 : 0;
+
+ const end = pagination
+ ? Math.min(pagination.page * pagination.limit, pagination.total)
+ : 0;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
Booking History
+
+
+ View your past and upcoming bookings
+
+
+
+
+
+
+ {bookings.length === 0 ? (
+
+
📅
+
+
No bookings yet
+
+
+ Book your first venue to see it here.
+
+
+ ) : (
+
+ {bookings.map((booking) => (
+
+ ))}
+
+ )}
+
+ {bookings.length > 0 && (
+ <>
+
+
+
+
+ Page {pagination.page} of {pagination.totalPages}
+
+
+
+
+
+
+ Showing {start} to {end} of {pagination.total} bookings
+
+ >
+ )}
+
+
+
+ >
+ );
+};
+
+export default BookingHistory;
diff --git a/client/src/presentation/pages/user/BookingSummary.jsx b/client/src/presentation/pages/user/BookingSummary.jsx
new file mode 100644
index 0000000000..aa9b817145
--- /dev/null
+++ b/client/src/presentation/pages/user/BookingSummary.jsx
@@ -0,0 +1,358 @@
+import { useLocation, useNavigate } from "react-router-dom";
+import { useDispatch, useSelector } from "react-redux";
+
+import { ROUTES } from "@/constants/routes";
+import { reserveBooking } from "@/redux/slices/UserBookingSlice";
+
+import Header from "@/presentation/components/common/Header";
+import Footer from "@/presentation/components/common/Footer";
+
+import { formatDateToDDMMYYYY } from "@/lib/utils";
+
+export default function BookingSummary() {
+ const { state } = useLocation();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+
+ const { loading, error } = useSelector(
+ (state) => state.userBooking
+ );
+
+
+
+ // ======================================
+ // NO BOOKING DATA
+ // ======================================
+
+ if (!state) {
+ return (
+ <>
+
+
+
+ Booking details not found.
+
+
+
+ >
+ );
+ }
+
+ const {
+ venue,
+ bookingDate,
+ bookingType,
+ startTime,
+ endTime,
+ guestCount,
+ } = state;
+
+ // ======================================
+ // VENUE ID
+ // ======================================
+
+ const venueId = venue?._id || venue?.id;
+
+ // ======================================
+ // NORMALIZED DATE
+ // ======================================
+
+ const normalizedDate = new Date(bookingDate)
+ .toISOString()
+ .split("T")[0];
+
+ // ======================================
+ // DAILY BOOKING TIME
+ // ======================================
+
+ const finalStartTime =
+ bookingType === "daily"
+ ? "00:00"
+ : startTime;
+
+ const finalEndTime =
+ bookingType === "daily"
+ ? "23:59"
+ : endTime;
+
+ // ======================================
+ // BOOKING DATA
+ // ======================================
+
+ const bookingData = {
+ venueId,
+ bookingDate: normalizedDate,
+ bookingType,
+ startTime: finalStartTime,
+ endTime: finalEndTime,
+ guestCount: Number(guestCount),
+ };
+
+ // ======================================
+ // PROCEED TO PAYMENT
+ // ======================================
+
+ const handleProceedToPayment = async () => {
+ if (
+ !venueId ||
+ !normalizedDate ||
+ !bookingType ||
+ !guestCount
+ ) {
+ alert(
+ "Missing booking details. Please go back and fill all fields."
+ );
+ return;
+ }
+
+ try {
+ const reservation = await dispatch(
+ reserveBooking(bookingData)
+ ).unwrap();
+
+ console.log(
+ "Reservation created:",
+ reservation
+ );
+
+ navigate(
+ ROUTES.USER.PAYMENT,
+ {
+ state: {
+ venue,
+
+ bookingDate: normalizedDate,
+
+ bookingType,
+
+ startTime: finalStartTime,
+
+ endTime: finalEndTime,
+
+ guestCount: Number(guestCount),
+
+ // Reservation ID
+ reservationId:
+ reservation.reservationId,
+
+ // Actual server-calculated amounts
+ totalAmount:
+ reservation.totalAmount,
+
+ advanceAmount:
+ reservation.advanceAmount,
+
+ remainingAmount:
+ reservation.remainingAmount,
+
+ expiresAt:
+ reservation.expiresAt,
+ },
+ }
+ );
+ } catch (error) {
+ console.error(
+ "Reservation failed:",
+ error
+ );
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ Booking Summary
+
+
+
+
+ {/* ============================== */}
+ {/* BOOKING DETAILS */}
+ {/* ============================== */}
+
+
+
+
+ Booking Details
+
+
+ {/* VENUE */}
+
+
+
+

+
+
+
+
+ {venue.name}
+
+
+
+ 📍{" "}
+ {venue.address?.city},{" "}
+ {venue.address?.state}
+
+
+
+
+
+
+ {/* BOOKING INFORMATION */}
+
+
+
+ {/* DATE */}
+
+
+
+ Event Date
+
+
+
+ {formatDateToDDMMYYYY(
+ normalizedDate
+ )}
+
+
+
+ {/* GUESTS */}
+
+
+
+ Guests
+
+
+
+ {guestCount}
+
+
+
+ {/* BOOKING TYPE */}
+
+
+
+ Booking Type
+
+
+
+ {bookingType === "daily"
+ ? "Full Day"
+ : "Hour Wise"}
+
+
+
+ {/* START TIME */}
+
+
+
+ Start Time
+
+
+
+ {finalStartTime}
+
+
+
+ {/* END TIME */}
+
+
+
+ End Time
+
+
+
+ {finalEndTime}
+
+
+
+
+
+
+
+ {/* ============================== */}
+ {/* PRICE SUMMARY */}
+ {/* ============================== */}
+
+
+
+
+ Price Summary
+
+
+
+
+
+
+ Booking Type
+
+
+
+ {bookingType === "daily"
+ ? "Full Day"
+ : "Hourly"}
+
+
+
+
+
+ Guest Count
+
+
+
+ {guestCount}
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ The final booking amount will be
+ calculated by the server after
+ checking the venue pricing and charges.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/pages/user/BrowseVenue.jsx b/client/src/presentation/pages/user/BrowseVenue.jsx
new file mode 100644
index 0000000000..85649fe95e
--- /dev/null
+++ b/client/src/presentation/pages/user/BrowseVenue.jsx
@@ -0,0 +1,312 @@
+import Header from "@/presentation/components/common/Header";
+import Footer from "@/presentation/components/common/Footer";
+import VenueCard from "@/presentation/components/common/VenueCard";
+import { Search } from "lucide-react";
+import { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { getVenues } from "@/redux/slices/UserVenueSlice";
+import { Amenities, Ratings, VenueCategory } from "@/constants/Venue";
+import {
+ getWishlist,
+ addToWishlist,
+ removeWishlist,
+} from "@/redux/slices/UserWishlistSlice";
+import { toast } from "react-hot-toast";
+
+export default function BrowseVenues() {
+ const dispatch = useDispatch()
+ const [search, setSearch] = useState('')
+ const [page, setPage] = useState(1)
+ const [selectedAmenities, setSelectedAmenities] = useState([])
+ const [selectedCategory, setSelectedCategory] = useState('')
+ const [selectedRating, setSelectedRating] = useState(0)
+ const [capacityType, setCapacityType] = useState("")
+ const [capacity, setCapacity] = useState("")
+ const [priceType, setPriceType] = useState("")
+ const [minPrice, setMinPrice] = useState("")
+ const [maxPrice, setMaxPrice] = useState("")
+ const [appliedFilters, setAppliedFilters] = useState({
+ amenities: [],
+ capacityType: "",
+ capacity: "",
+ priceType: "",
+ minPrice: "",
+ maxPrice: ""
+ })
+
+ const { venues, pagination } = useSelector((state) => state.userVenue);
+ const { wishlist } = useSelector((state) => state.userWishlist);
+
+ useEffect(() => {
+ dispatch(getVenues({
+ search,
+ amenities: appliedFilters.amenities,
+ category: selectedCategory,
+ rating: selectedRating,
+ capacityType: appliedFilters.capacityType,
+ capacity: appliedFilters.capacity,
+ priceType: appliedFilters.priceType,
+ minPrice: appliedFilters.minPrice,
+ maxPrice: appliedFilters.maxPrice,
+ page,
+ limit: 12
+ }))
+ }, [dispatch, search, appliedFilters, selectedCategory, selectedRating, page])
+
+ useEffect(() => {
+ dispatch(getWishlist());
+ }, [dispatch]);
+
+ const handleAddWishlist = async (venueId) => {
+ try {
+ await dispatch(addToWishlist(venueId)).unwrap();
+ dispatch(getWishlist());
+ toast.success("Added to wishlist");
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ const handleRemoveWishlist = async (venueId) => {
+ try {
+ await dispatch(removeWishlist(venueId)).unwrap();
+ dispatch(getWishlist());
+ toast.success("Removed from wishlist");
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ const isWishlisted = (venueId) => {
+ return wishlist?.some((item) => item.id === venueId);
+ };
+
+ return (
+ <>
+
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ Filter Venues
+
+
+ Price Type
+
+
+
+
+
+ {priceType &&(
+
+ setMinPrice(e.target.value)}
+ placeholder="Min"
+ className="border bg-gray-50 p-3 rounded-xl w-full"
+ />
+ setMaxPrice(e.target.value)}
+ placeholder="Max"
+ className="border bg-gray-50 p-3 rounded-xl w-full"
+ />
+
+ )}
+
+
+ Capacity Type
+
+
+
+
+
+ {capacityType && (
+
+ )}
+
Amenities
+ {Amenities.map(item=>(
+
+ ))}
+
+
+
+
+
+
+ {venues.map((venue) => (
+ {
+ if (isWishlisted(venue.id)) {
+ handleRemoveWishlist(venue.id);
+ } else {
+ handleAddWishlist(venue.id);
+ }
+ }}
+ />
+ ))}
+
+
+ {Array.from({length: pagination.venues.totalPages},(_,index)=>index+1).map((number)=>(
+
+ ))}
+
+
+
+
+ >
+)}
+
diff --git a/client/src/presentation/pages/user/ChangePassword.jsx b/client/src/presentation/pages/user/ChangePassword.jsx
new file mode 100644
index 0000000000..a256ecd484
--- /dev/null
+++ b/client/src/presentation/pages/user/ChangePassword.jsx
@@ -0,0 +1,44 @@
+import Header from "@/presentation/components/common/Header";
+import UserSidebar from "@/presentation/components/user/UserSidebar";
+import ChangePasswordForm from "@/presentation/components/user/ChangePasswordForm";
+import PasswordRequirements from "@/presentation/components/user/PasswordRequirements";
+
+const ChangePassword = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Change Password
+
+
+
+ Update your password to keep your account secure.
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ChangePassword;
\ No newline at end of file
diff --git a/client/src/presentation/pages/user/Payment.jsx b/client/src/presentation/pages/user/Payment.jsx
new file mode 100644
index 0000000000..6b9ab89e75
--- /dev/null
+++ b/client/src/presentation/pages/user/Payment.jsx
@@ -0,0 +1,627 @@
+import { useState } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+import { useDispatch, useSelector } from "react-redux";
+
+import { ROUTES } from "@/constants/routes";
+import Header from "@/presentation/components/common/Header";
+import Footer from "@/presentation/components/common/Footer";
+import { formatDateToDDMMYYYY } from "@/lib/utils";
+
+import { confirmBooking } from "@/redux/slices/UserBookingSlice";
+
+import toast from "react-hot-toast";
+
+export default function Payment() {
+ const { state } = useLocation();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ const { loading, error } = useSelector(
+ (state) => state.userBooking
+ );
+
+ const [paymentMethod, setPaymentMethod] =
+ useState("online");
+
+ // User can choose advance or full payment
+ const [paymentOption, setPaymentOption] =
+ useState("advance");
+
+ const [isProcessing, setIsProcessing] =
+ useState(false);
+
+ // ==============================
+ // HANDLE MISSING STATE
+ // ==============================
+
+ if (!state) {
+ return (
+ <>
+
+
+
+
+ Booking details not found.
+
+
+
+
+ >
+ );
+ }
+
+ // ==============================
+ // GET DATA FROM BOOKING SUMMARY
+ // ==============================
+
+ const {
+ venue,
+ selectedPackage,
+
+ bookingType,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+
+ // Important values from reservation
+ reservationId,
+ totalAmount,
+ advanceAmount,
+ remainingAmount,
+ expiresAt,
+ } = state;
+
+ // ==============================
+ // VENUE ID
+ // ==============================
+
+ const venueId =
+ venue?._id || venue?.id;
+
+ // ==============================
+ // NORMALIZE DATE
+ // ==============================
+
+ const normalizedDate =
+ new Date(bookingDate)
+ .toISOString()
+ .split("T")[0];
+
+ // ==============================
+ // SELECTED PAYMENT AMOUNT
+ // ==============================
+
+ const selectedPaymentAmount =
+ paymentOption === "full"
+ ? totalAmount
+ : advanceAmount;
+
+ // ==============================
+ // HANDLE PAYMENT SUCCESS
+ // ==============================
+
+ const handlePaymentSuccess = async () => {
+ if (!reservationId) {
+ toast.error(
+ "Reservation not found. Please go back and try again."
+ );
+ return;
+ }
+
+ if (!venueId) {
+ toast.error("Venue ID missing.");
+ return;
+ }
+
+ try {
+ setIsProcessing(true);
+
+ // ==================================
+ // CONFIRM EXISTING RESERVATION
+ // ==================================
+
+ const confirmedBooking =
+ await dispatch(
+ confirmBooking({
+ reservationId,
+ venueId,
+ bookingDate: normalizedDate,
+
+ // Send payment choice if your backend
+ // supports it
+ paymentOption,
+
+ paymentMethod,
+ })
+ ).unwrap();
+
+ console.log(
+ "Confirmed booking:",
+ confirmedBooking
+ );
+
+ toast.success(
+ "Booking confirmed successfully!"
+ );
+
+ // ==================================
+ // NAVIGATE TO SUCCESS PAGE
+ // ==================================
+
+ navigate(
+ ROUTES.USER.PAYMENT_SUCCESS,
+ {
+ state: {
+ venue,
+ selectedPackage,
+
+ bookingType,
+ bookingDate: normalizedDate,
+
+ startTime,
+ endTime,
+ guestCount,
+
+ reservationId,
+
+ totalAmount,
+ advanceAmount,
+ remainingAmount,
+
+ // Which amount user selected
+ paymentOption,
+
+ // Actual amount paid
+ paidAmount:
+ selectedPaymentAmount,
+
+ paymentMethod,
+
+ expiresAt,
+
+ paymentStatus: "success",
+ },
+ }
+ );
+ } catch (error) {
+ console.error(
+ "Booking confirmation failed:",
+ error
+ );
+
+ toast.error(
+ error?.message ||
+ "Booking confirmation failed."
+ );
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ // ==============================
+ // HANDLE PAYMENT FAILURE
+ // ==============================
+
+ const handlePaymentFailure = () => {
+ navigate(
+ ROUTES.USER.PAYMENT_FAILURE,
+ {
+ state: {
+ venue,
+ selectedPackage,
+
+ bookingType,
+
+ bookingDate: normalizedDate,
+
+ startTime,
+ endTime,
+
+ guestCount,
+
+ paymentMethod,
+
+ paymentOption,
+
+ totalAmount,
+ advanceAmount,
+ remainingAmount,
+
+ reservationId,
+
+ paymentStatus: "failure",
+ },
+ }
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ {/* PAGE TITLE */}
+
+
+ Payment
+
+
+
+
+ {/* ============================== */}
+ {/* PAYMENT SECTION */}
+ {/* ============================== */}
+
+
+
+
+ Choose Payment Method
+
+
+ {/* ============================== */}
+ {/* PAYMENT METHOD */}
+ {/* ============================== */}
+
+ {/* ONLINE PAYMENT */}
+
+
+
+
+
+ {/* CARD PAYMENT */}
+
+
+
+
+
+ {/* ============================== */}
+ {/* PAYMENT AMOUNT */}
+ {/* ============================== */}
+
+
+ Choose Amount to Pay
+
+
+ {/* ADVANCE PAYMENT */}
+
+
+ setPaymentOption("advance")
+ }
+ >
+
+
+
+ Remaining balance: ₹
+ {remainingAmount}
+
+
+
+
+ {/* FULL PAYMENT */}
+
+
+ setPaymentOption("full")
+ }
+ >
+
+
+
+ No remaining balance
+
+
+
+
+ {/* ============================== */}
+ {/* ERROR */}
+ {/* ============================== */}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* ============================== */}
+ {/* ACTION BUTTONS */}
+ {/* ============================== */}
+
+
+
+ {/* PAYMENT SUCCESS */}
+
+
+
+ {/* PAYMENT FAILURE */}
+
+
+
+
+
+
+
+ {/* ============================== */}
+ {/* BOOKING SUMMARY */}
+ {/* ============================== */}
+
+
+
+
+ Booking Summary
+
+
+ {/* VENUE */}
+
+
+ {venue?.name}
+
+
+ {/* DATE */}
+
+
+ {formatDateToDDMMYYYY(
+ normalizedDate
+ )}
+
+
+ {/* BOOKING TYPE */}
+
+
+
+ Booking Type:{" "}
+
+
+
+ {bookingType === "daily"
+ ? "Full Day"
+ : "Hour Wise"}
+
+
+
+
+
+ {/* TIME */}
+
+ {bookingType === "daily" ? (
+
+
+ Full day venue booking
+
+
+ ) : (
+
+
+ {startTime} - {endTime}
+
+
+ )}
+
+ {/* GUEST COUNT */}
+
+
+ {guestCount} guests
+
+
+ {/* ============================== */}
+ {/* PRICE SUMMARY */}
+ {/* ============================== */}
+
+
+
+
+
+
+ Total Amount
+
+
+
+ ₹{totalAmount}
+
+
+
+
+
+
+
+ Advance Amount
+
+
+
+ ₹{advanceAmount}
+
+
+
+
+
+
+
+ Remaining Balance
+
+
+
+ ₹{remainingAmount}
+
+
+
+
+
+
+ {/* SELECTED PAYMENT */}
+
+
+
+
+
+
+ Pay Now
+
+
+
+ ₹{selectedPaymentAmount}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/pages/user/PaymentFailure.jsx b/client/src/presentation/pages/user/PaymentFailure.jsx
new file mode 100644
index 0000000000..45a52384b5
--- /dev/null
+++ b/client/src/presentation/pages/user/PaymentFailure.jsx
@@ -0,0 +1,88 @@
+import { useLocation, useNavigate } from "react-router-dom";
+import { ROUTES } from "@/constants/routes";
+import Header from "@/presentation/components/common/Header";
+import Footer from "@/presentation/components/common/Footer";
+
+export default function PaymentFailure() {
+ const { state } = useLocation();
+ const navigate = useNavigate();
+
+ const {
+ venue,
+ selectedPackage,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+ totalAmount,
+ } = state || {};
+
+ return (
+ <>
+
+
+
+
+
+
+ ❌
+
+
+
+ Payment Failed
+
+
+
+ We couldn't process your payment.
+ Please try again.
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/pages/user/PaymentSuccess.jsx b/client/src/presentation/pages/user/PaymentSuccess.jsx
new file mode 100644
index 0000000000..5258652313
--- /dev/null
+++ b/client/src/presentation/pages/user/PaymentSuccess.jsx
@@ -0,0 +1,146 @@
+import { useLocation, useNavigate } from "react-router-dom";
+import { ROUTES } from "@/constants/routes";
+import Header from "@/presentation/components/common/Header";
+import Footer from "@/presentation/components/common/Footer";
+import { formatDateToDDMMYYYY } from "@/lib/utils";
+
+export default function PaymentSuccess() {
+ const { state } = useLocation();
+ const navigate = useNavigate();
+
+ if (!state) {
+ return (
+ <>
+
+
+
+ Payment details not found.
+
+
+
+ >
+ );
+ }
+
+ const {
+ venue,
+ selectedPackage,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+ totalAmount,
+ } = state;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ ✅
+
+
+
+ Payment Successful!
+
+
+
+ Your booking payment has been successfully processed.
+
+
+
+
+
+ Booking Details
+
+
+
+ {venue.name}
+
+
+
+ 📍 {venue.address?.city},{" "}
+ {venue.address?.state}
+
+
+
+
+
+
+ Event Date
+
+
+
+ {formatDateToDDMMYYYY(bookingDate)}
+
+
+
+
+
+ Guests
+
+
+
+ {guestCount}
+
+
+
+
+
+ Time
+
+
+
+ {startTime} - {endTime}
+
+
+
+
+
+ Amount Paid
+
+
+
+ ₹{totalAmount}
+
+
+
+
+
+ {selectedPackage && (
+
+
+ Package
+
+
+
+ {selectedPackage.name}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/pages/user/UserProfile.jsx b/client/src/presentation/pages/user/UserProfile.jsx
new file mode 100644
index 0000000000..27c067d13e
--- /dev/null
+++ b/client/src/presentation/pages/user/UserProfile.jsx
@@ -0,0 +1,204 @@
+import { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import toast from "react-hot-toast";
+import Swal from "sweetalert2";
+
+import Header from "@/presentation/components/common/Header";
+import UserSidebar from "@/presentation/components/user/UserSidebar";
+import UserProfileImage from "@/presentation/components/user/UserProfileImage";
+import UserProfileInformation from "@/presentation/components/user/UserProfileInformation";
+import UserEditProfileForm from "@/presentation/components/user/UserEditProfileForm";
+
+import {
+ getProfile,
+ requestEmailChangeOtp,
+ resendEmailOtp,
+ updateProfile,
+ updateProfileImage,
+ verifyEmailOtp,
+ removeProfileImage,
+} from "@/redux/slices/UserProfileSlice";
+
+const UserProfile = () => {
+ const dispatch = useDispatch();
+
+ const { user, loading, error } = useSelector((state) => state.userProfile);
+
+ const [isEditing, setIsEditing] = useState(false);
+
+ useEffect(() => {
+ dispatch(getProfile());
+ }, [dispatch]);
+
+ const handleSave = async (formData) => {
+ try {
+ const payload = {
+ fullName: formData.name,
+ phone: formData.phone,
+ };
+
+ await dispatch(updateProfile(payload)).unwrap();
+
+ toast.success("Profile updated successfully");
+ setIsEditing(false);
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ const handleImageChange = async (file) => {
+ try {
+ await dispatch(updateProfileImage(file)).unwrap();
+
+ toast.success("Profile picture updated successfully");
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ const handleRemoveImage = async () => {
+ const result = await Swal.fire({
+ title: "Remove Profile Photo?",
+ text: "Are you sure you want to remove your profile photo?",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#f59e0b", // Amber
+ cancelButtonColor: "#6b7280", // Gray
+ confirmButtonText: "Yes, Remove",
+ cancelButtonText: "Cancel",
+ reverseButtons: true,
+ });
+
+ if (!result.isConfirmed) return;
+
+ try {
+ await dispatch(removeProfileImage()).unwrap();
+
+ toast.success("Profile image removed");
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ const handleRequestEmailOtp = async (newEmail) => {
+ try {
+ await dispatch(requestEmailChangeOtp(newEmail)).unwrap();
+
+ toast.success("OTP sent to your email");
+ } catch (error) {
+ toast.error(error);
+ throw error;
+ }
+ };
+
+ const handleVerifyOtp = async (otp) => {
+ try {
+ await dispatch(verifyEmailOtp(otp)).unwrap();
+
+ toast.success("Email updated successfully");
+
+ await dispatch(getProfile());
+
+ setIsEditing(false);
+ } catch (error) {
+ toast.error(typeof error === "string" ? error : "Failed to update email");
+ }
+ };
+
+ const handleResendOtp = async () => {
+ try {
+ await dispatch(resendEmailOtp()).unwrap();
+
+ toast.success("OTP resent successfully");
+ } catch (error) {
+ toast.error(error);
+ throw error;
+ }
+ };
+
+ if (loading) {
+ return (
+ <>
+
+
+
+
+
+
+ Loading Profile...
+
+
+
+ >
+ );
+ }
+
+ if (error) {
+ return (
+ <>
+
+
+
+
+
+ {error}
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {/* Left Section */}
+
+
+
+
+ {/* Right Section */}
+
+ {isEditing ? (
+ setIsEditing(false)}
+ onRequestEmailOtp={handleRequestEmailOtp}
+ onVerifyOtp={handleVerifyOtp}
+ onResendOtp={handleResendOtp}
+ />
+ ) : (
+ setIsEditing(true)}
+ onAccountSettings={() => console.log("Account Settings")}
+ />
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default UserProfile;
diff --git a/client/src/presentation/pages/user/VenueDetails.jsx b/client/src/presentation/pages/user/VenueDetails.jsx
new file mode 100644
index 0000000000..f030b9594e
--- /dev/null
+++ b/client/src/presentation/pages/user/VenueDetails.jsx
@@ -0,0 +1,104 @@
+import { useEffect,useState } from "react";
+import { useParams } from "react-router-dom";
+import { useDispatch, useSelector } from "react-redux";
+import VenueReviews from "@/presentation/components/user/venueDetails/VenueReviews";
+import Header from "@/presentation/components/common/Header";
+import Footer from "@/presentation/components/common/Footer";
+import VenueAmenities from "@/presentation/components/user/venueDetails/VenueAmenities";
+import VenueGallery from "@/presentation/components/user/venueDetails/VenueGallery";
+import VenueHeader from "@/presentation/components/user/venueDetails/VenueHeader";
+import VenueAbout from "@/presentation/components/user/venueDetails/VenueAbout";
+import { getVenueById } from "@/redux/slices/UserVenueSlice";
+import BookingCard from "@/presentation/components/user/venueDetails/BookingCard";
+import SimilarVenues from "@/presentation/components/user/venueDetails/SimilarVenues";
+import VenueAvailability from "@/presentation/components/user/venueDetails/VenueAvailability";
+import HostedBy from "@/presentation/components/user/venueDetails/HostedBy";
+import { similarVenues } from "@/constants/mockVenues";
+import CancellationPolicy from "@/presentation/components/user/venueDetails/CancellationPolicy";
+
+export default function VenueDetails() {
+ const [selectedPackage, setSelectedPackage] = useState(null);
+
+ const [availability, setAvailability] = useState({
+ eventDate: "",
+ startTime: "",
+ endTime: "",
+ guestCount: "",
+ });
+ const { id } = useParams();
+ const dispatch = useDispatch();
+
+ const {
+ selectedVenue,
+ loading,
+ error,
+ } = useSelector((state) => state.userVenue);
+
+ useEffect(() => {
+ if (id) {
+ dispatch(getVenueById(id));
+ }
+ }, [dispatch, id]);
+
+ return (
+ <>
+
+
+
+
+
+ {loading && (
+
+ Loading venue...
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {!loading && !error && !selectedVenue && (
+
+ Venue not found
+
+ )}
+
+ {!loading && !error && selectedVenue && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/client/src/presentation/pages/user/Wishlist.jsx b/client/src/presentation/pages/user/Wishlist.jsx
new file mode 100644
index 0000000000..533077885e
--- /dev/null
+++ b/client/src/presentation/pages/user/Wishlist.jsx
@@ -0,0 +1,133 @@
+import { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import toast from "react-hot-toast";
+import { useNavigate } from "react-router-dom";
+
+import Header from "@/presentation/components/common/Header";
+import UserSidebar from "@/presentation/components/user/UserSidebar";
+import VenueCard from "@/presentation/components/common/VenueCard";
+
+import { getWishlist, removeWishlist } from "@/redux/slices/UserWishlistSlice";
+
+const Wishlist = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const { wishlist, loading, error } = useSelector(
+ (state) => state.userWishlist
+ );
+
+ useEffect(() => {
+ dispatch(getWishlist());
+ }, [dispatch]);
+
+ const handleRemoveWishlist = async (venueId) => {
+ try {
+ await dispatch(removeWishlist(venueId)).unwrap();
+
+ toast.success("Removed from wishlist");
+ } catch (error) {
+ toast.error(error);
+ }
+ };
+
+ if (loading) {
+ return (
+ <>
+
+
+
+
+
+
+
+ Loading Wishlist...
+
+
+
+ >
+ );
+ }
+
+ if (error) {
+ return (
+ <>
+
+
+
+
+
+
+ {error}
+
+
+ >
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {/* Header */}
+
+
+
My Wishlist
+
+
+ Venues you love and want to book later
+
+
+
+
+ {wishlist.length} Venues
+
+
+
+ {/* Empty Wishlist */}
+ {wishlist.length === 0 ? (
+
+
🤍
+
+
+ Your wishlist is empty
+
+
+
+ Save your favourite venues here.
+
+
+
+
+ ) : (
+
+ {wishlist.map((venue) => (
+
+ ))}
+
+ )}
+
+
+
+ >
+ );
+};
+
+export default Wishlist;
diff --git a/client/src/presentation/pages/vendor/.gitkeep b/client/src/presentation/pages/vendor/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/presentation/pages/vendor/AddVenue.jsx b/client/src/presentation/pages/vendor/AddVenue.jsx
new file mode 100644
index 0000000000..d48a806551
--- /dev/null
+++ b/client/src/presentation/pages/vendor/AddVenue.jsx
@@ -0,0 +1,383 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import VendorSidebar from "@/presentation/components/vendor/VendorSidebar";
+import VendorNavbar from "@/presentation/components/vendor/VendorNavbar";
+import { ROUTES } from "@/constants/routes";
+
+import AddVenueHeader from "@/presentation/components/vendor/addVenue/AddVenueHeader";
+import VenueDetailsForm from "@/presentation/components/vendor/addVenue/VenueDetailsForm";
+import AmenitiesForm from "@/presentation/components/vendor/addVenue/AmenitiesForm";
+import PricingForm from "@/presentation/components/vendor/addVenue/PricingForm";
+import ReviewForm from "@/presentation/components/vendor/addVenue/ReviewForm";
+
+import { useDispatch, useSelector } from "react-redux";
+import toast from "react-hot-toast";
+
+import {
+ fetchVendorProfile,
+ createVenue,
+ clearVenueState,
+} from "@/redux/slices/VendorVenueSlice";
+
+import { createVenueSchema } from "@/lib/validation/venueValidation";
+
+const AddVenue = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ // ==============================
+ // FORM STATE
+ // ==============================
+
+ const [venueName, setVenueName] = useState("");
+ const [category, setCategory] = useState("");
+ const [description, setDescription] = useState("");
+
+ const [addressLine1, setAddressLine1] = useState("");
+ const [city, setCity] = useState("");
+ const [state, setState] = useState("");
+ const [country, setCountry] = useState("");
+
+ const [phone, setPhone] = useState("");
+ const [pincode, setPincode] = useState("");
+
+ const [websiteUrl, setWebsiteUrl] = useState("");
+ const [googleMapLink, setGoogleMapLink] = useState("");
+
+ const [images, setImages] = useState([]);
+ const [amenities, setAmenities] = useState([]);
+
+ const [license, setLicense] = useState(null);
+
+ const [pricing, setPricing] = useState({
+ seatingCapacity: "",
+ standingCapacity: "",
+ pricePerHour:"",
+ pricePerDay: "",
+ securityDeposit: "",
+ weekendSurcharge: "",
+ minimumBookingHours: "",
+ });
+
+ // Client-side validation errors
+ const [errors, setErrors] = useState({});
+
+ // ==============================
+ // REDUX STATE
+ // ==============================
+
+ const {
+ //vendorId,
+ loading,
+ // success,
+ error,
+ } = useSelector((state) => state.vendorVenue);
+
+ // ==============================
+ // FETCH VENDOR PROFILE
+ // ==============================
+
+ useEffect(() => {
+ dispatch(fetchVendorProfile());
+
+ return () => {
+ dispatch(clearVenueState());
+ };
+ }, [dispatch]);
+
+ // ==============================
+ // RESET FORM
+ // ==============================
+
+ const resetForm = () => {
+ setVenueName("");
+ setCategory("");
+ setDescription("");
+
+ setAddressLine1("");
+ setCity("");
+ setState("");
+ setCountry("");
+
+ setPhone("");
+ setPincode("");
+
+ setWebsiteUrl("");
+ setGoogleMapLink("");
+
+ setImages([]);
+ setAmenities([]);
+ setLicense(null);
+
+ setPricing({
+ seatingCapacity: "",
+ standingCapacity: "",
+ pricePerHour:"",
+ pricePerDay: "",
+ securityDeposit: "",
+ weekendSurcharge: "",
+ minimumBookingHours: "",
+ });
+
+ setErrors({});
+ };
+
+ // ==============================
+ // HANDLE SUBMIT
+ // ==============================
+
+ const handleSubmit = async () => {
+ setErrors({});
+
+ const formValues = {
+ name: venueName,
+ category,
+ description,
+
+ addressLine1,
+ city,
+ state,
+ country,
+
+ phone,
+ pincode,
+
+ websiteUrl,
+ googleMapLink,
+
+ seatingCapacity: pricing.seatingCapacity,
+ standingCapacity: pricing.standingCapacity,
+ pricePerHour: pricing.pricePerHour,
+ pricePerDay: pricing.pricePerDay,
+ securityDeposit: pricing.securityDeposit,
+ weekendSurcharge: pricing.weekendSurcharge,
+ minimumBookingHours: pricing.minimumBookingHours,
+
+ amenities,
+ images,
+ license,
+ };
+
+ // ==============================
+ // ZOD VALIDATION
+ // ==============================
+
+ const result = createVenueSchema.safeParse(formValues);
+
+ if (!result.success) {
+ const fieldErrors = {};
+
+ result.error.issues.forEach((issue) => {
+ const fieldName = issue.path[0];
+
+ fieldErrors[fieldName] = issue.message;
+ });
+
+ setErrors(fieldErrors);
+
+ return;
+ }
+
+ // ==============================
+ // VENDOR VALIDATION
+ // ==============================
+
+ // if (!vendorId) {
+ // toast.error("Vendor profile is still loading.");
+
+ // return;
+ // }
+
+
+ // ==============================
+ // FORM DATA
+ // ==============================
+
+ const formData = new FormData();
+
+ formData.append("name", venueName);
+ formData.append("category", category);
+ formData.append("description", description);
+
+ formData.append("addressLine1", addressLine1);
+ formData.append("city", city);
+ formData.append("state", state);
+ formData.append("country", country);
+
+ formData.append("phone", phone);
+ formData.append("pincode", pincode);
+
+ formData.append("websiteUrl", websiteUrl);
+ formData.append("googleMapLink", googleMapLink);
+
+ formData.append(
+ "amenities",
+ JSON.stringify(amenities)
+ );
+
+ formData.append(
+ "seatingCapacity",
+ pricing.seatingCapacity
+ );
+
+ formData.append(
+ "standingCapacity",
+ pricing.standingCapacity
+ );
+
+ formData.append(
+ "pricePerHour",
+ String(pricing.pricePerHour)
+ );
+
+ formData.append(
+ "pricePerDay",
+ pricing.pricePerDay
+ );
+
+
+ formData.append(
+ "securityDeposit",
+ pricing.securityDeposit
+ );
+
+ formData.append(
+ "weekendSurcharge",
+ pricing.weekendSurcharge
+ );
+
+ formData.append(
+ "minimumBookingHours",
+ pricing.minimumBookingHours
+ );
+
+ // formData.append("vendorId", vendorId);
+
+ if (license) {
+ formData.append("license", license);
+ }
+
+ images.forEach((image) => {
+ formData.append("images", image);
+ });
+
+ // ==============================
+ // DISPATCH THUNK
+ // ==============================
+
+ try {
+ await dispatch(createVenue(formData)).unwrap();
+
+ toast.success("Venue created successfully!");
+
+ resetForm();
+
+ dispatch(clearVenueState());
+ navigate(ROUTES.VENDOR.VENUES);
+
+}
+catch (errorMessage) {
+ toast.error(
+ typeof errorMessage === "string"
+ ? errorMessage
+ : errorMessage?.message || "Failed to create venue"
+ );
+} };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+{error && (
+
+ {typeof error === "string"
+ ? error
+ : error?.message || "Something went wrong"}
+
+)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AddVenue;
\ No newline at end of file
diff --git a/client/src/presentation/pages/vendor/Bookings.jsx b/client/src/presentation/pages/vendor/Bookings.jsx
new file mode 100644
index 0000000000..9cb746b052
--- /dev/null
+++ b/client/src/presentation/pages/vendor/Bookings.jsx
@@ -0,0 +1,352 @@
+import {
+ useEffect,
+ useState,
+} from "react";
+
+import {
+ useDispatch,
+ useSelector,
+} from "react-redux";
+
+import {
+ fetchBookings,
+ fetchBookingById,
+ clearBookingDetails,
+} from "@/redux/slices/VendorBookingSlice";
+
+import VendorSidebar from "@/presentation/components/vendor/VendorSidebar";
+import VendorNavbar from "@/presentation/components/vendor/VendorNavbar";
+
+import BookingHeader from "@/presentation/components/vendor/booking/BookingHeader";
+import BookingStats from "@/presentation/components/vendor/booking/BookingStats";
+import BookingFilters from "@/presentation/components/vendor/booking/BookingFilters";
+import BookingTable from "@/presentation/components/vendor/booking/BookingTable";
+import BookingPagination from "@/presentation/components/vendor/booking/BookingPagination";
+import BookingDetailsModal from "@/presentation/components/vendor/booking/BookingDetailsModal";
+
+
+const Bookings = () => {
+
+ const dispatch = useDispatch();
+
+
+ // ==============================
+ // MODAL
+ // ==============================
+
+ const [
+ isModalOpen,
+ setIsModalOpen,
+ ] = useState(false);
+
+
+ // ==============================
+ // FILTER STATE
+ // ==============================
+
+ const [
+ search,
+ setSearch,
+ ] = useState("");
+
+
+ const [
+ status,
+ setStatus,
+ ] = useState("");
+
+
+ const [
+ currentPage,
+ setCurrentPage,
+ ] = useState(1);
+
+
+ // ==============================
+ // REDUX
+ // ==============================
+
+ const {
+ bookings,
+ loading,
+ error,
+ totalPages,
+ totalCount,
+ bookingDetails,
+ detailsLoading,
+ } = useSelector(
+ (state) => state.vendorBooking
+ );
+
+
+ // ==============================
+ // FETCH BOOKINGS
+ // ==============================
+
+ useEffect(() => {
+
+ dispatch(
+ fetchBookings({
+
+ page: currentPage,
+
+ limit: 20,
+
+ search,
+
+ status,
+
+ })
+ );
+
+ }, [
+ dispatch,
+ currentPage,
+ search,
+ status,
+ ]);
+
+
+ // ==============================
+ // RESET PAGE WHEN FILTER CHANGES
+ // ==============================
+
+ useEffect(() => {
+
+ setCurrentPage(1);
+
+ }, [
+ search,
+ status,
+ ]);
+
+
+ // ==============================
+ // VIEW BOOKING
+ // ==============================
+
+ const handleViewBooking = (bookingId) => {
+
+ setIsModalOpen(true);
+
+ dispatch(
+ fetchBookingById(bookingId)
+ );
+
+ };
+
+
+ // ==============================
+ // CLOSE MODAL
+ // ==============================
+
+ const handleCloseModal = () => {
+
+ setIsModalOpen(false);
+
+ dispatch(
+ clearBookingDetails()
+ );
+
+ };
+
+
+ // ==============================
+ // EXPORT CSV
+ // ==============================
+
+ const handleExport = () => {
+
+ if (!bookings.length) {
+ return;
+ }
+
+
+ const headers = [
+ "Booking ID",
+ "Customer",
+ "Venue",
+ "Event Date",
+ "Total Amount",
+ "Status",
+ "Payment Status",
+ ];
+
+
+ const rows = bookings.map(
+ (booking) => [
+
+ booking.id,
+
+ booking.userId?.fullName ||
+ booking.customer?.name ||
+ "-",
+
+ booking.venueId?.name ||
+ booking.venue?.name ||
+ "-",
+
+ booking.bookingDate,
+
+ booking.totalAmount,
+
+ booking.status,
+
+ booking.paymentStatus,
+
+ ]
+ );
+
+
+ const csvContent = [
+
+ headers.join(","),
+
+ ...rows.map(
+ (row) =>
+ row
+ .map(
+ (value) =>
+ `"${value ?? ""}"`
+ )
+ .join(",")
+ ),
+
+ ].join("\n");
+
+
+ const blob = new Blob(
+ [csvContent],
+ {
+ type: "text/csv;charset=utf-8;",
+ }
+ );
+
+
+ const url =
+ URL.createObjectURL(blob);
+
+
+ const link =
+ document.createElement("a");
+
+
+ link.href = url;
+
+ link.setAttribute(
+ "download",
+ "vendor-bookings.csv"
+ );
+
+
+ document.body.appendChild(link);
+
+ link.click();
+
+ document.body.removeChild(link);
+
+ URL.revokeObjectURL(url);
+
+ };
+
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* =========================
+ FILTERS
+ ========================= */}
+
+
+
+
+ {error && (
+
+
+
+ {error}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+};
+
+
+export default Bookings;
\ No newline at end of file
diff --git a/client/src/presentation/pages/vendor/Dashboard.jsx b/client/src/presentation/pages/vendor/Dashboard.jsx
new file mode 100644
index 0000000000..ce9485a80b
--- /dev/null
+++ b/client/src/presentation/pages/vendor/Dashboard.jsx
@@ -0,0 +1,146 @@
+import { useEffect } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import VendorSidebar from "@/presentation/components/vendor/VendorSidebar";
+import VendorNavbar from "@/presentation/components/vendor/VendorNavbar";
+import DashboardCard from "@/presentation/components/vendor/dashboard/DashboardCard";
+import RecentBookings from "@/presentation/components/vendor/dashboard/RecentBookings";
+import TopVenues from "@/presentation/components/vendor/dashboard/TopVenues";
+import BookingTrends from "@/presentation/components/vendor/dashboard/BookingTrends";
+import RevenueChart from "@/presentation/components/vendor/dashboard/RevenueChart";
+import WelcomeBanner from "@/presentation/components/vendor/dashboard/WelcomeBanner";
+
+import { fetchDashboard } from "@/redux/slices/VendorDashboardSlice";
+
+import {
+ IndianRupee,
+ CalendarDays,
+ Building2,
+ Clock3,
+} from "lucide-react";
+
+const Dashboard = () => {
+ const dispatch = useDispatch();
+
+ const {
+ dashboard,
+ loading,
+ error,
+ } = useSelector((state) => state.vendorDashboard);
+
+ useEffect(() => {
+ dispatch(fetchDashboard());
+ }, [dispatch]);
+
+ return (
+
+
+ {/* Sidebar */}
+
+
+ {/* Main Content */}
+
+
+ {/* Navbar */}
+
+
+
+
+ {/* Page Title */}
+
+ Vendor Dashboard
+
+
+ {/* Welcome Banner */}
+
+
+
+
+ {/* Error */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Loading */}
+ {loading && (
+
+ Loading dashboard...
+
+ )}
+
+ {/* Stats */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Charts */}
+
+
+
+
+
+
+
+
+ {/* Actions + Bookings */}
+
+
+
+
+
+
+
+ {/* Top Venues */}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Dashboard;
\ No newline at end of file
diff --git a/client/src/presentation/pages/vendor/EditVenue.jsx b/client/src/presentation/pages/vendor/EditVenue.jsx
new file mode 100644
index 0000000000..997cc3e18f
--- /dev/null
+++ b/client/src/presentation/pages/vendor/EditVenue.jsx
@@ -0,0 +1,694 @@
+import { useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+import { useDispatch, useSelector } from "react-redux";
+import toast from "react-hot-toast";
+
+import VendorSidebar from "@/presentation/components/vendor/VendorSidebar";
+import VendorNavbar from "@/presentation/components/vendor/VendorNavbar";
+
+import AddVenueHeader from "@/presentation/components/vendor/addVenue/AddVenueHeader";
+import VenueDetailsForm from "@/presentation/components/vendor/addVenue/VenueDetailsForm";
+import AmenitiesForm from "@/presentation/components/vendor/addVenue/AmenitiesForm";
+import PricingForm from "@/presentation/components/vendor/addVenue/PricingForm";
+import ReviewForm from "@/presentation/components/vendor/addVenue/ReviewForm";
+
+import { ROUTES } from "@/constants/routes";
+
+import {
+ fetchVendorProfile,
+ getVenueById,
+ updateVenue,
+ clearVenueState,
+} from "@/redux/slices/VendorVenueSlice";
+
+import { editVenueSchema } from "@/lib/validation/venueValidation";
+
+const EditVenue = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const { venueId } = useParams();
+
+ // ==============================
+ // REDUX STATE
+ // ==============================
+
+ const {
+ venue,
+ vendorId,
+ loading,
+ error,
+ } = useSelector((state) => state.vendorVenue);
+
+ // ==============================
+ // FORM STATE
+ // ==============================
+
+ const [venueName, setVenueName] = useState("");
+ const [category, setCategory] = useState("");
+ const [description, setDescription] = useState("");
+
+ const [addressLine1, setAddressLine1] = useState("");
+ const [city, setCity] = useState("");
+ const [state, setState] = useState("");
+ const [country, setCountry] = useState("");
+
+ const [phone, setPhone] = useState("");
+ const [pincode, setPincode] = useState("");
+
+ const [websiteUrl, setWebsiteUrl] = useState("");
+ const [googleMapLink, setGoogleMapLink] = useState("");
+
+ // ==============================
+ // IMAGES
+ // ==============================
+
+ const [images, setImages] = useState([]);
+ const [existingImages, setExistingImages] = useState([]);
+ const [deletedImages, setDeletedImages] = useState([]);
+
+ // ==============================
+ // LICENSE
+ // ==============================
+
+ const [existingLicense, setExistingLicense] = useState([]);
+ const [deletedLicense, setDeletedLicense] = useState([]);
+ const [newLicense, setNewLicense] = useState([]);
+
+ // ==============================
+ // AMENITIES
+ // ==============================
+
+ const [amenities, setAmenities] = useState([]);
+
+ // ==============================
+ // PRICING
+ // ==============================
+
+ const [pricing, setPricing] = useState({
+ seatingCapacity: "",
+ standingCapacity: "",
+ pricePerHour: "",
+ pricePerDay: "",
+ securityDeposit: "",
+ weekendSurcharge: "",
+ minimumBookingHours: "",
+ });
+
+ // ==============================
+ // VALIDATION ERRORS
+ // ==============================
+
+ const [errors, setErrors] = useState({});
+
+ // ==============================
+ // FETCH DATA
+ // ==============================
+
+ useEffect(() => {
+ if (!venueId) return;
+
+ dispatch(fetchVendorProfile());
+ dispatch(getVenueById(venueId));
+ }, [dispatch, venueId]);
+
+ // ==============================
+ // SET VENUE DATA
+ // ==============================
+
+ useEffect(() => {
+ if (!venue) return;
+
+ setVenueName(venue.name || "");
+ setCategory(venue.category || "");
+ setDescription(venue.description || "");
+
+ setAddressLine1(
+ venue.address?.addressLine1 || ""
+ );
+
+ setCity(
+ venue.address?.city || ""
+ );
+
+ setState(
+ venue.address?.state || ""
+ );
+
+ setCountry(
+ venue.address?.country || ""
+ );
+
+ setPhone(
+ String(venue.address?.phone || "")
+ );
+
+ setPincode(
+ String(venue.address?.pincode || "")
+ );
+
+ setWebsiteUrl(
+ venue.websiteUrl || ""
+ );
+
+ setGoogleMapLink(
+ venue.address?.googleMapLink || ""
+ );
+
+ setAmenities(
+ venue.amenities || []
+ );
+
+ setPricing({
+ seatingCapacity:
+ venue.seatingCapacity ?? "",
+
+ standingCapacity:
+ venue.standingCapacity ?? "",
+
+ pricePerHour:
+ venue.pricePerHour ?? "",
+
+ pricePerDay:
+ venue.pricePerDay ?? "",
+
+ securityDeposit:
+ venue.securityDeposit ?? "",
+
+ weekendSurcharge:
+ venue.weekendSurcharge ?? "",
+
+ minimumBookingHours:
+ venue.minimumBookingHours ?? "",
+ });
+
+ // Existing Cloudinary images
+ setExistingImages(
+ venue.images || []
+ );
+
+ // Existing Cloudinary license
+ setExistingLicense(
+ venue.license || []
+ );
+
+ }, [venue]);
+
+ // ==============================
+ // ERROR DISPLAY
+ // ==============================
+
+ useEffect(() => {
+ if (error) {
+ const errorMessage =
+ typeof error === "string"
+ ? error
+ : error?.message || "Something went wrong";
+
+ toast.error(errorMessage);
+ }
+ }, [error]);
+
+ // ==============================
+ // REMOVE EXISTING IMAGE
+ // ==============================
+
+ const handleRemoveExistingImage = (publicId) => {
+ setExistingImages((prev) =>
+ prev.filter(
+ (image) =>
+ image.publicId !== publicId
+ )
+ );
+
+ setDeletedImages((prev) =>
+ prev.includes(publicId)
+ ? prev
+ : [...prev, publicId]
+ );
+ };
+
+ // ==============================
+ // REMOVE EXISTING LICENSE
+ // ==============================
+
+ const handleRemoveExistingLicense = (publicId) => {
+ setExistingLicense((prev) =>
+ prev.filter(
+ (license) =>
+ license.publicId !== publicId
+ )
+ );
+
+ setDeletedLicense((prev) =>
+ prev.includes(publicId)
+ ? prev
+ : [...prev, publicId]
+ );
+ };
+
+ // ==============================
+ // HANDLE SUBMIT
+ // ==============================
+
+ const handleSubmit = async () => {
+ setErrors({});
+
+ // ------------------------------
+ // IMAGE VALIDATION
+ // ------------------------------
+
+ const totalImages =
+ existingImages.length +
+ images.length;
+
+ if (totalImages < 3) {
+ setErrors({
+ images:
+ "At least 3 images are required.",
+ });
+
+ return;
+ }
+
+ // ------------------------------
+ // LICENSE VALIDATION
+ // ------------------------------
+
+ const totalLicenses =
+ existingLicense.length +
+ newLicense.length;
+
+ if (totalLicenses === 0) {
+ setErrors({
+ license:
+ "At least one license PDF is required.",
+ });
+
+ return;
+ }
+
+ // ------------------------------
+ // VENDOR VALIDATION
+ // ------------------------------
+
+ if (!vendorId) {
+ toast.error(
+ "Vendor information is not available."
+ );
+
+ return;
+ }
+
+ // ------------------------------
+ // ZOD VALIDATION
+ // ------------------------------
+
+ const formValues = {
+ name: venueName,
+ description,
+ category,
+
+ vendorId,
+
+ websiteUrl,
+
+ addressLine1,
+ city,
+ state,
+ country,
+
+ phone,
+ pincode,
+
+ googleMapLink,
+
+ seatingCapacity:
+ pricing.seatingCapacity,
+
+ standingCapacity:
+ pricing.standingCapacity,
+
+ pricePerHour:
+ pricing.pricePerHour,
+
+ pricePerDay:
+ pricing.pricePerDay,
+
+ securityDeposit:
+ pricing.securityDeposit,
+
+ weekendSurcharge:
+ pricing.weekendSurcharge,
+
+ minimumBookingHours:
+ pricing.minimumBookingHours,
+
+ amenities,
+
+ deletedImages:
+ JSON.stringify(deletedImages),
+
+ deletedLicense:
+ JSON.stringify(deletedLicense),
+ };
+
+ const validationResult =
+ editVenueSchema.safeParse(
+ formValues
+ );
+
+ if (!validationResult.success) {
+ const fieldErrors = {};
+
+ validationResult.error.issues.forEach(
+ (issue) => {
+ const fieldName =
+ issue.path[0];
+
+ fieldErrors[fieldName] =
+ issue.message;
+ }
+ );
+
+ setErrors(fieldErrors);
+
+ return;
+ }
+
+ // ==============================
+ // CREATE FORM DATA
+ // ==============================
+
+ const formData = new FormData();
+
+ // Basic details
+ formData.append(
+ "name",
+ venueName
+ );
+
+ formData.append(
+ "description",
+ description
+ );
+
+ formData.append(
+ "category",
+ category
+ );
+
+ formData.append(
+ "vendorId",
+ vendorId
+ );
+
+ // Links
+ formData.append(
+ "websiteUrl",
+ websiteUrl
+ );
+
+ formData.append(
+ "googleMapLink",
+ googleMapLink
+ );
+
+ // Address
+ formData.append(
+ "addressLine1",
+ addressLine1
+ );
+
+ formData.append(
+ "city",
+ city
+ );
+
+ formData.append(
+ "state",
+ state
+ );
+
+ formData.append(
+ "country",
+ country
+ );
+
+ // IMPORTANT:
+ // These values are sent unchanged.
+ // If you want them to be non-editable,
+ // disable the inputs in VenueDetailsForm.
+ formData.append(
+ "phone",
+ phone
+ );
+
+ formData.append(
+ "pincode",
+ pincode
+ );
+
+ // Pricing
+ formData.append(
+ "seatingCapacity",
+ pricing.seatingCapacity
+ );
+
+ formData.append(
+ "standingCapacity",
+ pricing.standingCapacity
+ );
+
+ formData.append(
+ "pricePerHour",
+ pricing.pricePerHour
+ );
+
+ formData.append(
+ "pricePerDay",
+ pricing.pricePerDay
+ );
+
+ formData.append(
+ "securityDeposit",
+ pricing.securityDeposit
+ );
+
+ formData.append(
+ "weekendSurcharge",
+ pricing.weekendSurcharge
+ );
+
+ formData.append(
+ "minimumBookingHours",
+ pricing.minimumBookingHours
+ );
+
+ // Amenities
+ formData.append(
+ "amenities",
+ JSON.stringify(amenities)
+ );
+
+ // Deleted existing images
+ formData.append(
+ "deletedImages",
+ JSON.stringify(deletedImages)
+ );
+
+ // Deleted existing licenses
+ formData.append(
+ "deletedLicense",
+ JSON.stringify(deletedLicense)
+ );
+
+ // New images
+ images.forEach((image) => {
+ formData.append(
+ "images",
+ image
+ );
+ });
+
+ // New license
+ newLicense.forEach((license) => {
+ formData.append(
+ "license",
+ license
+ );
+ });
+
+ // ==============================
+ // UPDATE
+ // ==============================
+
+ try {
+ await dispatch(
+ updateVenue({
+ venueId,
+ formData,
+ })
+ ).unwrap();
+
+ toast.success(
+ "Venue updated successfully!"
+ );
+
+ dispatch(
+ clearVenueState()
+ );
+
+ navigate(
+ ROUTES.VENDOR.VENUES
+ );
+
+ } catch (err) {
+
+ const errorMessage =
+ typeof err === "string"
+ ? err
+ : err?.message ||
+ "Failed to update venue";
+
+ toast.error(
+ errorMessage
+ );
+ }
+ };
+
+ // ==============================
+ // UI
+ // ==============================
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Do not render the entire error object */}
+ {error && (
+
+ {typeof error === "string"
+ ? error
+ : error?.message ||
+ "Something went wrong"}
+
+ )}
+
+ {loading && !venue ? (
+
+
+ Loading venue details...
+
+
+ ) : (
+
+ <>
+
+
+
+
+
+
+
+
+
+ >
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default EditVenue;
\ No newline at end of file
diff --git a/client/src/presentation/pages/vendor/Profile.jsx b/client/src/presentation/pages/vendor/Profile.jsx
new file mode 100644
index 0000000000..0db0da0bc3
--- /dev/null
+++ b/client/src/presentation/pages/vendor/Profile.jsx
@@ -0,0 +1,113 @@
+import { useEffect, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import VendorSidebar from "@/presentation/components/vendor/VendorSidebar";
+import VendorNavbar from "@/presentation/components/vendor/VendorNavbar";
+
+import ProfileHeader from "@/presentation/components/vendor/profile/ProfileHeader";
+import ProfileStats from "@/presentation/components/vendor/profile/ProfileStats";
+import PersonalInformation from "@/presentation/components/vendor/profile/PersonalInformation";
+import BusinessInformation from "@/presentation/components/vendor/profile/BusinessInformation";
+import ChangePassword from "@/presentation/components/vendor/profile/ChangePassword";
+import NotificationSettings from "@/presentation/components/vendor/profile/NotificationSettings";
+import ProfileActions from "@/presentation/components/vendor/profile/ProfileActions";
+
+import {
+ fetchVendorProfile,
+ updateVendorProfile,
+} from "@/redux/slices/VendorProfileSlice";
+
+const Profile = () => {
+ const dispatch = useDispatch();
+
+ const {
+ profile,
+ loading,
+ updating,
+ error,
+ } = useSelector((state) => state.vendorProfile);
+
+ const [isEditing, setIsEditing] = useState(false);
+ const [formData, setFormData] = useState({});
+
+ useEffect(() => {
+ dispatch(fetchVendorProfile());
+ }, [dispatch]);
+
+ useEffect(() => {
+ if (profile) {
+ setFormData(profile);
+ }
+ }, [profile]);
+
+ const handleSave = async () => {
+ const result = await dispatch(updateVendorProfile(formData));
+
+ if (updateVendorProfile.fulfilled.match(result)) {
+ setIsEditing(false);
+ }
+ };
+
+ const handleCancel = () => {
+ setFormData(profile || {});
+ setIsEditing(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {loading && (
+
+ Loading profile...
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {isEditing && (
+
+ )}
+
+
+
+ );
+};
+
+export default Profile;
\ No newline at end of file
diff --git a/client/src/presentation/pages/vendor/Settings.jsx b/client/src/presentation/pages/vendor/Settings.jsx
new file mode 100644
index 0000000000..e5aa07b706
--- /dev/null
+++ b/client/src/presentation/pages/vendor/Settings.jsx
@@ -0,0 +1,68 @@
+import { useState } from "react";
+import VendorSidebar from "@/presentation/components/vendor/VendorSidebar";
+import VendorNavbar from "@/presentation/components/vendor/VendorNavbar";
+import SettingsHeader from "@/presentation/components/vendor/settings/SettingsHeader";
+import AccountSettingsCard from "@/presentation/components/vendor/settings/AccountSettingsCard";
+import NotificationPreferencesCard from "@/presentation/components/vendor/settings/NotificationPreferencesCard";
+import SecuritySettingsCard from "@/presentation/components/vendor/settings/SecuritySettingsCard";
+import DangerZoneCard from "@/presentation/components/vendor/settings/DangerZoneCard";
+import SettingsActions from "@/presentation/components/vendor/settings/SettingsActions";
+import SessionManagementCard from "@/presentation/components/vendor/settings/SessionManagementCard";
+
+const Settings = () => {
+ const [settings, setSettings] = useState({
+ email: "vendor@example.com",
+ phone: "+91 9876543210",
+ language: "English",
+ timezone: "Asia/Kolkata",
+ notifications: {
+ email: true,
+ sms: true,
+ marketing: false,
+ bookingUpdates: true,
+ },
+ });
+
+ const handleReset = () => {
+ setSettings({
+ email: "vendor@example.com",
+ phone: "+91 9876543210",
+ language: "English",
+ timezone: "Asia/Kolkata",
+ notifications: {
+ email: true,
+ sms: true,
+ marketing: false,
+ bookingUpdates: true,
+ },
+ });
+ };
+
+ const handleSave = () => {
+ console.log("Saved settings", settings);
+ alert("Settings saved successfully");
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Settings;
\ No newline at end of file
diff --git a/client/src/presentation/pages/vendor/VenueDetails.jsx b/client/src/presentation/pages/vendor/VenueDetails.jsx
new file mode 100644
index 0000000000..97fe71d1b8
--- /dev/null
+++ b/client/src/presentation/pages/vendor/VenueDetails.jsx
@@ -0,0 +1,275 @@
+import { useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { useDispatch, useSelector } from "react-redux";
+import toast from "react-hot-toast";
+
+import VendorSidebar from "@/presentation/components/vendor/VendorSidebar";
+import VendorNavbar from "@/presentation/components/vendor/VendorNavbar";
+import DeleteVenueDialog from "@/presentation/components/vendor/venues/DeleteVenueDialog";
+
+import {
+ getVenueById,
+ deleteVenue,
+} from "@/redux/slices/VendorVenueSlice";
+
+import { ROUTES } from "@/constants/routes";
+
+const VenueDetails = () => {
+ const { venueId } = useParams();
+
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const [deleteOpen, setDeleteOpen] = useState(false);
+
+ const {
+ venue,
+ loading,
+ error,
+ } = useSelector(
+ (state) => state.vendorVenue
+ );
+
+ useEffect(() => {
+ dispatch(getVenueById(venueId));
+ }, [dispatch, venueId]);
+
+ const handleEdit = () => {
+ navigate(
+ ROUTES.VENDOR.EDIT_VENUE.replace(
+ ":venueId",
+ venueId
+ )
+ );
+ };
+
+ const handleDelete = async () => {
+ try {
+ await dispatch(
+ deleteVenue(venueId)
+ ).unwrap();
+
+ toast.success(
+ "Venue deleted successfully"
+ );
+
+ navigate(
+ ROUTES.VENDOR.VENUES
+ );
+ } catch (error) {
+ toast.error(
+ typeof error === "string"
+ ? error
+ : error?.message || "Failed to delete venue"
+ );
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
+ Loading venue...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+
+
+
+
+ {typeof error === "string"
+ ? error
+ : error?.message || "Something went wrong"}
+
+
+
+
+ );
+ }
+
+ if (!venue) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {venue.name}
+
+
+
+ {venue.category}
+
+
+
+
+
+
+
+
+
+
+
+ {venue.images?.map((image, index) => (
+

+ ))}
+
+
+
+
+ Description
+
+
+
+ {venue.description}
+
+
+
+
+
+ Location Details
+
+
+
+
+ Address:{" "}
+ {venue.address?.addressLine1}
+
+
+
+ City:{" "}
+ {venue.address?.city}
+
+
+
+ State:{" "}
+ {venue.address?.state}
+
+
+
+ Country:{" "}
+ {venue.address?.country}
+
+
+
+ Pincode:{" "}
+ {venue.address?.pincode}
+
+
+
+ Phone:{" "}
+ {venue.address?.phone}
+
+
+
+
+
+
+ Capacity & Pricing
+
+
+
+
+ Seating Capacity:{" "}
+ {venue.seatingCapacity}
+
+
+
+ Standing Capacity:{" "}
+ {venue.standingCapacity}
+
+
+
+ Price Per Hour:{" "}
+ ₹{venue.pricePerHour}
+
+
+
+ Price Per Day:{" "}
+ ₹{venue.pricePerDay}
+
+
+
+ Security Deposit:{" "}
+ ₹{venue.securityDeposit}
+
+
+
+ Weekend Surcharge:{" "}
+ {venue.weekendSurcharge}%
+
+
+
+ Minimum Booking Hours:{" "}
+ {venue.minimumBookingHours}
+
+
+
+
+
+
+ Amenities
+
+
+
+ {venue.amenities?.map((amenity) => (
+
+ {amenity}
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default VenueDetails;
\ No newline at end of file
diff --git a/client/src/presentation/pages/vendor/VenueList.jsx b/client/src/presentation/pages/vendor/VenueList.jsx
new file mode 100644
index 0000000000..0042f966f1
--- /dev/null
+++ b/client/src/presentation/pages/vendor/VenueList.jsx
@@ -0,0 +1,196 @@
+import { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { useDispatch, useSelector } from "react-redux";
+import toast from "react-hot-toast";
+
+import VendorSidebar from "@/presentation/components/vendor/VendorSidebar";
+import VendorNavbar from "@/presentation/components/vendor/VendorNavbar";
+import VenueHeader from "@/presentation/components/vendor/venues/VenueHeader";
+import VenueFilters from "@/presentation/components/vendor/venues/VenueFilters";
+import VenueGrid from "@/presentation/components/vendor/venues/VenueGrid";
+import DeleteVenueDialog from "@/presentation/components/vendor/venues/DeleteVenueDialog";
+
+import { ROUTES } from "@/constants/routes";
+
+import {
+ fetchVenues,
+ deleteVenue,
+} from "@/redux/slices/VendorVenueSlice";
+
+const initialFilters = {
+ search: "",
+ status: "",
+ category: "",
+ priceType: "",
+ minPrice: "",
+ maxPrice: "",
+ capacityType: "",
+ capacity: "",
+ rating: "",
+ page: 1,
+ limit: 20,
+};
+
+const VenueList = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ const {
+ venues,
+ loading,
+ error,
+ } = useSelector((state) => state.vendorVenue);
+
+ const [filters, setFilters] = useState(initialFilters);
+ const [viewMode, setViewMode] = useState("grid");
+
+ const [deleteOpen, setDeleteOpen] = useState(false);
+ const [selectedVenueId, setSelectedVenueId] = useState(null);
+
+ const queryParams = useMemo(() => {
+ const sanitized = { ...filters };
+
+ delete sanitized.status;
+
+ Object.keys(sanitized).forEach((key) => {
+ if (
+ sanitized[key] === "" ||
+ sanitized[key] === null ||
+ sanitized[key] === undefined
+ ) {
+ delete sanitized[key];
+ }
+ });
+
+ return sanitized;
+ }, [filters]);
+
+ useEffect(() => {
+ dispatch(fetchVenues(queryParams));
+ }, [dispatch, queryParams]);
+
+ const handleViewVenue = (venueId) => {
+ navigate(
+ ROUTES.VENDOR.VENUE_DETAILS.replace(
+ ":venueId",
+ venueId
+ )
+ );
+ };
+
+ const handleEditVenue = (venueId) => {
+ navigate(
+ ROUTES.VENDOR.EDIT_VENUE.replace(
+ ":venueId",
+ venueId
+ )
+ );
+ };
+
+ const handleDeleteVenue = (venueId) => {
+ setSelectedVenueId(venueId);
+ setDeleteOpen(true);
+ };
+
+ const confirmDeleteVenue = async () => {
+ try {
+ await dispatch(
+ deleteVenue(selectedVenueId)
+ ).unwrap();
+
+ toast.success(
+ "Venue deleted successfully"
+ );
+
+ setDeleteOpen(false);
+ setSelectedVenueId(null);
+
+ dispatch(fetchVenues(queryParams));
+ } catch (err) {
+ toast.error(
+ typeof err === "string"
+ ? err
+ : err?.message || "Failed to delete venue"
+ );
+ }
+ };
+
+ const visibleVenues = useMemo(() => {
+ if (!filters.status) {
+ return venues;
+ }
+
+ return venues.filter(
+ (venue) =>
+ venue.approvalStatus === filters.status
+ );
+ }, [venues, filters.status]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ navigate(ROUTES.VENDOR.ADD_VENUE)
+ }
+ onToggleView={setViewMode}
+ viewMode={viewMode}
+ />
+
+ {error && (
+
+ {typeof error === "string"
+ ? error
+ : error?.message || "Something went wrong"}
+
+ )}
+
+ {loading && (
+
+ Loading venues...
+
+ )}
+
+ {!loading && !error && (
+
+ {visibleVenues.length} venue
+ {visibleVenues.length === 1
+ ? ""
+ : "s"}{" "}
+ found
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default VenueList;
\ No newline at end of file
diff --git a/client/src/redux/.gitkeep b/client/src/redux/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/redux/slices/.gitkeep b/client/src/redux/slices/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/src/redux/slices/AdminBookingSlice.js b/client/src/redux/slices/AdminBookingSlice.js
new file mode 100644
index 0000000000..2f3a20a1d5
--- /dev/null
+++ b/client/src/redux/slices/AdminBookingSlice.js
@@ -0,0 +1,173 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+ loading: false,
+ error: null,
+
+ bookings: [],
+ selectedBooking: null,
+
+ statistics: {
+ totalBookings: 0,
+ pendingBookings: 0,
+ confirmedBookings: 0,
+ cancelledBookings: 0,
+ completedBookings: 0,
+ },
+
+ pagination: {
+ totalPages: 0,
+ totalCount: 0,
+ },
+};
+
+// ==============================
+// GET ALL BOOKINGS
+// ==============================
+
+export const getBookings = createAsyncThunk(
+ "adminBooking/getBookings",
+ async (params = {}, { rejectWithValue }) => {
+ try {
+ const response = await api.get(API_ROUTES.ADMIN.BOOKING.BOOKINGS, {
+ params: {
+ search: params.search || "",
+ status: params.status || undefined,
+ paymentStatus: params.paymentStatus || undefined,
+ page: params.page || 1,
+ limit: params.limit || 10,
+ sortBy: params.sortBy || "desc",
+ },
+ });
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to fetch bookings."
+ );
+ }
+ }
+);
+
+// ==============================
+// GET BOOKING DETAILS
+// ==============================
+
+export const getBookingById = createAsyncThunk(
+ "adminBooking/getBookingById",
+ async (bookingId, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.ADMIN.BOOKING.GET_BY_ID(bookingId)
+ );
+ console.log(response.data);
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to fetch booking."
+ );
+ }
+ }
+);
+
+// ==============================
+// GET BOOKING STATISTICS
+// ==============================
+
+export const getBookingStats = createAsyncThunk(
+ "adminBooking/getBookingStats",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.ADMIN.BOOKING.STATISTICS
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch booking statistics."
+ );
+ }
+ }
+);
+
+const adminBookingSlice = createSlice({
+ name: "adminBooking",
+
+ initialState,
+
+ reducers: {},
+
+ extraReducers: (builder) => {
+ builder
+
+ // ==============================
+ // GET BOOKINGS
+ // ==============================
+
+ .addCase(getBookings.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(getBookings.fulfilled, (state, action) => {
+ state.loading = false;
+
+ state.bookings = action.payload.data;
+
+ state.pagination.totalPages =
+ action.payload.totalPages;
+
+ state.pagination.totalCount =
+ action.payload.totalCount;
+ })
+
+ .addCase(getBookings.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==============================
+ // GET BOOKING DETAILS
+ // ==============================
+
+ .addCase(getBookingById.pending, (state) => {
+ state.loading = true;
+ })
+
+ .addCase(getBookingById.fulfilled, (state, action) => {
+ state.loading = false;
+ state.selectedBooking = action.payload;
+ console.log(action.payload);
+ })
+
+ .addCase(getBookingById.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==============================
+ // GET STATISTICS
+ // ==============================
+
+ .addCase(getBookingStats.pending, (state) => {
+ state.loading = true;
+ })
+
+ .addCase(getBookingStats.fulfilled, (state, action) => {
+ state.loading = false;
+ state.statistics = action.payload;
+ })
+
+ .addCase(getBookingStats.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ });
+ },
+});
+
+export default adminBookingSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/AdminDashboardSlice.js b/client/src/redux/slices/AdminDashboardSlice.js
new file mode 100644
index 0000000000..e2b20819c9
--- /dev/null
+++ b/client/src/redux/slices/AdminDashboardSlice.js
@@ -0,0 +1,76 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+ loading: false,
+ error: null,
+
+ statistics: {
+ totalUsers: 0,
+ totalVendors: 0,
+ totalVenues: 0,
+ totalBookings: 0,
+ totalRevenue: 0,
+ pendingVendorApprovals: 0,
+ pendingVenueApprovals: 0,
+
+ bookingOverview: [],
+ revenueOverview: [],
+ },
+};
+
+// ==========================
+// GET DASHBOARD STATISTICS
+// ==========================
+
+export const getDashboardStatistics = createAsyncThunk(
+ "adminDashboard/getDashboardStatistics",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.ADMIN.DASHBOARD.STATISTICS
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch dashboard statistics."
+ );
+ }
+ }
+);
+
+const adminDashboardSlice = createSlice({
+ name: "adminDashboard",
+
+ initialState,
+
+ reducers: {},
+
+ extraReducers: (builder) => {
+ builder
+
+ // ==========================
+ // DASHBOARD STATS
+ // ==========================
+
+ .addCase(getDashboardStatistics.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(getDashboardStatistics.fulfilled, (state, action) => {
+ state.loading = false;
+ state.statistics = action.payload;
+ })
+
+ .addCase(getDashboardStatistics.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ });
+ },
+});
+
+export default adminDashboardSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/AdminPaymentSlice.js b/client/src/redux/slices/AdminPaymentSlice.js
new file mode 100644
index 0000000000..e8988f0b7f
--- /dev/null
+++ b/client/src/redux/slices/AdminPaymentSlice.js
@@ -0,0 +1,178 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+ loading: false,
+ error: null,
+
+ payments: [],
+ selectedPayment: null,
+
+ statistics: {
+ totalPayments: 0,
+ successfulPayments: 0,
+ pendingPayments: 0,
+ failedPayments: 0,
+ refundedPayments: 0,
+ totalRevenue: 0,
+ },
+
+ pagination: {
+ totalPages: 0,
+ totalCount: 0,
+ },
+};
+
+// ==============================
+// GET ALL PAYMENTS
+// ==============================
+
+export const getPayments = createAsyncThunk(
+ "adminPayment/getPayments",
+ async (params = {}, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.ADMIN.PAYMENT.PAYMENTS,
+ {
+ params: {
+ search: params.search || "",
+ paymentStatus: params.paymentStatus || undefined,
+ paymentMethod: params.paymentMethod || undefined,
+ paymentType: params.paymentType || undefined,
+ sortBy: params.sortBy || "desc",
+ page: params.page || 1,
+ limit: params.limit || 10,
+ },
+ }
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch payments."
+ );
+ }
+ }
+);
+
+// ==============================
+// GET PAYMENT DETAILS
+// ==============================
+
+export const getPaymentById = createAsyncThunk(
+ "adminPayment/getPaymentById",
+ async (paymentId, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.ADMIN.PAYMENT.GET_BY_ID(paymentId)
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch payment."
+ );
+ }
+ }
+);
+
+// ==============================
+// GET PAYMENT STATISTICS
+// ==============================
+
+export const getPaymentStats = createAsyncThunk(
+ "adminPayment/getPaymentStats",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.ADMIN.PAYMENT.STATISTICS
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch payment statistics."
+ );
+ }
+ }
+);
+
+const adminPaymentSlice = createSlice({
+ name: "adminPayment",
+
+ initialState,
+
+ reducers: {},
+
+ extraReducers: (builder) => {
+ builder
+
+ // ==========================
+ // GET PAYMENTS
+ // ==========================
+
+ .addCase(getPayments.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(getPayments.fulfilled, (state, action) => {
+ state.loading = false;
+
+ state.payments = action.payload.data;
+
+ state.pagination.totalPages =
+ action.payload.totalPages;
+
+ state.pagination.totalCount =
+ action.payload.totalCount;
+ })
+
+ .addCase(getPayments.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // GET PAYMENT DETAILS
+ // ==========================
+
+ .addCase(getPaymentById.pending, (state) => {
+ state.loading = true;
+ })
+
+ .addCase(getPaymentById.fulfilled, (state, action) => {
+ state.loading = false;
+ state.selectedPayment = action.payload;
+ })
+
+ .addCase(getPaymentById.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // GET PAYMENT STATS
+ // ==========================
+
+ .addCase(getPaymentStats.pending, (state) => {
+ state.loading = true;
+ })
+
+ .addCase(getPaymentStats.fulfilled, (state, action) => {
+ state.loading = false;
+ state.statistics = action.payload;
+ })
+
+ .addCase(getPaymentStats.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ });
+ },
+});
+
+export default adminPaymentSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/AdminUserSlice.js b/client/src/redux/slices/AdminUserSlice.js
new file mode 100644
index 0000000000..f29a4eaa5d
--- /dev/null
+++ b/client/src/redux/slices/AdminUserSlice.js
@@ -0,0 +1,182 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+ loading: false,
+ error: null,
+
+ users: [],
+
+ pagination: {
+ totalPages: 0,
+ totalCount: 0,
+ },
+};
+
+// ======================
+// GET USERS
+// ======================
+
+export const getUsers = createAsyncThunk(
+ "admin/getUsers",
+ async (params = {}, { rejectWithValue }) => {
+ try {
+
+ const response = await api.get(
+ API_ROUTES.ADMIN.USER.USERS,
+ {
+ params: {
+ search: params.search || "",
+ isBlocked:
+ params.isBlocked === undefined
+ ? undefined
+ : params.isBlocked,
+ page: params.page || 1,
+ limit: params.limit || 10,
+ },
+ }
+ );
+ console.log("success")
+ console.log(response)
+
+ return response.data.data;
+
+ } catch (error) {
+ console.log("error")
+ console.log(error)
+ console.log(error.response)
+ console.log(error.message)
+
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch users."
+ );
+
+ }
+ }
+);
+
+// ======================
+// BLOCK / UNBLOCK USER
+// ======================
+
+export const updateUserStatus = createAsyncThunk(
+ "admin/updateUserStatus",
+ async ({ userId, isBlocked }, { rejectWithValue }) => {
+
+ try {
+
+ const response = await api.patch(
+
+ API_ROUTES.ADMIN.USER.UPDATE_STATUS(userId),
+
+ {
+ isBlocked,
+ }
+
+ );
+
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to update user status."
+ );
+
+ }
+ }
+);
+
+const adminUserSlice = createSlice({
+
+ name: "adminUser",
+
+ initialState,
+
+ reducers: {},
+
+ extraReducers: (builder) => {
+
+ builder
+
+ // ==========================
+ // GET USERS
+ // ==========================
+
+ .addCase(getUsers.pending, (state) => {
+
+ state.loading = true;
+
+ state.error = null;
+
+ })
+
+ .addCase(getUsers.fulfilled, (state, action) => {
+
+ state.loading = false;
+
+ state.users = action.payload.data;
+
+ state.pagination.totalPages =
+ action.payload.totalPages;
+
+ state.pagination.totalCount =
+ action.payload.totalCount;
+
+ })
+
+ .addCase(getUsers.rejected, (state, action) => {
+
+ state.loading = false;
+
+ state.error = action.payload;
+
+ })
+
+ // ==========================
+ // UPDATE STATUS
+ // ==========================
+
+ .addCase(updateUserStatus.pending, (state) => {
+
+ state.loading = true;
+
+ })
+
+ .addCase(updateUserStatus.fulfilled, (state, action) => {
+
+ state.loading = false;
+
+ const updatedUser = action.payload;
+
+ const index = state.users.findIndex(
+
+ (user) => user._id === updatedUser._id
+
+ );
+
+ if (index !== -1) {
+
+ state.users[index] = updatedUser;
+
+ }
+
+ })
+
+ .addCase(updateUserStatus.rejected, (state, action) => {
+
+ state.loading = false;
+
+ state.error = action.payload;
+
+ });
+
+ },
+
+});
+
+export default adminUserSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/AdminVenueSlice.js b/client/src/redux/slices/AdminVenueSlice.js
new file mode 100644
index 0000000000..7ad97e0bf8
--- /dev/null
+++ b/client/src/redux/slices/AdminVenueSlice.js
@@ -0,0 +1,330 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+ loading: false,
+ error: null,
+ venues: [],
+ selectedVenue: null,
+ pagination: {
+ totalPages: 0,
+ totalCount: 0,
+ },
+};
+
+export const getVenues = createAsyncThunk(
+ "admin/getVenues",
+ async (params = {}, { rejectWithValue }) => {
+ console.log("Thunk called wih");
+ try {
+
+ const response = await api.get(
+ API_ROUTES.ADMIN.VENUE.VENUES,
+ {
+ params: {
+ search: params.search || "",
+ category: params.category || undefined,
+ approvalStatus: params.approvalStatus || undefined,
+ isBlocked: params.isBlocked,
+ page: params.page || 1,
+ limit: params.limit || 10,
+ },
+ }
+ );
+ console.log(response.data);
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch venues."
+ );
+
+ }
+ }
+);
+
+export const getVenueById = createAsyncThunk(
+ "admin/getVenueById",
+ async (venueId, { rejectWithValue }) => {
+console.log("Thunk getVenueById", venueId);
+ try {
+
+ const response = await api.get(
+ API_ROUTES.ADMIN.VENUE.GET_BY_ID(venueId)
+ );
+
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch venue."
+ );
+
+ }
+ }
+);
+
+export const approveVenue = createAsyncThunk(
+ "admin/approveVenue",
+ async (venueId, { rejectWithValue }) => {
+
+ try {
+
+ const response = await api.patch(
+ API_ROUTES.ADMIN.VENUE.APPROVE(venueId)
+ );
+
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to approve venue."
+ );
+
+ }
+ }
+);
+
+export const rejectVenue = createAsyncThunk(
+ "admin/rejectVenue",
+ async (
+ { venueId, rejectionReason },
+ { rejectWithValue }
+ ) => {
+
+ try {
+
+ const response = await api.patch(
+ API_ROUTES.ADMIN.VENUE.REJECT(venueId),
+ {
+ reason: rejectionReason,
+ }
+ );
+
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to reject venue."
+ );
+
+ }
+ }
+);
+
+export const updateVenueStatus = createAsyncThunk(
+ "admin/updateVenueStatus",
+ async (
+ { venueId, isBlocked },
+ { rejectWithValue }
+ ) => {
+
+ try {
+
+ const response = await api.patch(
+ API_ROUTES.ADMIN.VENUE.UPDATE_STATUS(venueId),
+ {
+ isBlocked,
+ }
+ );
+
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to update venue status."
+ );
+
+ }
+ }
+);
+
+const adminVenueSlice = createSlice({
+
+ name: "adminVenue",
+
+ initialState,
+
+ reducers: {},
+
+ extraReducers: (builder) => {
+
+ builder
+
+ // ==========================
+ // GET VENUES
+ // ==========================
+
+ .addCase(getVenues.pending, (state) => {
+
+ state.loading = true;
+ state.error = null;
+
+ })
+
+ .addCase(getVenues.fulfilled, (state, action) => {
+
+ state.loading = false;
+
+ state.venues = action.payload.data;
+
+ state.pagination.totalPages =
+ action.payload.totalPages;
+
+ state.pagination.totalCount =
+ action.payload.totalCount;
+
+ })
+
+ .addCase(getVenues.rejected, (state, action) => {
+
+ state.loading = false;
+ state.error = action.payload;
+
+ })
+
+ // ==========================
+ // GET VENUE BY ID
+ // ==========================
+
+ .addCase(getVenueById.pending, (state) => {
+
+ state.loading = true;
+
+ })
+
+ .addCase(getVenueById.fulfilled, (state, action) => {
+
+ state.loading = false;
+
+ state.selectedVenue = action.payload;
+
+ })
+
+ .addCase(getVenueById.rejected, (state, action) => {
+
+ state.loading = false;
+
+ state.error = action.payload;
+
+ })
+
+ // ==========================
+ // APPROVE
+ // ==========================
+
+ .addCase(approveVenue.fulfilled, (state, action) => {
+
+ const updatedVenue = action.payload;
+
+ const index =
+ state.venues.findIndex(
+ venue => venue.id === updatedVenue.id
+ );
+
+ if (index !== -1) {
+
+ state.venues[index] = updatedVenue;
+
+ }
+
+ })
+
+ // ==========================
+ // REJECT
+ // ==========================
+
+ .addCase(rejectVenue.fulfilled, (state, action) => {
+
+ const updatedVenue = action.payload;
+
+ const index =
+ state.venues.findIndex(
+ venue => venue.id === updatedVenue.id
+ );
+
+ if (index !== -1) {
+
+ state.venues[index] = updatedVenue;
+
+ }
+
+ })
+
+ // ==========================
+ // BLOCK / UNBLOCK
+ // ==========================
+
+ .addCase(updateVenueStatus.fulfilled, (state, action) => {
+
+ const updatedVenue = action.payload;
+
+ const index =
+ state.venues.findIndex(
+ venue => venue.id === updatedVenue.id
+ );
+
+ if (index !== -1) {
+
+ state.venues[index] = updatedVenue;
+
+ }
+
+ })
+
+ .addMatcher(
+
+ (action) =>
+ action.type.endsWith("/pending"),
+
+ (state) => {
+
+ state.loading = true;
+
+ }
+
+ )
+
+ .addMatcher(
+
+ (action) =>
+ action.type.endsWith("/rejected"),
+
+ (state, action) => {
+
+ state.loading = false;
+ state.error = action.payload;
+
+ }
+
+ )
+
+ .addMatcher(
+
+ (action) =>
+ action.type.endsWith("/fulfilled"),
+
+ (state) => {
+
+ state.loading = false;
+
+ }
+
+ );
+
+ },
+
+});
+
+export default adminVenueSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/AdminvendorSlice.js b/client/src/redux/slices/AdminvendorSlice.js
new file mode 100644
index 0000000000..832ec01c71
--- /dev/null
+++ b/client/src/redux/slices/AdminvendorSlice.js
@@ -0,0 +1,287 @@
+import { API_ROUTES } from "@/constants/apiRoutes"
+import api from "@/lib/axios"
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
+
+const initialState = {
+ loading: false,
+ error: null,
+
+ vendors: [],
+
+ pagination: {
+ totalPages: 0,
+ totalCount: 0,
+ },
+};
+
+export const getVendors = createAsyncThunk(
+ "admin/getVendors",
+ async (params = {}, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.ADMIN.VENDOR.VENDORS,
+ {
+ params: {
+ search: params.search || "",
+ status: params.approvalStatus || undefined,
+ isBlocked: params.isBlocked,
+ page: params.page || 1,
+ limit: params.limit || 10,
+ },
+ }
+ );
+ console.log("success")
+ console.log(response)
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch vendors."
+ );
+
+ }
+ }
+);
+
+export const approveVendor = createAsyncThunk(
+ "admin/approveVendor",
+ async (vendorId, { rejectWithValue }) => {
+
+ try {
+
+ const response = await api.patch(
+ API_ROUTES.ADMIN.VENDOR.APPROVE(vendorId)
+ );
+
+
+ return response.data.data;
+
+ } catch (error) {
+ console.log("approve error:",error.response?.data)
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to approve vendor."
+ );
+
+ }
+ }
+);
+export const rejectVendor = createAsyncThunk(
+ "admin/rejectVendor",
+ async (
+ { vendorId, rejectionReason },
+ { rejectWithValue }
+ ) => {
+
+ try {
+
+ const response = await api.patch(
+ API_ROUTES.ADMIN.VENDOR.REJECT(vendorId),
+ {
+ reason:rejectionReason,
+ }
+ );
+
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to reject vendor."
+ );
+
+ }
+ }
+);
+export const updateVendorStatus = createAsyncThunk(
+ "admin/updateVendorStatus",
+ async (
+ { vendorId, isBlocked },
+ { rejectWithValue }
+ ) => {
+
+ try {
+console.log("venId",vendorId)
+ const response = await api.patch(
+ API_ROUTES.ADMIN.VENDOR.UPDATE_STATUS(vendorId),
+ {
+ isBlocked,
+ }
+ );
+ console.log("update vendor ",response.data)
+
+ return response.data.data;
+
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to update vendor status."
+ );
+
+ }
+ }
+);
+
+const adminVendorSlice = createSlice({
+ name: "adminVendor",
+ initialState,
+ reducers: {},
+ extraReducers: (builder) => {
+
+ builder
+
+ // =========================
+ // GET VENDORS
+ // =========================
+
+ .addCase(getVendors.pending, (state) => {
+
+ state.loading = true;
+ state.error = null;
+
+ })
+
+ .addCase(getVendors.fulfilled, (state, action) => {
+
+ state.loading = false;
+
+ state.vendors = action.payload.data;
+
+ state.pagination.totalPages =
+ action.payload.totalPages;
+
+ state.pagination.totalCount =
+ action.payload.totalCount;
+
+ })
+
+ .addCase(getVendors.rejected, (state, action) => {
+
+ state.loading = false;
+
+ state.error = action.payload;
+
+ })
+
+ // =========================
+ // APPROVE VENDOR
+ // =========================
+
+ .addCase(approveVendor.pending, (state) => {
+
+ state.loading = true;
+
+ })
+
+ .addCase(approveVendor.fulfilled, (state, action) => {
+
+ state.loading = false;
+
+ const updatedVendor = action.payload;
+
+ const index = state.vendors.findIndex(
+
+ (vendor) => vendor._id === updatedVendor._id
+
+ );
+
+ if (index !== -1) {
+
+ state.vendors[index] = updatedVendor;
+
+ }
+
+ })
+
+ .addCase(approveVendor.rejected, (state, action) => {
+
+ state.loading = false;
+
+ state.error = action.payload;
+
+ })
+
+ // =========================
+ // REJECT VENDOR
+ // =========================
+
+ .addCase(rejectVendor.pending, (state) => {
+
+ state.loading = true;
+
+ })
+
+ .addCase(rejectVendor.fulfilled, (state, action) => {
+
+ state.loading = false;
+
+ const updatedVendor = action.payload;
+
+ const index = state.vendors.findIndex(
+
+ (vendor) => vendor._id === updatedVendor._id
+
+ );
+
+ if (index !== -1) {
+
+ state.vendors[index] = updatedVendor;
+
+ }
+
+ })
+
+ .addCase(rejectVendor.rejected, (state, action) => {
+
+ state.loading = false;
+
+ state.error = action.payload;
+
+ })
+
+ // =========================
+ // BLOCK / UNBLOCK
+ // =========================
+
+ .addCase(updateVendorStatus.pending, (state) => {
+
+ state.loading = true;
+
+ })
+
+ .addCase(updateVendorStatus.fulfilled, (state, action) => {
+
+ state.loading = false;
+
+ const updatedVendor = action.payload;
+
+ const index = state.vendors.findIndex(
+
+ (vendor) => vendor._id === updatedVendor._id
+
+ );
+
+ if (index !== -1) {
+
+ state.vendors[index] = updatedVendor;
+
+ }
+
+ })
+
+ .addCase(updateVendorStatus.rejected, (state, action) => {
+
+ state.loading = false;
+
+ state.error = action.payload;
+
+ });
+
+}
+});
+export default adminVendorSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/AuthSlice.js b/client/src/redux/slices/AuthSlice.js
new file mode 100644
index 0000000000..9fc83a8df6
--- /dev/null
+++ b/client/src/redux/slices/AuthSlice.js
@@ -0,0 +1,254 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+
+const initialState = {
+ loading: false,
+ error: null,
+ user: null,
+ role: null,
+ accessToken: null,
+ isAuthenticated: false,
+};
+
+
+export const registerUser = createAsyncThunk(
+ "auth/registerUser",
+ async ({ role, userData }, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.AUTH.REGISTER(role),
+ userData
+ );
+
+ return {
+ ...response.data,
+ role,
+ };
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Registration failed"
+ );
+ }
+ }
+);
+
+export const verifyOtp = createAsyncThunk(
+ "auth/verifyOtp",
+ async ({ role, email, otpCode }, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.AUTH.VERIFY_OTP(role),
+ { email, otpCode }
+ );
+
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "OTP verification failed"
+ );
+ }
+ }
+);
+
+
+export const resendOtp = createAsyncThunk(
+ "auth/resendOtp",
+ async ({ role, email }, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.AUTH.RESEND_OTP(role),
+ { email }
+ );
+
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to resend OTP"
+ );
+ }
+ }
+);
+
+export const login = createAsyncThunk(
+ "auth/login",
+ async ({ role, data }, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.AUTH.LOGIN(role),
+ data
+ );
+
+ console.log("response from login: ", response.data.data);
+ return response.data.data;
+
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Login failed"
+ );
+ }
+ }
+);
+
+export const logout = createAsyncThunk(
+ "auth/logout",
+ async ({ role }, { rejectWithValue }) => {
+ try {
+ const response = await api.post(API_ROUTES.AUTH.LOGOUT(role));
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Logout failed"
+ );
+ }
+ }
+);
+
+
+export const resetPassword = createAsyncThunk(
+ "auth/resetPassword",
+ async (
+ { role, token, password, confirmPassword },
+ { rejectWithValue }
+ ) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.AUTH.RESET_PASSWORD(role),
+ { token, password, confirmPassword }
+ );
+
+ return response.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Password reset failed"
+ );
+ }
+ }
+);
+
+
+export const checkAuth = createAsyncThunk(
+ "auth/checkAuth",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.get(API_ROUTES.AUTH.GETME);
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Check auth failed"
+ );
+ }
+ }
+);
+
+
+
+const authSlice = createSlice({
+ name: "auth",
+ initialState,
+
+ reducers: {
+ setAccessToken: (state, action) => {
+ state.accessToken = action.payload
+ },
+ },
+
+ extraReducers: (builder) => {
+ builder
+ .addCase(registerUser.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(registerUser.fulfilled, (state, action) => {
+ state.loading = false;
+ state.role = action.payload.role;
+ })
+ .addCase(registerUser.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+ .addCase(verifyOtp.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(verifyOtp.fulfilled, (state) => {
+ state.loading = false;
+ })
+ .addCase(verifyOtp.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+ .addCase(resendOtp.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(resendOtp.fulfilled, (state) => {
+ state.loading = false;
+ })
+ .addCase(resendOtp.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+ .addCase(login.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(login.fulfilled, (state, action) => {
+ state.loading = false;
+ state.accessToken = action.payload.accessToken
+ state.user = action.payload.user
+ state.role = action.payload.role
+ state.isAuthenticated = true;
+ })
+ .addCase(login.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ state.isAuthenticated = false;
+ })
+ .addCase(logout.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(logout.fulfilled, (state) => {
+ state.loading = false
+ state.user = null
+ state.accessToken = null
+ state.isAuthenticated = false
+ })
+ .addCase(logout.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+ .addCase(resetPassword.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(resetPassword.fulfilled, (state) => {
+ state.loading = false;
+ })
+ .addCase(resetPassword.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+ .addCase(checkAuth.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(checkAuth.fulfilled, (state, action) => {
+ state.loading = false;
+ state.accessToken = action.payload.accessToken
+ state.user = action.payload.user
+ state.isAuthenticated = true;
+ })
+ .addCase(checkAuth.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ state.isAuthenticated = false;
+ });
+ },
+});
+
+export const { setAccessToken } = authSlice.actions;
+
+export default authSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/UserBookingSlice.js b/client/src/redux/slices/UserBookingSlice.js
new file mode 100644
index 0000000000..2b709c5d99
--- /dev/null
+++ b/client/src/redux/slices/UserBookingSlice.js
@@ -0,0 +1,551 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+ bookings: [],
+ booking: null,
+ loading: false,
+ error: null,
+ currentBooking: null,
+
+ reservation: null,
+
+ totalPages: 1,
+
+ totalCount: 0,
+
+ currentPage: 1,
+
+ success: false,
+
+ pagination: {
+ currentPage: 1,
+ totalPages: 1,
+ totalBookings: 0,
+ limit: 5,
+ },
+
+
+};
+
+// ======================================
+// RESERVE BOOKING
+// ======================================
+
+export const reserveBooking = createAsyncThunk(
+ "userBooking/reserveBooking",
+
+ async (bookingData, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.USER.BOOKINGS.RESERVE,
+ bookingData
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to reserve booking"
+ );
+ }
+ }
+);
+
+// ======================================
+// CONFIRM BOOKING
+// ======================================
+
+export const confirmBooking = createAsyncThunk(
+ "userBooking/confirmBooking",
+
+ async (
+ {
+ reservationId,
+ venueId,
+ bookingDate,
+ paymentOption,
+ paymentMethod,
+ },
+ { rejectWithValue }
+ ) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.USER.BOOKINGS.CONFIRM,
+ {
+ reservationId,
+ venueId,
+ bookingDate,
+ paymentOption,
+ paymentMethod,
+ }
+ );
+
+ return response.data.data;
+
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to confirm booking"
+ );
+ }
+ }
+);
+
+
+// =========================
+// Get All Bookings
+// =========================
+
+export const getBookings = createAsyncThunk(
+ "userBooking/getBookings",
+ async ({ page = 1, limit = 5, status, }, { rejectWithValue }) => {
+ console.log(page, limit, status);
+ try {
+ const response = await api.get(API_ROUTES.USER.BOOKINGS.GET_ALL, {
+ params: {
+ page,
+ limit,
+ status,
+ },
+ });
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to fetch bookings"
+ );
+ }
+ }
+);
+
+// =========================
+// Get Booking By Id
+// =========================
+
+export const getBookingById = createAsyncThunk(
+ "userBooking/getBookingById",
+ async (bookingId, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.USER.BOOKINGS.GET_BY_ID(bookingId)
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to fetch booking"
+ );
+ }
+ }
+);
+
+// =========================
+// Cancel Booking
+// =========================
+export const cancelBooking = createAsyncThunk(
+ "userBooking/cancelBooking",
+ async ({ bookingId, cancellationReason }, { rejectWithValue }) => {
+ try {
+ const response = await api.patch(
+ API_ROUTES.USER.BOOKINGS.CANCEL(bookingId),
+ {
+ cancellationReason,
+ }
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to cancel booking"
+ );
+ }
+ }
+ );
+
+
+// ======================================
+// FETCH USER BOOKINGS
+// ======================================
+
+export const fetchUserBookings = createAsyncThunk(
+ "userBooking/fetchUserBookings",
+
+ async (
+ {
+ page = 1,
+ limit = 10,
+ status,
+ search,
+ sortBy,
+ } = {},
+ { rejectWithValue }
+ ) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.USER.BOOKINGS.GET_ALL,
+ {
+ params: {
+ page,
+ limit,
+ status: status || undefined,
+ search: search || undefined,
+ sortBy: sortBy || undefined,
+ },
+ }
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch bookings"
+ );
+ }
+ }
+);
+
+// ======================================
+// FETCH BOOKING BY ID
+// ======================================
+
+export const fetchBookingById = createAsyncThunk(
+ "userBooking/fetchBookingById",
+
+ async (bookingId, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.USER.BOOKINGS.GET_BY_ID(
+ bookingId
+ )
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch booking details"
+ );
+ }
+ }
+);
+
+const userBookingSlice = createSlice({
+ name: "userBooking",
+
+ initialState,
+
+ reducers: {
+ // ==============================
+ // CLEAR BOOKING ERROR
+ // ==============================
+
+ clearBookingError: (state) => {
+ state.error = null;
+ },
+
+ // ==============================
+ // CLEAR RESERVATION
+ // ==============================
+
+ clearReservation: (state) => {
+ state.reservation = null;
+ },
+
+ // ==============================
+ // CLEAR CURRENT BOOKING
+ // ==============================
+
+ clearCurrentBooking: (state) => {
+ state.currentBooking = null;
+ },
+
+ // ==============================
+ // RESET BOOKING STATE
+ // ==============================
+
+ resetBookingState: (state) => {
+ state.loading = false;
+
+ state.bookings = [];
+
+ state.currentBooking = null;
+
+ state.reservation = null;
+
+ state.totalPages = 1;
+
+ state.totalCount = 0;
+
+ state.currentPage = 1;
+
+ state.error = null;
+
+ state.success = false;
+ },
+ },
+
+ extraReducers: (builder) => {
+ builder
+
+ // =========================
+ // Get All Bookings
+ // =========================
+
+ .addCase(getBookings.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(getBookings.fulfilled, (state, action) => {
+ state.loading = false;
+ state.bookings = action.payload.bookings;
+ state.pagination = action.payload.pagination;
+ })
+
+ .addCase(getBookings.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // =========================
+ // Get Booking Details
+ // =========================
+
+ .addCase(getBookingById.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(getBookingById.fulfilled, (state, action) => {
+ state.loading = false;
+ state.booking = action.payload;
+ })
+
+ .addCase(getBookingById.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==================================
+ // RESERVE BOOKING
+ // ==================================
+
+
+ .addCase(
+ reserveBooking.pending,
+ (state) => {
+ state.loading = true;
+
+ state.error = null;
+
+ state.success = false;
+ }
+ )
+
+ .addCase(
+ reserveBooking.fulfilled,
+ (state, action) => {
+ state.loading = false;
+
+ state.reservation =
+ action.payload;
+
+ state.success = true;
+ }
+ )
+
+ .addCase(
+ reserveBooking.rejected,
+ (state, action) => {
+ state.loading = false;
+
+ state.error =
+ action.payload;
+
+ state.success = false;
+ }
+ )
+
+ // ==================================
+ // CONFIRM BOOKING
+ // ==================================
+
+
+ .addCase(
+ confirmBooking.pending,
+ (state) => {
+ state.loading = true;
+
+ state.error = null;
+
+ state.success = false;
+ }
+ )
+
+ .addCase(
+ confirmBooking.fulfilled,
+ (state, action) => {
+ state.loading = false;
+
+ state.currentBooking =
+ action.payload;
+
+ state.bookings.unshift(
+ action.payload
+ );
+
+ state.reservation = null;
+
+ state.success = true;
+ }
+ )
+
+ .addCase(
+ confirmBooking.rejected,
+ (state, action) => {
+ state.loading = false;
+
+ state.error =
+ action.payload;
+
+ state.success = false;
+ }
+ )
+
+ // =========================
+ // Cancel Booking
+ // =========================
+
+ .addCase(cancelBooking.pending, (state) => {
+ state.loading = true;
+ })
+
+ .addCase(cancelBooking.fulfilled, (state, action) => {
+ state.loading = false;
+
+ state.bookings = state.bookings.map((booking) =>
+ booking.id === action.payload.id
+ ? { ...booking, status: action.payload.status }
+ : booking
+ );
+ })
+
+ .addCase(cancelBooking.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+// ==================================
+ // FETCH USER BOOKINGS
+ // ==================================
+
+
+ .addCase(
+ fetchUserBookings.pending,
+ (state) => {
+ state.loading = true;
+
+ state.error = null;
+
+ state.success = false;
+ }
+ )
+
+ .addCase(
+ fetchUserBookings.fulfilled,
+ (state, action) => {
+ state.loading = false;
+
+ const payload =
+ action.payload;
+
+ /*
+ * Expected backend response:
+ *
+ * {
+ * bookings: [],
+ * totalPages: 1,
+ * totalCount: 10,
+ * currentPage: 1
+ * }
+ */
+
+ state.bookings =
+ payload?.bookings || [];
+
+ state.totalPages =
+ payload?.totalPages || 1;
+
+ state.totalCount =
+ payload?.totalCount || 0;
+
+ state.currentPage =
+ payload?.currentPage || 1;
+ }
+ )
+
+ .addCase(
+ fetchUserBookings.rejected,
+ (state, action) => {
+ state.loading = false;
+
+ state.error =
+ action.payload;
+
+ state.success = false;
+ }
+ )
+
+ // ==================================
+ // FETCH BOOKING BY ID
+ // ==================================
+
+
+ .addCase(
+ fetchBookingById.pending,
+ (state) => {
+ state.loading = true;
+
+ state.error = null;
+
+ state.success = false;
+ }
+ )
+
+ .addCase(
+ fetchBookingById.fulfilled,
+ (state, action) => {
+ state.loading = false;
+
+ state.currentBooking =
+ action.payload;
+ }
+ )
+
+ .addCase(
+ fetchBookingById.rejected,
+ (state, action) => {
+ state.loading = false;
+
+ state.error =
+ action.payload;
+ }
+ );
+ },
+});
+
+// ======================================
+// ACTIONS
+// ======================================
+
+export const {
+ clearBookingError,
+ clearReservation,
+ clearCurrentBooking,
+ resetBookingState,
+} = userBookingSlice.actions;
+
+
+export default userBookingSlice.reducer;
diff --git a/client/src/redux/slices/UserProfileSlice.js b/client/src/redux/slices/UserProfileSlice.js
new file mode 100644
index 0000000000..337ce5962a
--- /dev/null
+++ b/client/src/redux/slices/UserProfileSlice.js
@@ -0,0 +1,258 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+ loading: false,
+ error: null,
+ user: null,
+};
+
+// Get Profile
+export const getProfile = createAsyncThunk(
+ "user/profile",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.get(API_ROUTES.USER.PROFILE.PROFILE);
+
+ console.log("Profile API Response:", response.data);
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to fetch profile"
+ );
+ }
+ }
+);
+
+// Update Profile
+export const updateProfile = createAsyncThunk(
+ "user/updateProfile",
+ async (profileData, { rejectWithValue }) => {
+ try {
+ const response = await api.patch(
+ API_ROUTES.USER.PROFILE.PROFILE,
+ profileData
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to update profile"
+ );
+ }
+ }
+);
+
+export const updateProfileImage = createAsyncThunk(
+ "user/updateProfileImage",
+ async (imageFile, { rejectWithValue }) => {
+ try {
+ const formData = new FormData();
+
+ formData.append("profileImage", imageFile);
+
+ const response = await api.patch(
+ API_ROUTES.USER.PROFILE.PROFILE_IMAGE,
+ formData,
+ {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ }
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to upload image"
+ );
+ }
+ }
+);
+
+export const removeProfileImage = createAsyncThunk(
+ "userProfile/removeProfileImage",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.delete(API_ROUTES.USER.PROFILE.PROFILE_IMAGE);
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to remove profile image"
+ );
+ }
+ }
+);
+
+export const changePassword = createAsyncThunk(
+ "user/changePassword",
+ async (passwordData, { rejectWithValue }) => {
+ try {
+ const response = await api.patch(
+ API_ROUTES.USER.PROFILE.CHANGE_PASSWORD,
+ passwordData
+ );
+
+ return response.data.message;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to change password"
+ );
+ }
+ }
+);
+
+export const requestEmailChangeOtp = createAsyncThunk(
+ "user/requestEmailChangeOtp",
+ async (newEmail, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.USER.PROFILE.REQUEST_EMAIL_CHANGE_OTP,
+ {
+ newEmail,
+ }
+ );
+
+ return response.data.message;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to send OTP"
+ );
+ }
+ }
+);
+
+export const verifyEmailOtp = createAsyncThunk(
+ "user/verifyEmailOtp",
+ async (otp, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.USER.PROFILE.VERIFY_EMAIL_CHANGE_OTP,
+ {
+ otp,
+ }
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "OTP verification failed"
+ );
+ }
+ }
+);
+
+export const resendEmailOtp = createAsyncThunk(
+ "user/resendEmailOtp",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.USER.PROFILE.RESEND_EMAIL_CHANGE_OTP
+ );
+
+ return response.data.message;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to resend OTP"
+ );
+ }
+ }
+);
+
+const UserProfileSlice = createSlice({
+ name: "userProfile",
+ initialState,
+ reducers: {},
+
+ extraReducers: (builder) => {
+ builder
+
+ // GET PROFILE
+ .addCase(getProfile.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(getProfile.fulfilled, (state, action) => {
+ state.loading = false;
+ state.user = action.payload;
+ })
+
+ .addCase(getProfile.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // UPDATE PROFILE
+ .addCase(updateProfile.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(updateProfile.fulfilled, (state, action) => {
+ state.loading = false;
+ state.user = action.payload;
+ })
+
+ .addCase(updateProfile.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ //UPDATE PROFILE IMAGE
+ .addCase(updateProfileImage.pending, (state) => {
+ state.loading = true;
+ })
+
+ .addCase(updateProfileImage.fulfilled, (state, action) => {
+ state.loading = false;
+ state.user = action.payload;
+ })
+
+ .addCase(updateProfileImage.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ //REMOVE PROFILE IMAGE
+ .addCase(removeProfileImage.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(removeProfileImage.fulfilled, (state, action) => {
+ state.loading = false;
+ state.user = action.payload;
+ })
+
+ .addCase(removeProfileImage.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ //CHANGE PASSWORD
+ .addCase(changePassword.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(changePassword.fulfilled, (state) => {
+ state.loading = false;
+ })
+
+ .addCase(changePassword.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // Verify OTP
+ .addCase(verifyEmailOtp.fulfilled, (state, action) => {
+ state.user = action.payload;
+ });
+ },
+});
+
+export default UserProfileSlice.reducer;
diff --git a/client/src/redux/slices/UserVenueSlice.js b/client/src/redux/slices/UserVenueSlice.js
new file mode 100644
index 0000000000..c18ab8f595
--- /dev/null
+++ b/client/src/redux/slices/UserVenueSlice.js
@@ -0,0 +1,128 @@
+import { API_ROUTES } from "@/constants/apiRoutes"
+import api from "@/lib/axios"
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
+
+const initialState = {
+ loading: true,
+ error: null,
+ venues: [],
+ selectedVenue:null,
+ pagination: {
+ venues: {
+ totalPages: 0,
+ totalCount: 0
+ },
+ }
+}
+
+
+export const getVenues = createAsyncThunk('user/venues', async(params = {}, { rejectWithValue}) => {
+ try {
+ console.log('params: ', params)
+ const response = await api.get(API_ROUTES.USER.VENUE.VENUES, {
+ params: {
+ page: params.page || 1,
+ limit: params.limit || 10,
+ search: params.search || "",
+ category: params.category || undefined ,
+ amenities: params.amenities,
+ rating: params.rating || 0,
+ capacityType: params.capacityType || "",
+ capacity: params.capacity || "",
+ priceType: params.priceType || "",
+ minPrice: params.minPrice || "",
+ maxPrice: params.maxPrice || ""
+ },
+ paramsSerializer: {
+ indexes: null
+ }
+ })
+
+ console.log('response: ', response.data.data)
+ return response.data.data
+ } catch (error) {
+ return rejectWithValue("Failed to get venues", error)
+ }
+})
+
+export const getVenueById = createAsyncThunk(
+ 'user/venue-by-id',
+ async (id, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.USER.VENUE.GET_BY_ID(id)
+ );
+
+ return response.data.data
+ } catch (error) {
+
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to get venue details"
+ );
+ }
+ }
+);
+
+export const getTopVenues = createAsyncThunk('user/top-venues', async(_,{ rejectWithValue}) => {
+ try {
+ console.log('reached here...')
+ const response = await api.get(API_ROUTES.USER.VENUE.TOP_VENUES)
+ console.log('response: ', response.data.data)
+ return response.data.data
+ } catch (error) {
+ return rejectWithValue("Failed to get top venues", error)
+ }
+})
+
+
+const userVenueSlice = createSlice({
+ name: 'UserVenueSlice',
+ initialState,
+ reducers: {},
+ extraReducers: (builder) => {
+ builder
+ .addCase(getVenues.pending, (state) => {
+ state.loading = true
+ })
+ .addCase(getVenues.fulfilled, (state, action) => {
+ state.loading = false
+ state.venues = action.payload.data
+ state.pagination.venues.totalCount = action.payload.totalCount
+ state.pagination.venues.totalPages = action.payload.totalPages
+ })
+ .addCase(getVenues.rejected, (state, action) => {
+ state.loading = false
+ state.error = action.payload
+ })
+ .addCase(getTopVenues.pending, (state) => {
+ state.loading = true
+ })
+
+ // get venue by id
+
+ .addCase(getVenueById.pending, (state) => {
+ state.loading = true
+ state.error = null
+ })
+ .addCase(getVenueById.fulfilled, (state, action) => {
+ state.loading = false
+ state.selectedVenue = action.payload
+ })
+ .addCase(getVenueById.rejected, (state, action) => {
+ state.loading = false
+ state.error = action.payload
+ })
+
+
+ .addCase(getTopVenues.fulfilled, (state, action) => {
+ state.loading = false
+ state.venues = action.payload.venues
+ })
+ .addCase(getTopVenues.rejected, (state, action) => {
+ state.loading = false
+ state.error = action.payload
+ })
+ }
+})
+
+export default userVenueSlice.reducer
\ No newline at end of file
diff --git a/client/src/redux/slices/UserWishlistSlice.js b/client/src/redux/slices/UserWishlistSlice.js
new file mode 100644
index 0000000000..e26d4db1b0
--- /dev/null
+++ b/client/src/redux/slices/UserWishlistSlice.js
@@ -0,0 +1,101 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+ wishlist: [],
+ loading: false,
+ error: null,
+};
+
+// Get Wishlist
+export const getWishlist = createAsyncThunk(
+ "wishlist/getWishlist",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.get(API_ROUTES.USER.WISHLIST.GET);
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to fetch wishlist"
+ );
+ }
+ }
+);
+
+// Add Wishlist
+export const addToWishlist = createAsyncThunk(
+ "wishlist/addToWishlist",
+ async (venueId, { rejectWithValue }) => {
+ try {
+ const response = await api.post(
+ API_ROUTES.USER.WISHLIST.ADD(venueId)
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to add wishlist"
+ );
+ }
+ }
+);
+
+// Remove Wishlist
+export const removeWishlist = createAsyncThunk(
+ "wishlist/removeWishlist",
+ async (venueId, { rejectWithValue }) => {
+ try {
+ await api.delete(
+ API_ROUTES.USER.WISHLIST.REMOVE(venueId)
+ );
+
+ return venueId;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to remove wishlist"
+ );
+ }
+ }
+);
+
+const UserWishlistSlice = createSlice({
+ name: "userWishlist",
+ initialState,
+ reducers: {},
+
+ extraReducers: (builder) => {
+ builder
+
+ // Get Wishlist
+ .addCase(getWishlist.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(getWishlist.fulfilled, (state, action) => {
+ state.loading = false;
+ state.wishlist = action.payload;
+ })
+
+ .addCase(getWishlist.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // Add Wishlist
+ .addCase(addToWishlist.fulfilled, (state, action) => {
+ state.loading = false;
+ })
+
+ // Remove Wishlist
+ .addCase(removeWishlist.fulfilled, (state, action) => {
+ state.wishlist = state.wishlist.filter(
+ (venue) => venue.id !== action.payload
+ );
+ });
+ },
+});
+
+export default UserWishlistSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/VendorBookingSlice.js b/client/src/redux/slices/VendorBookingSlice.js
new file mode 100644
index 0000000000..3e3079339a
--- /dev/null
+++ b/client/src/redux/slices/VendorBookingSlice.js
@@ -0,0 +1,155 @@
+import { API_ROUTES } from "@/constants/apiRoutes";
+import api from "@/lib/axios";
+import {
+ createAsyncThunk,
+ createSlice,
+} from "@reduxjs/toolkit";
+
+const initialState = {
+ loading: false,
+
+ bookings: [],
+
+ totalPages: 0,
+ totalCount: 0,
+
+ bookingDetails: null,
+ detailsLoading: false,
+
+ error: null,
+};
+
+// ==============================
+// GET ALL VENDOR BOOKINGS
+// ==============================
+
+export const fetchBookings = createAsyncThunk(
+ "vendorBooking/fetchBookings",
+
+ async (params = {}, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.VENDOR.BOOKINGS,
+ {
+ params: {
+ page: params.page || 1,
+ limit: params.limit || 20,
+ search: params.search || "",
+ status: params.status || undefined,
+ },
+ }
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch bookings"
+ );
+ }
+ }
+);
+
+// ==============================
+// GET BOOKING BY ID
+// ==============================
+
+export const fetchBookingById = createAsyncThunk(
+ "vendorBooking/fetchBookingById",
+
+ async (bookingId, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.VENDOR.BOOKING_BY_ID(bookingId)
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch booking"
+ );
+ }
+ }
+);
+
+// ==============================
+// SLICE
+// ==============================
+
+const VendorBookingSlice = createSlice({
+ name: "vendorBooking",
+
+ initialState,
+
+ reducers: {
+ clearBookingDetails: (state) => {
+ state.bookingDetails = null;
+ state.detailsLoading = false;
+ },
+
+ clearBookingError: (state) => {
+ state.error = null;
+ },
+ },
+
+ extraReducers: (builder) => {
+ builder
+
+ // ==========================
+ // GET ALL BOOKINGS
+ // ==========================
+
+ .addCase(fetchBookings.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(fetchBookings.fulfilled, (state, action) => {
+ state.loading = false;
+
+ state.bookings =
+ action.payload.bookings || [];
+
+ state.totalCount =
+ action.payload.totalCount || 0;
+
+ state.totalPages =
+ action.payload.totalPages || 0;
+ })
+
+ .addCase(fetchBookings.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // GET BOOKING BY ID
+ // ==========================
+
+ .addCase(fetchBookingById.pending, (state) => {
+ state.detailsLoading = true;
+ state.error = null;
+ state.bookingDetails = null;
+ })
+
+ .addCase(fetchBookingById.fulfilled, (state, action) => {
+ state.detailsLoading = false;
+
+ state.bookingDetails =
+ action.payload;
+ })
+
+ .addCase(fetchBookingById.rejected, (state, action) => {
+ state.detailsLoading = false;
+ state.error = action.payload;
+ });
+ },
+});
+
+export const {
+ clearBookingDetails,
+ clearBookingError,
+} = VendorBookingSlice.actions;
+
+export default VendorBookingSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/VendorDashboardSlice.js b/client/src/redux/slices/VendorDashboardSlice.js
new file mode 100644
index 0000000000..f16ff77985
--- /dev/null
+++ b/client/src/redux/slices/VendorDashboardSlice.js
@@ -0,0 +1,69 @@
+import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+// ==============================
+// INITIAL STATE
+// ==============================
+const initialState = {
+ loading: false,
+ dashboard: null,
+ error: null,
+};
+
+// ==============================
+// GET VENDOR DASHBOARD
+// ==============================
+export const fetchDashboard = createAsyncThunk(
+ "vendorDashboard/fetchDashboard",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.get(API_ROUTES.VENDOR.DASHBOARD);
+ const data = response.data.data;
+ const normalized = {
+ recentBookings: data.recentBookings || [],
+ topVenues: data.topVenues || [],
+ totalBookings: data.stats?.totalBookings ?? 0,
+ totalVenues: data.stats?.totalVenues ?? 0,
+ pendingApprovals: data.stats?.pendingBookings ?? 0,
+ confirmedBookings: data.stats?.confirmedBookings ?? 0,
+ completedBookings: data.stats?.completedBookings ?? 0,
+ totalRevenue: data.stats?.totalRevenue ?? 0,
+ bookingTrend: data.stats?.bookingTrend || [],
+ monthlyRevenue: data.stats?.monthlyRevenue || [],
+ };
+
+ return normalized;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message || "Failed to fetch dashboard"
+ );
+ }
+ }
+);
+
+// ==============================
+// SLICE
+// ==============================
+const VendorDashboardSlice = createSlice({
+ name: "vendorDashboard",
+ initialState,
+ reducers: {},
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchDashboard.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(fetchDashboard.fulfilled, (state, action) => {
+ state.loading = false;
+ state.dashboard = action.payload;
+ })
+ .addCase(fetchDashboard.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ });
+ },
+});
+
+export default VendorDashboardSlice.reducer;
diff --git a/client/src/redux/slices/VendorProfileSlice.js b/client/src/redux/slices/VendorProfileSlice.js
new file mode 100644
index 0000000000..90e985a3ce
--- /dev/null
+++ b/client/src/redux/slices/VendorProfileSlice.js
@@ -0,0 +1,138 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+// ==============================
+// INITIAL STATE
+// ==============================
+
+const initialState = {
+ loading: false,
+ updating: false,
+ profile: null,
+ error: null,
+};
+
+// ==============================
+// FETCH VENDOR PROFILE
+// ==============================
+
+export const fetchVendorProfile = createAsyncThunk(
+ "vendorProfile/fetchVendorProfile",
+ async (_, { rejectWithValue }) => {
+ try {
+ const response = await api.get(
+ API_ROUTES.VENDOR.PROFILE
+ );
+
+ console.log(
+ "PROFILE RAW RESPONSE:",
+ response.data
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch profile"
+ );
+ }
+ }
+);
+
+// ==============================
+// UPDATE VENDOR PROFILE
+// ==============================
+
+export const updateVendorProfile = createAsyncThunk(
+ "vendorProfile/updateVendorProfile",
+ async (profileData, { rejectWithValue }) => {
+ try {
+ const response = await api.patch(
+ API_ROUTES.VENDOR.PROFILE,
+ profileData
+ );
+
+ return response.data.data;
+ } catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to update profile"
+ );
+ }
+ }
+);
+
+// ==============================
+// SLICE
+// ==============================
+
+const vendorProfileSlice = createSlice({
+ name: "vendorProfile",
+
+ initialState,
+
+ reducers: {},
+
+ extraReducers: (builder) => {
+ builder
+
+ // ==========================
+ // FETCH PROFILE
+ // ==========================
+
+ .addCase(
+ fetchVendorProfile.pending,
+ (state) => {
+ state.loading = true;
+ state.error = null;
+ }
+ )
+
+ .addCase(
+ fetchVendorProfile.fulfilled,
+ (state, action) => {
+ state.loading = false;
+ state.profile = action.payload;
+ }
+ )
+
+ .addCase(
+ fetchVendorProfile.rejected,
+ (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ }
+ )
+
+ // ==========================
+ // UPDATE PROFILE
+ // ==========================
+
+ .addCase(
+ updateVendorProfile.pending,
+ (state) => {
+ state.updating = true;
+ state.error = null;
+ }
+ )
+
+ .addCase(
+ updateVendorProfile.fulfilled,
+ (state, action) => {
+ state.updating = false;
+ state.profile = action.payload;
+ }
+ )
+
+ .addCase(
+ updateVendorProfile.rejected,
+ (state, action) => {
+ state.updating = false;
+ state.error = action.payload;
+ }
+ );
+ },
+});
+
+export default vendorProfileSlice.reducer;
\ No newline at end of file
diff --git a/client/src/redux/slices/VendorVenueSlice.js b/client/src/redux/slices/VendorVenueSlice.js
new file mode 100644
index 0000000000..5148f15864
--- /dev/null
+++ b/client/src/redux/slices/VendorVenueSlice.js
@@ -0,0 +1,399 @@
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import api from "@/lib/axios";
+import { API_ROUTES } from "@/constants/apiRoutes";
+
+const initialState = {
+vendorId: "",
+
+loading: false,
+success: false,
+venue: null,
+venues: [],
+
+totalPages: 1,
+totalCount: 0,
+
+error: null,
+};
+
+// ==============================
+// FETCH VENDOR PROFILE
+// ==============================
+
+export const fetchVendorProfile = createAsyncThunk(
+"vendorVenue/fetchVendorProfile",
+
+async (_, { rejectWithValue }) => {
+try {
+const response = await api.get(API_ROUTES.VENDOR.PROFILE);
+
+
+ return response.data.data.id;
+} catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to load vendor profile"
+ );
+}
+
+
+}
+);
+
+// ==============================
+// CREATE VENUE
+// ==============================
+
+export const createVenue = createAsyncThunk(
+"vendorVenue/createVenue",
+
+async (formData, { rejectWithValue }) => {
+try {
+const response = await api.post(
+API_ROUTES.VENDOR.CREATE_VENUE,
+formData,
+{
+headers: {
+"Content-Type": "multipart/form-data",
+},
+}
+);
+
+
+ return response.data.data;
+} catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to create venue"
+ );
+}
+
+
+}
+);
+
+// ==============================
+// FETCH ALL VENDOR VENUES
+// ==============================
+
+export const fetchVenues = createAsyncThunk(
+"vendorVenue/fetchVenues",
+
+async (params = {}, { rejectWithValue }) => {
+try {
+const response = await api.get(
+API_ROUTES.VENDOR.VENUES,
+{
+params,
+}
+);
+
+
+ return response.data.data;
+} catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch venues"
+ );
+}
+
+
+}
+);
+
+// ==============================
+// GET VENUE BY ID
+// ==============================
+
+export const getVenueById = createAsyncThunk(
+"vendorVenue/getVenueById",
+
+async (venueId, { rejectWithValue }) => {
+try {
+const response = await api.get(
+API_ROUTES.VENDOR.VENUE_BY_ID(venueId)
+);
+
+
+ return response.data.data;
+} catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to fetch venue"
+ );
+}
+
+
+}
+);
+
+// ==============================
+// UPDATE VENUE
+// ==============================
+
+export const updateVenue = createAsyncThunk(
+"vendorVenue/updateVenue",
+
+async (
+{ venueId, formData },
+{ rejectWithValue }
+) => {
+try {
+const response = await api.patch(
+API_ROUTES.VENDOR.UPDATE_VENUE(venueId),
+formData,
+{
+headers: {
+"Content-Type": "multipart/form-data",
+},
+}
+);
+
+
+ return response.data.data;
+} catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to update venue"
+ );
+}
+
+
+}
+);
+
+// ==============================
+// DELETE VENUE
+// ==============================
+
+export const deleteVenue = createAsyncThunk(
+"vendorVenue/deleteVenue",
+
+async (venueId, { rejectWithValue }) => {
+try {
+await api.delete(
+API_ROUTES.VENDOR.DELETE_VENUE(venueId)
+);
+
+
+ return venueId;
+} catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to delete venue"
+ );
+}
+
+
+}
+);
+
+// ==============================
+// UPDATE VENUE STATUS
+// ==============================
+
+export const updateVenueStatus = createAsyncThunk(
+"vendorVenue/updateVenueStatus",
+
+async (
+{ venueId, status },
+{ rejectWithValue }
+) => {
+try {
+const response = await api.patch(
+API_ROUTES.VENDOR.UPDATE_VENUE_STATUS(venueId),
+{ status }
+);
+
+ return response.data.data;
+} catch (error) {
+ return rejectWithValue(
+ error.response?.data?.message ||
+ "Failed to update venue status"
+ );
+}
+
+
+}
+);
+
+// ==============================
+// SLICE
+// ==============================
+
+const VendorVenueSlice = createSlice({
+name: "vendorVenue",
+
+initialState,
+
+reducers: {
+clearVenueState: (state) => {
+state.loading = false;
+state.success = false;
+state.error = null;
+state.venue = null;
+},
+},
+
+extraReducers: (builder) => {
+builder
+
+
+ // ==========================
+ // FETCH VENDOR PROFILE
+ // ==========================
+
+ .addCase(fetchVendorProfile.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(fetchVendorProfile.fulfilled, (state, action) => {
+ state.loading = false;
+ state.vendorId = action.payload;
+ })
+
+ .addCase(fetchVendorProfile.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // CREATE VENUE
+ // ==========================
+
+ .addCase(createVenue.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(createVenue.fulfilled, (state, action) => {
+ state.loading = false;
+ state.venue = action.payload;
+ })
+
+ .addCase(createVenue.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // FETCH VENUES
+ // ==========================
+
+ .addCase(fetchVenues.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(fetchVenues.fulfilled, (state, action) => {
+ state.loading = false;
+
+ state.venues = action.payload.data || [];
+ state.totalPages = action.payload.totalPages || 1;
+ state.totalCount = action.payload.totalCount || 0;
+ })
+
+ .addCase(fetchVenues.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // GET VENUE BY ID
+ // ==========================
+
+ .addCase(getVenueById.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(getVenueById.fulfilled, (state, action) => {
+ state.loading = false;
+ state.venue = action.payload;
+ })
+
+ .addCase(getVenueById.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // UPDATE VENUE
+ // ==========================
+
+ .addCase(updateVenue.pending, (state) => {
+ state.loading = true;
+ state.success = false;
+ state.error = null;
+ })
+
+ .addCase(updateVenue.fulfilled, (state, action) => {
+ state.loading = false;
+ state.success = true;
+ state.venue = action.payload;
+ })
+
+ .addCase(updateVenue.rejected, (state, action) => {
+ state.loading = false;
+ state.success = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // DELETE VENUE
+ // ==========================
+
+ .addCase(deleteVenue.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(deleteVenue.fulfilled, (state, action) => {
+ state.loading = false;
+
+ state.venues = state.venues.filter(
+ (venue) => venue.id !== action.payload
+ );
+ })
+
+ .addCase(deleteVenue.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ })
+
+ // ==========================
+ // UPDATE VENUE STATUS
+ // ==========================
+
+ .addCase(updateVenueStatus.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+
+ .addCase(updateVenueStatus.fulfilled, (state, action) => {
+ state.loading = false;
+
+ const updatedVenue = action.payload;
+
+ state.venues = state.venues.map((venue) =>
+ venue.id === updatedVenue.id
+ ? updatedVenue
+ : venue
+ );
+ })
+
+ .addCase(updateVenueStatus.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload;
+ });
+
+
+},
+});
+
+export const {
+clearVenueState,
+} = VendorVenueSlice.actions;
+
+export default VendorVenueSlice.reducer;
diff --git a/client/src/redux/store.js b/client/src/redux/store.js
new file mode 100644
index 0000000000..a73f982f92
--- /dev/null
+++ b/client/src/redux/store.js
@@ -0,0 +1,44 @@
+import { configureStore } from "@reduxjs/toolkit";
+import userVenueSlice from './slices/UserVenueSlice'
+import UserProfileSlice from "./slices/UserProfileSlice";
+import UserWishlistSlice from "./slices/UserWishlistSlice";
+import userBookingSlice from "./slices/UserBookingSlice";
+import adminUserSlice from './slices/AdminUserSlice';
+import adminVendorSlice from './slices/AdminvendorSlice';
+import VendorVenueSlice from './slices/VendorVenueSlice';
+import VendorDashboardSlice from './slices/VendorDashboardSlice'
+import VendorBookingSlice from './slices/VendorBookingSlice'
+import VendorProfileSlice from './slices/VendorProfileSlice'
+import adminVenueSlice from './slices/AdminVenueSlice'
+import adminBookingSlice from './slices/AdminBookingSlice'
+import adminPaymentSlice from './slices/AdminPaymentSlice'
+import adminDashboardSlice from './slices/AdminDashboardSlice'
+import authSlice from './slices/AuthSlice'
+import { injectStore } from "@/lib/axios";
+
+export const store = configureStore({
+ reducer: {
+ auth:authSlice,
+
+ adminUser: adminUserSlice,
+ adminVendor: adminVendorSlice,
+ adminVenue: adminVenueSlice,
+ adminBooking:adminBookingSlice,
+ adminPayment:adminPaymentSlice,
+ adminDashboard:adminDashboardSlice,
+
+
+ userVenue: userVenueSlice,
+ userProfile: UserProfileSlice,
+ userWishlist: UserWishlistSlice,
+ userBooking: userBookingSlice,
+
+ vendorVenue: VendorVenueSlice,
+ vendorDashboard: VendorDashboardSlice,
+ vendorBooking: VendorBookingSlice,
+ vendorProfile: VendorProfileSlice,
+ }
+})
+
+injectStore(store)
+
diff --git a/client/src/utils/.gitkeep b/client/src/utils/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/client/vite.config.js b/client/vite.config.js
new file mode 100644
index 0000000000..e8042ca719
--- /dev/null
+++ b/client/vite.config.js
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+import path from 'path'
+
+export default defineConfig({
+ plugins: [react(),tailwindcss(),],
+ resolve:{
+ alias:{
+ "@":path.resolve(__dirname,"./src")
+ },
+ },
+})
diff --git a/server/eslint.config.js b/server/eslint.config.js
new file mode 100644
index 0000000000..9c040b576c
--- /dev/null
+++ b/server/eslint.config.js
@@ -0,0 +1,47 @@
+// import js from "@eslint/js";
+// import globals from "globals";
+// import json from "@eslint/json";
+// import { defineConfig } from "eslint/config";
+
+// export default defineConfig([
+// { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
+// { files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] },
+// ]);
+
+import js from "@eslint/js";
+import globals from "globals";
+import json from "@eslint/json";
+import { defineConfig } from "eslint/config";
+
+export default defineConfig([
+ {
+ ignores: [
+ "node_modules/**",
+ ".env",
+ "package-lock.json"
+ ]
+ },
+ {
+ files: ["**/*.{js,mjs,cjs}"],
+ plugins: { js },
+ extends: ["js/recommended"],
+
+ languageOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ globals: globals.node
+ },
+
+ rules: {
+ "no-unused-vars": "warn",
+ "no-console": "off"
+ }
+ },
+
+ {
+ files: ["**/*.json"],
+ plugins: { json },
+ language: "json/json",
+ extends: ["json/recommended"]
+ }
+]);
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 0000000000..785bc03747
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,4061 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "server",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@aws-sdk/lib-storage": "^3.1058.0",
+ "aws-sdk": "^2.1693.0",
+ "bcrypt": "^6.0.0",
+ "bcryptjs": "^3.0.3",
+ "cloudinary": "^1.41.3",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.2",
+ "express": "^5.2.1",
+ "ioredis": "^5.11.1",
+ "jsonwebtoken": "^9.0.3",
+ "mongodb": "^7.2.0",
+ "mongoose": "^9.6.3",
+ "multer": "^2.1.1",
+ "multer-storage-cloudinary": "^4.0.0",
+ "node-cron": "^4.6.0",
+ "nodemailer": "^8.0.8",
+ "passport": "^0.7.0",
+ "passport-google-oauth20": "^2.0.0",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@eslint/json": "^2.0.0",
+ "eslint": "^10.4.1",
+ "globals": "^17.6.0",
+ "nodemon": "^3.1.14"
+ }
+ },
+ "node_modules/@aws-crypto/crc32": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
+ "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/crc32c": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz",
+ "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz",
+ "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
+ "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-js": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
+ "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/supports-web-crypto": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
+ "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
+ "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.222.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/client-s3": {
+ "version": "3.1058.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1058.0.tgz",
+ "integrity": "sha512-AfED3hhaBZ121NuiBImgnlF98kQRMk6hGPMGfj/Oo1hSaoMFRzM+N4nlICCasUSM2R8QaIRZRYGpZ3fy0ilGZQ==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@aws-crypto/sha1-browser": "5.2.0",
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/credential-provider-node": "^3.972.48",
+ "@aws-sdk/middleware-bucket-endpoint": "^3.972.17",
+ "@aws-sdk/middleware-expect-continue": "^3.972.14",
+ "@aws-sdk/middleware-flexible-checksums": "^3.974.23",
+ "@aws-sdk/middleware-location-constraint": "^3.972.11",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.44",
+ "@aws-sdk/middleware-ssec": "^3.972.11",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.30",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/fetch-http-handler": "^5.4.5",
+ "@smithy/node-http-handler": "^4.7.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/core": {
+ "version": "3.974.15",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.15.tgz",
+ "integrity": "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.9",
+ "@aws-sdk/xml-builder": "^3.972.26",
+ "@aws/lambda-invoke-store": "^0.2.2",
+ "@smithy/core": "^3.24.5",
+ "@smithy/signature-v4": "^5.4.5",
+ "@smithy/types": "^4.14.2",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/crc64-nvme": {
+ "version": "3.972.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.9.tgz",
+ "integrity": "sha512-P+QGozmXn2mZZI7sDgk+aUm+RTI61MPSFB+Ir2vjEjEbEsE4e7hYtzrDvAUxZy9ko81h53e11+F/GYlvwDkaOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.972.41",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.41.tgz",
+ "integrity": "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.972.43",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.43.tgz",
+ "integrity": "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/fetch-http-handler": "^5.4.5",
+ "@smithy/node-http-handler": "^4.7.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.972.46",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.46.tgz",
+ "integrity": "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/credential-provider-env": "^3.972.41",
+ "@aws-sdk/credential-provider-http": "^3.972.43",
+ "@aws-sdk/credential-provider-login": "^3.972.45",
+ "@aws-sdk/credential-provider-process": "^3.972.41",
+ "@aws-sdk/credential-provider-sso": "^3.972.45",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.45",
+ "@aws-sdk/nested-clients": "^3.997.13",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/credential-provider-imds": "^4.3.6",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-login": {
+ "version": "3.972.45",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.45.tgz",
+ "integrity": "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/nested-clients": "^3.997.13",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.972.48",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.48.tgz",
+ "integrity": "sha512-QIbtJP0olSLZ2ImEu636pP+7JJbPfaL3xSJIFXhu472CWuondCc4bGOa8OeyhOFet8z4H1D/ZFKXc39FboWwYA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "^3.972.41",
+ "@aws-sdk/credential-provider-http": "^3.972.43",
+ "@aws-sdk/credential-provider-ini": "^3.972.46",
+ "@aws-sdk/credential-provider-process": "^3.972.41",
+ "@aws-sdk/credential-provider-sso": "^3.972.45",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.45",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/credential-provider-imds": "^4.3.6",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.972.41",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.41.tgz",
+ "integrity": "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.972.45",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.45.tgz",
+ "integrity": "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/nested-clients": "^3.997.13",
+ "@aws-sdk/token-providers": "3.1056.0",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.972.45",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.45.tgz",
+ "integrity": "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/nested-clients": "^3.997.13",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/lib-storage": {
+ "version": "3.1058.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1058.0.tgz",
+ "integrity": "sha512-/uGg/qXRqDRABWYoahwzx1aCUPzjKDSfAoWSCRtiHU8mR3piDFNj7u5eYluyancllSB0oJAh3F0+hFmWdr7CxQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "buffer": "5.6.0",
+ "events": "3.3.0",
+ "stream-browserify": "3.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/client-s3": "^3.1058.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-bucket-endpoint": {
+ "version": "3.972.17",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.17.tgz",
+ "integrity": "sha512-lbDmWuHenc+kiwCNrxz4MyN6nkxCWyTXPIWuspJN0ibziu+8CXci7vI1bK9MAkwy8cwJOEXNu0gBM5S0uTGRIg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-expect-continue": {
+ "version": "3.972.14",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.14.tgz",
+ "integrity": "sha512-3TNFEVGO4sWZj9TEXOCZLzGEctXHnaO4fk2EQ8KVaboTbwHmEPEQrm17Xb9koImUIXEw0sgi2xtHjg7LuTS3rA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-flexible-checksums": {
+ "version": "3.974.23",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.23.tgz",
+ "integrity": "sha512-4nPKARo2lfKvQGUt2fPA5NlS/mEohckdxpuC9ecbjVfj7B7NFFYHeTg+Bf5BEQwdn3yRfUIzFiEkPp8Yuaw3wA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@aws-crypto/crc32c": "5.2.0",
+ "@aws-crypto/util": "5.2.0",
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/crc64-nvme": "^3.972.9",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-location-constraint": {
+ "version": "3.972.11",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.11.tgz",
+ "integrity": "sha512-hkfspNUP4criAH6ton6BGKgnm5dZx+7bUOy1YqlTfejDeUPAM23D81q/IX+hdlS3KUsfwGz5ADTqZWKBEUpf4A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.972.44",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.44.tgz",
+ "integrity": "sha512-8HQsRg1NpX8vR4vNl1E8pyLnqZroq9VSL2vZQVSgBqp6wv6365LzYD08/c9FFh/9FTg7YRc7aTtEmXF0ir/pqg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.30",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-ssec": {
+ "version": "3.972.11",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.11.tgz",
+ "integrity": "sha512-7PQvGNhtveKlvVqNahqWx5yrwxP7ecwAoB1dYBf8eKwfo2tzzCbNnW+q2nO3N066ktQaB4iBQbDRWtizm+amoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients": {
+ "version": "3.997.13",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.13.tgz",
+ "integrity": "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.30",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/fetch-http-handler": "^5.4.5",
+ "@smithy/node-http-handler": "^4.7.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.30",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz",
+ "integrity": "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/signature-v4": "^5.4.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/token-providers": {
+ "version": "3.1056.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1056.0.tgz",
+ "integrity": "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.974.15",
+ "@aws-sdk/nested-clients": "^3.997.13",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.973.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz",
+ "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-locate-window": {
+ "version": "3.965.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz",
+ "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder": {
+ "version": "3.972.26",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.26.tgz",
+ "integrity": "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.14.2",
+ "fast-xml-parser": "5.7.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws/lambda-invoke-store": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
+ "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@eslint/json/-/json-2.0.0.tgz",
+ "integrity": "sha512-P32ZJMIopNWQd1SFhd0tgjfA/hgzUuVSqHmMi2273QaLWHWimXq6V+qL4DNKnjGzO/aNECtYW+rEJ/pWB6uP+w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.1",
+ "@humanwhocodes/momoa": "^3.3.10",
+ "natural-compare": "^1.4.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+ "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/momoa": {
+ "version": "3.3.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz",
+ "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@ioredis/commands": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz",
+ "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==",
+ "license": "MIT"
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
+ "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
+ "license": "MIT",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "node_modules/@nodable/entities": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz",
+ "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@smithy/core": {
+ "version": "3.24.6",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz",
+ "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/credential-provider-imds": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.7.tgz",
+ "integrity": "sha512-xj8gq/bjFABAh6qWPSDCYcY3kzQIm4b561C+YnHH4zGq8rOgzQ3Shk+JGlpUxSd41UGiO6FkLdUCtNX1FAeHgg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/fetch-http-handler": {
+ "version": "5.4.6",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz",
+ "integrity": "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/node-http-handler": {
+ "version": "4.7.6",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.6.tgz",
+ "integrity": "sha512-3fya8i7GrJilQouk4cZJKdy5k8MWQBpjfXrRNaXDedH8r779tr0jcxyH3+yoTmsluc2+vF4S343yFbnvu8ExDQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.6",
+ "@smithy/types": "^4.14.3",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/signature-v4": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.5.tgz",
+ "integrity": "sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.24.5",
+ "@smithy/types": "^4.14.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/types": {
+ "version": "4.14.3",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz",
+ "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/append-field": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+ "license": "MIT"
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/aws-sdk": {
+ "version": "2.1693.0",
+ "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1693.0.tgz",
+ "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==",
+ "deprecated": "The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "buffer": "4.9.2",
+ "events": "1.1.1",
+ "ieee754": "1.1.13",
+ "jmespath": "0.16.0",
+ "querystring": "0.2.0",
+ "sax": "1.2.1",
+ "url": "0.10.3",
+ "util": "^0.12.4",
+ "uuid": "8.0.0",
+ "xml2js": "0.6.2"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/aws-sdk/node_modules/buffer": {
+ "version": "4.9.2",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+ "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4",
+ "isarray": "^1.0.0"
+ }
+ },
+ "node_modules/aws-sdk/node_modules/events": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+ "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/aws-sdk/node_modules/ieee754": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/base64url": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bcrypt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
+ "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^8.3.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/bcryptjs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bowser": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
+ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bson": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
+ "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cloudinary": {
+ "version": "1.41.3",
+ "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.41.3.tgz",
+ "integrity": "sha512-4o84y+E7dbif3lMns+p3UW6w6hLHEifbX/7zBJvaih1E9QNMZITENQ14GPYJC4JmhygYXsuuBb9bRA3xWEoOfg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "cloudinary-core": "^2.13.0",
+ "core-js": "^3.30.1",
+ "lodash": "^4.17.21",
+ "q": "^1.5.1"
+ },
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/cloudinary-core": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/cloudinary-core/-/cloudinary-core-2.14.1.tgz",
+ "integrity": "sha512-57rgZSQD2cJsz1rga6M7jIDQuEAzkwvq63vTvs3/I8rNpGLyHMoKoIvBkNS0Guv5RZ9KDReJhI2LmElk4D9U1g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "lodash": ">=4.0"
+ }
+ },
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz",
+ "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-parser": {
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
+ "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "0.7.2",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/core-js": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
+ "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.2",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+ "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz",
+ "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.6.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.2",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-xml-builder": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
+ "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.5.0",
+ "xml-naming": "^0.1.0"
+ }
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz",
+ "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@nodable/entities": "^2.1.0",
+ "fast-xml-builder": "^1.1.7",
+ "path-expression-matcher": "^1.5.0",
+ "strnum": "^2.2.3"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.6.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
+ "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ioredis": {
+ "version": "5.11.1",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz",
+ "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@ioredis/commands": "1.10.0",
+ "cluster-key-slot": "1.1.1",
+ "debug": "4.4.3",
+ "denque": "2.1.0",
+ "redis-errors": "1.2.0",
+ "redis-parser": "3.0.0",
+ "standard-as-callback": "2.1.0"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ioredis"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jmespath": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
+ "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/kareem": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz",
+ "integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "license": "MIT"
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz",
+ "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^7.2.0",
+ "mongodb-connection-string-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+ "snappy": "^7.3.2",
+ "socks": "^2.8.6"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "9.6.3",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.3.tgz",
+ "integrity": "sha512-vI6dTTlQnfMCyyQ5TrvhG0bCRs4dq5e1uFNPtOOWsOhn0fSg8AoIHjfyyCYr8aybyvPs845dRHGxsC3w/fHcBA==",
+ "license": "MIT",
+ "dependencies": {
+ "kareem": "3.3.0",
+ "mongodb": "~7.2",
+ "mpath": "0.9.0",
+ "mquery": "6.0.0",
+ "ms": "2.1.3",
+ "sift": "17.1.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mongoose"
+ }
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mquery": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
+ "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/multer": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
+ "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^1.0.0",
+ "busboy": "^1.6.0",
+ "concat-stream": "^2.0.0",
+ "type-is": "^1.6.18"
+ },
+ "engines": {
+ "node": ">= 10.16.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/multer-storage-cloudinary": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/multer-storage-cloudinary/-/multer-storage-cloudinary-4.0.0.tgz",
+ "integrity": "sha512-25lm9R6o5dWrHLqLvygNX+kBOxprzpmZdnVKH4+r68WcfCt8XV6xfQaMuAg+kUE5Xmr8mJNA4gE0AcBj9FJyWA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "cloudinary": "^1.21.0"
+ }
+ },
+ "node_modules/multer/node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/multer/node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz",
+ "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
+ "node_modules/node-cron": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.6.0.tgz",
+ "integrity": "sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
+ "node_modules/nodemailer": {
+ "version": "8.0.10",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.10.tgz",
+ "integrity": "sha512-BLFuSth7QtHOkBzyqTehWWyub0NTRDuK2Q2SQfnGLsrJnzyU+Yeh4WpV1eZGuARFj1xQJHIdnTuJZLP+b9R1GQ==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.14",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^10.2.1",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/oauth": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
+ "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/passport": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
+ "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "passport-strategy": "1.x.x",
+ "pause": "0.0.1",
+ "utils-merge": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-google-oauth20": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
+ "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
+ "license": "MIT",
+ "dependencies": {
+ "passport-oauth2": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/passport-oauth2": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
+ "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
+ "license": "MIT",
+ "dependencies": {
+ "base64url": "3.x.x",
+ "oauth": "0.10.x",
+ "passport-strategy": "1.x.x",
+ "uid2": "0.0.x",
+ "utils-merge": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-strategy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+ "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-expression-matcher": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
+ "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/pause": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/q": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
+ "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.0",
+ "teleport": ">=0.2.0"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==",
+ "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
+ "license": "MIT",
+ "dependencies": {
+ "redis-errors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/sax": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
+ "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==",
+ "license": "ISC"
+ },
+ "node_modules/semver": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
+ "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sift": {
+ "version": "17.1.3",
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
+ "license": "MIT"
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/standard-as-callback": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
+ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/stream-browserify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
+ "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "~2.0.4",
+ "readable-stream": "^3.5.0"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/strnum": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
+ "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/uid2": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
+ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
+ "license": "MIT"
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/url": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
+ "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "1.3.2",
+ "querystring": "0.2.0"
+ }
+ },
+ "node_modules/url/node_modules/punycode": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+ "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==",
+ "license": "MIT"
+ },
+ "node_modules/util": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+ "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz",
+ "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.21",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.21.tgz",
+ "integrity": "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/xml-naming": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
+ "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000000..2bf76bcaba
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "type": "module",
+ "main": "index.js",
+ "scripts": {
+ "dev": "nodemon src/server.js",
+ "start": "node src/server.js",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "dependencies": {
+ "@aws-sdk/lib-storage": "^3.1058.0",
+ "aws-sdk": "^2.1693.0",
+ "bcrypt": "^6.0.0",
+ "bcryptjs": "^3.0.3",
+ "cloudinary": "^1.41.3",
+ "cookie-parser": "^1.4.7",
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.2",
+ "express": "^5.2.1",
+ "ioredis": "^5.11.1",
+ "jsonwebtoken": "^9.0.3",
+ "mongodb": "^7.2.0",
+ "mongoose": "^9.6.3",
+ "multer": "^2.1.1",
+ "multer-storage-cloudinary": "^4.0.0",
+ "node-cron": "^4.6.0",
+ "nodemailer": "^8.0.8",
+ "passport": "^0.7.0",
+ "passport-google-oauth20": "^2.0.0",
+ "zod": "^4.4.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@eslint/json": "^2.0.0",
+ "eslint": "^10.4.1",
+ "globals": "^17.6.0",
+ "nodemon": "^3.1.14"
+ }
+}
diff --git a/server/src/application/admin/usecases/.gitkeep b/server/src/application/admin/usecases/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/application/admin/usecases/auth/admin.logOut.usecase.js b/server/src/application/admin/usecases/auth/admin.logOut.usecase.js
new file mode 100644
index 0000000000..bf01d12016
--- /dev/null
+++ b/server/src/application/admin/usecases/auth/admin.logOut.usecase.js
@@ -0,0 +1,39 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export class AdminLogoutUseCase {
+ constructor(
+ adminRepository,
+ hashService,
+ tokenService
+ ) {
+ this._adminRepository = adminRepository;
+ this._hashService = hashService;
+ this._tokenService = tokenService
+
+ }
+
+ async execute(refreshToken, accessToken) {
+ if(accessToken){
+ const expireInSeconds = process.env.ACCESS_TOKEN_MAX_AGE ? Math.floor(Number(process.env.ACCESS_TOKEN_MAX_AGE) / 1000 ): 3600
+ await this._tokenService.blackListToken(accessToken, expireInSeconds)
+ }
+ if (!refreshToken) {
+ throw new UnauthorizedError(authMessages.error.NO_REFRESH_TOKEN);
+ }
+
+ const { id, role } = this._tokenService.verifyRefreshToken(refreshToken)
+ if(!id || !role){
+ throw new UnauthorizedError(authMessages.error.UNAUTHORIZED)
+ }
+
+ const admin = await this._adminRepository.findById(id);
+
+ if (!admin) {
+ throw new UnauthorizedError(authMessages.error.INVALID_REFRESH_TOKEN);
+ }
+
+ const hashedRefreshToken = await this._hashService.hashToken(refreshToken)
+ await this._adminRepository.clearRefreshToken(hashedRefreshToken);
+ }
+}
diff --git a/server/src/application/admin/usecases/auth/admin.login.usecase.js b/server/src/application/admin/usecases/auth/admin.login.usecase.js
new file mode 100644
index 0000000000..e56b1c2a1d
--- /dev/null
+++ b/server/src/application/admin/usecases/auth/admin.login.usecase.js
@@ -0,0 +1,45 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+import { UserRole } from "../../../../domain/enums/UserRole.enum.js";
+
+export class LoginAdminUsecase {
+ constructor(
+ adminRepository,
+ hashService,
+ tokenService
+ ) {
+ this._adminRepository = adminRepository;
+ this._hashService = hashService;
+ this._tokenService = tokenService;
+ }
+
+ async execute({email, password}) {
+ const admin = await this._adminRepository.findByEmail(email);
+
+ if (!admin) {
+ throw new UnauthorizedError(authMessages.error.ADMIN_NOT_FOUND);
+ }
+
+ const isMatch = await this._hashService.compare(password, admin.password);
+ if (!isMatch) {
+ throw new UnauthorizedError(authMessages.error.INVALID_CREDENTIALS);
+ }
+
+ const payload = { id: admin.id, role: admin.role };
+ const accessToken = this._tokenService.generateAccessToken(payload);
+ const refreshToken = this._tokenService.generateRefreshToken(payload)
+ const hashedToken = await this._hashService.hashToken(refreshToken)
+ await this._adminRepository.updateRefreshToken(admin.id, hashedToken)
+
+ return {
+ accessToken,
+ refreshToken,
+ user: {
+ id: admin.id,
+ role: admin.role,
+ email: admin.email,
+ }
+ };
+ }
+}
+
diff --git a/server/src/application/admin/usecases/auth/admin.refreshToken.usecase.js b/server/src/application/admin/usecases/auth/admin.refreshToken.usecase.js
new file mode 100644
index 0000000000..395b8a75d9
--- /dev/null
+++ b/server/src/application/admin/usecases/auth/admin.refreshToken.usecase.js
@@ -0,0 +1,32 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export class AdminRefreshTokenUseCase {
+ constructor(adminRepository, tokenService, hashService) {
+ this._adminRepository = adminRepository;
+ this._tokenService = tokenService;
+ this._hashService = hashService
+ }
+
+ async execute(refreshToken) {
+ if (!refreshToken) {
+ throw new UnauthorizedError(authMessages.error.NO_REFRESH_TOKEN);
+ }
+
+ const {id, role} = this._tokenService.verifyRefreshToken(refreshToken);
+
+ const admin = await this._adminRepository.findById(id);
+
+ if (!admin) {
+ throw new UnauthorizedError(authMessages.error.REFRESH_TOKEN_REVOKED);
+ }
+
+ const payload = { id: admin.id, role: role };
+ const newAccessToken = this._tokenService.generateAccessToken(payload);
+ const newRefreshToken = this._tokenService.generateRefreshToken(payload);
+ const hashedRefreshToken = await this._hashService.hashToken(newRefreshToken)
+ await this._adminRepository.updateRefreshToken(admin.id, hashedRefreshToken);
+
+ return { accessToken: newAccessToken, refreshToken: newRefreshToken, user: admin };
+ }
+}
diff --git a/server/src/application/admin/usecases/booking/admin.getAllBookings.usecase.js b/server/src/application/admin/usecases/booking/admin.getAllBookings.usecase.js
new file mode 100644
index 0000000000..46709a5210
--- /dev/null
+++ b/server/src/application/admin/usecases/booking/admin.getAllBookings.usecase.js
@@ -0,0 +1,28 @@
+export class AdminGetAllBookingsUsecase {
+
+ constructor(bookingRepository) {
+ this._bookingRepository = bookingRepository;
+ }
+
+ async execute(
+ search,
+ status,
+ paymentStatus,
+ page,
+ limit,
+ sortBy,
+ bookingDate
+ ) {
+ return await this._bookingRepository.findAllFiltered({
+ search,
+ status,
+ paymentStatus,
+ page,
+ limit,
+ sortBy,
+ bookingDate
+ });
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/booking/admin.getBookingById.usecase.js b/server/src/application/admin/usecases/booking/admin.getBookingById.usecase.js
new file mode 100644
index 0000000000..bc0e02376b
--- /dev/null
+++ b/server/src/application/admin/usecases/booking/admin.getBookingById.usecase.js
@@ -0,0 +1,34 @@
+import { NotFoundError }
+from "../../../../domain/errors/NotFoundError.js";
+
+import { BookingMessages }
+from "../../../../shared/constants/messages/bookingMessages.js";
+
+export class AdminGetBookingByIdUsecase {
+
+ constructor(bookingRepository) {
+
+ this._bookingRepository = bookingRepository;
+
+ }
+
+ async execute(bookingId) {
+
+ const booking =
+ await this._bookingRepository.findById(
+ bookingId
+ );
+
+ if (!booking) {
+
+ throw new NotFoundError(
+ BookingMessages.error.BOOKING_NOT_FOUND
+ );
+
+ }
+
+ return booking;
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/booking/admin.getBookingStatistics.usecase.js b/server/src/application/admin/usecases/booking/admin.getBookingStatistics.usecase.js
new file mode 100644
index 0000000000..0bf34d5e66
--- /dev/null
+++ b/server/src/application/admin/usecases/booking/admin.getBookingStatistics.usecase.js
@@ -0,0 +1,18 @@
+export class AdminGetBookingStatisticsUsecase {
+
+ constructor(bookingRepository) {
+
+ this._bookingRepository =
+ bookingRepository;
+
+ }
+
+ async execute() {
+
+ return await this
+ ._bookingRepository
+ .getBookingStatistics();
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/dashboard/admin.getStatistics.usecase.js b/server/src/application/admin/usecases/dashboard/admin.getStatistics.usecase.js
new file mode 100644
index 0000000000..71da35dbc5
--- /dev/null
+++ b/server/src/application/admin/usecases/dashboard/admin.getStatistics.usecase.js
@@ -0,0 +1,9 @@
+export class AdminDashboardStatisticsUsecase {
+ constructor(dashboardRepository) {
+ this._dashboardRepository = dashboardRepository;
+ }
+
+ async execute() {
+ return await this._dashboardRepository.getDashboardStatistics();
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/payment/admin.getAllPayments.usecase.js b/server/src/application/admin/usecases/payment/admin.getAllPayments.usecase.js
new file mode 100644
index 0000000000..19af925b94
--- /dev/null
+++ b/server/src/application/admin/usecases/payment/admin.getAllPayments.usecase.js
@@ -0,0 +1,35 @@
+export class AdminGetAllPaymentsUsecase {
+
+ constructor(paymentRepository) {
+
+ this._paymentRepository = paymentRepository;
+
+ }
+
+ async execute(
+
+ search,
+ paymentStatus,
+ paymentMethod,
+ paymentType,
+ sortBy,
+ page,
+ limit
+
+ ) {
+
+ return await this._paymentRepository.findAllFiltered({
+
+ search,
+ paymentStatus,
+ paymentMethod,
+ paymentType,
+ sortBy,
+ page,
+ limit
+
+ });
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/payment/admin.getPaymentById.usecase.js b/server/src/application/admin/usecases/payment/admin.getPaymentById.usecase.js
new file mode 100644
index 0000000000..6cea57fc47
--- /dev/null
+++ b/server/src/application/admin/usecases/payment/admin.getPaymentById.usecase.js
@@ -0,0 +1,29 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { PaymentMessages } from "../../../../shared/constants/messages/paymentMessages.js";
+
+export class AdminGetPaymentByIdUsecase {
+
+ constructor(paymentRepository) {
+
+ this._paymentRepository = paymentRepository;
+
+ }
+
+ async execute(paymentId) {
+
+ const payment =
+ await this._paymentRepository.findById(paymentId);
+
+ if (!payment) {
+
+ throw new NotFoundError(
+ PaymentMessages.error.PAYMENT_NOT_FOUND
+ );
+
+ }
+
+ return payment;
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/payment/admin.getPaymentStatistics.usecase.js b/server/src/application/admin/usecases/payment/admin.getPaymentStatistics.usecase.js
new file mode 100644
index 0000000000..c419828e98
--- /dev/null
+++ b/server/src/application/admin/usecases/payment/admin.getPaymentStatistics.usecase.js
@@ -0,0 +1,15 @@
+export class AdminGetPaymentStatisticsUsecase {
+
+ constructor(paymentRepository) {
+
+ this._paymentRepository = paymentRepository;
+
+ }
+
+ async execute() {
+
+ return await this._paymentRepository.getPaymentStatistics();
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/user/admin.getAllUsers.usecase.js b/server/src/application/admin/usecases/user/admin.getAllUsers.usecase.js
new file mode 100644
index 0000000000..fe6109be96
--- /dev/null
+++ b/server/src/application/admin/usecases/user/admin.getAllUsers.usecase.js
@@ -0,0 +1,24 @@
+
+export class AdminGetAllUsersUsecase {
+
+ constructor(userRepository){
+ this._userRepository = userRepository
+ }
+
+ async execute(search, isBlocked,page, limit){
+
+ const { data, totalPages, totalCount } =
+ await this._userRepository.findAllFiltered({
+ search,
+ isBlocked,
+ page,
+ limit
+ })
+
+ return {
+ data,
+ totalPages,
+ totalCount
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/user/admin.unblockUser.usecase.js b/server/src/application/admin/usecases/user/admin.unblockUser.usecase.js
new file mode 100644
index 0000000000..8362b8ab8c
--- /dev/null
+++ b/server/src/application/admin/usecases/user/admin.unblockUser.usecase.js
@@ -0,0 +1,25 @@
+import { AppError } from "../../../../domain/errors/app.error.js";
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { statusCode } from "../../../../shared/constants/enums/statusCode.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+import { UserEntity } from "../../../../domain/entities/User.js";
+import { ConflictError } from "../../../../domain/errors/ConflictError.js";
+
+
+export class AdminUnblockUserUsecase {
+ constructor(userRepository){
+ this._userRepository = userRepository;
+ }
+
+ async execute(userId){
+ const user = await this._userRepository.findById(userId);
+ if(!user){
+ throw new NotFoundError(UserMessage.error.USER_NOT_FOUND);
+ }
+ if(!user.isBlocked) {
+ throw new ConflictError(UserMessage.error.USER_ALREADY_ACTIVE);
+ }
+ const unblockedUser = await this._userRepository.unblockUser(userId);
+ return unblockedUser;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/user/admin.updateUserStatus.usecase.js b/server/src/application/admin/usecases/user/admin.updateUserStatus.usecase.js
new file mode 100644
index 0000000000..f54bbf1def
--- /dev/null
+++ b/server/src/application/admin/usecases/user/admin.updateUserStatus.usecase.js
@@ -0,0 +1,20 @@
+import { AppError } from "../../../../domain/errors/app.error.js";
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { statusCode } from "../../../../shared/constants/enums/statusCode.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+import { UserEntity } from "../../../../domain/entities/User.js";
+
+export class AdminUpdateUserStatusUsecase {
+ constructor(userRepository){
+ this._userRepository = userRepository;
+ }
+
+ async execute(userId, isBlocked){
+ const user = await this._userRepository.findById(userId);
+ if(!user){
+ throw new NotFoundError(UserMessage.error.USER_NOT_FOUND);
+ }
+ const updatedUser = await this._userRepository.updateBlockStatus(userId, isBlocked);
+ return updatedUser;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/vendor/admin.approveVendor.usecase.js b/server/src/application/admin/usecases/vendor/admin.approveVendor.usecase.js
new file mode 100644
index 0000000000..7bd2e951c2
--- /dev/null
+++ b/server/src/application/admin/usecases/vendor/admin.approveVendor.usecase.js
@@ -0,0 +1,52 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { BadRequestError } from "../../../../domain/errors/BadRequestError.js";
+import { VendorMessages } from "../../../../shared/constants/messages/vendorMessages.js";
+import { VendorApprovalStatus } from "../../../../domain/enums/VendorApprovalStatus.enum.js";
+
+export class AdminApproveVendorUsecase {
+
+ constructor(vendorRepository, mailService) {
+ this._vendorRepository = vendorRepository;
+ this._mailService = mailService;
+ }
+
+ async execute(vendorId) {
+
+ const vendor =
+ await this._vendorRepository.findById(vendorId);
+
+ if (!vendor) {
+ throw new NotFoundError(
+ VendorMessages.error.VENDOR_NOT_FOUND
+ );
+ }
+
+ if (vendor.approvalStatus === VendorApprovalStatus.APPROVED) {
+ throw new BadRequestError(
+ VendorMessages.error.VENDOR_ALREADY_APPROVED
+ );
+ }
+
+ const approvedVendor =
+ await this._vendorRepository.approveVendor(vendorId);
+
+ if (!approvedVendor) {
+ throw new NotFoundError(
+ VendorMessages.error.VENDOR_NOT_FOUND
+ );
+ }
+
+ try {
+ await this._mailService.sendVendorApprovalMail(
+ approvedVendor
+ );
+ } catch (error) {
+ console.log(
+ "Approval email sending failed:",
+ error.message
+ );
+ }
+
+ return approvedVendor;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/vendor/admin.getAllVendors.usecase.js b/server/src/application/admin/usecases/vendor/admin.getAllVendors.usecase.js
new file mode 100644
index 0000000000..2c81727f52
--- /dev/null
+++ b/server/src/application/admin/usecases/vendor/admin.getAllVendors.usecase.js
@@ -0,0 +1,26 @@
+export class AdminGetAllVendorsUsecase {
+
+ constructor(vendorRepository){
+ this._vendorRepository =
+ vendorRepository
+ }
+
+ async execute(
+ search,
+ status,
+ isBlocked,
+ page,
+ limit
+ ){
+
+ return await
+ this._vendorRepository
+ .findAllFiltered({
+ search,
+ status,
+ isBlocked,
+ page,
+ limit
+ })
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/vendor/admin.getVendorById.usecase.js b/server/src/application/admin/usecases/vendor/admin.getVendorById.usecase.js
new file mode 100644
index 0000000000..355fbddd56
--- /dev/null
+++ b/server/src/application/admin/usecases/vendor/admin.getVendorById.usecase.js
@@ -0,0 +1,23 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { VendorMessages } from "../../../../shared/constants/messages/vendorMessages.js";
+
+export class AdminGetVendorByIdUsecase {
+
+ constructor(vendorRepository) {
+ this._vendorRepository =
+ vendorRepository
+ }
+
+ async execute(vendorId) {
+
+ const vendor =
+ await this._vendorRepository
+ .findById(vendorId)
+
+ if (!vendor) {
+ throw new NotFoundError(VendorMessages.error.VENDOR_NOT_FOUND);
+ }
+
+ return vendor
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/vendor/admin.rejectVendor.usecase.js b/server/src/application/admin/usecases/vendor/admin.rejectVendor.usecase.js
new file mode 100644
index 0000000000..42b3e43849
--- /dev/null
+++ b/server/src/application/admin/usecases/vendor/admin.rejectVendor.usecase.js
@@ -0,0 +1,62 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { BadRequestError } from "../../../../domain/errors/BadRequestError.js";
+import { VendorMessages } from "../../../../shared/constants/messages/vendorMessages.js";
+import { VendorApprovalStatus } from "../../../../domain/enums/VendorApprovalStatus.enum.js";
+
+export class AdminRejectVendorUsecase {
+
+ constructor(vendorRepository, mailService) {
+ this._vendorRepository = vendorRepository;
+ this._mailService = mailService;
+ }
+
+ async execute(vendorId, reason) {
+
+ const vendor =
+ await this._vendorRepository.findById(vendorId);
+
+ if (!vendor) {
+ throw new NotFoundError(
+ VendorMessages.error.VENDOR_NOT_FOUND
+ );
+ }
+
+ if (!reason?.trim()) {
+ throw new BadRequestError(
+ VendorMessages.error.REJECTION_REASON_REQUIRED
+ );
+ }
+
+ if (vendor.approvalStatus === VendorApprovalStatus.REJECTED) {
+ throw new BadRequestError(
+ VendorMessages.error.VENDOR_ALREADY_REJECTED
+ );
+ }
+
+ const rejectedVendor =
+ await this._vendorRepository.rejectVendor(
+ vendorId,
+ reason
+ );
+
+ if (!rejectedVendor) {
+ throw new NotFoundError(
+ VendorMessages.error.VENDOR_NOT_FOUND
+ );
+ }
+
+ try {
+ await this._mailService.sendVendorRejectionMail(
+ rejectedVendor,
+ reason
+ );
+ } catch (error) {
+ console.log(
+ "Rejection email sending failed:",
+ error.message
+ );
+ }
+
+ return rejectedVendor;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/vendor/admin.updateVendorStatus.js b/server/src/application/admin/usecases/vendor/admin.updateVendorStatus.js
new file mode 100644
index 0000000000..23d12f6b3a
--- /dev/null
+++ b/server/src/application/admin/usecases/vendor/admin.updateVendorStatus.js
@@ -0,0 +1,25 @@
+import { AppError } from "../../../../domain/errors/app.error.js";
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { statusCode } from "../../../../shared/constants/enums/statusCode.js";
+import { VendorMessages } from "../../../../shared/constants/messages/vendorMessages.js";
+import { VendorEntity } from "../../../../domain/entities/Vendor.js";
+import { BadRequestError } from "../../../../domain/errors/BadRequestError.js";
+
+export class AdminUpdateVendorStatusUsecase {
+ constructor(vendorRepository){
+ this._vendorRepository = vendorRepository;
+ }
+
+ async execute(vendorId, isBlocked){
+ const vendor = await this._vendorRepository.findById(vendorId);
+ if(!vendor){
+ throw new NotFoundError(VendorMessages.error.VENDOR_NOT_FOUND)
+
+ }
+ if(vendor.approvalStatus!=="APPROVED"){
+ throw new BadRequestError(VendorMessages.error.VENDOR_NOT_APPROVED_FOR_BLOCK_ACTION)
+ }
+ const updatedVendor = await this._vendorRepository.updateBlockStatus(vendorId, isBlocked);
+ return updatedVendor;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/venue/admin.approveVenue.usecase.js b/server/src/application/admin/usecases/venue/admin.approveVenue.usecase.js
new file mode 100644
index 0000000000..187fc181b7
--- /dev/null
+++ b/server/src/application/admin/usecases/venue/admin.approveVenue.usecase.js
@@ -0,0 +1,62 @@
+import { NotFoundError }
+from "../../../../domain/errors/NotFoundError.js";
+
+import { BadRequestError }
+from "../../../../domain/errors/BadRequestError.js";
+
+import { VenueMessages }
+from "../../../../shared/constants/messages/venueMessages.js";
+
+import { VenueStatus } from "../../../../domain/enums/Venue.enum.js";
+
+export class AdminApproveVenueUsecase {
+
+ constructor(
+ venueRepository,
+ mailService
+ ) {
+
+ this._venueRepository = venueRepository;
+ this._mailService = mailService;
+
+ }
+
+ async execute(venueId) {
+
+ const venue =
+ await this._venueRepository.findById(venueId);
+
+ if (!venue) {
+
+ throw new NotFoundError(
+ VenueMessages.error.VENUE_NOT_FOUND
+ );
+
+ }
+
+ if (
+ venue.approvalStatus ===
+ VenueStatus.ACTIVE
+ ) {
+
+ throw new BadRequestError(
+ VenueMessages.error.VENUE_ALREADY_APPROVED
+ );
+
+ }
+
+ const approvedVenue =
+ await this._venueRepository.approveVenue(
+ venueId
+ );
+
+ await this._mailService
+ .sendVenueApprovalMail(
+ approvedVenue
+ );
+
+ return approvedVenue;
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/venue/admin.getAllVenues.usecase.js b/server/src/application/admin/usecases/venue/admin.getAllVenues.usecase.js
new file mode 100644
index 0000000000..2151532219
--- /dev/null
+++ b/server/src/application/admin/usecases/venue/admin.getAllVenues.usecase.js
@@ -0,0 +1,27 @@
+export class AdminGetAllVenuesUsecase {
+
+ constructor(venueRepository) {
+ this._venueRepository = venueRepository;
+ }
+
+ async execute(
+ search,
+ category,
+ approvalStatus,
+ isBlocked,
+ page,
+ limit
+ ) {
+
+ return await this._venueRepository.findAllFiltered({
+ search,
+ category,
+ approvalStatus,
+ isBlocked,
+ page,
+ limit
+ });
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/venue/admin.getVenueById.usecase.js b/server/src/application/admin/usecases/venue/admin.getVenueById.usecase.js
new file mode 100644
index 0000000000..50876c4d8e
--- /dev/null
+++ b/server/src/application/admin/usecases/venue/admin.getVenueById.usecase.js
@@ -0,0 +1,30 @@
+import { NotFoundError }
+from "../../../../domain/errors/NotFoundError.js";
+
+import { VenueMessages }
+from "../../../../shared/constants/messages/venueMessages.js";
+
+export class AdminGetVenueByIdUsecase {
+
+ constructor(venueRepository) {
+ this._venueRepository = venueRepository;
+ }
+
+ async execute(venueId) {
+
+ const venue =
+ await this._venueRepository.findById(venueId);
+
+ if (!venue) {
+
+ throw new NotFoundError(
+ VenueMessages.error.VENUE_NOT_FOUND
+ );
+
+ }
+
+ return venue;
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/venue/admin.rejectVenue.usecase.js b/server/src/application/admin/usecases/venue/admin.rejectVenue.usecase.js
new file mode 100644
index 0000000000..b6ec5fd081
--- /dev/null
+++ b/server/src/application/admin/usecases/venue/admin.rejectVenue.usecase.js
@@ -0,0 +1,77 @@
+import { NotFoundError }
+from "../../../../domain/errors/NotFoundError.js";
+
+import { BadRequestError }
+from "../../../../domain/errors/BadRequestError.js";
+
+import { VenueMessages }
+from "../../../../shared/constants/messages/venueMessages.js";
+
+import { VenueStatus } from "../../../../domain/enums/Venue.enum.js";
+
+export class AdminRejectVenueUsecase {
+
+ constructor(
+ venueRepository,
+ mailService
+ ) {
+
+ this._venueRepository = venueRepository;
+ this._mailService = mailService;
+
+ }
+
+ async execute(
+ venueId,
+ reason
+ ) {
+
+ const venue =
+ await this._venueRepository.findById(
+ venueId
+ );
+
+ if (!venue) {
+
+ throw new NotFoundError(
+ VenueMessages.error.VENUE_NOT_FOUND
+ );
+
+ }
+
+ if (!reason?.trim()) {
+
+ throw new BadRequestError(
+ VenueMessages.error.REJECTION_REASON_REQUIRED
+ );
+
+ }
+
+ if (
+ venue.approvalStatus ===
+ VenueStatus.REJECTED
+ ) {
+
+ throw new BadRequestError(
+ VenueMessages.error.VENUE_ALREADY_REJECTED
+ );
+
+ }
+
+ const rejectedVenue =
+ await this._venueRepository.rejectVenue(
+ venueId,
+ reason
+ );
+
+ await this._mailService
+ .sendVenueRejectionMail(
+ rejectedVenue,
+ reason
+ );
+
+ return rejectedVenue;
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/admin/usecases/venue/admin.updateVenueStatus.usecase.js b/server/src/application/admin/usecases/venue/admin.updateVenueStatus.usecase.js
new file mode 100644
index 0000000000..d2bb49fa8c
--- /dev/null
+++ b/server/src/application/admin/usecases/venue/admin.updateVenueStatus.usecase.js
@@ -0,0 +1,65 @@
+import { NotFoundError }
+ from "../../../../domain/errors/NotFoundError.js";
+
+import { BadRequestError }
+ from "../../../../domain/errors/BadRequestError.js";
+
+import { VenueMessages }
+ from "../../../../shared/constants/messages/venueMessages.js";
+
+import { VenueStatus } from "../../../../domain/enums/Venue.enum.js";
+
+export class AdminUpdateVenueBlockStatusUsecase {
+
+ constructor(
+ venueRepository
+ ) {
+
+ this._venueRepository =
+ venueRepository;
+
+ }
+
+ async execute({
+
+ venueId,
+
+ isBlocked
+
+ }) {
+
+ const venue =
+ await this._venueRepository.findById(
+ venueId
+ );
+
+ if (!venue) {
+
+ throw new NotFoundError(
+ VenueMessages.error.VENUE_NOT_FOUND
+ );
+
+ }
+
+ if (
+ venue.approvalStatus !==
+ VenueStatus.ACTIVE
+ ) {
+
+ throw new BadRequestError(
+ VenueMessages.error
+ .ONLY_APPROVED_VENUE_CAN_BE_BLOCKED
+ );
+
+ }
+
+ const updatedVenue = await this._venueRepository
+ .updateBlockStatus(
+ venueId,
+ isBlocked
+ );
+ return updatedVenue
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/common/unified.getMe.usecase.js b/server/src/application/common/unified.getMe.usecase.js
new file mode 100644
index 0000000000..d01c93affd
--- /dev/null
+++ b/server/src/application/common/unified.getMe.usecase.js
@@ -0,0 +1,43 @@
+import { NotFoundError } from "../../domain/errors/NotFoundError.js"
+import { UnauthorizedError } from "../../domain/errors/UnauthorizedError.js"
+import { authMessages } from "../../shared/constants/messages/authMessages.js"
+
+export class UnifiedGetMeUsecase {
+ constructor (
+ tokenService,
+ repositories
+ ) {
+ this._tokenService = tokenService
+ this._repositories = repositories
+ }
+
+ async execute(refreshToken) {
+ const { id, role } = await this._tokenService.verifyRefreshToken(refreshToken)
+
+ if(!id || !role){
+ throw new UnauthorizedError(authMessages.error.UNAUTHORIZED)
+ }
+ const repository = this._repositories[role]
+ const user = await repository.findById(id)
+
+ if(!user){
+ throw new NotFoundError(authMessages.error.USER_NOT_FOUND)
+ }
+
+ const payload = { id: user.id, role: user.role };
+ const accessToken = this._tokenService.generateAccessToken(payload);
+
+ return {
+ accessToken,
+ user: {
+ id: user.id,
+ name: user.fullName,
+ email: user.email,
+ role: user.role,
+ isVerified: user.isVerified,
+ approvalStatus: user.approvalStatus,
+ profileImage: user.profileImage.url
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/mapper/Admin.mapper.js b/server/src/application/mapper/Admin.mapper.js
new file mode 100644
index 0000000000..d79b2ae6fa
--- /dev/null
+++ b/server/src/application/mapper/Admin.mapper.js
@@ -0,0 +1,31 @@
+import {AdminEntity} from "../../domain/entities/Admin.js";
+
+export class AdminMapper {
+
+ static mapToEntity(doc) {
+ return new AdminEntity({
+ id: doc._id?.toString(),
+ fullName: doc.fullName,
+ email: doc.email,
+ password: doc.password,
+ role: doc.role,
+ isDeleted: doc.isDeleted,
+ refreshToken: doc.refreshToken,
+ // isActive: doc.isActive,
+ createdAt: doc.createdAt,
+ updatedAt: doc.updatedAt,
+ });
+ }
+
+ static mapToPersistence(entity) {
+ return {
+ fullName: entity.fullName,
+ email: entity.email,
+ password: entity.password,
+ role: entity.role,
+ isDeleted: entity.isDeleted,
+ refreshToken: entity.refreshToken
+ // isActive: entity.isActive,
+ };
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/mapper/Booking.mapper.js b/server/src/application/mapper/Booking.mapper.js
new file mode 100644
index 0000000000..3ae5f9f645
--- /dev/null
+++ b/server/src/application/mapper/Booking.mapper.js
@@ -0,0 +1,108 @@
+import { Types } from "mongoose";
+import { Booking } from "../../domain/entities/Booking.js";
+
+export class BookingMapper {
+ static mapToEntity(doc) {
+ if (!doc) return null;
+
+ return new Booking({
+ id: doc._id ? doc._id.toString() : null,
+
+ // userId: doc.userId ? doc.userId.toString() : null,
+
+ //venueId: doc.venueId ? doc.venueId.toString() : null,
+
+ // vendorId: doc. vendorId? doc.vendorId.toString() : null,
+ userId: doc.userId
+ ? {
+ id: doc.userId._id?.toString(),
+ fullName: doc.userId.fullName,
+ email: doc.userId.email,
+ phone: doc.userId.phone,
+ }
+ : null,
+ vendorId: doc.vendorId
+ ? {
+ id: doc.vendorId._id?.toString(),
+ fullName: doc.vendorId.fullName,
+ email: doc.vendorId.email,
+ phone: doc.vendorId.phone,
+ companyName: doc.vendorId.companyName,
+ }
+ : null,
+ venueId: doc.venueId
+ ? {
+ id: doc.venueId._id?.toString(),
+ name: doc.venueId.name,
+ category: doc.venueId.category,
+ address: doc.venueId.address,
+ seatingCapacity: doc.venueId.seatingCapacity,
+ standingCapacity: doc.venueId.standingCapacity,
+ images: doc.venueId.images,
+ }
+ : null,
+
+ bookingDate: doc.bookingDate,
+
+ startTime: doc.startTime,
+
+ endTime: doc.endTime,
+
+ guestCount: doc.guestCount,
+
+ totalAmount: doc.totalAmount,
+
+ advanceAmount: doc.advanceAmount,
+
+ paidAmount: doc.paidAmount,
+
+ remainingAmount: doc.remainingAmount,
+
+ status: doc.status,
+
+ paymentStatus: doc.paymentStatus,
+
+ cancellationReason: doc.cancellationReason,
+
+ createdAt: doc.createdAt,
+
+ updatedAt: doc.updatedAt,
+ });
+ }
+
+ static mapToPersistence(entity) {
+ if (!entity) return null;
+
+ return {
+ userId: entity.userId ? new Types.ObjectId(entity.userId) : null,
+
+ venueId: entity.venueId ? new Types.ObjectId(entity.venueId) : null,
+
+ vendorId: entity.vendorId ? new Types.ObjectId(entity.vendorId) : null,
+
+ bookingDate: entity.bookingDate,
+
+ startTime: entity.startTime,
+
+ endTime: entity.endTime,
+
+ guestCount: entity.guestCount,
+
+ totalAmount: entity.totalAmount,
+
+ advanceAmount: entity.advanceAmount,
+
+ paidAmount: entity.paidAmount,
+
+ remainingAmount: entity.remainingAmount,
+
+ status: entity.status,
+
+ paymentStatus: entity.paymentStatus,
+
+ cancellationReason: entity.cancellationReason,
+ createdAt: entity.createdAt,
+ updatedAt: entity.updatedAt,
+ };
+ }
+}
diff --git a/server/src/application/mapper/Payment.mapper.js b/server/src/application/mapper/Payment.mapper.js
new file mode 100644
index 0000000000..663c08de0e
--- /dev/null
+++ b/server/src/application/mapper/Payment.mapper.js
@@ -0,0 +1,80 @@
+import { PaymentEntity } from "../../domain/entities/Payment.js";
+export class PaymentMapper {
+
+ static mapToEntity(document) {
+
+ if (!document) return null;
+
+ return new PaymentEntity({
+
+ id: document._id.toString(),
+
+ bookingId: document.bookingId
+ ? {
+ id: document.bookingId._id?.toString(),
+ bookingDate: document.bookingId.bookingDate,
+ startTime: document.bookingId.startTime,
+ endTime: document.bookingId.endTime,
+ totalAmount: document.bookingId.totalAmount,
+ status: document.bookingId.status,
+ paymentStatus: document.bookingId.paymentStatus,
+ }
+ : null,
+
+ userId: document.userId,
+
+ vendorId: document.vendorId,
+
+ amount: document.amount,
+
+ paymentType: document.paymentType,
+
+ paymentMethod: document.paymentMethod,
+
+ paymentStatus: document.paymentStatus,
+
+ refundAmount: document.refundAmount,
+
+ refundReason: document.refundReason,
+
+ refundedAt: document.refundedAt,
+
+ createdAt: document.createdAt,
+
+ updatedAt: document.updatedAt
+
+ });
+
+ }
+
+ static mapToPersistence(entity) {
+
+ return {
+
+ bookingId: entity.bookingId,
+
+ userId: entity.userId,
+
+ vendorId: entity.vendorId,
+
+ amount: entity.amount,
+
+ paymentType: entity.paymentType,
+
+ paymentMethod: entity.paymentMethod,
+
+ paymentStatus: entity.paymentStatus,
+
+ refundAmount: entity.refundAmount,
+
+ refundReason: entity.refundReason,
+
+ refundedAt: entity.refundedAt,
+ createdAt: entity.createdAt,
+ updatedAt: entity.updatedAt
+
+ };
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/mapper/User.mapper.js b/server/src/application/mapper/User.mapper.js
new file mode 100644
index 0000000000..99f35b509d
--- /dev/null
+++ b/server/src/application/mapper/User.mapper.js
@@ -0,0 +1,61 @@
+import { UserEntity } from "../../domain/entities/User.js";
+import { VenueMapper } from "./Venue.mapper.js";
+
+export class UserMapper {
+ static mapToEntity(doc) {
+ return new UserEntity({
+ id: doc._id?.toString(),
+ fullName: doc.fullName,
+ email: doc.email,
+ phone: doc.phone,
+ password: doc.password,
+ googleId: doc.googleId ?? null,
+ role: doc.role,
+ isOtpVerified: doc.isOtpVerified,
+ otpCode: doc.otpCode,
+ otpExpiresAt: doc.otpExpiresAt,
+ isBlocked: doc.isBlocked,
+ createdAt: doc.createdAt,
+ updatedAt: doc.updatedAt,
+ refreshToken: doc.refreshToken,
+ resetToken: doc.resetToken,
+ resetTokenExpiry: doc.resetTokenExpiry,
+ isVerified: doc.isVerified,
+ profileImage: doc.profileImage,
+ pendingEmail: doc.pendingEmail,
+ isDeleted: doc.isDeleted,
+ wishlist: doc.wishlist?.map(item =>
+ item?._id
+ ? VenueMapper.mapToEntity(item)
+ : item.toString()
+ ) || [],
+ });
+ }
+
+ static mapToPersistence(entity) {
+ return {
+ fullName: entity.fullName,
+ email: entity.email,
+ phone: entity.phone,
+ password: entity.password,
+ googleId: entity.googleId,
+ role: entity.role,
+ isOtpVerified: entity.isOtpVerified,
+ otpCode: entity.otpCode,
+ otpExpiresAt: entity.otpExpiresAt,
+ isBlocked: entity.isBlocked,
+ createdAt: entity.createdAt,
+ updatedAt: entity.updatedAt,
+ refreshToken: entity.refreshToken,
+ resetToken: entity.resetToken,
+ resetTokenExpiry: entity.resetTokenExpiry,
+ isVerified: entity.isVerified,
+ profileImage: entity.profileImage,
+ pendingEmail: entity.pendingEmail,
+ isDeleted: entity.isDeleted,
+ wishlist: entity.wishlist?.map(item =>
+ item.id ? item.id : item
+ ) || [],
+ };
+ }
+}
diff --git a/server/src/application/mapper/Vendor.mapper.js b/server/src/application/mapper/Vendor.mapper.js
new file mode 100644
index 0000000000..7ae9751c04
--- /dev/null
+++ b/server/src/application/mapper/Vendor.mapper.js
@@ -0,0 +1,59 @@
+import { VendorEntity } from "../../domain/entities/Vendor.js";
+
+export class VendorMapper {
+
+ static mapToEntity(doc) {
+
+ if (!doc) return null;
+
+ return new VendorEntity({
+ id: doc._id?.toString(),
+ fullName: doc.fullName,
+ email: doc.email,
+ phone: doc.phone,
+ password: doc.password,
+ profileImage: doc.profileImage,
+ companyName: doc.companyName,
+ address: doc.address,
+ bio: doc.bio,
+ role: doc.role,
+ isDeleted: doc.isDeleted,
+ isVerified: doc.isVerified,
+ refreshToken: doc.refreshToken,
+ resetToken: doc.resetToken,
+ resetTokenExpiry: doc.resetTokenExpiry,
+ createdAt: doc.createdAt,
+ updatedAt: doc.updatedAt,
+ isBlocked:doc.isBlocked,
+ approvalStatus:doc.approvalStatus,
+ rejectionReason:doc.rejectionReason,
+ });
+ }
+
+ static mapToPersistence(entity) {
+
+ if (!entity) return null;
+
+ return {
+ fullName: entity.fullName,
+ email: entity.email,
+ phone: entity.phone,
+ password: entity.password,
+ profileImage: entity.profileImage,
+ companyName: entity.companyName,
+ address: entity.address,
+ bio: entity.bio,
+ role: entity.role,
+ isDeleted: entity.isDeleted,
+ isVerified: entity.isVerified,
+ refreshToken: entity.refreshToken,
+ resetToken: entity.resetToken,
+ resetTokenExpiry: entity.resetTokenExpiry,
+ createdAt:entity.createdAt,
+ updatedAt:entity.updatedAt,
+ isBlocked:entity.isBlocked,
+ approvalStatus:entity.approvalStatus,
+ rejectionReason:entity.rejectionReason,
+ };
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/mapper/Venue.mapper.js b/server/src/application/mapper/Venue.mapper.js
new file mode 100644
index 0000000000..b42f24bb6d
--- /dev/null
+++ b/server/src/application/mapper/Venue.mapper.js
@@ -0,0 +1,75 @@
+import { VenueEntity } from '../../domain/entities/Venue.js'
+import { Types } from 'mongoose'
+
+
+export class VenueMapper {
+ static mapToEntity(doc){
+ console.log("VENUE DOC:", doc);
+ const venue = new VenueEntity({
+ id: doc._id.toString(),
+ name: doc.name,
+ //vendorId: doc.vendorId ? doc.vendorId.toString() : null,
+ vendorId: doc.vendorId
+ ? {
+ id: doc.vendorId._id?.toString(),
+ fullName: doc.vendorId.fullName,
+ email: doc.vendorId.email,
+ phone: doc.vendorId.phone,
+ companyName: doc.vendorId.companyName,
+ }
+ : null,
+
+ description: doc.description,
+ category: doc.category,
+ websiteUrl: doc.websiteUrl,
+ address: doc.address,
+ seatingCapacity: doc.seatingCapacity,
+ standingCapacity: doc.standingCapacity,
+ pricePerHour: doc.pricePerHour,
+ pricePerDay: doc.pricePerDay,
+ securityDeposit: doc.securityDeposit,
+ availabilityRules: doc.availabilityRules,
+ weekendSurcharge: doc.weekendSurcharge,
+ minimumBookingHours: doc.minimumBookingHours,
+ amenities: doc.amenities,
+ images: doc.images,
+ license: doc.license,
+ // status: doc.status,
+ isDeleted: doc.isDeleted,
+ rating: doc.rating,
+ reviews: doc.reviews,
+ approvalStatus : doc.approvalStatus,
+ isBlocked : doc.isBlocked,
+ rejectionReason : doc.rejectionReason
+ })
+ return venue
+ }
+ static mapToPersistence(entity){
+ return {
+ name: entity.name,
+ vendorId: entity.vendorId ? new Types.ObjectId(entity.vendorId) : null,
+ description: entity.description,
+ category: entity.category,
+ websiteUrl: entity.websiteUrl,
+ address: entity.address,
+ seatingCapacity: entity.seatingCapacity,
+ standingCapacity: entity.standingCapacity,
+ pricePerHour: entity.pricePerHour,
+ pricePerDay: entity.pricePerDay,
+ securityDeposit: entity.securityDeposit,
+ availabilityRules: entity.availabilityRules,
+ weekendSurcharge: entity.weekendSurcharge,
+ minimumBookingHours: entity.minimumBookingHours,
+ amenities: entity.amenities,
+ images: entity.images,
+ license: entity.license,
+ // status: entity.status,
+ isDeleted: entity.isDeleted,
+ rating: entity.rating,
+ reviews: entity.reviews,
+ approvalStatus : entity.approvalStatus,
+ isBlocked : entity.isBlocked,
+ rejectionReason : entity.rejectionReason
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/services/.gitkeep b/server/src/application/services/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/application/services/cloudinaryService.js b/server/src/application/services/cloudinaryService.js
new file mode 100644
index 0000000000..6c314b275d
--- /dev/null
+++ b/server/src/application/services/cloudinaryService.js
@@ -0,0 +1,4 @@
+export class ICloudinaryService {
+ async deleteImage(publicId) {}
+ async deleteImages(publicIds) {}
+}
\ No newline at end of file
diff --git a/server/src/application/services/hashService.js b/server/src/application/services/hashService.js
new file mode 100644
index 0000000000..19a9680a70
--- /dev/null
+++ b/server/src/application/services/hashService.js
@@ -0,0 +1,11 @@
+export class IHashService {
+ async hash(password) {
+ throw new Error("Method not implemented");
+ }
+ hashToken(token){
+ throw new Error("Method not implemented")
+ }
+ async compare(password, hashedPassword){
+ throw new Error("Method not implemented")
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/services/mailService.js b/server/src/application/services/mailService.js
new file mode 100644
index 0000000000..b761570bd4
--- /dev/null
+++ b/server/src/application/services/mailService.js
@@ -0,0 +1,43 @@
+export class MailService {
+
+ async sendVendorApprovalMail(vendor) {
+ throw new Error("Method not implemented");
+ }
+
+ async sendVendorRejectionMail(vendor, reason) {
+ throw new Error("Method not implemented");
+ }
+
+ async sendVenueApprovalMail(venue) {
+ throw new Error("Method not implemented");
+ }
+
+ async sendVenueRejectionMail(venue, reason) {
+ throw new Error("Method not implemented");
+ }
+
+ async sendForgotPasswordMail(user, resetLink) {
+ throw new Error("Method not implemented");
+ }
+ async sendEmailChangeOtp(email, name, otp) {
+ throw new Error("Method not implemented");
+ }
+
+ // async resendEmailChangeOtp(email, otp) {
+ // throw new Error("Method not implemented");
+ // }
+
+ async SendVerifiyRegisterOtp(email, name, otp) {
+ throw new Error("Method not implemented")
+ }
+
+ async sendBookingConfirmationMail(booking) {
+ throw new Error("Method not implemented");
+ }
+ async sendPaymentReminderMail(reminderData) {
+ throw new Error("Method not implemented");
+ }
+ async sendBookingCancellationMail(cancellationData) {
+ throw new Error("Method not implemented");
+}
+}
\ No newline at end of file
diff --git a/server/src/application/services/otpService.js b/server/src/application/services/otpService.js
new file mode 100644
index 0000000000..2b826851cb
--- /dev/null
+++ b/server/src/application/services/otpService.js
@@ -0,0 +1,11 @@
+export class IOtpService {
+ async generate() {
+ throw new Error("Method not implemented");
+ }
+ async hash(otp){
+ throw new Error("Method not implemented")
+ }
+ async compare(enteredOtp, hashedOtp){
+ throw new Error("Method not implemented")
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/services/otpStoreService.js b/server/src/application/services/otpStoreService.js
new file mode 100644
index 0000000000..eedf68d516
--- /dev/null
+++ b/server/src/application/services/otpStoreService.js
@@ -0,0 +1,11 @@
+export class IOtpStoreService {
+ async saveOtp() {
+ throw new Error("Method not implemented");
+ }
+ async getOtp(otp){
+ throw new Error("Method not implemented")
+ }
+ async deleteOtp(enteredOtp, hashedOtp){
+ throw new Error("Method not implemented")
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/services/tokenService.js b/server/src/application/services/tokenService.js
new file mode 100644
index 0000000000..d9fffdcb47
--- /dev/null
+++ b/server/src/application/services/tokenService.js
@@ -0,0 +1,26 @@
+export class ITokenService {
+ async generateAccessToken(id, email, role) {
+ throw new Error("Method not implemented");
+ }
+ async generateRefreshToken(id, role){
+ throw new Error("Method not implemented")
+ }
+ async generateResetToken(){
+ throw new Error("Method not implemented")
+ }
+ async getResetTokenExpiry(){
+ throw new Error("Method not implemented")
+ }
+ async verifyAccessToken(token){
+ throw new Error("Method not implemented")
+ }
+ async verifyRefreshToken(token){
+ throw new Error("Method not implemented")
+ }
+ async blackListToken(token, expiresInSeconds){
+ throw new Error("Method not implemented")
+ }
+ async isTokenBlacklisted(token){
+ throw new Error("Method not implemented")
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/.gitkeep b/server/src/application/user/usecases/.gitkeep
new file mode 100644
index 0000000000..25df00eeee
--- /dev/null
+++ b/server/src/application/user/usecases/.gitkeep
@@ -0,0 +1,35 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+
+export class UserUpdateProfileImageUsecase {
+
+ constructor(userRepository){
+ this._userRepository = userRepository;
+ }
+
+ async execute(userId, profileImage){
+
+ const user = await this._userRepository.findById(userId);
+
+ if(!user){
+ throw new NotFoundError("User not found");
+ }
+
+ if(user.isBlocked){
+ throw new ValidationError(
+ "Blocked users cannot update profile image"
+ );
+ }
+
+ if(!profileImage){
+ throw new ValidationError("Profile image is required");
+ }
+
+ const updatedUser = await this._userRepository.updateProfileImage(
+ userId,
+ profileImage
+ );
+
+ return updatedUser;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/account/user.updateAccountStatus.usecase.js b/server/src/application/user/usecases/account/user.updateAccountStatus.usecase.js
new file mode 100644
index 0000000000..3041e1fd04
--- /dev/null
+++ b/server/src/application/user/usecases/account/user.updateAccountStatus.usecase.js
@@ -0,0 +1,43 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+
+export class UserUpdateAccountStatusUsecase {
+
+ constructor(userRepository){
+ this._userRepository = userRepository;
+ }
+
+ async execute(userId, isActive){
+
+ const user = await this._userRepository.findById(userId);
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ );
+ }
+
+ if(user.isBlocked){
+ throw new ValidationError(
+ UserMessage.error.USER_BLOCKED_UPDATE_ACCOUNT_STATUS
+ );
+ }
+
+ if(user.isActive === isActive){
+ throw new ValidationError(
+ isActive
+ ? UserMessage.error.ACCOUNT_ALREADY_ACTIVE
+ : UserMessage.error.ACCOUNT_ALREADY_INACTIVE
+ );
+ }
+
+ const updatedUser =
+ await this._userRepository.updateAccountStatus(
+ userId,
+ isActive
+ );
+
+ return updatedUser;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/auth/GoogleAuthUseCase.js b/server/src/application/user/usecases/auth/GoogleAuthUseCase.js
new file mode 100644
index 0000000000..9ea7ff7a6b
--- /dev/null
+++ b/server/src/application/user/usecases/auth/GoogleAuthUseCase.js
@@ -0,0 +1,42 @@
+import { UserEntity } from "../../../../domain/entities/User.js";
+import { UserRole } from "../../../../domain/enums/UserRole.enum.js";
+
+export default class GoogleAuthUseCase {
+ constructor(userRepository) {
+ this._userRepository = userRepository;
+ }
+
+ async execute(profile) {
+ const { id: googleId, displayName, emails } = profile;
+ const email = emails[0].value;
+
+ // Check if user already exists by googleId
+ let user = await this._userRepository.findByGoogleId(googleId);
+
+ if (user) {
+ return user;
+ }
+
+ // Check if user exists with same email (registered normally before)
+ user = await this._userRepository.findByEmail(email);
+
+ if (user) {
+ // Link googleId to existing account
+ return await this._userRepository.update(user.id, {
+ ...user,
+ googleId
+ });
+ }
+
+ // New user — create account (no password, OTP auto-verified)
+ const userEntity = new UserEntity({
+ fullName: displayName,
+ email,
+ googleId,
+ role: UserRole.CUSTOMER,
+ isOtpVerified: true // Google already verified the email
+ });
+
+ return await this._userRepository.create(userEntity);
+ }
+}
diff --git a/server/src/application/user/usecases/auth/user.forgotPassword.useCase.js b/server/src/application/user/usecases/auth/user.forgotPassword.useCase.js
new file mode 100644
index 0000000000..9768e3797c
--- /dev/null
+++ b/server/src/application/user/usecases/auth/user.forgotPassword.useCase.js
@@ -0,0 +1,40 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export default class UserForgotPasswordUseCase {
+ constructor(
+ userRepository,
+ tokenService,
+ mailService,
+ hashService
+ ) {
+ this._userRepository = userRepository;
+ this._tokenService = tokenService;
+ this._mailService = mailService;
+ this._hashService = hashService;
+ }
+
+ async execute({email}) {
+ const user = await this._userRepository.findByEmail(email);
+
+ if (!user) {
+ throw new NotFoundError(authMessages.error.USER_NOT_FOUND_WITH_EMAIL);
+ }
+
+ const resetToken = this._tokenService.generateResetToken();
+ const resetTokenExpiry = this._tokenService.getResetTokenExpiry();
+
+ const hashedResetToken = this._hashService.hashToken(resetToken)
+ user.resetToken = hashedResetToken
+ user.resetTokenExpiry = resetTokenExpiry
+ const updated = await this._userRepository.update(user.id, user);
+
+ const resetLink = `${process.env.FRONTEND_URL}/reset-password?role=${updated.role}&token=${resetToken}`;
+ console.log("link", resetLink)
+ await this._mailService.sendForgotPasswordMail(user, resetLink);
+
+ return {
+ success: true
+ };
+ }
+}
diff --git a/server/src/application/user/usecases/auth/user.loginUser.userCase.js b/server/src/application/user/usecases/auth/user.loginUser.userCase.js
new file mode 100644
index 0000000000..aa4e86bbb3
--- /dev/null
+++ b/server/src/application/user/usecases/auth/user.loginUser.userCase.js
@@ -0,0 +1,50 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+import { UserRole } from "../../../../domain/enums/UserRole.enum.js";
+
+export default class LoginUserUseCase {
+ constructor(userRepository, hashService, tokenService) {
+ this._userRepository = userRepository;
+ this._hashService = hashService;
+ this._tokenService = tokenService;
+ }
+
+ async execute({email, password}) {
+ const user = await this._userRepository.findByEmail(email);
+
+ if (!user) {
+ throw new NotFoundError(authMessages.error.USER_NOT_FOUND);
+ }
+
+ console.log("user from login: ", user)
+ const isMatch = await this._hashService.compare(password, user.password);
+
+ if (!isMatch) {
+ throw new UnauthorizedError(authMessages.error.INVALID_CREDENTIALS);
+ }
+
+ if (!user.isOtpVerified) {
+ throw new UnauthorizedError(authMessages.error.OTP_VERIFICATION_REQUIRED);
+ }
+
+ const payload = { id: user.id, role: user.role };
+ const accessToken = this._tokenService.generateAccessToken(payload);
+ const refreshToken = this._tokenService.generateRefreshToken(payload);
+ const hashedToken = await this._hashService.hashToken(refreshToken)
+ await this._userRepository.updateRefreshToken(user.id, hashedToken);
+
+ return {
+ accessToken,
+ refreshToken,
+ user:{
+ id: user.id,
+ name: user.fullName,
+ email: user.email,
+ role: user.role,
+ isVerified: user.isVerified,
+ profileImage: user.profileImage.url
+ }
+ };
+ }
+}
diff --git a/server/src/application/user/usecases/auth/user.logout.useCase.js b/server/src/application/user/usecases/auth/user.logout.useCase.js
new file mode 100644
index 0000000000..624fa0b45b
--- /dev/null
+++ b/server/src/application/user/usecases/auth/user.logout.useCase.js
@@ -0,0 +1,39 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export default class UserLogoutUseCase {
+ constructor(
+ userRepository,
+ hashService,
+ tokenService
+ ) {
+ this._userRepository = userRepository;
+ this._hashService = hashService;
+ this._tokenService = tokenService
+
+ }
+
+ async execute(refreshToken, accessToken) {
+ if(accessToken){
+ const expireInSeconds = process.env.ACCESS_TOKEN_MAX_AGE ? Math.floor(Number(process.env.ACCESS_TOKEN_MAX_AGE) / 1000 ): 3600
+ await this._tokenService.blackListToken(accessToken, expireInSeconds)
+ }
+ if (!refreshToken) {
+ throw new UnauthorizedError(authMessages.error.NO_REFRESH_TOKEN);
+ }
+
+ const { id, role } = this._tokenService.verifyRefreshToken(refreshToken)
+ if(!id || !role){
+ throw new UnauthorizedError(authMessages.error.UNAUTHORIZED)
+ }
+
+ const user = await this._userRepository.findById(id);
+
+ if (!user) {
+ throw new UnauthorizedError(authMessages.error.INVALID_REFRESH_TOKEN);
+ }
+
+ const hashedRefreshToken = await this._hashService.hashToken(refreshToken)
+ await this._userRepository.clearRefreshToken(hashedRefreshToken);
+ }
+}
diff --git a/server/src/application/user/usecases/auth/user.refreshToken.useCase.js b/server/src/application/user/usecases/auth/user.refreshToken.useCase.js
new file mode 100644
index 0000000000..a7419a80f4
--- /dev/null
+++ b/server/src/application/user/usecases/auth/user.refreshToken.useCase.js
@@ -0,0 +1,32 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export default class UserRefreshTokenUseCase {
+ constructor(userRepository, tokenService, hashService) {
+ this._userRepository = userRepository;
+ this._tokenService = tokenService;
+ this._hashService = hashService
+ }
+
+ async execute(refreshToken) {
+ if (!refreshToken) {
+ throw new UnauthorizedError(authMessages.error.NO_REFRESH_TOKEN);
+ }
+
+ const {id, role} = this._tokenService.verifyRefreshToken(refreshToken);
+
+ const user = await this._userRepository.findById(id);
+
+ if (!user) {
+ throw new UnauthorizedError(authMessages.error.REFRESH_TOKEN_REVOKED);
+ }
+
+ const payload = { id: user.id, role: role };
+ const newAccessToken = this._tokenService.generateAccessToken(payload);
+ const newRefreshToken = this._tokenService.generateRefreshToken(payload);
+ const hashedRefreshToken = await this._hashService.hashToken(newRefreshToken)
+ await this._userRepository.updateRefreshToken(user.id, hashedRefreshToken);
+
+ return { accessToken: newAccessToken, refreshToken: newRefreshToken, user };
+ }
+}
diff --git a/server/src/application/user/usecases/auth/user.registerUser.useCase.js b/server/src/application/user/usecases/auth/user.registerUser.useCase.js
new file mode 100644
index 0000000000..1c8131aea1
--- /dev/null
+++ b/server/src/application/user/usecases/auth/user.registerUser.useCase.js
@@ -0,0 +1,57 @@
+import { ConflictError } from "../../../../domain/errors/ConflictError.js";
+import { UserRole } from "../../../../domain/enums/UserRole.enum.js";
+import { UserEntity } from "../../../../domain/entities/User.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+
+
+export class RegisterUserUseCase {
+ constructor(
+ userRepository,
+ hashService,
+ otpService,
+ otpStoreService,
+ mailService
+ ) {
+ this._userRepository = userRepository;
+ this._hashService = hashService;
+ this._otpService = otpService;
+ this._otpStoreService = otpStoreService;
+ this._mailService = mailService
+ }
+
+ async execute({
+ fullName,
+ email,
+ phone,
+ password
+ }) {
+ const existing = await this._userRepository.findByEmail(email);
+ console.log('existing user: ', existing)
+ if (existing) {
+ throw new ConflictError(authMessages.error.EMAIL_ALREADY_EXISTS);
+ }
+
+ const hashedPassword = await this._hashService.hash(password);
+ const user = new UserEntity({
+ fullName: fullName,
+ email: email,
+ phone: phone,
+ password: hashedPassword,
+ role: UserRole.CUSTOMER,
+ });
+
+ const savedUser = await this._userRepository.create(user)
+
+ const otp = this._otpService.generate();
+ console.log('otp is:', otp)
+ const hashedOtp = await this._otpService.hash(otp);
+ await this._otpStoreService.saveOtp(savedUser.id, hashedOtp, 120)
+
+ // await this._mailService.sendVerifiyRegisterOtp(savedUser.email, savedUser.fullName, otp)
+
+ return {
+ success: true
+ }
+ }
+}
diff --git a/server/src/application/user/usecases/auth/user.resendOtp.useCase.js b/server/src/application/user/usecases/auth/user.resendOtp.useCase.js
new file mode 100644
index 0000000000..bf8bdd39cf
--- /dev/null
+++ b/server/src/application/user/usecases/auth/user.resendOtp.useCase.js
@@ -0,0 +1,34 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export default class UserResendOtpUseCase {
+ constructor(
+ userRepository,
+ otpService,
+ otpStoreService,
+ mailService
+ ) {
+ this._userRepository = userRepository;
+ this._otpService = otpService;
+ this._otpStoreService = otpStoreService;
+ this._mailService = mailService
+ }
+
+ async execute({email}) {
+ const user = await this._userRepository.findByEmail(email);
+
+ if (!user) {
+ throw new NotFoundError(authMessages.error.USER_NOT_FOUND);
+ }
+
+ const otp = this._otpService.generate();
+ console.log('otp is:', otp)
+ const hashedOtp = await this._otpService.hash(otp);
+ await this._otpStoreService.saveOtp(user.id, hashedOtp, 120)
+ await this._mailService.sendVerifiyRegisterOtp(user.email, user.fullName, otp)
+
+ return {
+ success: true
+ };
+ }
+}
diff --git a/server/src/application/user/usecases/auth/user.resetPassword.useCase.js b/server/src/application/user/usecases/auth/user.resetPassword.useCase.js
new file mode 100644
index 0000000000..42827c5439
--- /dev/null
+++ b/server/src/application/user/usecases/auth/user.resetPassword.useCase.js
@@ -0,0 +1,52 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { AppError } from "../../../../domain/errors/app.error.js";
+import { statusCode } from "../../../../shared/constants/enums/statusCode.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export default class UserResetPasswordUseCase {
+ constructor(userRepository, hashService) {
+ this._userRepository = userRepository;
+ this._hashService = hashService;
+ }
+
+ async execute(token, password) {
+
+ const hashedResetToken = this._hashService.hashToken(token)
+ const user = await this._userRepository.findByResetToken(hashedResetToken);
+ console.log("from usecase: ", user)
+
+ if (!user) {
+ throw new NotFoundError(authMessages.error.USER_NOT_FOUND);
+ }
+
+ if (!user.resetToken || !user.resetTokenExpiry) {
+ throw new UnauthorizedError(authMessages.error.NO_RESET_REQUEST);
+ }
+
+ const hashedIncomingToken = this._hashService.hashToken(token)
+ if (user.resetToken !== hashedIncomingToken) {
+ throw new UnauthorizedError(authMessages.error.INVALID_RESET_TOKEN);
+ }
+
+ if (user.resetTokenExpiry < new Date()) {
+ throw new UnauthorizedError(authMessages.error.RESET_TOKEN_EXPIRED);
+ }
+
+ const hashedPassword = await this._hashService.hash(password);
+
+ const updatedUser = await this._userRepository.update(user.id, {
+ password: hashedPassword,
+ resetToken: null,
+ resetTokenExpiry: null,
+ });
+
+ if (!updatedUser) {
+ throw new AppError(authMessages.error.RESET_PASSWORD_FAILED, statusCode.SERVER_ERROR);
+ }
+
+ return {
+ success: true
+ };
+ }
+}
diff --git a/server/src/application/user/usecases/auth/user.verifyOtp.useCase.js b/server/src/application/user/usecases/auth/user.verifyOtp.useCase.js
new file mode 100644
index 0000000000..b2554a4910
--- /dev/null
+++ b/server/src/application/user/usecases/auth/user.verifyOtp.useCase.js
@@ -0,0 +1,48 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export default class UserVerifyOtpUseCase {
+ constructor(
+ userRepository,
+ otpService,
+ otpStoreService
+ ) {
+ this._userRepository = userRepository;
+ this._otpService = otpService;
+ this._otpStoeService = otpStoreService
+ }
+
+ async execute({email, otpCode}) {
+ const user = await this._userRepository.findByEmail(email);
+
+ if (!user) {
+ throw new NotFoundError(authMessages.error.USER_NOT_FOUND);
+ }
+
+ if (user.isOtpVerified) {
+ throw new UnauthorizedError(authMessages.error.ALREADY_OTP_VERIFIED);
+ }
+
+ const storedOtp = await this._otpStoeService.getOtp(user.id)
+ if(!storedOtp){
+ throw new NotFoundError(authMessages.error.OTP_EXPIRED)
+ }
+
+ const isOtpValid = await this._otpService.compare(otpCode, storedOtp);
+
+ if (!isOtpValid) {
+ throw new UnauthorizedError(authMessages.error.INVALID_OTP);
+ }
+
+ const verifiedUser = await this._userRepository.verifyOtp(user.id);
+
+ if (!verifiedUser) {
+ throw new UnauthorizedError(authMessages.error.OTP_VERIFY_FAILED);
+ }
+
+ return {
+ success: true
+ }
+ }
+}
diff --git a/server/src/application/user/usecases/booking/user.availability.usecase.js b/server/src/application/user/usecases/booking/user.availability.usecase.js
new file mode 100644
index 0000000000..ba4633be3a
--- /dev/null
+++ b/server/src/application/user/usecases/booking/user.availability.usecase.js
@@ -0,0 +1,34 @@
+export class UserAvailabilityUsecase {
+ constructor(bookingRepository) {
+ this._bookingRepository = bookingRepository;
+ }
+
+ async execute(venueId, month, year) {
+ const bookings = await this._bookingRepository.findByVenue(venueId, month, year);
+
+ const availability = {};
+
+ bookings.forEach((b) => {
+ if (!b.bookingDate) return; // guard
+
+ const dateKey = b.bookingDate.toISOString().split("T")[0];
+
+ if (!availability[dateKey]) {
+ availability[dateKey] = { slots: [], status: "available" };
+ }
+
+ availability[dateKey].slots.push({
+ startTime: b.startTime,
+ endTime: b.endTime,
+ });
+
+ if (b.bookingType === "daily") {
+ availability[dateKey].status = "booked";
+ } else if (availability[dateKey].status !== "booked") {
+ availability[dateKey].status = "partial";
+ }
+ });
+
+ return availability;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/booking/user.cancelBooking.usecase.js b/server/src/application/user/usecases/booking/user.cancelBooking.usecase.js
new file mode 100644
index 0000000000..34ac784e7b
--- /dev/null
+++ b/server/src/application/user/usecases/booking/user.cancelBooking.usecase.js
@@ -0,0 +1,161 @@
+import { BookingStatus } from "../../../../domain/enums/Booking.enum.js";
+import { PaymentStatus } from "../../../../domain/enums/Payment.enum.js";
+
+import { BadRequestError } from "../../../../domain/errors/BadRequestError.js";
+import { ForbiddenError } from "../../../../domain/errors/Forbidden.error.js";
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+
+import { BookingMessages } from "../../../../shared/constants/messages/bookingMessages.js";
+
+export class UserCancelBookingUsecase {
+
+ constructor(
+ bookingRepository,
+ mailService
+ ) {
+
+ this._bookingRepository = bookingRepository;
+ this._mailService = mailService;
+
+ }
+
+ async execute(
+ userId,
+ bookingId,
+ cancellationReason
+ ) {
+
+ const booking =
+ await this._bookingRepository.findById(
+ bookingId
+ );
+
+ if (!booking) {
+
+ throw new NotFoundError(
+ BookingMessages.error.BOOKING_NOT_FOUND
+ );
+
+ }
+
+ if (booking.userId.id.toString() !== userId.toString()) {
+
+ throw new ForbiddenError(
+ BookingMessages.error.FORBIDDEN
+ );
+
+ }
+
+ if (booking.status === BookingStatus.CANCELLED) {
+
+ throw new BadRequestError(
+ BookingMessages.error.BOOKING_ALREADY_CANCELLED
+ );
+
+ }
+
+ if (booking.status === BookingStatus.COMPLETED) {
+
+ throw new BadRequestError(
+ BookingMessages.error.BOOKING_COMPLETED_CANNOT_CANCEL
+ );
+
+ }
+
+ const today = new Date();
+
+ const bookingDate = new Date(
+ booking.bookingDate
+ );
+
+ const differenceInDays = Math.ceil(
+
+ (bookingDate - today) /
+
+ (1000 * 60 * 60 * 24)
+
+ );
+
+ let refundAmount = 0;
+
+ if (differenceInDays >= 3) {
+
+ if (
+
+ booking.paymentStatus === PaymentStatus.PARTIAL
+
+ ) {
+
+ refundAmount = booking.advanceAmount;
+
+ }
+
+ else if (
+
+ booking.paymentStatus === PaymentStatus.PAID
+
+ ) {
+
+ refundAmount =Math.round( booking.paidAmount * 0.90)
+
+ }
+
+ }
+
+ booking.cancel(
+ cancellationReason
+ );
+
+ await this._bookingRepository.cancelBooking(
+
+ booking.id,
+
+ booking.status,
+
+ booking.cancellationReason
+
+ );
+ const emailData = {
+
+ customerName: booking.userId.fullName,
+
+ email: booking.userId.email,
+
+ venueName: booking.venueId.name,
+
+ bookingDate: booking.bookingDate,
+
+ startTime: booking.startTime,
+
+ endTime: booking.endTime,
+
+ totalAmount: booking.totalAmount,
+
+ paidAmount: booking.paidAmount,
+
+ refundAmount,
+
+ cancellationReason
+
+ };
+
+ await this._mailService.sendBookingCancellationMail(
+ emailData
+ );
+
+ return {
+
+ bookingId: booking.id,
+
+ status: booking.status,
+
+ refundAmount,
+
+ message:
+ BookingMessages.success.BOOKING_CANCELLED
+
+ };
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/booking/user.confirmBooking.usecase.js b/server/src/application/user/usecases/booking/user.confirmBooking.usecase.js
new file mode 100644
index 0000000000..d274ec5cc6
--- /dev/null
+++ b/server/src/application/user/usecases/booking/user.confirmBooking.usecase.js
@@ -0,0 +1,137 @@
+import { Booking } from "../../../../domain/entities/Booking.js";
+
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+
+import { BookingMessages } from "../../../../shared/constants/messages/bookingMessages.js";
+
+export class UserConfirmBookingUsecase {
+ constructor(
+ bookingRepository,
+ reservationService,
+ userRepository,
+ venueRepository,
+ mailService
+ ) {
+ this._bookingRepository = bookingRepository;
+ this._reservationService =reservationService;
+ this._userRepository = userRepository;
+ this._venueRepository = venueRepository;
+ this._mailService = mailService;
+ }
+
+ async execute({
+ reservationId,
+ venueId,
+ bookingDate
+ }) {
+const reservationKey = `reservation:${venueId}:${bookingDate}`;
+console.log("Looking up reservation with key:", reservationKey);
+ const reservations =
+ await this._reservationService.getReservation(reservationKey);
+
+ if (!reservations || reservations.length === 0) {
+ throw new NotFoundError(
+ BookingMessages.error.RESERVATION_NOT_FOUND
+ );
+ }
+
+ const reservation = reservations.find(
+ (item) => item.reservationId === reservationId
+ );
+
+ if (!reservation) {
+ throw new NotFoundError(
+ BookingMessages.error.RESERVATION_NOT_FOUND
+ );
+ }
+ const user =
+ await this._userRepository.findById(
+ reservation.userId
+ );
+ if (!user) {
+ throw new NotFoundError(BookingMessages.error.USER_NOT_FOUND);
+ }
+ const venue =
+ await this._venueRepository.findById(
+ reservation.venueId
+ );
+
+
+ if (!venue) {
+ throw new NotFoundError(BookingMessages.error.VENUE_NOT_FOUND);
+ }
+
+ const booking = new Booking({
+ userId: reservation.userId,
+ venueId: reservation.venueId,
+ vendorId: reservation.vendorId,
+ bookingDate: reservation.bookingDate,
+ startTime: reservation.startTime,
+ endTime: reservation.endTime,
+ guestCount: reservation.guestCount,
+ totalAmount: reservation.totalAmount,
+ advanceAmount: reservation.advanceAmount,
+ remainingAmount: reservation.remainingAmount
+ });
+
+ booking.payAmount(reservation.advanceAmount);
+
+ booking.confirm();
+
+ const savedBooking =
+ await this._bookingRepository.create(booking);
+
+
+ const emailData = {
+
+ customerName: user.fullName,
+
+ email: user.email,
+
+ venueName: venue.name,
+
+ bookingDate: reservation.bookingDate,
+
+ startTime: reservation.startTime,
+
+ endTime: reservation.endTime,
+
+ guestCount: reservation.guestCount,
+
+ bookingType: reservation.bookingType,
+
+ totalAmount: reservation.totalAmount,
+
+ paidAmount: reservation.advanceAmount,
+
+ remainingAmount: reservation.remainingAmount
+
+ };
+
+ const updatedReservations = reservations.filter(
+ (item) => item.reservationId !== reservationId
+ );
+
+ if (updatedReservations.length > 0) {
+
+ await this._reservationService.reserveSlot(
+ reservationKey,
+ updatedReservations,
+ 600
+ );
+
+ } else {
+
+ await this._reservationService.deleteReservation(
+ reservationKey
+ );
+
+ }
+
+ await this._mailService.sendBookingConfirmationMail(
+ emailData
+ );
+
+ return savedBooking;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/booking/user.getBookingById.usecase.js b/server/src/application/user/usecases/booking/user.getBookingById.usecase.js
new file mode 100644
index 0000000000..07c2ac109e
--- /dev/null
+++ b/server/src/application/user/usecases/booking/user.getBookingById.usecase.js
@@ -0,0 +1,25 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { BookingMessages } from "../../../../shared/constants/messages/bookingMessages.js";
+
+export class UserGetBookingByIdUsecase {
+ constructor(bookingRepository) {
+ this._bookingRepository = bookingRepository;
+ }
+
+ async execute(userId, bookingId) {
+
+ const booking =
+ await this._bookingRepository.getUserBookingById(
+ userId,
+ bookingId
+ );
+
+ if (!booking) {
+ throw new NotFoundError(
+ BookingMessages.error.BOOKING_NOT_FOUND
+ );
+ }
+
+ return booking;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/booking/user.getBookings.usecase.js b/server/src/application/user/usecases/booking/user.getBookings.usecase.js
new file mode 100644
index 0000000000..3d5e682449
--- /dev/null
+++ b/server/src/application/user/usecases/booking/user.getBookings.usecase.js
@@ -0,0 +1,32 @@
+
+import { BookingMessages } from "../../../../shared/constants/messages/bookingMessages.js";
+
+export class UserGetBookingsUsecase {
+
+ constructor(bookingRepository) {
+ this._bookingRepository = bookingRepository;
+ }
+
+ async execute(
+ userId,
+ page,
+ limit,
+ status,
+ search,
+ sortBy
+ ) {
+
+ return await this._bookingRepository.getUserBookings(
+ userId,
+ {
+ page,
+ limit,
+ status,
+ search,
+ sortBy
+ }
+ );
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/booking/user.paymentReminder.usecase.js b/server/src/application/user/usecases/booking/user.paymentReminder.usecase.js
new file mode 100644
index 0000000000..084fee0c0c
--- /dev/null
+++ b/server/src/application/user/usecases/booking/user.paymentReminder.usecase.js
@@ -0,0 +1,89 @@
+export class UserPaymentReminderUsecase {
+
+ constructor(
+ bookingRepository,
+ userRepository,
+ venueRepository,
+ mailService
+ ) {
+ this._bookingRepository = bookingRepository;
+ this._userRepository = userRepository;
+ this._venueRepository = venueRepository;
+ this._mailService = mailService;
+ }
+
+ async execute() {
+
+ const tomorrow = new Date();
+
+ tomorrow.setDate(
+ tomorrow.getDate() + 1
+ );
+
+ tomorrow.setHours(
+ 0,
+ 0,
+ 0,
+ 0
+ );
+
+ const bookings =
+ await this._bookingRepository.getBookingsForPaymentReminder(
+ tomorrow
+ );
+
+ for (const booking of bookings) {
+
+ const user =
+ await this._userRepository.findById(
+ booking.userId
+ );
+
+ const venue =
+ await this._venueRepository.findById(
+ booking.venueId
+ );
+
+ if (!user || !venue) {
+ continue;
+ }
+
+ const emailData = {
+
+ customerName: user.fullName,
+
+ email: user.email,
+
+ venueName: venue.name,
+
+ bookingDate: booking.bookingDate,
+
+ startTime: booking.startTime,
+
+ endTime: booking.endTime,
+
+ totalAmount: booking.totalAmount,
+
+ paidAmount: booking.paidAmount,
+
+ remainingAmount: booking.remainingAmount
+
+ };
+
+ try {
+
+ await this._mailService.sendPaymentReminderMail(
+ emailData
+ );
+
+ } catch (error) {
+
+ console.error("Failed to send payment reminder", error);
+
+ }
+
+ }
+
+}
+
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/booking/user.reserveBooking.usecase.js b/server/src/application/user/usecases/booking/user.reserveBooking.usecase.js
new file mode 100644
index 0000000000..24f31fdf77
--- /dev/null
+++ b/server/src/application/user/usecases/booking/user.reserveBooking.usecase.js
@@ -0,0 +1,227 @@
+import { VenueStatus } from "../../../../domain/enums/Venue.enum.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { ConflictError } from "../../../../domain/errors/ConflictError.js";
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+
+import { BookingMessages } from "../../../../shared/constants/messages/bookingMessages.js";
+
+export class UserReserveBookingUsecase {
+ constructor(bookingRepository, venueRepository, reservationService) {
+ this._bookingRepository = bookingRepository;
+ this._venueRepository = venueRepository;
+ this._reservationService = reservationService;
+ }
+
+ async execute(userId, bookingData) {
+ let {
+ venueId,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+ bookingType,
+ } = bookingData;
+
+
+ //error check delete after console.log("=== Request Data ===");
+ console.log("=== Request Data ===");
+console.log({
+ venueId,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+ bookingType,
+});
+
+
+ // ===== Venue validation =====
+ const venue = await this._venueRepository.findById(venueId);
+ if (!venue) throw new NotFoundError(BookingMessages.error.VENUE_NOT_FOUND);
+ if (venue.isDeleted) throw new ValidationError(BookingMessages.error.VENUE_DELETED);
+ if (venue.isBlocked) throw new ValidationError(BookingMessages.error.VENUE_BLOCKED);
+ if (venue.approvalStatus !== VenueStatus.ACTIVE) {
+ throw new ValidationError(BookingMessages.error.VENUE_NOT_APPROVED);
+ }
+
+ // ===== Date validation =====
+ if (!bookingDate) throw new ValidationError(BookingMessages.error.BOOKING_DATE_REQUIRED);
+
+const today = new Date();
+
+const todayUTC = new Date(
+ Date.UTC(
+ today.getUTCFullYear(),
+ today.getUTCMonth(),
+ today.getUTCDate()
+ )
+);
+
+const selectedDate = new Date(`${bookingDate}T00:00:00.000Z`);
+
+if (selectedDate < todayUTC) {
+ throw new ValidationError(BookingMessages.error.BOOKING_DATE_INVALID);
+}
+
+ let bookingDuration = 0;
+
+ // ===== Time validation only for hourly bookings =====
+ if (bookingType === "hourly") {
+ if (!startTime || !endTime) {
+ throw new ValidationError(BookingMessages.error.BOOKING_TIME_REQUIRED);
+ }
+
+ const [startHour, startMinute] = startTime.split(":").map(Number);
+ const [endHour, endMinute] = endTime.split(":").map(Number);
+
+ const bookingStartMinutes = startHour * 60 + startMinute;
+ const bookingEndMinutes = endHour * 60 + endMinute;
+
+ const currentDate = new Date();
+ const isToday = currentDate.toDateString() === selectedDate.toDateString();
+
+ if (isToday) {
+ const currentMinutes = currentDate.getHours() * 60 + currentDate.getMinutes();
+ if (bookingStartMinutes <= currentMinutes) {
+ throw new ValidationError(BookingMessages.error.BOOKING_TIME_INVALID);
+ }
+ }
+
+ // Venue open/close validation
+ const [openHour, openMinute] = venue.availabilityRules.openTime.split(":").map(Number);
+ const [closeHour, closeMinute] = venue.availabilityRules.closeTime.split(":").map(Number);
+
+ const venueOpenMinutes = openHour * 60 + openMinute;
+ const venueCloseMinutes = closeHour * 60 + closeMinute;
+
+ if (
+ bookingStartMinutes < venueOpenMinutes ||
+ bookingEndMinutes > venueCloseMinutes ||
+ bookingStartMinutes >= bookingEndMinutes
+ ) {
+ throw new ValidationError(BookingMessages.error.BOOKING_TIME_INVALID);
+ }
+
+ bookingDuration = (bookingEndMinutes - bookingStartMinutes) / 60;
+ if (bookingDuration < venue.minimumBookingHours) {
+ throw new ValidationError(BookingMessages.error.MINIMUM_BOOKING_HOURS);
+ }
+ } else if (bookingType === "daily") {
+ startTime="00:00";
+ endTime="23:59";
+ bookingDuration=24;
+ } else {
+ throw new ValidationError(BookingMessages.error.INVALID_BOOKING_TYPE);
+ }
+
+ // ===== Closed days validation =====
+ const dayName = selectedDate.toLocaleDateString("en-US", { weekday: "long" });
+ if (venue.availabilityRules.closedDays.includes(dayName)) {
+ throw new ValidationError(BookingMessages.error.VENUE_CLOSED);
+ }
+
+ // ===== Capacity validation =====
+ const maxCapacity = Math.max(venue.seatingCapacity, venue.standingCapacity);
+ if (guestCount > maxCapacity) {
+ throw new ValidationError(BookingMessages.error.CAPACITY_EXCEEDED);
+ }
+// ===== Overlap check =====
+console.log("=== Checking DB Overlap ===");
+console.log({
+ venueId,
+ selectedDate,
+ startTime,
+ endTime,
+});
+
+ const hasOverlappingBooking = await this._bookingRepository.hasOverlappingBooking(
+ venueId,
+ selectedDate,
+ startTime,
+ endTime
+ );
+ if (hasOverlappingBooking) {
+ throw new ConflictError(BookingMessages.error.SLOT_ALREADY_BOOKED);
+ }
+
+ // ===== Temporary reservation check =====
+
+const reservationKey = `reservation:${venueId}:${bookingDate}`;
+
+ const reservations = await this._reservationService.getReservation(reservationKey);
+ if (reservations && reservations.length > 0) {
+ const hasOverlappingReservation = reservations.some(
+ (reservation) => reservation.startTime < endTime && reservation.endTime > startTime
+ );
+ if (hasOverlappingReservation) {
+ throw new ConflictError(BookingMessages.error.SLOT_TEMPORARILY_RESERVED);
+ }
+ }
+
+ // ===== Pricing =====
+ let bookingAmount;
+ if (bookingType === "hourly") {
+ bookingAmount = bookingDuration * venue.pricePerHour;
+ } else {
+ bookingAmount = venue.pricePerDay;
+ }
+
+ const bookingDay = selectedDate.getDay();
+ const isWeekend = bookingDay === 0 || bookingDay === 6;
+ const weekendCharge = isWeekend ? venue.weekendSurcharge || 0 : 0;
+ const securityDeposit = venue.securityDeposit || 0;
+
+ const totalAmount = bookingAmount + weekendCharge + securityDeposit;
+
+ // Advance payment calculation
+ const hoursDifference = (selectedDate - todayUTC) / (1000 * 60 * 60);
+ let advanceAmount;
+ let remainingAmount;
+
+ if (hoursDifference > 72) {
+ advanceAmount = Math.round(totalAmount * 0.2);
+ remainingAmount = totalAmount - advanceAmount;
+ } else {
+ advanceAmount = totalAmount;
+ remainingAmount = 0;
+ }
+
+ const reservationId = this._reservationService.generateReservationId();
+ const expiresAt = new Date(Date.now() + 600 * 1000);
+
+ const reservationData = {
+ reservationId,
+ userId,
+ venueId,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+ bookingType,
+ totalAmount,
+ advanceAmount,
+ remainingAmount,
+ expiresAt,
+ vendorId: venue.vendorId,
+ };
+
+ const reservationList = reservations || [];
+ reservationList.push(reservationData);
+
+ await this._reservationService.reserveSlot(reservationKey, reservationList, 600);
+
+ return {
+ reservationId,
+ venueId,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+ bookingType,
+ totalAmount,
+ advanceAmount,
+ remainingAmount,
+ expiresAt,
+ };
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/profile/requestEmailchangeOtp.js b/server/src/application/user/usecases/profile/requestEmailchangeOtp.js
new file mode 100644
index 0000000000..70339dd04f
--- /dev/null
+++ b/server/src/application/user/usecases/profile/requestEmailchangeOtp.js
@@ -0,0 +1,68 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ConflictError } from "../../../../domain/errors/ConflictError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+
+
+export class RequestEmailChangeOtpUsecase{
+ constructor(
+ userRepository,
+ hashService,
+ otpService,
+ mailService
+ ) {
+ this._userRepository = userRepository;
+ this._hashService = hashService;
+ this._otpService = otpService;
+ this._mailService = mailService;
+ }
+ async execute(userId,newEmail){
+ const user=await this._userRepository.findById(userId)
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ )
+ }
+ if(user.isBlocked){
+ throw new ValidationError(
+ UserMessage.error.USER_BLOCKED_EMAIL_CHANGE
+ )
+ }
+ if(user.email===newEmail){
+ throw new ValidationError(
+ UserMessage.error.EMAIL_SAME_AS_CURRENT
+ )
+ }
+
+ const existingUser=
+ await this._userRepository.findByEmail(newEmail)
+ if(existingUser){
+ throw new ConflictError(
+ UserMessage.error.EMAIL_ALREADY_EXISTS
+ )
+ }
+
+ const otp = this._otpService.generate();
+
+ const otpExpiresAt = this._otpService.getExpiry();
+ console.log("OTP => ", otp)
+ const hashedOtp = await this._hashService.hash(otp);
+
+ await this._userRepository.saveEmailChangeOtp(
+ userId,
+ newEmail,
+ hashedOtp,
+ otpExpiresAt
+ )
+
+ await this._mailService.sendEmailChangeOtp(
+ newEmail,
+ user.fullName,
+ otp
+ );
+ return{
+ message: UserMessage.success.OTP_SENT
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/profile/resendEmailChangeOtp.usecase.js b/server/src/application/user/usecases/profile/resendEmailChangeOtp.usecase.js
new file mode 100644
index 0000000000..55db5811e5
--- /dev/null
+++ b/server/src/application/user/usecases/profile/resendEmailChangeOtp.usecase.js
@@ -0,0 +1,54 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+
+export class ResendEmailChangeOtpUsecase {
+ constructor(userRepository, hashService, otpService, mailService) {
+ this._userRepository = userRepository;
+ this._hashService = hashService;
+ this._otpService = otpService;
+ this._mailService = mailService;
+ }
+
+ async execute(userId){
+
+ const user = await this._userRepository.findById(userId);
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ );
+ }
+
+ if(!user.pendingEmail){
+ throw new ValidationError(
+ UserMessage.error.EMAIL_CHANGE_REQUEST_NOT_FOUND
+ );
+ }
+
+ const otp = this._otpService.generate();
+
+ const otpExpiresAt = this._otpService.getExpiry();
+
+ const hashedOtp = await this._hashService.hash(otp);
+
+
+
+ await this._userRepository.saveEmailChangeOtp(
+ userId,
+ user.pendingEmail,
+ hashedOtp,
+ otpExpiresAt
+ );
+
+ await this._mailService.sendEmailChangeOtp(
+ user.pendingEmail,
+ user.fullName,
+ otp
+ );
+
+ return {
+ message: UserMessage.success.OTP_RESENT
+ };
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/profile/user.changePassword.usecase.js b/server/src/application/user/usecases/profile/user.changePassword.usecase.js
new file mode 100644
index 0000000000..b948cff384
--- /dev/null
+++ b/server/src/application/user/usecases/profile/user.changePassword.usecase.js
@@ -0,0 +1,38 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+
+export class UserChangePasswordUsecase {
+ constructor(userRepository, hashService) {
+ this._userRepository = userRepository;
+ this._hashService = hashService;
+ }
+
+ async execute({ userId, currentPassword, newPassword }) {
+ const user = await this._userRepository.findById(userId);
+
+ if (!user) {
+ throw new NotFoundError(UserMessage.error.USER_NOT_FOUND);
+ }
+
+ const isPasswordCorrect = await this._hashService.compare(
+ currentPassword,
+ user.password
+ );
+ if (!isPasswordCorrect) {
+ throw new ValidationError(UserMessage.error.INVALID_CURRENT_PASSWORD);
+ }
+
+ const samePassword = await this._hashService.compare(
+ newPassword,
+ user.password,
+ );
+ if (samePassword) {
+ throw new ValidationError(UserMessage.error.SAME_PASSWORD);
+ }
+
+ const hashedPassword = await this._hashService.hash(newPassword);
+ await this._userRepository.updatePassword(userId, hashedPassword);
+ return null;
+ }
+}
diff --git a/server/src/application/user/usecases/profile/user.getProfile.usecase.js b/server/src/application/user/usecases/profile/user.getProfile.usecase.js
new file mode 100644
index 0000000000..cc109d965d
--- /dev/null
+++ b/server/src/application/user/usecases/profile/user.getProfile.usecase.js
@@ -0,0 +1,25 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+
+export class UserGetProfileUsecase{
+ constructor(userRepository){
+ this._userRepository=userRepository
+ }
+ async execute(userId){
+ const user=await this._userRepository.findById(userId)
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ )
+ }
+
+ if(user.isBlocked){
+ throw new NotFoundError(
+ UserMessage.error.USER_ACCOUNT_BLOCKED
+ )
+ }
+
+ return user
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/profile/user.removeProfileImage.usecase.js b/server/src/application/user/usecases/profile/user.removeProfileImage.usecase.js
new file mode 100644
index 0000000000..d9f24f65da
--- /dev/null
+++ b/server/src/application/user/usecases/profile/user.removeProfileImage.usecase.js
@@ -0,0 +1,35 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+
+export class UserRemoveProfileImageUsecase{
+
+ constructor(userRepository){
+ this._userRepository=userRepository;
+ }
+
+ async execute(userId){
+
+ const user=await this._userRepository.findById(userId);
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ );
+ }
+
+ if(user.isBlocked){
+ throw new ValidationError(
+ UserMessage.error.USER_BLOCKED_REMOVE_PROFILE_IMAGE
+ );
+ }
+
+ if(!user.profileImage){
+ throw new ValidationError(
+ UserMessage.error.PROFILE_IMAGE_NOT_FOUND
+ );
+ }
+
+ return await this._userRepository.removeProfileImage(userId);
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/profile/user.updateProfile.usecase.js b/server/src/application/user/usecases/profile/user.updateProfile.usecase.js
new file mode 100644
index 0000000000..5a3cbd72a4
--- /dev/null
+++ b/server/src/application/user/usecases/profile/user.updateProfile.usecase.js
@@ -0,0 +1,30 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+
+export class UserUpdateProfileUsecase{
+ constructor(userRepository){
+ this._userRepository=userRepository
+ }
+ async execute(userId,fullName,phone){
+ const user=await this._userRepository.findById(userId)
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ )
+ }
+ if(user.isBlocked){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ )
+ }
+ const updatedUser=await this._userRepository.update(
+ userId,
+ {
+ fullName,
+ phone
+ }
+ )
+ return updatedUser
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/profile/user.updateProfileImage.usecase.js b/server/src/application/user/usecases/profile/user.updateProfileImage.usecase.js
new file mode 100644
index 0000000000..51bb5af3d1
--- /dev/null
+++ b/server/src/application/user/usecases/profile/user.updateProfileImage.usecase.js
@@ -0,0 +1,41 @@
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+
+
+export class UserUpdateProfileImageUsecase {
+
+ constructor(userRepository){
+ this._userRepository = userRepository;
+ }
+
+ async execute(userId, profileImage){
+
+ const user = await this._userRepository.findById(userId);
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ );
+ }
+
+ if(user.isBlocked){
+ throw new ValidationError(
+ UserMessage.error.USER_BLOCKED_UPDATE_PROFILE_IMAGE
+ );
+ }
+
+ if(!profileImage){
+ throw new ValidationError(
+ UserMessage.error.PROFILE_IMAGE_REQUIRED
+ );
+ }
+
+ const updatedUser = await this._userRepository.updateProfileImage(
+ userId,
+ profileImage
+ );
+
+ return updatedUser;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/profile/verifyEmailChangeOtp.usecase.js b/server/src/application/user/usecases/profile/verifyEmailChangeOtp.usecase.js
new file mode 100644
index 0000000000..f851e39f26
--- /dev/null
+++ b/server/src/application/user/usecases/profile/verifyEmailChangeOtp.usecase.js
@@ -0,0 +1,50 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+
+
+export class VerifyEmailChangeOtpUsecase{
+ constructor(
+ userRepository,
+ hashService
+ ) {
+ this._userRepository = userRepository;
+ this._hashService = hashService;
+ }
+ async execute(userId,otp){
+ const user=await this._userRepository.findByIdWithOtp(userId)
+
+ if(!user){
+ throw new NotFoundError(UserMessage.error.USER_NOT_FOUND)
+ }
+ if(!user.pendingEmail){
+ throw new ValidationError(
+ UserMessage.error.EMAIL_CHANGE_REQUEST_NOT_FOUND
+ )
+ }
+ if(!user.otpCode){
+ throw new ValidationError(
+ UserMessage.error.OTP_NOT_FOUND
+ )
+ }
+
+ if(new Date()>user.otpExpiresAt){
+ throw new ValidationError(
+ UserMessage.error.OTP_EXPIRED
+ )
+ }
+
+ const isValid = await this._hashService.compare(otp, user.otpCode);
+
+ if(!isValid){
+ throw new ValidationError(
+ UserMessage.error.INVALID_OTP
+ )
+ }
+
+ const updatedUser=
+ await this._userRepository.updateEmailAfterVerification(userId)
+
+ return updatedUser
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/venue/user.getAllVenue.usecase.js b/server/src/application/user/usecases/venue/user.getAllVenue.usecase.js
new file mode 100644
index 0000000000..f8b445cde5
--- /dev/null
+++ b/server/src/application/user/usecases/venue/user.getAllVenue.usecase.js
@@ -0,0 +1,18 @@
+import { VenueStatus } from "../../../../domain/enums/Venue.enum.js"
+
+export class UserGetAllVenuesUsecase {
+ constructor (
+ venueRepository
+ ) {
+ this._venueRepository = venueRepository
+ }
+
+ async execute(search, category,rating, amenities, capacityType, capacity, priceType, minPrice, maxPrice, page, limit) {
+ const { data, totalPages, totalCount } = await this._venueRepository.findAllFiltered({search, category, rating, amenities, capacityType, capacity, priceType, minPrice, maxPrice,isBlocked: false, approvalStatus: VenueStatus.ACTIVE, page, limit})
+ return {
+ data,
+ totalPages,
+ totalCount
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/venue/user.getSimilarVenues.usecase.js b/server/src/application/user/usecases/venue/user.getSimilarVenues.usecase.js
new file mode 100644
index 0000000000..aa3bb56342
--- /dev/null
+++ b/server/src/application/user/usecases/venue/user.getSimilarVenues.usecase.js
@@ -0,0 +1,30 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js"
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js"
+import { VenueMessages } from "../../../../shared/constants/messages/venueMessages.js"
+
+export class UserGetSimilarVenuesUsecase {
+ constructor (
+ venueRepository,
+ userRepository
+ ){
+ this._venueRepository = venueRepository
+ this._userRepository = userRepository
+ }
+
+ async execute(userId, venueId) {
+ const user = await this._userRepository.findById(userId)
+ if(!user){
+ throw new NotFoundError(authMessages.error.USER_NOT_FOUND)
+ }
+
+ const venue = await this._venueRepository.findById(venueId)
+ if(!venue){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+
+ const similarVenues = await this._venueRepository.findSimilarVenues(venue.id, venue.category)
+ return {
+ similarVenues
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/venue/user.getTopVenue.usacase.js b/server/src/application/user/usecases/venue/user.getTopVenue.usacase.js
new file mode 100644
index 0000000000..08453f3b17
--- /dev/null
+++ b/server/src/application/user/usecases/venue/user.getTopVenue.usacase.js
@@ -0,0 +1,14 @@
+export class UserGetTopVenuesUsecase {
+ constructor(
+ venueRepository
+ ){
+ this._venueRepository = venueRepository
+ }
+
+ async execute(){
+ const venues = await this._venueRepository.findTopVenues()
+ return {
+ venues
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/venue/user.getVenueById.usecase.js b/server/src/application/user/usecases/venue/user.getVenueById.usecase.js
new file mode 100644
index 0000000000..7a78214bd1
--- /dev/null
+++ b/server/src/application/user/usecases/venue/user.getVenueById.usecase.js
@@ -0,0 +1,28 @@
+import { VenueStatus } from '../../../../domain/enums/Venue.enum.js'
+import { VenueMessages } from '../../../../shared/constants/messages/venueMessages.js'
+import { NotFoundError } from '../../../../domain/errors/NotFoundError.js'
+import { ForbiddenError } from '../../../../domain/errors/forbidden.error.js'
+
+
+export class UserGetVenueByIdUsecase {
+ constructor (
+ venueRepository
+ ) {
+ this._venueRepository = venueRepository
+ }
+
+ async execute(venueId){
+ const venue = await this._venueRepository.findById(venueId)
+ if(!venue){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+ if(venue.isDeleted){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+ if(venue.approvalStatus !== VenueStatus.ACTIVE){
+ throw new ForbiddenError(VenueMessages.error.NOT_ACTIVE_VENUE)
+ }
+ return venue
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/wishlist/user.addToWishlist.usecase.js b/server/src/application/user/usecases/wishlist/user.addToWishlist.usecase.js
new file mode 100644
index 0000000000..3240a47a06
--- /dev/null
+++ b/server/src/application/user/usecases/wishlist/user.addToWishlist.usecase.js
@@ -0,0 +1,70 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { VenueStatus } from "../../../../domain/enums/Venue.enum.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+import { VenueMessages } from "../../../../shared/constants/messages/venueMessages.js";
+
+export class UserAddToWishlistUsecase {
+
+ constructor(userRepository, venueRepository){
+ this._userRepository = userRepository;
+ this._venueRepository = venueRepository;
+ }
+
+ async execute(userId, venueId){
+
+ const user = await this._userRepository.findById(userId);
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ );
+ }
+
+ if(user.isBlocked){
+ throw new ValidationError(
+ UserMessage.error.USER_ACCOUNT_BLOCKED
+ );
+ }
+
+ const venue = await this._venueRepository.findById(venueId);
+
+ if(!venue){
+ throw new NotFoundError(
+ VenueMessages.error.VENUE_NOT_FOUND
+ );
+ }
+
+ if(venue.isDeleted){
+ throw new NotFoundError(
+ VenueMessages.error.VENUE_NOT_FOUND
+ );
+ }
+
+ // if(!venue.isAdminVerified){
+ // throw new ValidationError(
+ // VenueMessages.error.NOT_ADMIN_VERIFIED
+ // );
+ // }
+
+ if(venue.approvalStatus !== VenueStatus.ACTIVE){
+ console.log('venue status : ', venue.approvalStatus)
+ throw new ValidationError(
+ VenueMessages.error.NOT_ACTIVE_VENUE
+ );
+ }
+
+ const result = await this._userRepository.addToWishlist(
+ userId,
+ venueId
+ );
+
+ if(result?.alreadyExists){
+ throw new ValidationError(
+ UserMessage.error.WISHLIST_ALREADY_EXISTS
+ );;
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/wishlist/user.getWishlist.usecase.js b/server/src/application/user/usecases/wishlist/user.getWishlist.usecase.js
new file mode 100644
index 0000000000..0f85538d1b
--- /dev/null
+++ b/server/src/application/user/usecases/wishlist/user.getWishlist.usecase.js
@@ -0,0 +1,32 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+import { VenueMessages } from "../../../../shared/constants/messages/venueMessages.js";
+
+export class UserGetWishlistUsecase {
+
+ constructor(userRepository){
+ this._userRepository = userRepository;
+ }
+
+ async execute(userId){
+
+ const user = await this._userRepository.findById(userId);
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ );
+ }
+
+ if(user.isBlocked){
+ throw new ValidationError(
+ UserMessage.error.USER_ACCOUNT_BLOCKED
+ );
+ }
+
+ const wishlist = await this._userRepository.getWishlist(userId);
+
+ return wishlist.wishlist;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/user/usecases/wishlist/user.removeWishlist.usecase.js b/server/src/application/user/usecases/wishlist/user.removeWishlist.usecase.js
new file mode 100644
index 0000000000..ec01124e17
--- /dev/null
+++ b/server/src/application/user/usecases/wishlist/user.removeWishlist.usecase.js
@@ -0,0 +1,50 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../../shared/constants/messages/userMessages.js";
+import { VenueMessages } from "../../../../shared/constants/messages/venueMessages.js";
+
+export class UserRemoveWishlistUsecase{
+
+ constructor(userRepository,venueRepository){
+ this._userRepository=userRepository;
+ this._venueRepository=venueRepository;
+ }
+
+ async execute(userId,venueId){
+
+ const user=await this._userRepository.findById(userId);
+
+ if(!user){
+ throw new NotFoundError(
+ UserMessage.error.USER_NOT_FOUND
+ );
+ }
+
+ if(user.isBlocked){
+ throw new ValidationError(
+ UserMessage.error.USER_ACCOUNT_BLOCKED
+ );
+ }
+
+ const venue=await this._venueRepository.findById(venueId);
+
+ if(!venue){
+ throw new NotFoundError(
+ VenueMessages.error.VENUE_NOT_FOUND
+ );
+ }
+
+ const result=await this._userRepository.removeWishlist(
+ userId,
+ venueId
+ );
+
+ if(result?.notFound){
+ throw new ValidationError(
+ UserMessage.error.WISHLIST_NOT_FOUND
+ );
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/.gitkeep b/server/src/application/vendor/usecases/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/application/vendor/usecases/auth/vendor.forgotPassword.usecase.js b/server/src/application/vendor/usecases/auth/vendor.forgotPassword.usecase.js
new file mode 100644
index 0000000000..7b30bc218f
--- /dev/null
+++ b/server/src/application/vendor/usecases/auth/vendor.forgotPassword.usecase.js
@@ -0,0 +1,41 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export default class VendorForgotPasswordUseCase {
+ constructor(
+ vendorRepository,
+ tokenService,
+ mailService,
+ hashService
+ ) {
+ this._vendorRepository = vendorRepository;
+ this._tokenService = tokenService;
+ this._mailService = mailService;
+ this._hashService = hashService;
+ }
+
+ async execute({email}) {
+ const vendor = await this._vendorRepository.findByEmail(email);
+
+ if (!vendor) {
+ throw new NotFoundError(authMessages.error.VENDOR_NOT_FOUND);
+ }
+
+ const resetToken = this._tokenService.generateResetToken();
+ const resetTokenExpiry = this._tokenService.getResetTokenExpiry();
+
+ const hashedResetToken = this._hashService.hashToken(resetToken)
+ vendor.resetToken = hashedResetToken
+ vendor.resetTokenExpiry = resetTokenExpiry
+ const updated = await this._vendorRepository.update(vendor.id, vendor);
+
+ console.log('updated user', updated)
+ const resetLink = `${process.env.FRONTEND_URL}/reset-password?role=${updated.role}&token=${resetToken}`;
+ console.log("link", resetLink)
+ // await this._mailService.sendForgotPasswordMail(vendor, resetLink);
+
+ return {
+ success: true
+ };
+ }
+}
diff --git a/server/src/application/vendor/usecases/auth/vendor.loginVendor.useCase.js b/server/src/application/vendor/usecases/auth/vendor.loginVendor.useCase.js
new file mode 100644
index 0000000000..bc1bed6666
--- /dev/null
+++ b/server/src/application/vendor/usecases/auth/vendor.loginVendor.useCase.js
@@ -0,0 +1,53 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export class LoginVendorUsecase {
+ constructor(
+ vendorRepository,
+ hashService,
+ tokenService
+ ) {
+ this._vendorRepository = vendorRepository;
+ this._hashService = hashService;
+ this._tokenService = tokenService;
+ }
+
+ async execute({email, password}) {
+ const vendor = await this._vendorRepository.findByEmail(email);
+
+ if (!vendor) {
+ throw new UnauthorizedError(authMessages.error.OWNER_NOT_FOUND);
+ }
+
+ const isMatch = await this._hashService.compare(password, vendor.password);
+ if (!isMatch) {
+ throw new UnauthorizedError(authMessages.error.INVALID_CREDENTIALS);
+ }
+ console.log("login user: ", vendor)
+
+ if(!vendor.isVerified){
+ throw new UnauthorizedError(authMessages.error.OTP_VERIFICATION_REQUIRED)
+ }
+
+ const payload = { id: vendor.id, role: vendor.role }
+ const accessToken = this._tokenService.generateAccessToken( payload );
+ const refreshToken = this._tokenService.generateRefreshToken(payload)
+ const hashedToken = await this._hashService.hashToken(refreshToken)
+ await this._vendorRepository.updateRefreshToken(vendor.id, hashedToken)
+
+ return {
+ accessToken,
+ refreshToken,
+ user:{
+ id: vendor.id,
+ name: vendor.fullName,
+ email: vendor.email,
+ role: vendor.role,
+ isVerified: vendor.isVerified,
+ profileImage: vendor.profileImage.url,
+ approvalStatus: vendor.approvalStatus
+ }
+ };
+ }
+}
+
diff --git a/server/src/application/vendor/usecases/auth/vendor.logout.usecase.js b/server/src/application/vendor/usecases/auth/vendor.logout.usecase.js
new file mode 100644
index 0000000000..1c5cad29b4
--- /dev/null
+++ b/server/src/application/vendor/usecases/auth/vendor.logout.usecase.js
@@ -0,0 +1,40 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export class VendorLogoutUseCase {
+ constructor(
+ vendorRepository,
+ hashService,
+ tokenService
+ ) {
+ this._vendorRepository = vendorRepository;
+ this._hashService = hashService;
+ this._tokenService = tokenService
+
+ }
+
+ async execute(refreshToken, accessToken) {
+ if(accessToken){
+ const expireInSeconds = process.env.ACCESS_TOKEN_MAX_AGE ? Math.floor(Number(process.env.ACCESS_TOKEN_MAX_AGE) / 1000 ): 3600
+ await this._tokenService.blackListToken(accessToken, expireInSeconds)
+ }
+ if (!refreshToken) {
+ throw new UnauthorizedError(authMessages.error.NO_REFRESH_TOKEN);
+ }
+
+ const payload = this._tokenService.verifyRefreshToken(refreshToken)
+
+ if(!payload){
+ throw new UnauthorizedError(authMessages.error.UNAUTHORIZED)
+ }
+
+ const vendor = await this._vendorRepository.findById(payload.id);
+
+ if (!vendor) {
+ throw new UnauthorizedError(authMessages.error.INVALID_REFRESH_TOKEN);
+ }
+
+ const hashedRefreshToken = await this._hashService.hashToken(refreshToken)
+ await this._vendorRepository.clearRefreshToken(hashedRefreshToken);
+ }
+}
diff --git a/server/src/application/vendor/usecases/auth/vendor.refreshToken.usecase.js b/server/src/application/vendor/usecases/auth/vendor.refreshToken.usecase.js
new file mode 100644
index 0000000000..ab424e2c20
--- /dev/null
+++ b/server/src/application/vendor/usecases/auth/vendor.refreshToken.usecase.js
@@ -0,0 +1,32 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export class VendorRefreshTokenUseCase {
+ constructor(vendorRepository, tokenService, hashService) {
+ this._vendorRepository = vendorRepository;
+ this._tokenService = tokenService;
+ this._hashService = hashService
+ }
+
+ async execute(refreshToken) {
+ if (!refreshToken) {
+ throw new UnauthorizedError(authMessages.error.NO_REFRESH_TOKEN);
+ }
+
+ const {id, role} = this._tokenService.verifyRefreshToken(refreshToken);
+
+ const vendor = await this._vendorRepository.findById(id);
+
+ if (!vendor) {
+ throw new UnauthorizedError(authMessages.error.REFRESH_TOKEN_REVOKED);
+ }
+
+ const payload = { id: vendor.id, role: role };
+ const newAccessToken = this._tokenService.generateAccessToken(payload);
+ const newRefreshToken = this._tokenService.generateRefreshToken(payload);
+ const hashedRefreshToken = await this._hashService.hashToken(newRefreshToken)
+ await this._vendorRepository.updateRefreshToken(vendor.id, hashedRefreshToken);
+
+ return { accessToken: newAccessToken, refreshToken: newRefreshToken, vendor };
+ }
+}
diff --git a/server/src/application/vendor/usecases/auth/vendor.registerVendor.useCase.js b/server/src/application/vendor/usecases/auth/vendor.registerVendor.useCase.js
new file mode 100644
index 0000000000..25178e6ae0
--- /dev/null
+++ b/server/src/application/vendor/usecases/auth/vendor.registerVendor.useCase.js
@@ -0,0 +1,58 @@
+import { ConflictError } from "../../../../domain/errors/ConflictError.js";
+import { VendorEntity } from "../../../../domain/entities/Vendor.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+import { UserRole } from "../../../../domain/enums/UserRole.enum.js";
+
+export class RegisterVendorUsecase {
+ constructor(
+ vendorRepository,
+ hashService,
+ otpService,
+ otpStoreService,
+ mailService
+ ) {
+ this._vendorRepository = vendorRepository;
+ this._hashService = hashService;
+ this._otpService = otpService;
+ this._otpStoreService = otpStoreService;
+ this._mailService = mailService
+ }
+
+ async execute({
+ fullName,
+ email,
+ phone,
+ password
+ }) {
+ // console.log("email: ", email)
+ const existing = await this._vendorRepository.findByEmail(email);
+
+ // console.log('from vendor usecase: ', existing)
+ if (existing) {
+ throw new ConflictError(authMessages.error.EMAIL_ALREADY_EXISTS);
+ }
+
+ const hashedPassword = await this._hashService.hash(password);
+
+ const vendor = new VendorEntity({
+ fullName: fullName,
+ email: email,
+ phone: phone,
+ password: hashedPassword,
+ role: UserRole.VENDOR
+ });
+
+ const savedVendor = await this._vendorRepository.create(vendor);
+ const otp = this._otpService.generate()
+ console.log('vendor otp is:', otp)
+
+ const hashedOtp = await this._otpService.hash(otp)
+ await this._otpStoreService.saveOtp(savedVendor.id, hashedOtp, 120)
+ // await this._mailService.sendVerifiyRegisterOtp(savedVendor.email, savedVendor.fullName, otp)
+
+ return {
+ success: true
+ }
+ }
+}
+
diff --git a/server/src/application/vendor/usecases/auth/vendor.resendOtp.usecase.js b/server/src/application/vendor/usecases/auth/vendor.resendOtp.usecase.js
new file mode 100644
index 0000000000..d1fb92fa48
--- /dev/null
+++ b/server/src/application/vendor/usecases/auth/vendor.resendOtp.usecase.js
@@ -0,0 +1,34 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export default class VendorrResendOtpUseCase {
+ constructor(
+ vendorRepository,
+ otpService,
+ otpStoreService,
+ mailService
+ ) {
+ this._vendorRepository = vendorRepository;
+ this._otpService = otpService;
+ this._otpStoreService = otpStoreService;
+ this._mailService = mailService
+ }
+
+ async execute({email}) {
+ const vendor = await this._vendorRepository.findByEmail(email);
+
+ if (!vendor) {
+ throw new NotFoundError(authMessages.error.VENDOR_NOT_FOUND);
+ }
+
+ const otp = this._otpService.generate();
+ console.log('otp is:', otp)
+ const hashedOtp = await this._otpService.hash(otp);
+ await this._otpStoreService.saveOtp(vendor.id, hashedOtp, 120)
+ await this._mailService.sendVerifiyRegisterOtp(vendor.email, vendor.fullName, otp)
+
+ return {
+ success: true
+ };
+ }
+}
diff --git a/server/src/application/vendor/usecases/auth/vendor.resetPassword.usecase.js b/server/src/application/vendor/usecases/auth/vendor.resetPassword.usecase.js
new file mode 100644
index 0000000000..b1967be339
--- /dev/null
+++ b/server/src/application/vendor/usecases/auth/vendor.resetPassword.usecase.js
@@ -0,0 +1,51 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { AppError } from "../../../../domain/errors/app.error.js";
+import { statusCode } from "../../../../shared/constants/enums/statusCode.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export class VendorResetPasswordUseCase {
+ constructor(vendorRepository, hashService) {
+ this._vendorRepository = vendorRepository;
+ this._hashService = hashService;
+ }
+
+ async execute(token, password) {
+ const hashedResetToken = this._hashService.hashToken(token)
+
+ const vendor = await this._vendorRepository.findByResetToken(hashedResetToken);
+
+ if (!vendor) {
+ throw new NotFoundError(authMessages.error.VENDOR_NOT_FOUND);
+ }
+
+ if (!vendor.resetToken || !vendor.resetTokenExpiry) {
+ throw new UnauthorizedError(authMessages.error.NO_RESET_REQUEST);
+ }
+
+ const hashedIncomingToken = this._hashService.hashToken(token)
+ if (vendor.resetToken !== hashedIncomingToken) {
+ throw new UnauthorizedError(authMessages.error.INVALID_RESET_TOKEN);
+ }
+
+ if (vendor.resetTokenExpiry < new Date()) {
+ throw new UnauthorizedError(authMessages.error.RESET_TOKEN_EXPIRED);
+ }
+
+ const hashedPassword = await this._hashService.hash(password);
+
+ const updatedVendor = await this._vendorRepository.update(vendor.id, {
+ password: hashedPassword,
+ resetToken: null,
+ resetTokenExpiry: null,
+ });
+
+ if (!updatedVendor) {
+ throw new AppError(authMessages.error.RESET_PASSWORD_FAILED, statusCode.SERVER_ERROR);
+ }
+
+ return {
+ success: true
+ };
+ }
+}
diff --git a/server/src/application/vendor/usecases/auth/vendor.verifyOtp.usecase.js b/server/src/application/vendor/usecases/auth/vendor.verifyOtp.usecase.js
new file mode 100644
index 0000000000..ffba70a78e
--- /dev/null
+++ b/server/src/application/vendor/usecases/auth/vendor.verifyOtp.usecase.js
@@ -0,0 +1,48 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js";
+
+export class VendorVerifyOtpUseCase {
+ constructor(
+ vendorRepository,
+ otpService,
+ otpStoreService
+ ) {
+ this._vendorRepository = vendorRepository;
+ this._otpService = otpService;
+ this._otpStoeService = otpStoreService
+ }
+
+ async execute({email, otpCode}) {
+ const vendor = await this._vendorRepository.findByEmail(email);
+
+ if (!vendor) {
+ throw new NotFoundError(authMessages.error.VENDOR_NOT_FOUND);
+ }
+
+ if (vendor.isOtpVerified) {
+ throw new UnauthorizedError(authMessages.error.ALREADY_OTP_VERIFIED);
+ }
+
+ const storedOtp = await this._otpStoeService.getOtp(vendor.id)
+ if(!storedOtp){
+ throw new NotFoundError(authMessages.error.OTP_EXPIRED)
+ }
+
+ const isOtpValid = await this._otpService.compare(otpCode, storedOtp);
+
+ if (!isOtpValid) {
+ throw new UnauthorizedError(authMessages.error.INVALID_OTP);
+ }
+
+ const verifiedVendor = await this._vendorRepository.verifyOtp(vendor.id);
+
+ if (!verifiedVendor) {
+ throw new UnauthorizedError(authMessages.error.OTP_VERIFY_FAILED);
+ }
+
+ return {
+ success: true
+ }
+ }
+}
diff --git a/server/src/application/vendor/usecases/booking/getBookingByIdUsecase.js b/server/src/application/vendor/usecases/booking/getBookingByIdUsecase.js
new file mode 100644
index 0000000000..6404779830
--- /dev/null
+++ b/server/src/application/vendor/usecases/booking/getBookingByIdUsecase.js
@@ -0,0 +1,69 @@
+import { BookingMessages } from "../../../../shared/constants/messages/bookingMessages.js";
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ForbiddenError } from "../../../../domain/errors/forbidden.error.js";
+
+export class GetBookingByIdUsecase {
+ constructor(bookingRepository) {
+ this._bookingRepository = bookingRepository;
+ }
+
+ async execute({ bookingId, vendorId }) {
+ const booking =
+ await this._bookingRepository.findById(bookingId);
+
+ if (!booking) {
+ throw new NotFoundError(
+ BookingMessages.error.BOOKING_NOT_FOUND
+ );
+ }
+
+ // Get the actual vendor ID
+ const bookingVendorId =
+ booking.vendorId?.id ||
+ booking.vendorId?._id ||
+ booking.vendorId;
+
+ // Compare both as strings
+ if (
+ bookingVendorId.toString() !==
+ vendorId.toString()
+ ) {
+ throw new ForbiddenError(
+ BookingMessages.error.FORBIDDEN
+ );
+ }
+
+ return {
+ booking: {
+ id: booking._id,
+ bookingDate: booking.bookingDate,
+ startTime: booking.startTime,
+ endTime: booking.endTime,
+ guestCount: booking.guestCount,
+ status: booking.status,
+ },
+
+ customer: {
+ id: booking.userId._id,
+ name: booking.userId.fullName,
+ email: booking.userId.email,
+ phone: booking.userId.phone,
+ },
+
+ venue: {
+ id: booking.venueId._id,
+ name: booking.venueId.name,
+ category: booking.venueId.category,
+ address: booking.venueId.address,
+ },
+
+ payment: {
+ totalAmount: booking.totalAmount,
+ advanceAmount: booking.advanceAmount,
+ paidAmount: booking.paidAmount,
+ remainingAmount: booking.remainingAmount,
+ paymentStatus: booking.paymentStatus,
+ },
+ };
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/booking/getVendorBookingsUsecase.js b/server/src/application/vendor/usecases/booking/getVendorBookingsUsecase.js
new file mode 100644
index 0000000000..4b39676454
--- /dev/null
+++ b/server/src/application/vendor/usecases/booking/getVendorBookingsUsecase.js
@@ -0,0 +1,17 @@
+export class GetVendorBookingsUsecase {
+
+ constructor( bookingRepository) {
+ this._bookingRepository =
+ bookingRepository
+ }
+
+ async execute({vendorId, page, limit, status, search}) {
+
+ const bookings =
+ await this._bookingRepository
+ .findByOwnerId(vendorId, {page, limit, status, search})
+ return bookings
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/dashboard/GetDashboardStatsUsecase.js b/server/src/application/vendor/usecases/dashboard/GetDashboardStatsUsecase.js
new file mode 100644
index 0000000000..0362c19816
--- /dev/null
+++ b/server/src/application/vendor/usecases/dashboard/GetDashboardStatsUsecase.js
@@ -0,0 +1,61 @@
+import { BookingStatus } from "../../../../domain/enums/Booking.enum.js";
+
+export class GetDashboardStatsUsecase {
+ constructor(
+ venueRepository,
+
+ bookingRepository
+ ) {
+ this._venueRepository = venueRepository;
+
+ this._bookingRepository = bookingRepository;
+ }
+
+ async execute(vendorId) {
+ const totalVenues = await this._venueRepository.countByOwnerId(vendorId);
+
+ const totalBookings = await this._bookingRepository.countByOwnerId(vendorId);
+
+ const topVenues = await this._bookingRepository.getTopVenues(vendorId);
+
+ const recentBookings = await this._bookingRepository.getRecentBookings(
+ vendorId
+ );
+
+ const pendingBookings =
+ await this._bookingRepository.countByOwnerIdAndStatus(
+ vendorId,
+ BookingStatus.PENDING
+ );
+
+ const confirmedBookings =
+ await this._bookingRepository.countByOwnerIdAndStatus(
+ vendorId,
+ BookingStatus.CONFIRMED
+ );
+
+ const completedBookings =
+ await this._bookingRepository.countByOwnerIdAndStatus(
+ vendorId,
+ BookingStatus.COMPLETED
+ );
+
+ return {
+ stats: {
+ totalVenues,
+
+ totalBookings,
+
+ pendingBookings,
+
+ confirmedBookings,
+
+ completedBookings,
+ },
+
+ topVenues,
+
+ recentBookings,
+ };
+ }
+}
diff --git a/server/src/application/vendor/usecases/profile/changeVendorPassword.usecase.js b/server/src/application/vendor/usecases/profile/changeVendorPassword.usecase.js
new file mode 100644
index 0000000000..f60cb1264e
--- /dev/null
+++ b/server/src/application/vendor/usecases/profile/changeVendorPassword.usecase.js
@@ -0,0 +1,42 @@
+import { NotFoundError } from "../../../../domain/errors/NotFoundError.js";
+import { ValidationError } from "../../../../domain/errors/ValidationError.js";
+import { VendorMessages } from "../../../../shared/constants/messages/vendorMessages.js";
+
+export class ChangeVendorPasswordUsecase {
+ constructor(vendorRepository, hashService) {
+ this._vendorRepository = vendorRepository;
+ this._hashService = hashService;
+ }
+
+ async execute({ vendorId, currentPassword, newPassword }) {
+ const vendor = await this._vendorRepository.findById(vendorId);
+
+ if (!vendor) {
+ throw new NotFoundError(VendorMessages.error.VENDOR_NOT_FOUND);
+ }
+
+ const isPasswordCorrect = await this._hashService.compare(
+ currentPassword,
+ vendor.password
+ );
+
+ if (!isPasswordCorrect) {
+ throw new ValidationError(VendorMessages.error.INVALID_CURRENT_PASSWORD);
+ }
+
+ const samePassword = await this._hashService.compare(
+ newPassword,
+ vendor.password
+ );
+
+ if (samePassword) {
+ throw new ValidationError(VendorMessages.error.SAME_PASSWORD);
+ }
+
+ const hashedPassword = await this._hashService.hash(newPassword);
+
+ await this._vendorRepository.updatePassword(vendorId, hashedPassword);
+
+ return null;
+ }
+}
diff --git a/server/src/application/vendor/usecases/profile/getVendorProfile.usecase.js b/server/src/application/vendor/usecases/profile/getVendorProfile.usecase.js
new file mode 100644
index 0000000000..073239f231
--- /dev/null
+++ b/server/src/application/vendor/usecases/profile/getVendorProfile.usecase.js
@@ -0,0 +1,23 @@
+import { NotFoundError } from '../../../../domain/errors/NotFoundError.js'
+import { VendorMessages } from '../../../../shared/constants/messages/vendorMessages.js'
+
+export class GetVendorProfileUsecase {
+
+ constructor(VendorRepository) {
+ this._vendorRepository = VendorRepository
+ }
+
+ async execute(vendorId) {
+
+ const vendor =
+ await this._vendorRepository.findById(vendorId)
+
+ if (!vendor) {
+ throw new NotFoundError(
+ VendorMessages.error.VENDOR_NOT_FOUND
+ )
+ }
+
+ return vendor
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/profile/updateVendorProfile.usecase.js b/server/src/application/vendor/usecases/profile/updateVendorProfile.usecase.js
new file mode 100644
index 0000000000..976e7e8be2
--- /dev/null
+++ b/server/src/application/vendor/usecases/profile/updateVendorProfile.usecase.js
@@ -0,0 +1,67 @@
+import { NotFoundError } from '../../../../domain/errors/NotFoundError.js'
+import { ConflictError } from '../../../../domain/errors/ConflictError.js'
+import { VendorMessages } from '../../../../shared/constants/messages/vendorMessages.js'
+
+export class VendorUpdateProfileUsecase {
+
+ constructor(VendorRepository) {
+ this._vendorRepository = VendorRepository
+ }
+
+ async execute({
+ vendorId,
+ fullName,
+ phone,
+ profileImage,
+ companyName,
+ address,
+ bio
+ }) {
+
+ const vendor =
+ await this._vendorRepository.findById(vendorId)
+
+ if (!vendor) {
+ throw new NotFoundError(
+ VendorMessages.error.VENDOR_NOT_FOUND
+ )
+ }
+
+ if (phone && phone !== vendor.phone) {
+
+ const existingVendor =
+ await this._vendorRepository.findByPhone(phone)
+
+ if (existingVendor) {
+ throw new ConflictError(
+ VendorMessages.error.PHONE_ALREADY_EXISTS
+ )
+ }
+ }
+
+ vendor.fullName = fullName??vendor.fullName
+ vendor.phone = phone??vendor
+ if (profileImage) {
+ vendor.profileImage = {
+ publicId:profileImage.publicId ?? vendor.profileImage.publicId,
+ url: profileImage ?? vendor.profileImage.url}};
+ vendor.companyName = companyName ?? vendor.companyName;
+ if(address){
+ vendor.address = {
+ addressLine1: address?.addressLine1 || vendor.address.addressLine1,
+ city: address?.city || vendor.address.city,
+ state: address?.state || vendor.address.state,
+ pincode: address?.pincode || vendor.address.pincode,
+
+ };}
+ vendor.bio = bio ?? vendor.bio;
+
+ const updatedVendor =
+ await this._vendorRepository.update(
+ vendor.id,
+ vendor
+ )
+
+ return updatedVendor
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/venue/vendor.createVenue.usecase.js b/server/src/application/vendor/usecases/venue/vendor.createVenue.usecase.js
new file mode 100644
index 0000000000..18c975cd37
--- /dev/null
+++ b/server/src/application/vendor/usecases/venue/vendor.createVenue.usecase.js
@@ -0,0 +1,104 @@
+import { VenueMessages } from '../../../../shared/constants/messages/venueMessages.js'
+import { VenueEntity } from '../../../../domain/entities/Venue.js'
+import { VenueStatus } from '../../../../domain/enums/Venue.enum.js'
+import { ValidationError } from '../../../../domain/errors/ValidationError.js'
+import { UnauthorizedError } from '../../../../domain/errors/UnauthorizedError.js'
+import { ConflictError } from '../../../../domain/errors/ConflictError.js'
+import { authMessages } from '../../../../shared/constants/messages/authMessages.js'
+import { UserRole } from '../../../../domain/enums/UserRole.enum.js'
+
+export class VendorCreateVenueUsecase {
+ constructor (
+ venueRepository,
+ vendorRepository
+ ) {
+ this._venueRepository = venueRepository;
+ this._vendorRepository = vendorRepository;
+ }
+
+ async execute({
+ vendorId,
+ name,
+ description,
+ category,
+ websiteUrl,
+ addressLine1,
+ city,
+ state,
+ country,
+ phone,
+ pincode,
+ googleMapLink,
+ seatingCapacity = 0,
+ standingCapacity = 0,
+ pricePerHour = 0,
+ pricePerDay = 0,
+ securityDeposit = 0,
+ availabilityRules = {},
+ weekendSurcharge = 0,
+ minimumBookingHours = 0,
+ amenities = [],
+ images = [],
+ license = []
+
+ }) {
+ const vendor = await this._vendorRepository.findById(vendorId)
+ if(!vendor){
+ throw new UnauthorizedError(authMessages.error.OWNER_NOT_FOUND)
+ }
+
+ if(vendor.role !== UserRole.VENDOR){
+ throw new UnauthorizedError(VenueMessages.error.CANNOT_ADD_VENUE)
+ }
+ const existing = await this._venueRepository.findByVendorAndName(vendorId, name)
+ if(existing){
+ throw new ConflictError(VenueMessages.error.ALREADY_EXISTING)
+ }
+
+ if(images.length < 3){
+ throw new ValidationError(VenueMessages.error.REQUIRE_ATLEAST_THREE_IMAGES)
+ }
+
+ if(license.length < 1){
+ throw new ValidationError(VenueMessages.error.VENUE_LICENSE_REQUIRED)
+ }
+
+ const newVenue = new VenueEntity({
+ id: '',
+ name,
+ vendorId,
+ description,
+ category,
+ websiteUrl,
+ address: {
+ addressLine1,
+ city,
+ state,
+ country,
+ pincode,
+ phone,
+ googleMapLink
+ },
+ seatingCapacity,
+ standingCapacity,
+ pricePerHour,
+ pricePerDay,
+ securityDeposit,
+ availabilityRules,
+ weekendSurcharge,
+ minimumBookingHours,
+ amenities,
+ images,
+ license,
+ isDeleted: false,
+ rating: 0,
+ reviews: [],
+ approvalStatus: VenueStatus.PENDING
+ })
+
+ const savedVenue = await this._venueRepository.create(newVenue)
+ return {
+ venue: savedVenue
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/venue/vendor.deleteVenue.usecase.js b/server/src/application/vendor/usecases/venue/vendor.deleteVenue.usecase.js
new file mode 100644
index 0000000000..cff10928b0
--- /dev/null
+++ b/server/src/application/vendor/usecases/venue/vendor.deleteVenue.usecase.js
@@ -0,0 +1,35 @@
+import { VenueMessages } from '../../../../shared/constants/messages/venueMessages.js'
+import { NotFoundError } from '../../../../domain/errors/NotFoundError.js'
+import { ForbiddenError } from '../../../../domain/errors/forbidden.error.js'
+import { UnauthorizedError } from '../../../../domain/errors/UnauthorizedError.js'
+import { authMessages } from '../../../../shared/constants/messages/authMessages.js'
+
+export class VendorDeleteVenueUsecase {
+ constructor (
+ venueRepository,
+ vendorRepository
+ ) {
+ this._venueRepository = venueRepository
+ this._vendorRepository = vendorRepository
+ }
+
+ async execute(vendorId, venueId) {
+ const vendor = await this._vendorRepository.findById(vendorId)
+ if(!vendor){
+ throw new UnauthorizedError(authMessages.error.VENDOR_NOT_FOUND)
+ }
+ const venue = await this._venueRepository.findById(venueId)
+ if(!venue){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+ if(venue.vendorId.id.toString() !== vendorId){
+ throw new ForbiddenError(VenueMessages.error.FORBIDDEN)
+ }
+
+ if(venue.isDeleted){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+
+ return await this._venueRepository.delete(venue.id)
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/venue/vendor.editVenue.usecase.js b/server/src/application/vendor/usecases/venue/vendor.editVenue.usecase.js
new file mode 100644
index 0000000000..88bc96a31a
--- /dev/null
+++ b/server/src/application/vendor/usecases/venue/vendor.editVenue.usecase.js
@@ -0,0 +1,118 @@
+import { VenueStatus } from "../../../../domain/enums/Venue.enum.js"
+import { VenueMessages } from "../../../../shared/constants/messages/venueMessages.js"
+import { NotFoundError } from '../../../../domain/errors/NotFoundError.js'
+import { ConflictError } from '../../../../domain/errors/ConflictError.js'
+import { ForbiddenError } from '../../../../domain/errors/forbidden.error.js'
+import { ValidationError } from '../../../../domain/errors/ValidationError.js'
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js"
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js"
+
+
+export class VendorEditVenueUsecase {
+ constructor (
+ VenueRepository,
+ cloudinaryService,
+ vendorRepository,
+ ) {
+ this._venueRepository = VenueRepository
+ this._cloudinaryService = cloudinaryService
+ this._vendorRepository = vendorRepository
+ }
+
+ async execute({
+ venueId,
+ vendorId,
+ name,
+ description,
+ category,
+ websiteUrl,
+ addressLine1,
+ city,
+ state,
+ country,
+ phone,
+ pincode,
+ googleMapLink,
+ seatingCapacity = 0,
+ standingCapacity = 0,
+ pricePerHour = 0,
+ pricePerDay = 0,
+ securityDeposit = 0,
+ availabilityRules = {},
+ weekendSurcharge = 0,
+ minimumBookingHours = 0,
+ amenities = [],
+ newImages = [],
+ newLicense = [],
+ deletedImages = [],
+ deletedLicense = []
+ }) {
+ const vendor = await this._vendorRepository.findById(vendorId)
+ if(!vendor){
+ throw new UnauthorizedError(authMessages.error.VENDOR_NOT_FOUND)
+ }
+ const venue = await this._venueRepository.findById(venueId)
+ if(!venue){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+
+ if(venue.vendorId.id.toString() !== vendorId){
+ throw new ForbiddenError(VenueMessages.error.FORBIDDEN)
+ }
+
+ if(venue.isDeleted){
+ throw new NotFoundError(VenueMessages.error.CANNOT_UPDATE_DELETED_VENUE)
+ }
+ if(venue.approvalStatus === VenueStatus.INACTIVE || venue.approvalStatus === VenueStatus.SUSPENDED){
+ throw new ConflictError(VenueMessages.error.CANNOT_UPDATE_INACTIVE_VENUE)
+ }
+
+ const remainingImages = venue.images.filter(image => !deletedImages.includes(image.publicId))
+ const remainingLicense = venue.license.filter(l => !deletedLicense.includes(l.publicId))
+ const finalLicense = [...remainingLicense, ...newLicense]
+
+
+ if(finalLicense.length === 0){
+ throw new ValidationError(VenueMessages.error.VENUE_LICENSE_REQUIRED)
+ }
+
+ const finalImages = [...remainingImages, ...newImages]
+ if(finalImages.length < 3){
+ throw new ValidationError(VenueMessages.error.REQUIRE_ATLEAST_THREE_IMAGES)
+ }
+
+ venue.name = name
+ venue.description = description
+ venue.category = category
+ venue.websiteUrl = websiteUrl
+ venue.address.addressLine1 = addressLine1
+ venue.address.city = city
+ venue.address.state = state
+ venue.address.country = country
+ venue.address.pincode = pincode
+ venue.address.phone = phone
+ venue.address.googleMapLink = googleMapLink
+ venue.seatingCapacity = seatingCapacity
+ venue.standingCapacity = standingCapacity
+ venue.pricePerDay = pricePerDay
+ venue.pricePerHour = pricePerHour
+ venue.securityDeposit = securityDeposit
+ venue.availabilityRules = availabilityRules
+ venue.weekendSurcharge = weekendSurcharge
+ venue.minimumBookingHours = minimumBookingHours
+ venue.amenities = amenities
+ venue.images = [...remainingImages, ...newImages]
+ venue.license = finalLicense
+
+ const updatedVenue = await this._venueRepository.update(venue.id, venue)
+
+ if(deletedImages.length){
+ await this._cloudinaryService.deleteImages(deletedImages)
+ }
+ if(deletedLicense.length){
+ await this._cloudinaryService.deleteImages(deletedLicense)
+ }
+
+ return updatedVenue
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/venue/vendor.getAllVenues.usecase.js b/server/src/application/vendor/usecases/venue/vendor.getAllVenues.usecase.js
new file mode 100644
index 0000000000..e73d57330e
--- /dev/null
+++ b/server/src/application/vendor/usecases/venue/vendor.getAllVenues.usecase.js
@@ -0,0 +1,27 @@
+import { UnauthorizedError } from "../../../../domain/errors/UnauthorizedError.js"
+import { authMessages } from "../../../../shared/constants/messages/authMessages.js"
+
+export class VendorGetAllVenuesUsecase {
+ constructor (
+ venueRepository,
+ vendorRepository
+ ) {
+ this._venueRepository = venueRepository
+ this._vendorRepository = vendorRepository
+ }
+
+ async execute(vendorId, page, limit, category, status, search) {
+ const vendor = await this._vendorRepository.findById(vendorId)
+ if(!vendor){
+ throw new UnauthorizedError(authMessages.error.VENDOR_NOT_FOUND)
+ }
+ const { data, totalPages, totalCount } = await this._venueRepository.findAllFiltered({vendorId, search, category, status, page, limit})
+
+ return {
+ data,
+ totalPages,
+ totalCount
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/venue/vendor.getVenueById.usecase.js b/server/src/application/vendor/usecases/venue/vendor.getVenueById.usecase.js
new file mode 100644
index 0000000000..daaac08717
--- /dev/null
+++ b/server/src/application/vendor/usecases/venue/vendor.getVenueById.usecase.js
@@ -0,0 +1,31 @@
+import { VenueMessages } from '../../../../shared/constants/messages/venueMessages.js'
+import { NotFoundError } from '../../../../domain/errors/NotFoundError.js'
+import { UnauthorizedError } from '../../../../domain/errors/UnauthorizedError.js'
+import { authMessages } from '../../../../shared/constants/messages/authMessages.js'
+
+
+export class VendorGetVenueByIdUsecase {
+ constructor (
+ venueRepository,
+ vendorRepository
+ ) {
+ this._venueRepository = venueRepository
+ this._vendorRepository = vendorRepository
+ }
+
+ async execute(vendorId, venueId) {
+ const vendor = await this._vendorRepository.findById(vendorId)
+ if(!vendor){
+ throw new UnauthorizedError(authMessages.error.VENDOR_NOT_FOUND)
+ }
+
+ const venue = await this._venueRepository.findById(venueId)
+ if(!venue){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+ if(venue.isDeleted){
+ throw new NotFoundError(VenueMessages.error.DELETED_VENUE)
+ }
+ return venue
+ }
+}
\ No newline at end of file
diff --git a/server/src/application/vendor/usecases/venue/venue.updateVenueStatus.usecase.js b/server/src/application/vendor/usecases/venue/venue.updateVenueStatus.usecase.js
new file mode 100644
index 0000000000..e6b7f19cfb
--- /dev/null
+++ b/server/src/application/vendor/usecases/venue/venue.updateVenueStatus.usecase.js
@@ -0,0 +1,41 @@
+import { VenueMessages } from '../../../../shared/constants/messages/venueMessages.js'
+import { ConflictError } from '../../../../domain/errors/ConflictError.js'
+import { NotFoundError } from '../../../../domain/errors/NotFoundError.js'
+import { ForbiddenError } from '../../../../domain/errors/forbidden.error.js'
+import { UnauthorizedError } from '../../../../domain/errors/UnauthorizedError.js'
+import { authMessages } from '../../../../shared/constants/messages/authMessages.js'
+
+
+
+export class VendorUpdateVenueStatusUsecase {
+ constructor (
+ venueRepository,
+ vendorRepository
+ ) {
+ this._venueRepository = venueRepository
+ this._vendorRepository = vendorRepository
+ }
+
+ async execute({vendorId, venueId, status}) {
+ const vendor = await this._vendorRepository.findById(vendorId)
+ if(!vendor){
+ throw new UnauthorizedError(authMessages.error.VENDOR_NOT_FOUND)
+ }
+ const venue = await this._venueRepository.findById(venueId)
+ if(!venue){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+ if(venue.isDeleted){
+ throw new NotFoundError(VenueMessages.error.VENUE_NOT_FOUND)
+ }
+ if(venue.vendorId.id.toString() !== vendorId) {
+ throw new ForbiddenError(VenueMessages.error.FORBIDDEN)
+ }
+
+ if(venue.approvalStatus === status) {
+ throw new ConflictError(VenueMessages.error.STATUS_ALREADY_SET)
+ }
+ venue.approvalStatus = status
+ return await this._venueRepository.update(venue.id, venue)
+ }
+}
\ No newline at end of file
diff --git a/server/src/domain/entities/.gitkeep b/server/src/domain/entities/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/domain/entities/Admin.js b/server/src/domain/entities/Admin.js
new file mode 100644
index 0000000000..b535984498
--- /dev/null
+++ b/server/src/domain/entities/Admin.js
@@ -0,0 +1,24 @@
+export class AdminEntity {
+ constructor({
+ id,
+ fullName,
+ email,
+ password = null,
+ role,
+ isDeleted = false,
+ refreshToken = [],
+ createdAt,
+ updatedAt,
+ }) {
+ this.id = id;
+ this.fullName = fullName;
+ this.email = email;
+ this.password = password;
+ this.role = role;
+ this.refreshToken = refreshToken;
+ this.isDeleted = isDeleted;
+ this.createdAt = createdAt;
+ this.updatedAt = updatedAt;
+ }
+}
+
diff --git a/server/src/domain/entities/Booking.js b/server/src/domain/entities/Booking.js
new file mode 100644
index 0000000000..bd5cfbc896
--- /dev/null
+++ b/server/src/domain/entities/Booking.js
@@ -0,0 +1,156 @@
+import { BookingStatus } from "../enums/Booking.enum.js";
+import { PaymentStatus } from "../enums/Payment.enum.js";
+
+export class Booking {
+ constructor({
+ id = null,
+ userId,
+ venueId,
+ vendorId,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+ totalAmount,
+ advanceAmount = 0,
+ paidAmount = 0,
+ remainingAmount = totalAmount,
+ status = BookingStatus.PENDING,
+ paymentStatus = PaymentStatus.PENDING,
+ cancellationReason = null,
+ createdAt = new Date(),
+ updatedAt = new Date(),
+ }) {
+ this.id = id;
+ this.userId = userId;
+ this.venueId = venueId;
+ this.vendorId = vendorId;
+ this.bookingDate = bookingDate;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ this.guestCount = guestCount;
+ this.totalAmount = totalAmount;
+ this.advanceAmount = advanceAmount;
+ this.paidAmount = paidAmount;
+ this.remainingAmount = remainingAmount;
+ this.status = status;
+ this.paymentStatus = paymentStatus;
+ this.cancellationReason = cancellationReason;
+ this.createdAt = createdAt;
+ this.updatedAt = updatedAt;
+ }
+
+ payAmount(amount) {
+ this.paidAmount += amount;
+
+ if (this.paidAmount >= this.totalAmount) {
+ this.paidAmount = this.totalAmount;
+ this.remainingAmount = 0;
+ this.paymentStatus = PaymentStatus.PAID;
+ } else {
+ this.remainingAmount = this.totalAmount - this.paidAmount;
+ this.paymentStatus = PaymentStatus.PARTIAL;
+ }
+
+ this.touch();
+ }
+
+ canPayBalance(today = new Date()) {
+ const eventDate = new Date(this.bookingDate);
+ const deadline = new Date(eventDate);
+
+ deadline.setDate(deadline.getDate() - 1);
+
+ return today <= deadline;
+ }
+
+ payBalance(amount) {
+ if (!this.canPayBalance()) {
+ throw new Error("Balance payment must be paid at least 1 day before the event");
+ }
+
+ if (amount !== this.remainingAmount) {
+ throw new Error("Balance payment amount must match remaining amount");
+ }
+
+ this.payAmount(amount);
+ }
+
+ markPaymentFailed() {
+ this.paymentStatus = PaymentStatus.FAILED;
+ this.touch();
+ }
+
+ confirm() {
+ if (this.status === BookingStatus.CANCELLED) {
+ throw new Error("Cancelled booking cannot be confirmed");
+ }
+
+ if (this.status === BookingStatus.REJECTED) {
+ throw new Error("Rejected booking cannot be confirmed");
+ }
+
+ if (
+ this.paymentStatus !== PaymentStatus.PARTIAL &&
+ this.paymentStatus !== PaymentStatus.PAID
+ ) {
+ throw new Error("Booking cannot be confirmed before advance payment");
+ }
+
+ this.status = BookingStatus.CONFIRMED;
+ this.touch();
+ }
+
+
+
+ cancel(reason = null) {
+ if (this.status === BookingStatus.COMPLETED) {
+ throw new Error("Completed booking cannot be cancelled");
+ }
+
+ if (this.status === BookingStatus.CANCELLED) {
+ return;
+ }
+
+ this.status = BookingStatus.CANCELLED;
+ this.cancellationReason = reason;
+ this.touch();
+ }
+
+ complete() {
+ if (this.status === BookingStatus.COMPLETED) {
+ return;
+ }
+
+ if (this.status !== BookingStatus.CONFIRMED) {
+ throw new Error("Only confirmed booking can be completed");
+ }
+
+ if (this.paymentStatus !== PaymentStatus.PAID) {
+ throw new Error("Booking cannot be completed before full payment");
+ }
+
+ this.status = BookingStatus.COMPLETED;
+ this.touch();
+ }
+
+ refund() {
+ if (this.paymentStatus === PaymentStatus.REFUNDED) {
+ return;
+ }
+
+ if (
+ this.paymentStatus !== PaymentStatus.PAID &&
+ this.paymentStatus !== PaymentStatus.PARTIAL
+ ) {
+ throw new Error("Only paid bookings can be refunded");
+ }
+
+ this.paymentStatus = PaymentStatus.REFUNDED;
+ this.touch();
+ }
+
+ touch() {
+ this.updatedAt = new Date();
+ }
+}
\ No newline at end of file
diff --git a/server/src/domain/entities/Payment.js b/server/src/domain/entities/Payment.js
new file mode 100644
index 0000000000..1058b4b376
--- /dev/null
+++ b/server/src/domain/entities/Payment.js
@@ -0,0 +1,65 @@
+import { PaymentStatus } from "../enums/Payment.enum.js";
+import { PaymentType } from "../enums/PaymentType.enum.js";
+import { PaymentMethod } from "../enums/PaymentMethod.enum.js";
+
+export class PaymentEntity {
+
+ constructor({
+
+ id = null,
+
+ bookingId,
+
+ userId,
+
+ vendorId,
+
+ amount,
+
+ paymentType,
+
+ paymentMethod,
+
+ paymentStatus = PaymentStatus.PENDING,
+
+ refundAmount = 0,
+
+ refundReason = null,
+
+ refundedAt = null,
+
+ createdAt = new Date(),
+
+ updatedAt = new Date()
+
+ }) {
+
+ this.id = id;
+
+ this.bookingId = bookingId;
+
+ this.userId = userId;
+
+ this.vendorId = vendorId;
+
+ this.amount = amount;
+
+ this.paymentType = paymentType;
+
+ this.paymentMethod = paymentMethod;
+
+ this.paymentStatus = paymentStatus;
+
+ this.refundAmount = refundAmount;
+
+ this.refundReason = refundReason;
+
+ this.refundedAt = refundedAt;
+
+ this.createdAt = createdAt;
+
+ this.updatedAt = updatedAt;
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/domain/entities/User.js b/server/src/domain/entities/User.js
new file mode 100644
index 0000000000..9a54b28402
--- /dev/null
+++ b/server/src/domain/entities/User.js
@@ -0,0 +1,50 @@
+export class UserEntity {
+ constructor({
+ id,
+ fullName,
+ email,
+ phone,
+ password,
+ googleId = null,
+ role,
+ isVerified = false,
+ isOtpVerified = false,
+ profileImage = {
+ publicId: "",
+ url: ""
+ },
+ isBlocked = false,
+ isDeleted = false,
+ wishlist = [],
+ createdAt,
+ updatedAt,
+ refreshToken,
+ resetToken,
+ resetTokenExpiry,
+ pendingEmail = null,
+ otpCode = null,
+ otpExpiresAt = null
+ }) {
+ this.id = id;
+ this.fullName = fullName;
+ this.email = email;
+ this.phone = phone;
+ this.password = password;
+ this.googleId = googleId;
+ this.role = role;
+ this.isVerified = isVerified;
+ this.isOtpVerified = isOtpVerified;
+ this.profileImage = profileImage;
+ this.isBlocked = isBlocked;
+ this.wishlist = wishlist;
+ this.createdAt = createdAt;
+ this.updatedAt = updatedAt;
+ this.refreshToken = refreshToken;
+ this.resetToken = resetToken;
+ this.resetTokenExpiry = resetTokenExpiry;
+ this.pendingEmail = pendingEmail;
+ this.otpCode = otpCode;
+ this.otpExpiresAt = otpExpiresAt;
+ this.isDeleted = isDeleted;
+ }
+}
diff --git a/server/src/domain/entities/Vendor.js b/server/src/domain/entities/Vendor.js
new file mode 100644
index 0000000000..86a7a04fac
--- /dev/null
+++ b/server/src/domain/entities/Vendor.js
@@ -0,0 +1,49 @@
+import { VendorApprovalStatus } from "../enums/VendorApprovalStatus.enum.js";
+
+export class VendorEntity {
+ constructor({
+ id,
+ fullName,
+ email,
+ phone,
+ password,
+ profileImage = { publicId: "", url: "" },
+ companyName = "",
+ address = { addressLine1: "", city: "", state: "", pincode: "" },
+ bio = "",
+ role,
+ // businessName,
+ resetToken,
+ resetTokenExpiry,
+ isVerified = false,
+ createdAt,
+ updatedAt,
+ isBlocked = false,
+ isDeleted = false,
+ refreshToken = [],
+ approvalStatus = VendorApprovalStatus.PENDING,
+ rejectionReason = null,
+ }) {
+ this.id = id;
+ this.fullName = fullName;
+ this.email = email;
+ this.phone = phone;
+ this.password = password;
+ this.profileImage = profileImage;
+ this.companyName = companyName;
+ this.address = address;
+ this.bio = bio;
+ this.role = role;
+ // this.businessName = businessName;
+ this.isVerified = isVerified;
+ this.resetToken = resetToken;
+ this.resetTokenExpiry = resetTokenExpiry;
+ this.createdAt = createdAt,
+ this.updatedAt =updatedAt,
+ this.isBlocked = isBlocked;
+ this.isDeleted = isDeleted;
+ this.refreshToken = refreshToken;
+ this.approvalStatus = approvalStatus;
+ this.rejectionReason = rejectionReason;
+ }
+}
diff --git a/server/src/domain/entities/Venue.js b/server/src/domain/entities/Venue.js
new file mode 100644
index 0000000000..e745a86117
--- /dev/null
+++ b/server/src/domain/entities/Venue.js
@@ -0,0 +1,73 @@
+import { VenueStatus } from "../enums/Venue.enum.js";
+
+export class VenueEntity {
+ constructor ({
+ id,
+ name,
+ vendorId,
+ description,
+ category,
+ websiteUrl,
+ address = {
+ addressLine1: '',
+ city: '',
+ state: '',
+ country: '',
+ pincode: '',
+ phone: '',
+ googleMapLink: ''
+ },
+ seatingCapacity = 0,
+ standingCapacity = 0,
+ pricePerHour = 0,
+ pricePerDay = 0,
+ securityDeposit = 0,
+ availabilityRules = {},
+ weekendSurcharge = 0,
+ minimumBookingHours = 0,
+ amenities = [],
+ images = [],
+ license = [],
+ // status = VenueStatus.PENDING,
+ isDeleted = false,
+ rating = 0,
+ reviews = [],
+ approvalStatus = VenueStatus.PENDING,
+ isBlocked = false,
+ rejectionReason = null
+
+ } = {}) {
+ this.id = id;
+ this.vendorId = vendorId;
+ this.name = name;
+ this.description = description;
+ this.category = category;
+ this.websiteUrl = websiteUrl;
+ this.address = address;
+ // this.addressLine1 = addressLine1;
+ // this.city = city;
+ // this.state = state;
+ // this.country = country;
+ // this.pincode = pincode;
+ // this.phone = phone;
+ // this.googleMapLink = googleMapLink;
+ this.seatingCapacity = seatingCapacity;
+ this.standingCapacity = standingCapacity;
+ this.pricePerDay = pricePerDay;
+ this.pricePerHour = pricePerHour;
+ this.securityDeposit = securityDeposit;
+ this.weekendSurcharge = weekendSurcharge;
+ this.minimumBookingHours = minimumBookingHours;
+ this.availabilityRules = availabilityRules;
+ this.amenities = amenities;
+ this.images = images;
+ this.license = license;
+ // this.status = status;
+ this.rating = rating;
+ this.reviews = reviews;
+ this.isDeleted = isDeleted;
+ this.approvalStatus = approvalStatus,
+ this.isBlocked = isBlocked,
+ this.rejectionReason = rejectionReason
+ }
+}
\ No newline at end of file
diff --git a/server/src/domain/enums/.gitkeep b/server/src/domain/enums/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/domain/enums/Booking.enum.js b/server/src/domain/enums/Booking.enum.js
new file mode 100644
index 0000000000..b216486fbd
--- /dev/null
+++ b/server/src/domain/enums/Booking.enum.js
@@ -0,0 +1,14 @@
+export const BookingStatus = Object.freeze({
+ PENDING: "pending",
+ CONFIRMED: "confirmed",
+ CANCELLED: "cancelled",
+ COMPLETED: "completed",
+ REJECTED:"rejected"
+
+ });
+
+
+ export const BookingType = Object.freeze({
+ HOURLY: "HOURLY",
+ FULL_DAY: "FULL_DAY"
+});
\ No newline at end of file
diff --git a/server/src/domain/enums/Payment.enum.js b/server/src/domain/enums/Payment.enum.js
new file mode 100644
index 0000000000..7b8ad2fcfa
--- /dev/null
+++ b/server/src/domain/enums/Payment.enum.js
@@ -0,0 +1,8 @@
+export const PaymentStatus = Object.freeze({
+ PENDING: "pending",
+ PARTIAL: "partial",
+ PAID: "paid",
+ FAILED: "failed",
+ SUCCESS:"success",
+ REFUNDED: "refunded",
+ });
\ No newline at end of file
diff --git a/server/src/domain/enums/PaymentMethod.enum.js b/server/src/domain/enums/PaymentMethod.enum.js
new file mode 100644
index 0000000000..53fcd2b759
--- /dev/null
+++ b/server/src/domain/enums/PaymentMethod.enum.js
@@ -0,0 +1,6 @@
+export const PaymentMethod = Object.freeze({
+ CASH: "CASH",
+ CARD: "card",
+ UPI: "upi",
+ NET_BANKING: "net_banking",
+ });
\ No newline at end of file
diff --git a/server/src/domain/enums/PaymentType.enum.js b/server/src/domain/enums/PaymentType.enum.js
new file mode 100644
index 0000000000..be2899bd48
--- /dev/null
+++ b/server/src/domain/enums/PaymentType.enum.js
@@ -0,0 +1,5 @@
+export const PaymentType = Object.freeze({
+ ADVANCE: "advance",
+ BALANCE: "balance",
+ FULL: "full",
+ });
\ No newline at end of file
diff --git a/server/src/domain/enums/UserRole.enum.js b/server/src/domain/enums/UserRole.enum.js
new file mode 100644
index 0000000000..c82f55bd1a
--- /dev/null
+++ b/server/src/domain/enums/UserRole.enum.js
@@ -0,0 +1,7 @@
+export const UserRole = Object.freeze({
+ CUSTOMER: "customer",
+ ADMIN: "admin",
+ VENDOR: "vendor"
+});
+
+
diff --git a/server/src/domain/enums/VendorApprovalStatus.enum.js b/server/src/domain/enums/VendorApprovalStatus.enum.js
new file mode 100644
index 0000000000..f90aa8ffb2
--- /dev/null
+++ b/server/src/domain/enums/VendorApprovalStatus.enum.js
@@ -0,0 +1,5 @@
+export const VendorApprovalStatus = {
+ PENDING: "PENDING",
+ APPROVED: "APPROVED",
+ REJECTED: "REJECTED"
+}
\ No newline at end of file
diff --git a/server/src/domain/enums/Venue.enum.js b/server/src/domain/enums/Venue.enum.js
new file mode 100644
index 0000000000..8cb451864c
--- /dev/null
+++ b/server/src/domain/enums/Venue.enum.js
@@ -0,0 +1,47 @@
+export const VenueCategory = {
+ BEACH_SIDE: 'Beach Side',
+ CONFERENCE_HALL: 'Conference Hall',
+ AUDITORIUM: 'Auditorium',
+ BANQUET_HALL: 'Banquet Hall',
+ PARTY_HALL: 'Party Hall',
+ ROOFTOP: 'Rooftop',
+ CAFE: 'Cafe',
+ FARM_HOUSE: 'Farm House',
+ PALACE: 'Palace',
+ STUDIO: 'Studio',
+ OUTDOOR_GARDEN: 'Outdoor Garden',
+ RESORT: 'Resort',
+ HOTEL: 'Hotel'
+}
+
+
+export const Amenities = {
+ WIFI: 'Wifi',
+ PARKING: 'Parking',
+ AIR_CONDITIONING: 'Air Conditioning',
+ CATERING_KITCHEN: 'Catering Kitchen',
+ SOUND_SYSTEM: 'Sound System',
+ PROJECTOR: 'Projector',
+ STAGE: 'Stage',
+ DANCE_FLOOR: 'Dance Floor',
+ OUTDOOR_AREA: 'Outdoor Area',
+ VALET_PARKING: 'Valet Parking',
+ GENERATOR_BACKUP: 'Generator Backup',
+ CCTV_SECURITY: 'CCTV Security',
+ GREEN_ROOM: 'Green Room',
+ BRIDAL_SUIT: 'Bridal Suite',
+ SWIMMING_POOL: 'Swimming Pool',
+ ELEVATOR: 'Elevator',
+ BAR_COUNTER: 'Bar Counter',
+ PHOTO_BOOTH: 'Photo Booth'
+}
+
+export const VenueStatus = {
+ ACTIVE: 'ACTIVE',
+ INACTIVE: 'INACTIVE',
+ PENDING: 'PENDING',
+ REJECTED: 'REJECTED',
+ SUSPENDED: 'SUSPENDED',
+ DRAFT: 'DRAFT',
+}
+
diff --git a/server/src/domain/errors/.gitkeep b/server/src/domain/errors/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/domain/errors/BadRequestError.js b/server/src/domain/errors/BadRequestError.js
new file mode 100644
index 0000000000..d86bed54d7
--- /dev/null
+++ b/server/src/domain/errors/BadRequestError.js
@@ -0,0 +1,9 @@
+import { AppError } from "./app.error.js";
+import { statusCode } from "../../shared/constants/enums/statusCode.js";
+export class BadRequestError extends Error {
+ constructor(message = "Bad Request") {
+ super(message);
+ this.name = "BadRequestError";
+ this.statusCode = statusCode.BAD_REQUEST;
+ }
+}
\ No newline at end of file
diff --git a/server/src/domain/errors/ConflictError.js b/server/src/domain/errors/ConflictError.js
new file mode 100644
index 0000000000..f18ab059a3
--- /dev/null
+++ b/server/src/domain/errors/ConflictError.js
@@ -0,0 +1,9 @@
+import { AppError } from "./app.error.js";
+import { statusCode } from "../../shared/constants/enums/statusCode.js";
+
+export class ConflictError extends AppError {
+ constructor(message = "Resource already exists") {
+ super(message, statusCode.CONFLICT);
+ this.name = "ConflictError";
+ }
+}
diff --git a/server/src/domain/errors/NotFoundError.js b/server/src/domain/errors/NotFoundError.js
new file mode 100644
index 0000000000..92425b0071
--- /dev/null
+++ b/server/src/domain/errors/NotFoundError.js
@@ -0,0 +1,9 @@
+import { AppError } from "./app.error.js";
+import { statusCode } from "../../shared/constants/enums/statusCode.js";
+
+export class NotFoundError extends AppError {
+ constructor(message = "Resource not found") {
+ super(message, statusCode.NOT_FOUND);
+ this.name = "NotFoundError";
+ }
+}
diff --git a/server/src/domain/errors/UnauthorizedError.js b/server/src/domain/errors/UnauthorizedError.js
new file mode 100644
index 0000000000..cd8c0b3b28
--- /dev/null
+++ b/server/src/domain/errors/UnauthorizedError.js
@@ -0,0 +1,9 @@
+import { AppError } from "./app.error.js";
+import { statusCode } from "../../shared/constants/enums/statusCode.js";
+
+export class UnauthorizedError extends AppError {
+ constructor(message = "Unauthorized") {
+ super(message, statusCode.UNAUTHORIZED);
+ this.name = "UnauthorizedError";
+ }
+}
diff --git a/server/src/domain/errors/ValidationError.js b/server/src/domain/errors/ValidationError.js
new file mode 100644
index 0000000000..1b3a331e8d
--- /dev/null
+++ b/server/src/domain/errors/ValidationError.js
@@ -0,0 +1,9 @@
+import { AppError } from "./app.error.js";
+import { statusCode } from "../../shared/constants/enums/statusCode.js";
+
+export class ValidationError extends AppError {
+ constructor(message = "Validation failed") {
+ super(message, statusCode.BAD_REQUEST);
+ this.name = "ValidationError";
+ }
+}
diff --git a/server/src/domain/errors/app.error.js b/server/src/domain/errors/app.error.js
new file mode 100644
index 0000000000..d23d4477c2
--- /dev/null
+++ b/server/src/domain/errors/app.error.js
@@ -0,0 +1,11 @@
+export class AppError extends Error {
+ statusCode;
+ constructor(message, statusCode) {
+ super(message)
+ this.statusCode = statusCode
+ this.name = 'AppError'
+
+ Object.setPrototypeOf(this, AppError.prototype)
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/domain/errors/forbidden.error.js b/server/src/domain/errors/forbidden.error.js
new file mode 100644
index 0000000000..53a7407077
--- /dev/null
+++ b/server/src/domain/errors/forbidden.error.js
@@ -0,0 +1,10 @@
+import { statusCode } from "../../shared/constants/enums/statusCode.js";
+import { authMessages } from "../../shared/constants/messages/authMessages.js";
+import { AppError } from "./app.error.js";
+
+export class ForbiddenError extends AppError {
+ constructor(message = authMessages.error.UNAUTHORIZED) {
+ super(message, statusCode.FORBIDDEN);
+ this.name = "ForbiddednError";
+ }
+}
diff --git a/server/src/domain/repositories/.gitkeep b/server/src/domain/repositories/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/domain/repositories/IAdmin.repository.js b/server/src/domain/repositories/IAdmin.repository.js
new file mode 100644
index 0000000000..1949d25f11
--- /dev/null
+++ b/server/src/domain/repositories/IAdmin.repository.js
@@ -0,0 +1,40 @@
+export class IAdminRepository {
+ async create(data) {
+ throw new Error('Method not implemented');
+ }
+
+ async findById(id) {
+ throw new Error('Method not implemented');
+ }
+
+ async findAll() {
+ throw new Error('Method not implemented');
+ }
+
+ async update(id, data) {
+ throw new Error('Method not implemented');
+ }
+
+ async delete(id) {
+ throw new Error('Method not implemented');
+ }
+
+ async softDelete(id) {
+ throw new Error('Method not implemented');
+ }
+
+ async findByEmail(email) {
+ throw new Error('Method not implemented');
+ }
+
+ async updateRefreshToken(adminId, refreshToken){
+ throw new Error("Method not implemented")
+ }
+
+ async clearRefreshToken(token){
+ throw new Error("Method not implemented")
+ }
+ async getDashboardStatistics() {
+ throw new Error("Method not implemented.");
+ }
+}
\ No newline at end of file
diff --git a/server/src/domain/repositories/IBooking.repository.js b/server/src/domain/repositories/IBooking.repository.js
new file mode 100644
index 0000000000..5f37d04271
--- /dev/null
+++ b/server/src/domain/repositories/IBooking.repository.js
@@ -0,0 +1,73 @@
+export class BookingRepository {
+ async create(booking) {
+ throw new Error("Method not implemented");
+ }
+
+ async findById(id) {
+ throw new Error("Method not implemented");
+ }
+
+ async findByUserId(userId) {
+ throw new Error("Method not implemented");
+ }
+
+ async findByOwnerId(vendorId, filters) {
+ throw new Error("Method not implemented");
+ }
+
+ async findByVenueAndDate(venueId, bookingDate) {
+ throw new Error("Method not implemented");
+ }
+
+ async update(id, booking) {
+ throw new Error("Method not implemented");
+ }
+ async findAllFiltered(query={}) {
+ throw new Error("Method not implemented");
+ }
+ async getBookingStatistics() {
+ throw new Error("Method not implemented");
+ }
+
+ async countByOwnerId(vendorId) {
+ throw new Error("Method not implemented")
+ }
+
+ async countByOwnerIdAndStatus(vendorId, status) {
+ throw new Error("Method not implemented")
+ }
+
+ async getTopVenues(vendorId) {
+ throw new Error("Method not implemented");
+ }
+
+ async getRecentBookings(vendorId) {
+ throw new Error("Method not implemented");
+ }
+ async hasOverlappingBooking(
+ venueId,
+ bookingDate,
+ startTime,
+ endTime
+ ) {
+ throw new Error("Method not implemented.");
+ }
+
+ async getUserBookings(userId){
+ throw new Error("method not implemented")
+ }
+
+ async getUserBookingById(userId,bookingId){
+ throw new Error("method not implemented")
+ }
+ async getBookingsForPaymentReminder(date) {
+ throw new Error("Method not implemented");
+ }
+ async cancelBooking(
+ bookingId,
+ status,
+ cancellationReason
+ ) {
+ throw new Error("Method not implemented");
+ }
+ }
\ No newline at end of file
diff --git a/server/src/domain/repositories/IPayment.repository.js b/server/src/domain/repositories/IPayment.repository.js
new file mode 100644
index 0000000000..052d42737a
--- /dev/null
+++ b/server/src/domain/repositories/IPayment.repository.js
@@ -0,0 +1,29 @@
+export class IPaymentRepository {
+ async create(payment) {
+ throw new Error("Method not implemented");
+ }
+
+ async findById(paymentId) {
+ throw new Error("Method not implemented");
+ }
+
+ async findByBookingId(bookingId) {
+ throw new Error("Method not implemented");
+ }
+
+ async findByBookingIdAndType(bookingId, paymentType) {
+ throw new Error("Method not implemented");
+ }
+
+ async update(paymentId, data) {
+ throw new Error("Method not implemented");
+ }
+
+ async findAllFiltered(query = {}) {
+ throw new Error("Method not implemented");
+ }
+
+ async getPaymentStatistics() {
+ throw new Error("Method not implemented");
+ }
+ }
\ No newline at end of file
diff --git a/server/src/domain/repositories/IUser.repository.js b/server/src/domain/repositories/IUser.repository.js
new file mode 100644
index 0000000000..62f5e43e03
--- /dev/null
+++ b/server/src/domain/repositories/IUser.repository.js
@@ -0,0 +1,105 @@
+export class IUserRepository {
+
+ async findById(id) {
+ throw new Error("Method not implemented");
+ }
+
+ async findByResetToken(token) {
+ throw new Error("Method not implemented");
+ }
+
+ async findAllFiltered(query = {}) {
+ throw new Error("Method not implemented");
+ }
+
+ async updatePassword(id, password){
+ throw new Error("Methos not implemented");
+ }
+
+ // Admin block/unblock user
+ async updateBlockStatus(id, isBlocked) {
+ throw new Error("Method not implemented");
+ }
+
+ async create(data) {
+ throw new Error("Method not implemented");
+ }
+
+ async update(id, data) {
+ throw new Error("Method not implemented");
+ }
+
+ async verifyOtp(userId) {
+ throw new Error('Method not implemented');
+ }
+
+ async delete(id) {
+ throw new Error("Method not implemented");
+ }
+
+ async softDelete(id) {
+ throw new Error("Method not implemented");
+ }
+
+ // includeOtp needed for VerifyOtpUseCase
+ async findByEmail(email, includePassword = false, includeOtp = false) {
+ throw new Error('Method not implemented');
+ }
+
+ // async findByPhone(phone) {
+ // throw new Error("Method not implemented");
+ // }
+
+ async findByRefreshToken(refreshToken) {
+ throw new Error('Method not implemented');
+ }
+
+ async updateRefreshToken(userId, refreshToken) {
+ throw new Error("Method not implemented");
+ }
+
+ async clearRefreshToken(token) {
+ throw new Error('Method not implemented');
+ }
+
+ async findByGoogleId(googleId) {
+ throw new Error('Method not implemented');
+ }
+
+ // user profile
+ async saveEmailChangeOtp(userId, pendingEmail, otpCode, otpExpiresAt) {
+ throw new Error("Method not implemented");
+ }
+
+ async updateEmailAfterVerification(userId) {
+ throw new Error("Method not implemented");
+ }
+
+ async findByIdWithOtp(userId) {
+ throw new Error("Method not implemented");
+ }
+
+ async clearEmailChangeOtp(userId) {
+ throw new Error("Method not implemented");
+ }
+
+ async addToWishlist(userId, venueId) {
+ throw new Error("Method not implemented");
+ }
+
+ async getWishlist(userId) {
+ throw new Error("Method not implemented");
+ }
+
+ async removeWishlist(userId, venueId) {
+ throw new Error("Method not implemented");
+ }
+
+ async updateProfileImage(userId, profileImage) {
+ throw new Error("Method not implemented");
+ }
+
+ async removeProfileImage(userId) {
+ throw new Error("Method not implemented");
+ }
+}
diff --git a/server/src/domain/repositories/IVendor.repository.js b/server/src/domain/repositories/IVendor.repository.js
new file mode 100644
index 0000000000..d07e52b28e
--- /dev/null
+++ b/server/src/domain/repositories/IVendor.repository.js
@@ -0,0 +1,75 @@
+export class IVendorRepository {
+ async create(data) {
+ throw new Error('Method not implemented');
+ }
+
+ async findById(id) {
+ throw new Error('Method not implemented');
+ }
+
+ async findByResetToken(token) {
+ throw new Error("Method not implemented");
+ }
+
+ async findAll() {
+ throw new Error('Method not implemented');
+ }
+
+ async findAllFiltered(query={}) {
+ throw new Error('Method not implemented');
+ }
+
+ async updatePassword(id, password) {
+ throw new Error("Method not implemented");
+ }
+
+ async verifyOtp(vendorId) {
+ throw new Error('Method not implemented');
+ }
+
+ async approveVendor(vendorId){
+ throw new Error('Method not implemented');
+ }
+
+ async rejectVendor(vendorId,reason){
+ throw new Error('Method not implemented');
+ }
+
+
+ //Admin block/unblock vendor
+ async updateBlockStatus(vendorId, isBlocked) {
+ throw new Error('Method not implemented');
+ }
+
+ async update(id, data) {
+ throw new Error('Method not implemented');
+ }
+
+ async delete(id) {
+ throw new Error('Method not implemented');
+ }
+
+ async softDelete(id) {
+ throw new Error('Method not implemented');
+ }
+
+ async findByEmail(email, includePassword = false) {
+ throw new Error('Method not implemented');
+ }
+
+ async findByPhone(phone) {
+ throw new Error('Method not implemented');
+ }
+
+ async findByRefreshToken(refreshToken) {
+ throw new Error('Method not implemented');
+ }
+
+ async updateRefreshToken(vendorId, refreshToken) {
+ throw new Error('Method not implemented');
+ }
+
+ async clearRefreshToken(token) {
+ throw new Error("Method not implemented")
+ }
+}
\ No newline at end of file
diff --git a/server/src/domain/repositories/IVenue.repository.js b/server/src/domain/repositories/IVenue.repository.js
new file mode 100644
index 0000000000..a4c5a999fc
--- /dev/null
+++ b/server/src/domain/repositories/IVenue.repository.js
@@ -0,0 +1,53 @@
+export class IVenueRepository {
+ async create(venue){
+ throw new Error('Method not implemnted')
+ }
+ async findById(id){
+ throw new Error('Method not implemented')
+ }
+ async update(id, data){
+ throw new Error('Method not implemented')
+ }
+ async findByVendorAndName(query = {}){
+ throw new Error('Method not implemented')
+ }
+ async delete(id){
+ throw new Error('Method not implemented')
+ }
+ async findAllFiltered(query = {}){
+ throw new Error('Method not implemented')
+ }
+ async findTopVenues(){
+ throw new Error("Method not implemented")
+ }
+ async approveVenue(id){
+ throw new Error('Method not implemented')
+ }
+ async rejectVenue(id, reason){
+ throw new Error('Method not implemented')
+ }
+ async updateBlockStatus(id, isBlocked){
+ throw new Error('Method not implemented')
+ }
+ async findSimilarVenues(venueId, category){
+ throw new Error("Method Not implmented")
+ }
+
+
+
+
+
+
+
+
+ // mapToEntity(doc){
+ // throw new Error('Method not implemented')
+ // }
+ // mapToPersistence(venue){
+ // throw new Error('Method not implemented')
+ // }
+
+ async countByOwnerId(ownerId) {
+ throw new Error("Method not implemented")
+ }
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/config/.gitkeep b/server/src/infrastructure/config/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/infrastructure/config/cloudinary.config.js b/server/src/infrastructure/config/cloudinary.config.js
new file mode 100644
index 0000000000..fc3f4d89e4
--- /dev/null
+++ b/server/src/infrastructure/config/cloudinary.config.js
@@ -0,0 +1,11 @@
+import { v2 as cloudinary } from "cloudinary";
+import dotenv from "dotenv";
+
+dotenv.config();
+cloudinary.config({
+ cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
+ api_key: process.env.CLOUDINARY_API_KEY,
+ api_secret: process.env.CLOUDINARY_API_SECRET,
+});
+
+export default cloudinary;
\ No newline at end of file
diff --git a/server/src/infrastructure/config/jwt.config.js b/server/src/infrastructure/config/jwt.config.js
new file mode 100644
index 0000000000..a1304f248b
--- /dev/null
+++ b/server/src/infrastructure/config/jwt.config.js
@@ -0,0 +1,10 @@
+export const jwtConfig = {
+ accessToken: {
+ secret: process.env.JWT_ACCESS_SECRET,
+ expiresIn: Number(process.env.ACCESS_TOKEN_TTL)
+ },
+ refreshToken: {
+ secret: process.env.JWT_REFRESH_SECRET,
+ expiresIn: Number(process.env.REFRESH_TOKEN_TTL)
+ }
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/config/mail.config.js b/server/src/infrastructure/config/mail.config.js
new file mode 100644
index 0000000000..24e78fd155
--- /dev/null
+++ b/server/src/infrastructure/config/mail.config.js
@@ -0,0 +1,12 @@
+import dotenv from "dotenv";
+dotenv.config();
+
+import nodemailer from "nodemailer";
+
+export const transporter = nodemailer.createTransport({
+ service: "gmail",
+ auth: {
+ user: process.env.EMAIL_USER,
+ pass: process.env.EMAIL_PASS
+ }
+});
\ No newline at end of file
diff --git a/server/src/infrastructure/config/mongo.config.js b/server/src/infrastructure/config/mongo.config.js
new file mode 100644
index 0000000000..3ba9e7d560
--- /dev/null
+++ b/server/src/infrastructure/config/mongo.config.js
@@ -0,0 +1,11 @@
+import mongoose from 'mongoose'
+
+export const connectDB =async () => {
+ try {
+ await mongoose.connect(process.env.MONGODB_URI)
+ console.log('Database connected')
+ } catch (error) {
+ console.log('Database connection failed', error)
+ process.exit(1)
+ }
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/config/passport.config.js b/server/src/infrastructure/config/passport.config.js
new file mode 100644
index 0000000000..2fbbe5fb4a
--- /dev/null
+++ b/server/src/infrastructure/config/passport.config.js
@@ -0,0 +1,27 @@
+import passport from 'passport';
+import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
+import { UserRepository } from '../repositories/user.repository.js';
+import GoogleAuthUseCase from '../../application/user/usecases/GoogleAuthUseCase.js';
+
+const userRepository = new UserRepository();
+const googleAuthUseCase = new GoogleAuthUseCase(userRepository);
+
+passport.use(
+ new GoogleStrategy(
+ {
+ clientID: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ callbackURL: process.env.GOOGLE_CALLBACK_URL
+ },
+ async (accessToken, refreshToken, profile, done) => {
+ try {
+ const user = await googleAuthUseCase.execute(profile);
+ return done(null, user);
+ } catch (error) {
+ return done(error, null);
+ }
+ }
+ )
+);
+
+export default passport;
diff --git a/server/src/infrastructure/config/redis.config.js b/server/src/infrastructure/config/redis.config.js
new file mode 100644
index 0000000000..366abe0ced
--- /dev/null
+++ b/server/src/infrastructure/config/redis.config.js
@@ -0,0 +1,11 @@
+import Redis from 'ioredis'
+
+export const redisClient = new Redis(process.env.REDIS_URL)
+
+redisClient.on("connect", () => {
+ console.log("Redis connected")
+})
+
+redisClient.on("error", (error) => {
+ console.log(error)
+})
\ No newline at end of file
diff --git a/server/src/infrastructure/database/models/Admin.model.js b/server/src/infrastructure/database/models/Admin.model.js
new file mode 100644
index 0000000000..2c35a2b833
--- /dev/null
+++ b/server/src/infrastructure/database/models/Admin.model.js
@@ -0,0 +1,39 @@
+import mongoose from "mongoose";
+import { UserRole } from "../../../domain/enums/UserRole.enum.js";
+
+
+const adminSchema = new mongoose.Schema(
+ {
+ fullName: {
+ type: String,
+ required: true
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true
+ },
+ password: {
+ type: String,
+ required: true,
+ },
+ refreshToken: {
+ type: [String],
+ default: []
+ },
+ role: {
+ type: String,
+ enum: Object.values(UserRole),
+ default: UserRole.ADMIN
+ },
+ isDeleted: {
+ type: Boolean,
+ default: false
+ }
+ },
+ {
+ timestamps: true
+ }
+);
+
+export default mongoose.model("Admin", adminSchema);
diff --git a/server/src/infrastructure/database/models/BookingModel.js b/server/src/infrastructure/database/models/BookingModel.js
new file mode 100644
index 0000000000..4c5a6d4614
--- /dev/null
+++ b/server/src/infrastructure/database/models/BookingModel.js
@@ -0,0 +1,146 @@
+import mongoose from "mongoose";
+import { BookingStatus } from "../../../domain/enums/Booking.enum.js";
+import { PaymentStatus } from "../../../domain/enums/Payment.enum.js";
+
+const bookingSchema = new mongoose.Schema(
+
+ {
+ userId: {
+
+ type: mongoose.Schema.Types.ObjectId,
+
+ ref: "User",
+
+ required: true
+
+ },
+
+ venueId: {
+
+ type: mongoose.Schema.Types.ObjectId,
+
+ ref: "Venue",
+
+ required: true
+
+ },
+
+ vendorId: {
+
+ type: mongoose.Schema.Types.ObjectId,
+
+ ref: "Vendor",
+
+ required: true
+
+ },
+
+ bookingDate: {
+
+ type: Date,
+
+ required: true
+
+ },
+
+ startTime: {
+
+ type: String,
+
+ required: true
+
+ },
+
+ endTime: {
+
+ type: String,
+
+ required: true
+
+ },
+
+ guestCount: {
+
+ type: Number,
+
+ required: true
+
+ },
+
+ totalAmount: {
+
+ type: Number,
+
+ required: true
+
+ },
+
+ advanceAmount: {
+
+ type: Number,
+
+ default: 0
+
+ },
+
+ paidAmount: {
+
+ type: Number,
+
+ default: 0
+
+ },
+
+ remainingAmount: {
+
+ type: Number,
+
+ required: true
+
+ },
+
+ status: {
+
+ type: String,
+
+ enum: Object.values(BookingStatus),
+
+ default: BookingStatus.PENDING
+
+ },
+
+ paymentStatus: {
+
+ type: String,
+
+ enum: Object.values(PaymentStatus),
+
+ default: PaymentStatus.PENDING
+
+ },
+
+ cancellationReason: {
+
+ type: String,
+
+ default: null
+
+ },
+
+ },
+
+ {
+
+ timestamps: true
+
+ }
+
+)
+
+export const BookingModel = mongoose.model(
+
+ "Booking",
+
+ bookingSchema
+
+)
\ No newline at end of file
diff --git a/server/src/infrastructure/database/models/Payment.model.js b/server/src/infrastructure/database/models/Payment.model.js
new file mode 100644
index 0000000000..bb03d582dc
--- /dev/null
+++ b/server/src/infrastructure/database/models/Payment.model.js
@@ -0,0 +1,118 @@
+import mongoose, { Schema, Types } from 'mongoose';
+import { PaymentStatus } from "../../../domain/enums/Payment.enum.js";
+import { PaymentMethod } from '../../../domain/enums/PaymentMethod.enum.js';
+import { PaymentType } from '../../../domain/enums/PaymentType.enum.js';
+
+const paymentSchema = new mongoose.Schema(
+
+ {
+
+ bookingId: {
+
+ type: mongoose.Schema.Types.ObjectId,
+
+ ref: "Booking",
+
+ required: true
+
+ },
+
+ userId: {
+
+ type: mongoose.Schema.Types.ObjectId,
+
+ ref: "User",
+
+ required: true
+
+ },
+
+ vendorId: {
+
+ type: mongoose.Schema.Types.ObjectId,
+
+ ref: "Vendor",
+
+ required: true
+
+ },
+
+ amount: {
+
+ type: Number,
+
+ required: true
+
+ },
+
+ paymentType: {
+
+ type: String,
+
+ enum: Object.values(PaymentType),
+
+ required: true
+
+ },
+
+ paymentMethod: {
+
+ type: String,
+
+ enum: Object.values(PaymentMethod),
+
+ required: true
+
+ },
+
+ paymentStatus: {
+
+ type: String,
+
+ enum: Object.values(PaymentStatus),
+
+ default: PaymentStatus.PENDING
+
+ },
+
+ refundAmount: {
+
+ type: Number,
+
+ default: 0
+
+ },
+
+ refundReason: {
+
+ type: String,
+
+ default: null
+
+ },
+
+ refundedAt: {
+
+ type: Date,
+
+ default: null
+
+ }
+
+ },
+
+ {
+
+ timestamps: true
+
+ }
+
+);
+
+export const PaymentModel = mongoose.model(
+
+ "Payment",
+
+ paymentSchema
+
+);
\ No newline at end of file
diff --git a/server/src/infrastructure/database/models/User.model.js b/server/src/infrastructure/database/models/User.model.js
new file mode 100644
index 0000000000..cf8da3c1fa
--- /dev/null
+++ b/server/src/infrastructure/database/models/User.model.js
@@ -0,0 +1,97 @@
+import mongoose from "mongoose";
+import { UserRole } from "../../../domain/enums/UserRole.enum.js";
+const userSchema = new mongoose.Schema({
+ fullName: {
+ type: String,
+ required: true
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true
+ },
+ phone: {
+ type: String,
+ unique: true,
+ sparse: true,
+ default: null
+ },
+ password: {
+ type: String,
+ required: false,
+ },
+ googleId: {
+ type: String,
+ default: null,
+ select: false
+ },
+ role: {
+ type: String,
+ enum: Object.values(UserRole),
+ default: UserRole.CUSTOMER
+ },
+ isOtpVerified: {
+ type: Boolean,
+ default: false
+ },
+ otpCode: {
+ type: String,
+ default: null,
+ select: false
+ },
+ otpExpiresAt: {
+ type: Date,
+ default: null
+ },
+ isBlocked: {
+ type: Boolean,
+ default: false
+ },
+ isDeleted: {
+ type: Boolean,
+ default: false
+ },
+ refreshToken: {
+ type: [String],
+ default: [],
+ },
+ resetToken: {
+ type: String,
+ },
+ resetTokenExpiry: {
+ type: Date,
+ },
+ isVerified: {
+ type: Boolean,
+ default: false
+ },
+ profileImage: {
+ publicId: {
+ type: String,
+ default: ""
+ },
+ url: {
+ type: String,
+ default: ""
+ }
+ },
+ pendingEmail: {
+ type: String,
+ default: null
+ },
+ isActive: {
+ type: Boolean,
+ default: true
+ },
+ wishlist: [{
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Venue",
+ }]
+},
+{
+ timestamps: true
+})
+
+
+export const UserModel = mongoose.model("User", userSchema)
+
diff --git a/server/src/infrastructure/database/models/Vendor.model.js b/server/src/infrastructure/database/models/Vendor.model.js
new file mode 100644
index 0000000000..5776c3d78e
--- /dev/null
+++ b/server/src/infrastructure/database/models/Vendor.model.js
@@ -0,0 +1,53 @@
+import mongoose from "mongoose";
+import { UserRole } from "../../../domain/enums/UserRole.enum.js";
+import { VendorApprovalStatus } from '../../../domain/enums/VendorApprovalStatus.enum.js';
+
+const vendorSchema = new mongoose.Schema(
+ {
+ fullName: { type: String, required: true },
+ email: { type: String, required: true, unique: true },
+ phone: { type: String, required: true, unique: true },
+ // businessName: { type: String, required: false },
+ password: { type: String, required: true },
+ profileImage: {
+ publicId: { type: String, default: "" },
+ url: { type: String, default: "" }
+ },
+ companyName: { type: String, default: "" },
+ address: {
+ addressLine1: { type: String, default: "" },
+ city: { type: String, default: "" },
+ state: { type: String, default: "" },
+ pincode: { type: String, default: "" }
+ },
+ bio: { type: String, default: "" },
+ role: {
+ type: String,
+ enum: Object.values(UserRole),
+ default: UserRole.VENDOR
+ },
+ isBlocked: { type: Boolean, default: false },
+ isDeleted: { type: Boolean, default: false },
+ isVerified: { type: Boolean, default: false},
+ refreshToken: {
+ type: [String],
+ default: [],
+ select: false
+ },
+ approvalStatus: {
+ type: String,
+ enum: Object.values(VendorApprovalStatus),
+ default: VendorApprovalStatus.PENDING
+ },
+ rejectionReason: { type: String, default: "" },
+ resetToken: {
+ type: String,
+ },
+ resetTokenExpiry: {
+ type: Date,
+ },
+ },
+ { timestamps: true }
+);
+
+export default mongoose.model("Vendor", vendorSchema);
diff --git a/server/src/infrastructure/database/models/Venue.model.js b/server/src/infrastructure/database/models/Venue.model.js
new file mode 100644
index 0000000000..f352bf842c
--- /dev/null
+++ b/server/src/infrastructure/database/models/Venue.model.js
@@ -0,0 +1,178 @@
+import mongoose, { Schema, Types } from 'mongoose'
+import { VenueCategory, VenueStatus } from '../../../domain/enums/Venue.enum.js'
+
+const VenueSchema = new Schema({
+ name: {
+ type: String,
+ required: true
+ },
+ description: {
+ type: String,
+ required: true
+ },
+ vendorId: {
+ type: Types.ObjectId,
+ ref: 'Vendor',
+ required: true
+ },
+ category: {
+ type: String,
+ enum: Object.values(VenueCategory),
+ required: true
+ },
+ websiteUrl: {
+ type: String,
+ required: false
+ },
+ address: {
+ addressLine1: {
+ type: String,
+ required: true
+ },
+ city: {
+ type: String,
+ required: true
+ },
+ state: {
+ type: String,
+ required: true
+ },
+ country: {
+ type: String,
+ required: true
+ },
+ pincode: {
+ type: String
+ },
+ phone: {
+ type: String,
+ required: true
+ },
+ googleMapLink: {
+ type: String
+ }
+ },
+ seatingCapacity: {
+ type: Number,
+ required: true,
+ min: 1
+ },
+ standingCapacity: {
+ type: Number,
+ required: true,
+ min: 1
+ },
+ pricePerHour: {
+ type: Number,
+ required: true,
+ min: 1
+ },
+ pricePerDay: {
+ type: Number,
+ required: true,
+ min: 1
+ },
+ availabilityRules: {
+ openTime: {
+ type: String,
+ required: true,
+ default: '08:00'
+ },
+ closeTime: {
+ type: String,
+ required: true,
+ default: '22:00'
+ },
+ closedDays: {
+ type: [String],
+ default: []
+ }
+ },
+ amenities: {
+ type: [String],
+ default: []
+ },
+ images: [{
+ publicId: {
+ type: String,
+ required: true
+ },
+ url: {
+ type: String,
+ required: true
+ }
+ }],
+ securityDeposit: {
+ type: Number,
+ default: 0,
+ min: 0
+ },
+ weekendSurcharge: {
+ type: Number,
+ default: 0,
+ min: 0
+ },
+ minimumBookingHours: {
+ type: Number,
+ default: 1
+ },
+ // status: {
+ // type: String,
+ // enum: Object.values(VenueStatus),
+ // default: VenueStatus.PENDING
+ // },
+ license: [{
+ publicId: {
+ type: String,
+ required: true
+ },
+ url: {
+ type: String,
+ required: true
+ }
+ }],
+ rating: {
+ type: Number,
+ default: 0
+ },
+ reviews: [{
+ userId: {
+ type: Types.ObjectId,
+ ref: 'User'
+ },
+ rating: {
+ type: Number,
+ default: 0,
+ min: 0,
+ max: 5
+ },
+ review: {
+ type: String
+ },
+ createdAt: {
+ type: Date,
+ default: Date.now()
+ }
+ }],
+ isDeleted: {
+ type: Boolean,
+ default: false
+ },
+ isBlocked: {
+ type: Boolean,
+ default: false
+ },
+ rejectionReason: {
+ type: String,
+ default: null
+ },
+ approvalStatus : {
+ type: String,
+ enum: Object.values(VenueStatus),
+ default: VenueStatus.PENDING,
+ }
+}, {
+ timestamps: true
+})
+
+export const VenueModel = mongoose.model('Venue', VenueSchema)
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/.gitkeep b/server/src/infrastructure/emailTemplates/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/infrastructure/emailTemplates/admin.vendorApprovalTemplate.js b/server/src/infrastructure/emailTemplates/admin.vendorApprovalTemplate.js
new file mode 100644
index 0000000000..edcba2521e
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/admin.vendorApprovalTemplate.js
@@ -0,0 +1,16 @@
+export const vendorApprovalTemplate = ({ vendorName }) => {
+ return {
+ subject: "Vendor Account Approved",
+ html: `
+
+
Hello ${vendorName},
+
Congratulations! 🎉
+
Your vendor account has been approved by the admin.
+
You can now log in and continue using the platform as an approved vendor.
+
+
Best regards,
+
BookMyVenue Team
+
+ `
+ };
+};
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/admin.vendorRejectionTemplate.js b/server/src/infrastructure/emailTemplates/admin.vendorRejectionTemplate.js
new file mode 100644
index 0000000000..5eb643ef84
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/admin.vendorRejectionTemplate.js
@@ -0,0 +1,16 @@
+export const vendorRejectionTemplate = ({ vendorName, reason }) => {
+ return {
+ subject: "Vendor Application Rejected",
+ html: `
+
+
Hello ${vendorName},
+
We regret to inform you that your vendor application has been rejected.
+
Reason: ${reason}
+
Please review the reason and resubmit your application if applicable.
+
+
Best regards,
+
BookMyVenue Team
+
+ `
+ };
+};
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/admin.venueApprovalTemplate.js b/server/src/infrastructure/emailTemplates/admin.venueApprovalTemplate.js
new file mode 100644
index 0000000000..8d12016153
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/admin.venueApprovalTemplate.js
@@ -0,0 +1,40 @@
+export const adminVenueApprovalTemplate = ({
+
+ vendorName,
+
+ venueName
+
+}) => ({
+
+ subject:
+ "Venue Approved",
+
+ html: `
+
+
Hello ${vendorName},
+
+
+
+ Congratulations!
+
+
+
+
+
+ Your venue
+
+ ${venueName}
+
+ has been approved successfully.
+
+
+
+
+
+ It is now visible for customers.
+
+
+
+ `
+
+});
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/admin.venueRejectionTemplate.js b/server/src/infrastructure/emailTemplates/admin.venueRejectionTemplate.js
new file mode 100644
index 0000000000..bda6746d2a
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/admin.venueRejectionTemplate.js
@@ -0,0 +1,47 @@
+export const adminVenueRejectionTemplate = ({
+
+ vendorName,
+
+ venueName,
+
+ reason
+
+}) => ({
+
+ subject:
+ "Venue Rejected",
+
+ html: `
+
+
Hello ${vendorName},
+
+
+
+ Unfortunately,
+
+ your venue
+
+ ${venueName}
+
+ has been rejected.
+
+
+
+
+
+ Reason:
+
+ ${reason}
+
+
+
+
+
+ Please update your venue
+ and submit again.
+
+
+
+ `
+
+});
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/forgotPasswordTemplate.js b/server/src/infrastructure/emailTemplates/forgotPasswordTemplate.js
new file mode 100644
index 0000000000..5c33185907
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/forgotPasswordTemplate.js
@@ -0,0 +1,50 @@
+export const forgotPasswordTemplate = (fullName, resetLink) => `
+
+
+
+
+
+
+
+
+
+
+
Hi ${fullName},
+
We received a request to reset your BookMyVenue password. Click the button below to set a new password:
+
+
+
+
Or copy this link:
+
+ ${resetLink}
+
+
+
This link expires in 1 hour.
+
+
+ ⚠️ If you didn't request a password reset, please ignore this email or contact support immediately.
+
+
+
+
+
+
+`;
diff --git a/server/src/infrastructure/emailTemplates/user.bookingCancellationTemplate.js b/server/src/infrastructure/emailTemplates/user.bookingCancellationTemplate.js
new file mode 100644
index 0000000000..5683b3ac9a
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/user.bookingCancellationTemplate.js
@@ -0,0 +1,37 @@
+export const bookingCancellationTemplate = ({
+ customerName,
+ venueName,
+ bookingDate,
+ startTime,
+ endTime,
+ paidAmount,
+ refundAmount,
+ cancellationReason
+}) => {
+
+ return {
+
+ subject: "Booking Cancelled Successfully",
+
+ html: `
+
Booking Cancelled
+
+
Hello ${customerName},
+
+
Your booking has been cancelled successfully.
+
+
+ - Venue: ${venueName}
+ - Date: ${bookingDate}
+ - Time: ${startTime} - ${endTime}
+ - Paid Amount: ₹${paidAmount}
+ - Refund Amount: ₹${refundAmount}
+ - Reason: ${cancellationReason}
+
+
+
Thank you for choosing BookMyVenue.
+ `
+
+ };
+
+};
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/user.bookingConfirmationTemplate.js b/server/src/infrastructure/emailTemplates/user.bookingConfirmationTemplate.js
new file mode 100644
index 0000000000..124c5209cd
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/user.bookingConfirmationTemplate.js
@@ -0,0 +1,102 @@
+export const bookingConfirmationTemplate = ({
+ customerName,
+ venueName,
+ bookingDate,
+ startTime,
+ endTime,
+ guestCount,
+ bookingType,
+ totalAmount,
+ paidAmount,
+ remainingAmount
+}) => {
+
+ return {
+ subject: "Booking Confirmed - BookMyVenue",
+
+ html: `
+
+
+
+
+
Booking Confirmation
+
+
+
+
Booking Confirmed 🎉
+
+
Dear ${customerName},
+
+
+ Your venue booking has been confirmed successfully.
+ Thank you for choosing BookMyVenue.
+
+
+
+
+
Booking Details
+
+
+
+ | Venue |
+ ${venueName} |
+
+
+
+ | Booking Date |
+ ${bookingDate} |
+
+
+
+ | Time |
+ ${startTime} - ${endTime} |
+
+
+
+ | Booking Type |
+ ${bookingType} |
+
+
+
+ | Guests |
+ ${guestCount} |
+
+
+
+ | Total Amount |
+ ₹${totalAmount} |
+
+
+
+ | Paid Amount |
+ ₹${paidAmount} |
+
+
+
+ | Remaining Amount |
+ ₹${remainingAmount} |
+
+
+
+
+
+
+ Please keep this email for your reference.
+
+
+
+ We look forward to hosting your event.
+
+
+
+
+
+ Regards,
+ BookMyVenue Team
+
+
+
+
+ `
+ };
+};
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/user.emailChangeOtpTemplate.js b/server/src/infrastructure/emailTemplates/user.emailChangeOtpTemplate.js
new file mode 100644
index 0000000000..e1aa85be18
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/user.emailChangeOtpTemplate.js
@@ -0,0 +1,61 @@
+export const emailChangeOtpTemplate = (fullName, otpCode) => `
+
+
+
+
+
+
+
+
+
+
+
+
Hi ${fullName},
+
+
+ We received a request to change the email address associated with your
+ BookMyVenue account.
+
+
+
+ Please verify this request by entering the OTP below:
+
+
+
+
${otpCode}
+
⏱️ Expires in 10 minutes
+
+
+
How to verify:
+
+ 1. Copy the OTP above
+ 2. Enter it in the email verification screen
+ 3. Confirm your new email address
+
+
+
+ ⚠️ If you did not request to change your email address, please ignore this email. Your account will remain unchanged.
+
+
+
+
+
+
+
+`;
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/user.paymentReminderTemplate.js b/server/src/infrastructure/emailTemplates/user.paymentReminderTemplate.js
new file mode 100644
index 0000000000..84e6aa9a0f
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/user.paymentReminderTemplate.js
@@ -0,0 +1,98 @@
+export const paymentReminderTemplate = ({
+ customerName,
+ venueName,
+ bookingDate,
+ startTime,
+ endTime,
+ totalAmount,
+ paidAmount,
+ remainingAmount
+}) => {
+
+ return {
+
+ subject: "Payment Reminder - BookMyVenue",
+
+ html: `
+
+
+
+
+
Payment Reminder
+
+
+
+
+
+ Payment Reminder
+
+
+
Dear ${customerName},
+
+
+ This is a friendly reminder that your booking is scheduled for
+ ${bookingDate}.
+
+
+
+ According to your booking, you still have a remaining balance that
+ needs to be paid before your event.
+
+
+
+
+
Booking Details
+
+
+
+
+ | Venue |
+ ${venueName} |
+
+
+
+ | Date |
+ ${bookingDate} |
+
+
+
+ | Time |
+ ${startTime} - ${endTime} |
+
+
+
+ | Total Amount |
+ ₹${totalAmount} |
+
+
+
+ | Paid Amount |
+ ₹${paidAmount} |
+
+
+
+ | Remaining Amount |
+ ₹${remainingAmount} |
+
+
+
+
+
+
+
+ Kindly complete the remaining payment before your booking date to avoid any inconvenience.
+
+
+
+
+
+ Thank you for choosing BookMyVenue.
+
+
+
+
+ `
+
+ };
+
+};
\ No newline at end of file
diff --git a/server/src/infrastructure/emailTemplates/verifyRegisterOtpTemplate.js b/server/src/infrastructure/emailTemplates/verifyRegisterOtpTemplate.js
new file mode 100644
index 0000000000..3400dd0a22
--- /dev/null
+++ b/server/src/infrastructure/emailTemplates/verifyRegisterOtpTemplate.js
@@ -0,0 +1,47 @@
+export const VerifyRegisterotpTemplate = (fullName, otpCode) => `
+
+
+
+
+
+
+
+
+
+
+
Hi ${fullName},
+
Thank you for signing up! To complete your registration, please verify your email address using the OTP code below:
+
+
+
${otpCode}
+
⏱️ Expires in 2 minutes
+
+
+
How to use:
+
1. Copy the code above
2. Enter it in the verification field
3. Complete your registration
+
+
+ ⚠️ Do not share this code with anyone. We will never ask for this code.
+
+
+
+
+
+
+`;
diff --git a/server/src/infrastructure/repositories/.gitkeep b/server/src/infrastructure/repositories/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/infrastructure/repositories/admin.repository.js b/server/src/infrastructure/repositories/admin.repository.js
new file mode 100644
index 0000000000..2dc3d58d4f
--- /dev/null
+++ b/server/src/infrastructure/repositories/admin.repository.js
@@ -0,0 +1,273 @@
+import { AdminMapper } from "../../application/mapper/Admin.mapper.js";
+import { IAdminRepository } from "../../domain/repositories/IAdmin.repository.js";
+import AdminModel from "../database/models/Admin.model.js";
+import { UserModel } from "../database/models/User.model.js";
+import { VenueModel } from "../database/models/Venue.model.js";
+import VendorModel from "../database/models/Vendor.model.js";
+import { BookingModel } from "../database/models/BookingModel.js";
+import { PaymentModel } from "../database/models/Payment.model.js";
+import { PaymentStatus } from "../../domain/enums/Payment.enum.js";
+import { VendorApprovalStatus } from "../../domain/enums/VendorApprovalStatus.enum.js";
+import { VenueStatus } from "../../domain/enums/Venue.enum.js";
+
+
+export class AdminRepository extends IAdminRepository {
+
+ async create(admin) {
+ const data = AdminMapper.mapToPersistence(admin);
+
+ const document = await AdminModel.create(data);
+
+ return AdminMapper.mapToEntity(document);
+ }
+
+ async findById(id) {
+ const document = await AdminModel.findById(id);
+
+ if (!document) return null;
+
+ return AdminMapper.mapToEntity(document);
+ }
+
+ async findByEmail(email) {
+ let document = await AdminModel.findOne({
+ email,
+ isDeleted: false,
+ });
+
+ if (!document) return null;
+
+ return AdminMapper.mapToEntity(document);
+ }
+
+ async findAll() {
+ const documents = await AdminModel.find({
+ isDeleted: false,
+ }).sort({ createdAt: -1 });
+
+ return documents.map((doc) =>
+ AdminMapper.mapToEntity(doc)
+ );
+ }
+
+ async update(id, admin) {
+ const data = AdminMapper.mapToPersistence(admin);
+
+ const document = await AdminModel.findByIdAndUpdate(
+ id,
+ { $set: data },
+ { new: true }
+ );
+
+ if (!document) return null;
+
+ return AdminMapper.mapToEntity(document);
+ }
+
+ async softDelete(id) {
+ const document = await AdminModel.findByIdAndUpdate(
+ id,
+ {
+ isDeleted: true,
+ },
+ {
+ new: true,
+ }
+ );
+
+ if (!document) return null;
+
+ return AdminMapper.mapToEntity(document);
+ }
+
+ async delete(id) {
+ return await AdminModel.findByIdAndDelete(id);
+ }
+
+ async updateRefreshToken(adminId, refreshToken) {
+ const doc = await AdminModel.findByIdAndUpdate(
+ adminId,
+ { $push: {refreshToken} },
+ { new: true }
+ );
+ if (!doc) return null;
+ return AdminMapper.mapToEntity(doc);
+ }
+
+ async clearRefreshToken(token) {
+ await AdminModel.findOneAndUpdate(
+ {refreshToken: token},
+ { $pull: {refreshToken: token } },
+ { new: true }
+ );
+ }
+ //Admin dashboard
+ async getDashboardStatistics() {
+
+ const [
+
+ totalUsers,
+
+ totalVendors,
+
+ pendingVendorApprovals,
+
+ totalVenues,
+
+ pendingVenueApprovals,
+
+ totalBookings,
+
+ ] = await Promise.all([
+
+ UserModel.countDocuments(),
+
+ VendorModel.countDocuments(),
+
+ VendorModel.countDocuments({
+ approvalStatus: VendorApprovalStatus.PENDING,
+ }),
+
+ VenueModel.countDocuments(),
+
+ VenueModel.countDocuments({
+ approvalStatus: VenueStatus.PENDING,
+ }),
+
+ BookingModel.countDocuments(),
+
+ ]);
+
+ const revenue = await PaymentModel.aggregate([
+
+ {
+ $match: {
+ paymentStatus: PaymentStatus.SUCCESS,
+ },
+ },
+
+ {
+ $group: {
+ _id: null,
+ totalRevenue: {
+ $sum: "$amount",
+ },
+ },
+ },
+
+ ]);
+
+ const bookingOverview = await BookingModel.aggregate([
+ {
+ $group: {
+ _id: {
+ month: { $month: {
+ $toDate: "$createdAt"
+ } },
+ },
+ bookings: {
+ $sum: 1,
+ },
+ },
+ },
+ {
+ $sort: {
+ "_id.month": 1,
+ },
+ },
+]);
+/*const months = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+];
+
+const formattedBookingOverview = bookingOverview.map((item) => ({
+ month: months[item._id.month - 1],
+ bookings: item.bookings,
+}));*/
+const formattedBookingOverview = bookingOverview.map((item) => ({
+ month: item._id.month,
+ bookings: item.bookings,
+}));
+
+const revenueOverview = await PaymentModel.aggregate([
+ {
+ $match: {
+ paymentStatus: PaymentStatus.SUCCESS,
+
+ },
+ },
+
+ {
+ $group: {
+ _id: {
+ month: {
+ $month: {
+ $toDate: "$createdAt"
+ }
+ }
+ },
+ revenue: {
+ $sum: "$amount"
+ }
+ }
+},
+ {
+ $sort: {
+ "_id.month": 1,
+ },
+ },
+]);
+
+/*const formattedRevenueOverview = revenueOverview.map((item) => ({
+ month: months[item._id.month - 1],
+ revenue: item.revenue,
+}));*/
+
+const formattedRevenueOverview = revenueOverview.map((item) => ({
+ month: item._id.month,
+ revenue: item.revenue,
+}));
+
+ return {
+
+ summary: {
+
+ totalUsers,
+
+ totalVendors,
+
+ pendingVendorApprovals,
+
+ totalVenues,
+
+ pendingVenueApprovals,
+
+ totalBookings,
+
+ totalRevenue:
+ revenue.length > 0
+ ? revenue[0].totalRevenue
+ : 0,
+
+ },
+ "bookingOverview": formattedBookingOverview,
+ "revenueOverview": formattedRevenueOverview,
+
+ };
+
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/repositories/booking.repository.js b/server/src/infrastructure/repositories/booking.repository.js
new file mode 100644
index 0000000000..a0cd7a1f9c
--- /dev/null
+++ b/server/src/infrastructure/repositories/booking.repository.js
@@ -0,0 +1,538 @@
+import { BookingRepository } from "../../domain/repositories/IBooking.repository.js";
+import mongoose from "mongoose";
+import { BookingModel } from "../database/models/BookingModel.js";
+import { UserModel } from "../database/models/User.model.js";
+import { VenueModel } from "../database/models/Venue.model.js";
+import VendorModel from "../database/models/Vendor.model.js";
+import { BookingMapper } from "../../application/mapper/Booking.mapper.js";
+import { BookingStatus } from "../../domain/enums/Booking.enum.js";
+import { Types } from "mongoose";
+import { PaymentStatus } from "../../domain/enums/Payment.enum.js";
+
+
+
+export class BookingRepositoryImpl extends BookingRepository {
+
+ async create(entity) {
+
+ const doc = await BookingModel.create(
+
+ BookingMapper.mapToPersistence(entity)
+
+ );
+ return BookingMapper.mapToEntity(doc);
+ }
+
+ async findById(id) {
+ const booking =
+ await BookingModel
+ .findById(id)
+ .populate("userId", "fullName email phone")
+ .populate("vendorId", "fullName email phone companyName")
+ .populate("venueId");
+ if (!booking) return null;
+ console.log("Mongo Booking:", booking);
+ return BookingMapper.mapToEntity(booking);
+ }
+
+ async findByUserId(userId) {
+ const docs = await BookingModel.find({
+ userId,
+ });
+
+ return docs.map((doc) => BookingMapper.mapToEntity(doc));
+ }
+
+ async findByOwnerId(
+ vendorId,
+ {
+ page,
+
+ limit,
+
+ status,
+
+ search,
+ }
+ ) {
+ const filter = { vendorId };
+
+ if (status) {
+ filter.status = status;
+ }
+
+ let docs = await BookingModel.find(filter)
+
+ .populate("userId", "fullName email")
+
+ .populate("venueId", "name")
+
+ .sort({
+ createdAt: -1,
+ });
+
+ if (search) {
+ const keyword = search.trim().toLowerCase();
+
+ docs = docs.filter(
+ (doc) =>
+ doc.userId?.fullName?.toLowerCase().includes(keyword) ||
+ doc.venueId?.name?.toLowerCase().includes(keyword) ||
+ doc._id.toString().includes(keyword)
+ );
+ }
+
+ const total = docs.length;
+
+ const skip = (page - 1) * limit;
+
+ docs = docs.slice(skip, skip + Number(limit));
+
+ return {
+ bookings: docs.map((doc) => BookingMapper.mapToEntity(doc)),
+
+ pagination: {
+ total,
+
+ page: Number(page),
+
+ limit: Number(limit),
+
+ totalPages: Math.ceil(total / limit),
+ },
+ };
+ }
+
+ async findByVenueAndDate(
+ venueId,
+
+ bookingDate
+ ) {
+ const docs = await BookingModel.find({
+ venueId,
+
+ bookingDate,
+ });
+
+ return docs.map((doc) => BookingMapper.mapToEntity(doc));
+ }
+
+ async update(
+ id,
+
+ entity
+ ) {
+ const doc = await BookingModel.findByIdAndUpdate(
+ id,
+
+ BookingMapper.mapToPersistence(entity),
+
+ {
+ new: true,
+ }
+ );
+
+ return BookingMapper.mapToEntity(doc);
+ }
+
+ async countByOwnerId(vendorId) {
+ return await BookingModel.countDocuments({
+ vendorId,
+ });
+ }
+
+ async countByOwnerIdAndStatus(
+ vendorId,
+
+ status
+ ) {
+ return await BookingModel.countDocuments({
+ vendorId,
+
+ status,
+ });
+ }
+
+ async getTopVenues(vendorId) {
+ const result = await BookingModel.aggregate([
+ {
+ $match: {
+ vendorId: new mongoose.Types.ObjectId(vendorId),
+ },
+ },
+
+ {
+ $group: {
+ _id: "$venueId",
+ bookings: { $sum: 1 },
+ },
+ },
+
+ {
+ $sort: {
+ bookings: -1,
+ },
+ },
+
+ {
+ $limit: 5,
+ },
+
+ {
+ $lookup: {
+ from: "venues",
+ localField: "_id",
+ foreignField: "_id",
+ as: "venue",
+ },
+ },
+
+ {
+ $unwind: "$venue",
+ },
+
+ {
+ $project: {
+ _id: 0,
+ venueId: "$venue._id",
+ name: "$venue.name",
+ bookings: 1,
+ },
+ },
+ ]);
+
+ return result;
+ }
+
+ async getRecentBookings(vendorId) {
+ const docs = await BookingModel.find({
+ vendorId
+ })
+
+ .populate("userId", "fullName")
+
+ .populate("venueId", "name")
+
+ .sort({
+ createdAt: -1,
+ })
+
+ .limit(5);
+
+ return docs.map((doc) => ({
+ bookingId: doc._id,
+
+ customer: doc.userId?.fullName,
+
+ venue: doc.venueId?.name,
+
+ amount: doc.totalAmount,
+
+ status: doc.status,
+
+ bookingDate: doc.bookingDate,
+ }));
+ }
+
+
+ async findAllFiltered(query) {
+ const filter = {};
+ if (query.status) { filter.status = query.status; }
+ if (query.paymentStatus) { filter.paymentStatus = query.paymentStatus; }
+ if (query.search) {
+ const regex = new RegExp(query.search, "i");
+ const userIds = await UserModel.find({ fullName: regex }).distinct("_id");
+ const vendorIds = await VendorModel.find({ $or: [{ fullName: regex }, { companyName: regex }] }).distinct("_id");
+ const venueIds = await VenueModel.find({ name: regex }).distinct("_id"); const searchFilter = [{ userId: { $in: userIds } }, { vendorId: { $in: vendorIds } }, { venueId: { $in: venueIds } }];
+ if (Types.ObjectId.isValid(query.search)) { searchFilter.push({ _id: new Types.ObjectId(query.search) }); }
+ filter.$or = searchFilter;
+ }
+ const totalCount = await BookingModel.countDocuments(filter);
+ const totalPages = Math.ceil(totalCount / query.limit);
+ const data = await BookingModel.aggregate([
+ { $match: filter }, { $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" } }, { $unwind: { path: "$user", preserveNullAndEmptyArrays: true } },
+ { $lookup: { from: "vendors", localField: "vendorId", foreignField: "_id", as: "vendor" } },
+ { $unwind: { path: "$vendor", preserveNullAndEmptyArrays: true } },
+ { $lookup: { from: "venues", localField: "venueId", foreignField: "_id", as: "venue" } },
+ { $unwind: { path: "$venue", preserveNullAndEmptyArrays: true } },
+ { $sort: { bookingDate: query.sortBy === "asc" ? 1 : -1 } },
+ { $skip: (query.page - 1) * query.limit }, { $limit: query.limit }]);
+ return { data, totalCount, totalPages };
+ }
+ async getBookingStatistics() {
+
+ const result =
+ await BookingModel.aggregate([
+
+ {
+
+ $group: {
+
+ _id: null,
+
+ totalBookings: {
+ $sum: 1
+ },
+
+ pendingBookings: {
+
+ $sum: {
+
+ $cond: [
+
+ {
+ $eq: [
+ "$status",
+ BookingStatus.PENDING
+ ]
+ },
+
+ 1,
+
+ 0
+
+ ]
+
+ }
+
+ },
+
+ confirmedBookings: {
+
+ $sum: {
+
+ $cond: [
+
+ {
+ $eq: [
+ "$status",
+ BookingStatus.CONFIRMED
+ ]
+ },
+
+ 1,
+
+ 0
+
+ ]
+
+ }
+
+ },
+ rejectedBookings: {
+ $sum: {
+ $cond: [
+ { $eq: ["$status", BookingStatus.REJECTED] },
+ 1,
+ 0
+ ]
+ }
+ },
+
+ cancelledBookings: {
+
+ $sum: {
+
+ $cond: [
+
+ {
+ $eq: [
+ "$status",
+ BookingStatus.CANCELLED
+ ]
+ },
+
+ 1,
+
+ 0
+
+ ]
+
+ }
+
+ },
+
+ completedBookings: {
+
+ $sum: {
+
+ $cond: [
+
+ {
+ $eq: [
+ "$status",
+ BookingStatus.COMPLETED
+ ]
+ },
+
+ 1,
+
+ 0
+
+ ]
+
+ }
+
+ }
+
+ }
+
+ }
+
+ ]);
+
+ return result[0] || {
+
+ totalBookings: 0,
+
+ pendingBookings: 0,
+
+ confirmedBookings: 0,
+ rejectedBookings: 0,
+
+ cancelledBookings: 0,
+
+ completedBookings: 0
+
+ };
+
+ }
+async hasOverlappingBooking(
+ venueId,
+ bookingDate,
+ startTime,
+ endTime
+) {
+ console.log("=== Repository Input ===");
+ console.log({
+ venueId,
+ bookingDate,
+ startTime,
+ endTime,
+ });
+
+ const booking = await BookingModel.findOne({
+ venueId,
+ bookingDate,
+ status: {
+ $ne: BookingStatus.CANCELLED,
+ },
+ startTime: {
+ $lt: endTime,
+ },
+ endTime: {
+ $gt: startTime,
+ },
+ });
+
+ console.log("=== Booking Found ===");
+ console.log(booking);
+
+ return Boolean(booking);
+}
+ async getUserBookings(
+ userId,
+ {
+ page,
+ limit,
+ status,
+ search,
+ sortBy
+ }
+ ) {
+
+ const filter = { userId };
+
+ if (status) {
+ filter.status = status;
+ }
+
+ let docs = await BookingModel.find(filter)
+ .populate("venueId")
+ .sort({
+ bookingDate: sortBy === "asc" ? 1 : -1
+ });
+
+ if (search) {
+
+ const keyword = search.trim().toLowerCase();
+
+ docs = docs.filter(
+ (doc) =>
+ doc.venueId?.name?.toLowerCase().includes(keyword) ||
+ doc._id.toString().includes(keyword)
+ );
+
+ }
+
+ const total = docs.length;
+
+ const skip = (page - 1) * limit;
+
+ docs = docs.slice(skip, skip + Number(limit));
+
+ return {
+ bookings: docs.map((doc) =>
+ BookingMapper.mapToEntity(doc)
+ ),
+ pagination: {
+ total,
+ page: Number(page),
+ limit: Number(limit),
+ totalPages: Math.ceil(total / limit)
+ }
+ };
+
+ }
+ async getUserBookingById(userId, bookingId) {
+
+ const booking = await BookingModel
+ .findOne({
+ _id: bookingId,
+ userId
+ })
+ .populate("venueId")
+ .populate("vendorId", "fullName companyName email phone");
+
+ if (!booking) {
+ return null;
+ }
+
+ return BookingMapper.mapToEntity(booking);
+
+ }
+
+ async getBookingsForPaymentReminder(date) {
+
+ return await BookingModel.find({
+ bookingDate: date,
+ status: BookingStatus.CONFIRMED,
+ paymentStatus: PaymentStatus.PARTIAL
+ });
+
+ }
+ async cancelBooking(
+ bookingId,
+ status,
+ cancellationReason
+ ) {
+
+ const booking = await BookingModel.findByIdAndUpdate(
+ bookingId,
+ {
+ status,
+ cancellationReason
+ },
+ {
+ new: true
+ }
+ );
+
+ if (!booking) {
+ return null;
+ }
+
+ return BookingMapper.mapToEntity(booking);
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/repositories/payment.repository.js b/server/src/infrastructure/repositories/payment.repository.js
new file mode 100644
index 0000000000..a19c75465e
--- /dev/null
+++ b/server/src/infrastructure/repositories/payment.repository.js
@@ -0,0 +1,252 @@
+import { IPaymentRepository } from "../../domain/repositories/IPayment.repository.js";
+import { PaymentModel } from "../database/models/Payment.model.js";
+import { PaymentMapper } from "../../application/mapper/Payment.mapper.js";
+import { UserModel } from "../database/models/User.model.js";
+import VendorModel from "../database/models/Vendor.model.js";
+import { BookingModel } from "../database/models/BookingModel.js";
+import { Types } from "mongoose";
+import { PaymentStatus } from "../../domain/enums/Payment.enum.js";
+
+export class PaymentRepository extends IPaymentRepository {
+
+ async create(payment) {
+
+ const document = await PaymentModel.create(
+ PaymentMapper.mapToPersistence(payment)
+ );
+
+ return PaymentMapper.mapToEntity(document);
+
+}
+async findById(paymentId) {
+
+const payment = await PaymentModel.findById(paymentId)
+
+ .populate(
+ "bookingId",
+ "bookingDate startTime endTime totalAmount status paymentStatus"
+ )
+
+ .populate(
+ "userId",
+ "fullName email phone"
+ )
+
+ .populate(
+ "vendorId",
+ "fullName companyName email phone"
+ );
+ if (!payment) return null;
+
+ return PaymentMapper.mapToEntity(payment);
+
+}
+
+async findByBookingId(bookingId) {
+
+ const documents = await PaymentModel.find(bookingId);
+ if (!documents) return null;
+
+ return PaymentMapper.mapToEntity(bookingId);
+
+}
+
+async update(paymentId, data) {
+
+ const document = await PaymentModel.findByIdAndUpdate(
+
+ paymentId,
+
+ data,
+
+ {
+ new: true
+ }
+
+ );
+ if (!document) return null
+
+ return PaymentMapper.mapToEntity(document);
+
+}
+async getPaymentStatistics() {
+
+ const [
+
+ totalPayments,
+
+ successfulPayments,
+
+ pendingPayments,
+
+ failedPayments,
+
+ refundedPayments
+
+ ] = await Promise.all([
+
+ PaymentModel.countDocuments(),
+
+ PaymentModel.countDocuments({
+ paymentStatus: PaymentStatus.SUCCESS
+ }),
+
+ PaymentModel.countDocuments({
+ paymentStatus: PaymentStatus.PENDING
+ }),
+
+ PaymentModel.countDocuments({
+ paymentStatus: PaymentStatus.FAILED
+ }),
+
+ PaymentModel.countDocuments({
+ paymentStatus: PaymentStatus.REFUNDED
+ })
+
+ ]);
+
+
+ const revenue = await PaymentModel.aggregate([
+
+ {
+ $match: {
+ paymentStatus: PaymentStatus.SUCCESS
+ }
+ },
+
+ {
+ $group: {
+ _id: null,
+ totalRevenue: {
+ $sum: "$amount"
+ }
+ }
+ }
+
+ ]);
+
+ return {
+
+ totalPayments,
+
+ successfulPayments,
+
+ pendingPayments,
+
+ failedPayments,
+
+ refundedPayments,
+
+ totalRevenue:
+ revenue.length > 0
+ ? revenue[0].totalRevenue
+ : 0
+
+ };
+
+}
+async findAllFiltered(query = {}) {
+
+ const filter = {};
+
+ if (query.paymentStatus) {
+ filter.paymentStatus = query.paymentStatus;
+ }
+
+ if (query.paymentMethod) {
+ filter.paymentMethod = query.paymentMethod;
+ }
+
+ if (query.paymentType) {
+ filter.paymentType = query.paymentType;
+ }
+
+ if (query.search) {
+
+ const regex = new RegExp(query.search, "i");
+
+ const userIds = await UserModel.find({
+ fullName: regex
+ }).distinct("_id");
+
+ const vendorIds = await VendorModel.find({
+ fullName: regex
+ }).distinct("_id");
+
+ const bookingIds = await BookingModel.find().distinct("_id");
+
+ const searchFilter = [
+
+ {
+ userId: {
+ $in: userIds
+ }
+ },
+
+ {
+ vendorId: {
+ $in: vendorIds
+ }
+ }
+
+ ];
+
+ if (Types.ObjectId.isValid(query.search)) {
+
+ searchFilter.push({
+
+ _id: new Types.ObjectId(query.search)
+
+ });
+
+ searchFilter.push({
+
+ bookingId: new Types.ObjectId(query.search)
+
+ });
+
+ }
+
+ filter.$or = searchFilter;
+
+ }
+
+ const totalCount =
+ await PaymentModel.countDocuments(filter);
+
+ const totalPages =
+ Math.ceil(totalCount / query.limit);
+
+ const documents =
+ await PaymentModel.find(filter)
+
+ .populate("bookingId")
+
+ .populate("userId", "fullName email")
+
+ .populate("vendorId", "fullName companyName")
+
+ .sort({
+ createdAt:
+ query.sortBy === "asc"
+ ? 1
+ : -1
+ })
+
+ .skip((query.page - 1) * query.limit)
+
+ .limit(query.limit);
+
+ return {
+
+ data: documents,
+
+ totalCount,
+
+ totalPages
+
+ };
+
+}
+
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/repositories/user.repository.js b/server/src/infrastructure/repositories/user.repository.js
new file mode 100644
index 0000000000..bd79430332
--- /dev/null
+++ b/server/src/infrastructure/repositories/user.repository.js
@@ -0,0 +1,343 @@
+import { UserMapper } from "../../application/mapper/User.mapper.js";
+import { IUserRepository } from "../../domain/repositories/IUser.repository.js";
+import { UserModel } from "../database/models/User.model.js";
+
+export class UserRepository extends IUserRepository {
+ async findById(id) {
+ const document = await UserModel.findById(id);
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async findByResetToken(token){
+ const document = await UserModel.findOne({resetToken: token})
+ if(!document) return null
+ return UserMapper.mapToEntity(document)
+ }
+
+ async findAllFiltered(query = {}) {
+ const filter = {};
+
+ if (query.search) {
+ filter.$or = [
+ {
+ fullName: {
+ $regex: query.search,
+ $options: "i",
+ },
+ },
+ {
+ email: {
+ $regex: query.search,
+ $options: "i",
+ },
+ },
+ ];
+ }
+ if (query.isBlocked !== undefined) {
+ filter.isBlocked = query.isBlocked === "true";
+ }
+
+ const page = query.page;
+ const limit = query.limit;
+
+ const skip = limit * (page - 1);
+
+ const totalCount = await UserModel.countDocuments(filter);
+
+ const totalPages = Math.ceil(totalCount / limit);
+
+ const documents = await UserModel.find(filter)
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(limit);
+
+ return {
+ data: documents.map((doc) => UserMapper.mapToEntity(doc)),
+ totalCount,
+ totalPages,
+ };
+ }
+
+ async updatePassword(id, hashedPassword) {
+ await UserModel.findByIdAndUpdate(id, {
+ password: hashedPassword,
+ });
+ }
+
+ async updateBlockStatus(id, isBlocked) {
+ const document = await UserModel.findByIdAndUpdate(
+ id,
+ {
+ isBlocked,
+ },
+ {
+ new: true,
+ }
+ );
+
+ if (!document) return null;
+ console.log(document);
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async create(user) {
+ const data = UserMapper.mapToPersistence(user);
+
+ const document = await UserModel.create(data);
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async findByEmail(email) {
+ let document = await UserModel.findOne({
+ email,
+ isDeleted: { $ne: true },
+ });
+
+ if (!document) return null;
+
+ // console.log('from repo: ', document)
+ return UserMapper.mapToEntity(document);
+ }
+
+ async verifyOtp(userId) {
+ const document = await UserModel.findByIdAndUpdate(
+ userId,
+ {
+ isOtpVerified: true,
+ },
+ {
+ new: true,
+ }
+ );
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ // async findByPhone(phone) {
+ // const document = await UserModel.findOne({
+ // phone,
+ // isDeleted: { $ne: true }
+ // });
+
+ // if (!document) return null;
+
+ // return UserMapper.mapToEntity(document);
+ // }
+
+ async update(id, user) {
+ const data = UserMapper.mapToPersistence(user);
+
+ const document = await UserModel.findByIdAndUpdate(
+ id,
+ { $set: data },
+ { new: true }
+ );
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+ //--
+ async findByRefreshToken(refreshToken) {
+ const document = await UserModel.findOne({
+ refreshToken,
+ isDeleted: { $ne: true },
+ }).select("+password");
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async updateRefreshToken(userId, refreshToken) {
+ const document = await UserModel.findByIdAndUpdate(
+ userId,
+ { $push: { refreshToken } },
+ { new: true }
+ );
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async clearRefreshToken(token) {
+ await UserModel.findOneAndUpdate(
+ { refreshToken: token },
+ { $pull: { refreshToken: token } },
+ { new: true }
+ );
+ }
+
+ async softDelete(id) {
+ const document = await UserModel.findByIdAndUpdate(
+ id,
+ { isDeleted: true },
+ { new: true }
+ );
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async delete(id) {
+ return await UserModel.findByIdAndDelete(id);
+ }
+
+ async findByGoogleId(googleId) {
+ const document = await UserModel.findOne({
+ googleId,
+ isDeleted: { $ne: true },
+ }).select("+googleId");
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async saveEmailChangeOtp(userId, pendingEmail, otpCode, otpExpiresAt) {
+
+ console.log('pending email : ', pendingEmail)
+ const document = await UserModel.findByIdAndUpdate(
+ userId,
+ {
+ pendingEmail,
+ otpCode,
+ otpExpiresAt,
+ },
+ { new: true }
+ );
+
+ console.log('document : ', document)
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async updateEmailAfterVerification(userId) {
+ const user = await UserModel.findById(userId);
+ if (!user) return null;
+
+ const document = await UserModel.findByIdAndUpdate(
+ userId,
+ {
+ email: user.pendingEmail,
+ pendingEmail: null,
+ otpCode: null,
+ otpExpiresAt: null,
+ },
+ { new: true }
+ );
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async findByIdWithOtp(userId) {
+ const document = await UserModel.findById(userId).select("+otpCode");
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async addToWishlist(userId, venueId) {
+ const user = await UserModel.findById(userId);
+
+ if (!user) return null;
+
+ const alreadyExists = user.wishlist.some((id) => id.toString() === venueId);
+
+ if (alreadyExists) {
+ return { alreadyExists: true };
+ }
+
+ user.wishlist.push(venueId);
+
+ await user.save();
+
+ return UserMapper.mapToEntity(user);
+ }
+
+ async getWishlist(userId) {
+ const document = await UserModel.findById(userId).populate("wishlist");
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async removeWishlist(userId, venueId) {
+ const user = await UserModel.findById(userId);
+
+ if (!user) return null;
+
+ const exists = user.wishlist.some((id) => id.toString() === venueId);
+
+ if (!exists) {
+ return { notFound: true };
+ }
+
+ user.wishlist.pull(venueId);
+
+ await user.save();
+
+ return UserMapper.mapToEntity(user);
+ }
+
+ async updateAccountStatus(userId, isActive) {
+ const document = await UserModel.findByIdAndUpdate(
+ userId,
+ { isActive },
+ { new: true }
+ );
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async updateProfileImage(userId, profileImage) {
+ const document = await UserModel.findByIdAndUpdate(
+ userId,
+ {
+ profileImage: {
+ publicId: profileImage.publicId,
+ url: profileImage.url,
+ },
+ },
+ { new: true }
+ );
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+
+ async removeProfileImage(userId) {
+ const document = await UserModel.findByIdAndUpdate(
+ userId,
+ {
+ profileImage: {
+ publicId: "",
+ url: "",
+ },
+ },
+ { new: true }
+ );
+
+ if (!document) return null;
+
+ return UserMapper.mapToEntity(document);
+ }
+}
diff --git a/server/src/infrastructure/repositories/vendor.repository.js b/server/src/infrastructure/repositories/vendor.repository.js
new file mode 100644
index 0000000000..feb29f9051
--- /dev/null
+++ b/server/src/infrastructure/repositories/vendor.repository.js
@@ -0,0 +1,209 @@
+import { IVendorRepository } from "../../domain/repositories/IVendor.repository.js";
+import VendorModel from "../database/models/Vendor.model.js";
+import { VendorMapper } from "../../application/mapper/Vendor.mapper.js";
+import { VendorApprovalStatus } from "../../domain/enums/VendorApprovalStatus.enum.js";
+
+class VendorRepositoryImpl extends IVendorRepository {
+
+ async create(entity) {
+ const doc = await VendorModel.create(VendorMapper.mapToPersistence(entity));
+ return VendorMapper.mapToEntity(doc);
+ }
+
+ async findById(id) {
+ const document = await VendorModel.findById(id);
+ if (!document) return null;
+ return VendorMapper.mapToEntity(document);
+ }
+
+ async findByResetToken(token){
+ const document = await VendorModel.findOne({resetToken: token})
+ if(!document) return null
+ return VendorMapper.mapToEntity(document)
+ }
+
+ async findAll() {
+ const docs = await VendorModel.find({ isDeleted: false });
+ return docs.map((doc) => VendorMapper.mapToEntity(doc));
+ }
+
+ async updatePassword(
+ vendorId,
+ hashedPassword
+ ) {
+ await VendorModel.findByIdAndUpdate(
+ vendorId,
+ {
+ password: hashedPassword
+ }
+ );
+ }
+
+ async findAllFiltered(query = {}) {
+ const filter = {};
+
+ // Search
+ if (query.search) {
+ filter.$or = [
+ {
+ fullName: {
+ $regex: query.search,
+ $options: "i",
+ },
+ },
+ {
+ email: {
+ $regex: query.search,
+ $options: "i",
+ },
+ },
+ {
+ companyName: {
+ $regex: query.search,
+ $options: "i",
+ },
+ },
+ ];
+ }
+
+ // Approval Status
+ if (query.status) {
+ filter.approvalStatus = query.status;
+ }
+
+ // Block / Unblock Filter
+ if (query.isBlocked !== undefined) {
+ filter.isBlocked =
+ query.isBlocked === true ||
+ query.isBlocked === "true";
+ }
+ console.log("Query:", query);
+ console.log("Filter:", filter);
+
+ const skip = query.limit * (query.page - 1);
+ const totalCount = await VendorModel.countDocuments(filter);
+ const totalPages = Math.ceil(totalCount / query.limit);
+ const documents = await VendorModel.find(filter)
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(query.limit);
+
+ return {
+ data: documents.map(doc => VendorMapper.mapToEntity(doc)),
+ totalCount,
+ totalPages
+ };
+ }
+
+ async verifyOtp(vendorId) {
+ const document = await VendorModel.findByIdAndUpdate(
+ vendorId,
+ {
+ isVerified: true,
+ },
+ {
+ new: true
+ }
+ );
+
+ if (!document) return null;
+
+ return VendorMapper.mapToEntity(document);
+ }
+
+ async approveVendor(vendorId) {
+ const updatedVendor = await VendorModel.findByIdAndUpdate(
+ vendorId,
+ { approvalStatus: VendorApprovalStatus.APPROVED, rejectionReason: null },
+ { new: true }
+ );
+ if (!updatedVendor) return null;
+ return VendorMapper.mapToEntity(updatedVendor);
+ }
+
+ async rejectVendor(vendorId, reason) {
+ const updatedVendor = await VendorModel.findByIdAndUpdate(
+ vendorId,
+ { approvalStatus: VendorApprovalStatus.REJECTED, rejectionReason: reason },
+ { new: true }
+ );
+ if (!updatedVendor) return null;
+ return VendorMapper.mapToEntity(updatedVendor);
+ }
+
+ async updateBlockStatus(vendorId, isBlocked) {
+ const document = await VendorModel.findByIdAndUpdate(
+ vendorId,
+ { isBlocked },
+ { new: true }
+ );
+ if (!document) return null;
+ return VendorMapper.mapToEntity(document);
+ }
+
+ async update(id, entity) {
+ const doc = await VendorModel.findOneAndUpdate(
+ { _id: id, isDeleted: false },
+ VendorMapper.mapToPersistence(entity),
+ { new: true }
+ );
+ if (!doc) return null;
+ return VendorMapper.mapToEntity(doc);
+ }
+
+ async delete(id) {
+ return await VendorModel.findByIdAndDelete(id);
+ }
+
+ async softDelete(id) {
+ const doc = await VendorModel.findOneAndUpdate(
+ { _id: id, isDeleted: false },
+ { isDeleted: true },
+ { new: true }
+ );
+ return doc ? VendorMapper.mapToEntity(doc) : null;
+ }
+
+ async findByEmail(email) {
+ const doc = await VendorModel.findOne({
+ email,
+ isDeleted: {$ne: true}
+ });
+ if(!doc){
+ return null
+ }
+
+ return VendorMapper.mapToEntity(doc)
+ }
+
+ async findByPhone(phone) {
+ const doc = await VendorModel.findOne({ phone, isDeleted: false });
+ return doc ? VendorMapper.mapToEntity(doc) : null;
+ }
+
+ async findByRefreshToken(refreshToken) {
+ const doc = await VendorModel.findOne({ refreshToken, isDeleted: false }).select("+password");
+ if (!doc) return null;
+ return VendorMapper.mapToEntity(doc);
+ }
+
+ async updateRefreshToken(vendorId, refreshToken) {
+ const doc = await VendorModel.findByIdAndUpdate(
+ vendorId,
+ { $push: {refreshToken} },
+ { new: true }
+ );
+ if (!doc) return null;
+ return VendorMapper.mapToEntity(doc);
+ }
+
+ async clearRefreshToken(token) {
+ await VendorModel.findOneAndUpdate(
+ {refreshToken: token},
+ { $pull: {refreshToken: token } },
+ { new: true }
+ );
+ }
+}
+
+export default VendorRepositoryImpl;
diff --git a/server/src/infrastructure/repositories/venue.repository.js b/server/src/infrastructure/repositories/venue.repository.js
new file mode 100644
index 0000000000..0e22f5fa27
--- /dev/null
+++ b/server/src/infrastructure/repositories/venue.repository.js
@@ -0,0 +1,345 @@
+import { VenueMapper } from "../../application/mapper/Venue.mapper.js";
+import { IVenueRepository } from "../../domain/repositories/IVenue.repository.js";
+import { VenueModel } from '../database/models/Venue.model.js'
+import { VenueStatus } from "../../domain/enums/Venue.enum.js";
+
+export class VenueRepository extends IVenueRepository {
+
+async findById(id) {
+
+ const document =
+ await VenueModel.findById(id)
+ .populate(
+ "vendorId",
+ "fullName email phone companyName"
+ );
+
+ if (!document) return null;
+
+ return VenueMapper.mapToEntity(document);
+
+}
+
+ async create(venue) {
+ const data = VenueMapper.mapToPersistence(venue)
+ const document = await VenueModel.create(data)
+ return VenueMapper.mapToEntity(document)
+ }
+
+ async update(id, venue) {
+ const data = VenueMapper.mapToPersistence(venue)
+ const document = await VenueModel.findByIdAndUpdate(
+ id,
+ { $set: data },
+ { new: true }
+ )
+ if (!document) return null
+ return VenueMapper.mapToEntity(document)
+ }
+
+ async findByVendorAndName(vendorId, name) {
+ const document = await VenueModel.findOne({
+ vendorId,
+ name
+ })
+ if (!document) return null
+ return VenueMapper.mapToEntity(document)
+ }
+
+ async findAllFiltered(query = {}) {
+ // console.log('query: ', query)
+
+ const filter = {
+ isDeleted: false
+ };
+
+ // Vendor - only own venues
+ if (query.vendorId) {
+ filter.vendorId = query.vendorId;
+ }
+
+ // User/Admin - approval status
+ if (query.approvalStatus) {
+ filter.approvalStatus = query.approvalStatus;
+ }
+
+ // Venue status (AVAILABLE, UNAVAILABLE, etc.)
+ if (query.status) {
+ filter.status = query.status;
+ }
+
+ // Blocked / Unblocked
+ if (query.isBlocked !== undefined) {
+ filter.isBlocked = query.isBlocked === "true";
+ }
+
+ // Category
+ if (query.category) {
+ filter.category = query.category;
+ }
+
+ // Price filter
+ if (query.price) {
+ filter.$or = [
+ { pricePerHour: { $lte: query.price } },
+ { pricePerDay: { $lte: query.price } }
+ ];
+ }
+
+ // Min / Max price
+ if (query.priceType) {
+
+ if(query.priceType === 'day'){
+ filter.pricePerDay = {};
+
+ if (query.minPrice) {
+ filter.pricePerDay.$gte = query.minPrice;
+ }
+
+ if (query.maxPrice) {
+ filter.pricePerDay.$lte = query.maxPrice;
+ }
+ }
+ if(query.priceType === 'hour'){
+ filter.pricePerHour = {};
+
+ if (query.minPrice) {
+ filter.pricePerHour.$gte = query.minPrice;
+ }
+
+ if (query.maxPrice) {
+ filter.pricePerHour.$lte = query.maxPrice;
+ }
+ }
+ }
+
+ // Rating
+ if (query.rating) {
+ filter.rating = {
+ $gte: query.rating
+ }
+ }
+
+ // Amenities
+ if (query.amenities) {
+ filter.amenities = {
+ $all: query.amenities
+ };
+ }
+
+ if(query.capacityType){
+ if(query.capacityType === 'seating'){
+ filter.seatingCapacity = {
+ $gte: query.capacity
+ }
+ }
+ if(query.capacityType === 'standing'){
+ filter.standingCapacity = {
+ $gte: query.capacity
+ }
+ }
+ }
+ // Search
+ if (query.search) {
+
+ filter.$or = [
+
+ {
+ name: {
+ $regex: query.search,
+ $options: "i"
+ }
+ },
+
+ {
+ "address.addressLine1": {
+ $regex: query.search,
+ $options: "i"
+ }
+ },
+
+ {
+ "address.city": {
+ $regex: query.search,
+ $options: "i"
+ }
+ },
+
+ {
+ "address.state": {
+ $regex: query.search,
+ $options: "i"
+ }
+ }
+
+ ];
+
+ }
+
+ const skip = query.limit * (query.page - 1);
+
+ const totalCount =
+ await VenueModel.countDocuments(filter);
+
+ const totalPages =
+ Math.ceil(totalCount / query.limit);
+
+ const documents =
+ await VenueModel.find(filter)
+ .populate(
+ "vendorId",
+ "fullName email phone"
+ )
+ .sort({
+ createdAt: -1
+ })
+ .skip(skip)
+ .limit(query.limit);
+
+ return {
+
+ data: documents.map((d) => VenueMapper.mapToEntity(d)),
+
+ totalCount,
+
+ totalPages
+
+ };
+
+ }
+
+ async approveVenue(id) {
+
+ const venue =
+ await VenueModel.findByIdAndUpdate(
+
+ id,
+
+ {
+
+ approvalStatus: VenueStatus.ACTIVE,
+
+ rejectionReason: null
+
+ },
+
+ {
+
+ new: true
+
+ }
+
+ ).populate(
+ "vendorId",
+ "fullName email"
+ );
+
+ if (!venue) return null;
+
+ return VenueMapper.mapToEntity(venue);
+
+ }
+
+ async rejectVenue(id, reason) {
+
+ const venue =
+ await VenueModel.findByIdAndUpdate(
+
+ id,
+
+ {
+
+ approvalStatus: VenueStatus.REJECTED,
+
+ rejectionReason: reason
+
+ },
+
+ {
+
+ new: true
+
+ }
+
+ ).populate(
+ "vendorId",
+ "fullName email"
+ );
+
+ if (!venue) return null;
+
+ return VenueMapper.mapToEntity(venue);
+
+ }
+
+ async updateBlockStatus(
+ id,
+ isBlocked
+ ) {
+
+ const venue =
+ await VenueModel.findByIdAndUpdate(
+
+ id,
+
+ {
+ isBlocked
+ },
+
+ {
+
+ new: true
+
+ }
+
+ );
+
+ if (!venue) return null;
+
+ return VenueMapper.mapToEntity(venue);
+
+ }
+
+ async delete(id) {
+ return await VenueModel.findByIdAndUpdate(
+ id,
+ { isDeleted: true },
+ { new: true }
+ )
+ }
+
+ async countByOwnerId(ownerId) {
+ return await VenueModel.countDocuments({
+ vendorId: ownerId,
+
+ isDeleted: false,
+ });
+ }
+
+ async findTopVenues() {
+ const documents = await VenueModel
+ .find({
+ isDeleted: false,
+ isBlocked: false,
+ approvalStatus: VenueStatus.ACTIVE
+ })
+ .sort({
+ rating: -1,
+ createdAt: -1
+ })
+ .limit(4)
+ .lean()
+ return documents.map(d => VenueMapper.mapToEntity(d))
+ }
+
+ async findSimilarVenues(venueId, category){
+ const documents = await VenueModel.find({
+ _id: { $ne: venueId },
+ category,
+ approvalStatus: VenueStatus.ACTIVE,
+ isDeleted: false,
+ isBlocked: false
+ })
+ return documents.map(doc => VenueMapper.mapToEntity(doc))
+ }
+}
diff --git a/server/src/infrastructure/schedulers/paymentReminder.scheduler.js b/server/src/infrastructure/schedulers/paymentReminder.scheduler.js
new file mode 100644
index 0000000000..98a5697ec4
--- /dev/null
+++ b/server/src/infrastructure/schedulers/paymentReminder.scheduler.js
@@ -0,0 +1,10 @@
+import cron from "node-cron";
+import { iUserPaymentReminderUsecase } from "../../presentation/controllers/di.js";
+
+cron.schedule("0 9 * * *", async () => {
+ try {
+ await iUserPaymentReminderUsecase.execute();
+ } catch (error) {
+ console.error("Payment reminder scheduler failed:", error);
+ }
+});
\ No newline at end of file
diff --git a/server/src/infrastructure/services/.gitkeep b/server/src/infrastructure/services/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/infrastructure/services/HashService.js b/server/src/infrastructure/services/HashService.js
new file mode 100644
index 0000000000..e4d545185d
--- /dev/null
+++ b/server/src/infrastructure/services/HashService.js
@@ -0,0 +1,19 @@
+import bcrypt from "bcryptjs";
+import crypto from 'crypto'
+import { IHashService } from '../../application/services/hashService.js'
+
+export class HashService extends IHashService {
+ async hash(password) {
+ const saltRounds = Number(process.env.BCRYPT_SALT_ROUNDS)
+ return await bcrypt.hash(password, saltRounds);
+ }
+
+ async compare(password, hashedPassword) {
+ return await bcrypt.compare(password, hashedPassword);
+ }
+
+ hashToken(token) {
+ return crypto.createHash('sha256').update(token).digest("hex")
+ }
+}
+
diff --git a/server/src/infrastructure/services/MailService.js b/server/src/infrastructure/services/MailService.js
new file mode 100644
index 0000000000..223be8120c
--- /dev/null
+++ b/server/src/infrastructure/services/MailService.js
@@ -0,0 +1,216 @@
+import { MailService } from "../../application/services/mailService.js";
+import { transporter } from "../config/mail.config.js";
+import { vendorApprovalTemplate } from "../emailTemplates/admin.vendorApprovalTemplate.js";
+import { vendorRejectionTemplate } from "../emailTemplates/admin.vendorRejectionTemplate.js";
+import { adminVenueApprovalTemplate } from "../emailTemplates/admin.venueApprovalTemplate.js";
+import { adminVenueRejectionTemplate } from "../emailTemplates/admin.venueRejectionTemplate.js";
+import { forgotPasswordTemplate } from "../emailTemplates/forgotPasswordTemplate.js";
+import { VerifyRegisterotpTemplate } from "../emailTemplates/verifyRegisterOtpTemplate.js";
+import { emailChangeOtpTemplate } from "../emailTemplates/user.emailChangeOtpTemplate.js";
+import { bookingConfirmationTemplate } from "../emailTemplates/user.bookingConfirmationTemplate.js"
+import { paymentReminderTemplate } from "../emailTemplates/user.paymentReminderTemplate.js";
+import { bookingCancellationTemplate } from "../emailTemplates/user.bookingCancellationTemplate.js";
+// General-purpose send function used by auth use cases
+// export const sendMail = async (to, subject, html) => {
+// await transporter.sendMail({
+// from: process.env.EMAIL_USER,
+// to,
+// subject,
+// html
+// });
+// };
+
+export class MailServiceImpl extends MailService {
+
+ async sendVendorApprovalMail(vendor) {
+
+ const { subject, html } =
+ vendorApprovalTemplate({
+ vendorName: vendor.fullName
+ });
+
+ await transporter.sendMail({
+ from: process.env.EMAIL_USER,
+ to: vendor.email,
+ subject,
+ html
+ });
+
+ console.log("Approval mail sent.");
+ }
+
+
+ async sendVendorRejectionMail(vendor, reason) {
+
+ const { subject, html } =
+ vendorRejectionTemplate({
+ vendorName: vendor.fullName,
+ reason
+ });
+
+ await transporter.sendMail({
+ from: process.env.EMAIL_USER,
+ to: vendor.email,
+ subject,
+ html
+ });
+
+ console.log("Rejection mail sent.");
+ }
+
+ async sendVenueApprovalMail(venue){
+
+ const { subject, html } =
+ adminVenueApprovalTemplate({
+
+ venueName: venue.name,
+ vendorName: venue.vendorId.fullName
+
+ });
+
+ await transporter.sendMail({
+
+ from: process.env.EMAIL_USER,
+
+ to: venue.vendorId.email,
+
+ subject,
+
+ html
+
+ });
+
+ }
+
+ async sendVenueRejectionMail(venue, reason) {
+
+ const { subject, html } =
+ adminVenueRejectionTemplate({
+ venueName: venue.name,
+ vendorName: venue.vendorId.fullName,
+ reason
+ });
+
+ await transporter.sendMail({
+ from: process.env.EMAIL_USER,
+ to: venue.vendorId.email,
+ subject,
+ html
+ });
+
+ console.log("Venue rejection mail sent.");
+ }
+
+ async sendForgotPasswordMail(user, resetLink) {
+ await transporter.sendMail({
+ from: process.env.EMAIL_USER,
+ to: user.email,
+ subject: 'Reset Your BookMyVenue Password',
+ html: forgotPasswordTemplate(user.fullName, resetLink)
+ });
+
+ console.log("Forgot password mail sent.");
+ }
+
+ async sendVerifiyRegisterOtp(email, name, otpCode) {
+ await transporter.sendMail({
+ from: process.env.EMAIL_USER,
+ to: email,
+ subject: 'Your BookMyVenue OTP Code - Verify Your Email',
+ html: VerifyRegisterotpTemplate(name, otpCode)
+ });
+
+ console.log("OTP mail sent.");
+ }
+
+ async sendEmailChangeOtp(email,name, otp) {
+ await transporter.sendMail({
+ from: process.env.EMAIL_USER,
+ to: email,
+ subject: 'Your Email change OTP',
+ html: emailChangeOtpTemplate(name, otp)
+ })
+ }
+ async sendBookingConfirmationMail(booking) {
+
+ const { subject, html } = bookingConfirmationTemplate({
+
+ customerName: booking.customerName,
+ venueName: booking.venueName,
+ bookingDate: booking.bookingDate,
+ startTime: booking.startTime,
+ endTime: booking.endTime,
+ guestCount: booking.guestCount,
+ bookingType: booking.bookingType,
+ totalAmount: booking.totalAmount,
+ paidAmount: booking.paidAmount,
+ remainingAmount: booking.remainingAmount
+
+ });
+
+ await transporter.sendMail({
+
+ from: process.env.EMAIL_USER,
+ to: booking.email,
+ subject,
+ html
+
+ });
+
+ console.log("Booking confirmation mail sent.");
+
+ }
+ async sendPaymentReminderMail(reminderData) {
+
+ const { subject, html } = paymentReminderTemplate({
+ customerName: reminderData.customerName,
+ venueName: reminderData.venueName,
+ bookingDate: reminderData.bookingDate,
+ startTime: reminderData.startTime,
+ endTime: reminderData.endTime,
+ totalAmount: reminderData.totalAmount,
+ paidAmount: reminderData.paidAmount,
+ remainingAmount: reminderData.remainingAmount
+ });
+
+ await transporter.sendMail({
+ from: process.env.EMAIL_USER,
+ to: reminderData.email,
+ subject,
+ html
+ });
+
+ console.log("Payment reminder mail sent.");
+
+ }
+
+
+ async sendBookingCancellationMail(cancellationData) {
+
+ const { subject, html } = bookingCancellationTemplate({
+
+ customerName: cancellationData.customerName,
+ venueName: cancellationData.venueName,
+ bookingDate: cancellationData.bookingDate,
+ startTime: cancellationData.startTime,
+ endTime: cancellationData.endTime,
+ paidAmount: cancellationData.paidAmount,
+ refundAmount: cancellationData.refundAmount,
+ cancellationReason: cancellationData.cancellationReason
+
+ });
+
+ await transporter.sendMail({
+
+ from: process.env.EMAIL_USER,
+ to: cancellationData.email,
+ subject,
+ html
+
+ });
+
+ console.log("Booking cancellation mail sent.");
+
+}
+
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/services/OtpService.js b/server/src/infrastructure/services/OtpService.js
new file mode 100644
index 0000000000..e7c3b88578
--- /dev/null
+++ b/server/src/infrastructure/services/OtpService.js
@@ -0,0 +1,21 @@
+import { IOtpService } from '../../application/services/otpService.js'
+import bcrypt from "bcryptjs";
+
+export class OtpService extends IOtpService {
+ generate() {
+ return Math.floor(100000 + Math.random() * 900000).toString();
+ }
+
+ async hash(otp) {
+ const saltRounds = Number(process.env.BCRYPT_SALT_ROUNDS)
+ return await bcrypt.hash(otp, saltRounds);
+ }
+
+ async compare(otp, hashedOtp) {
+ return await bcrypt.compare(otp, hashedOtp);
+ }
+
+ getExpiry(minutes = 10) {
+ return new Date(Date.now() + minutes * 60 * 1000);
+ }
+}
diff --git a/server/src/infrastructure/services/OtpStoreService.js b/server/src/infrastructure/services/OtpStoreService.js
new file mode 100644
index 0000000000..d876ed3ed5
--- /dev/null
+++ b/server/src/infrastructure/services/OtpStoreService.js
@@ -0,0 +1,22 @@
+import { IOtpStoreService } from '../../application/services/otpStoreService.js'
+
+
+export class OtpStoreService extends IOtpStoreService {
+
+ constructor( redis ) {
+ super()
+ this._redis = redis
+ }
+
+ async saveOtp(userId, otp, ttlSeconds) {
+ await this._redis.set(userId, otp, "EX", ttlSeconds)
+ }
+
+ async getOtp(userId) {
+ return await this._redis.get(userId)
+ }
+
+ async deleteOtp(userId) {
+ await this._redis.del(userId)
+ }
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/services/TokenService.js b/server/src/infrastructure/services/TokenService.js
new file mode 100644
index 0000000000..5a9a230e93
--- /dev/null
+++ b/server/src/infrastructure/services/TokenService.js
@@ -0,0 +1,55 @@
+import jwt from "jsonwebtoken";
+import crypto from "crypto";
+import { jwtConfig } from '../config/jwt.config.js'
+import { UnauthorizedError } from "../../domain/errors/UnauthorizedError.js";
+import { ITokenService } from "../../application/services/tokenService.js";
+import { authMessages } from "../../shared/constants/messages/authMessages.js";
+import { redisClient } from '../config/redis.config.js'
+
+export class TokenService extends ITokenService{
+ generateAccessToken(payload) {
+ const accessTokenSecret = jwtConfig.accessToken.secret
+ const token = jwt.sign(payload, accessTokenSecret, { expiresIn: jwtConfig.accessToken.expiresIn });
+ console.log("Generated Access Token:");
+ console.log(jwt.decode(token));
+ return token
+ }
+
+ generateRefreshToken(payload) {
+ const refreshTokenSecret = jwtConfig.refreshToken.secret
+ return jwt.sign(payload, refreshTokenSecret, { expiresIn: jwtConfig.refreshToken.expiresIn });
+ }
+
+ generateResetToken() {
+ return crypto.randomBytes(32).toString('hex');
+ }
+
+ getResetTokenExpiry() {
+ return new Date(Date.now() + 60 * 60 * 1000); // 1 hour
+ }
+
+ verifyAccessToken(token) {
+ const accessTokenSecret = jwtConfig.accessToken.secret
+ return jwt.verify(token, accessTokenSecret);
+
+ }
+
+ verifyRefreshToken(token) {
+ try {
+ const refreshTokenSecret = jwtConfig.refreshToken.secret
+ return jwt.verify(token, refreshTokenSecret);
+ } catch {
+ throw new UnauthorizedError(authMessages.error.INVALID_REFRESH_TOKEN);
+ }
+ }
+
+ async blackListToken(token, expiresInSeconds) {
+ await redisClient.set(`bl_${token}`, "true", "EX", expiresInSeconds)
+ }
+
+ async isTokenBlacklisted(token) {
+ const result = await redisClient.exists(`bl_${token}`)
+ return result === 1
+ }
+}
+
diff --git a/server/src/infrastructure/services/cloudinaryService.js b/server/src/infrastructure/services/cloudinaryService.js
new file mode 100644
index 0000000000..51d156c8bf
--- /dev/null
+++ b/server/src/infrastructure/services/cloudinaryService.js
@@ -0,0 +1,15 @@
+import cloudinary from "../config/cloudinary.config.js";
+import { ICloudinaryService } from '../../application/services/cloudinaryService.js'
+
+
+export class CloudinaryService extends ICloudinaryService {
+ async deleteImage(publicId){
+ return await cloudinary.uploader.destroy(publicId)
+ }
+
+ async deleteImages(publicIdS){
+ for(let publicId of publicIdS){
+ await cloudinary.uploader.destroy(publicId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/infrastructure/services/reservationService.js b/server/src/infrastructure/services/reservationService.js
new file mode 100644
index 0000000000..04744414bd
--- /dev/null
+++ b/server/src/infrastructure/services/reservationService.js
@@ -0,0 +1,32 @@
+import crypto from "crypto";
+
+export class ReservationService {
+
+ constructor(redisClient) {
+ this.redisClient = redisClient;
+ }
+
+ generateReservationId() {
+ return crypto.randomUUID();
+ }
+
+ async reserveSlot(key, value, ttl = 600) {
+ await this.redisClient.set(
+ key,
+ JSON.stringify(value),
+ "EX",
+ ttl
+ );
+ }
+
+ async getReservation(key) {
+ const reservation = await this.redisClient.get(key);
+
+ return reservation ? JSON.parse(reservation) : null;
+ }
+
+ async deleteReservation(key) {
+ await this.redisClient.del(key);
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/.gitkeep b/server/src/presentation/controllers/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/presentation/controllers/admin/.gitkeep b/server/src/presentation/controllers/admin/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/presentation/controllers/admin/admin.authController.js b/server/src/presentation/controllers/admin/admin.authController.js
new file mode 100644
index 0000000000..22cf1c2764
--- /dev/null
+++ b/server/src/presentation/controllers/admin/admin.authController.js
@@ -0,0 +1,47 @@
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+import { authMessages } from "../../../shared/constants/messages/authMessages.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js"
+
+
+const REFRESH_COOKIE_OPTIONS = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ maxAge: 7 * 24 * 60 * 60 * 1000
+};
+
+export class AdminAuthController {
+ constructor (
+ adminLoginUsecase,
+ adminLogoutUsecase,
+ adminRefreshToken,
+ ) {
+ this._loginusecase = adminLoginUsecase;
+ this._logoutUsecase = adminLogoutUsecase;
+ this._refreshTokenUseCase = adminRefreshToken;
+ }
+
+
+ login = asyncHandler ( async (req, res) => {
+ console.log("login usecase: ", this._loginusecase)
+ const { accessToken, refreshToken, user } = await this._loginusecase.execute({...req.body});
+ res.cookie("refreshToken", refreshToken, REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, '', { accessToken, user });
+ })
+
+ refreshToken = asyncHandler(async (req, res) => {
+ const token = req.cookies?.refreshToken;
+ const { accessToken, refreshToken, user } = await this._refreshTokenUseCase.execute(token);
+ res.cookie("refreshToken", refreshToken, REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, authMessages.success.TOKEN_REFRESHED, { accessToken, user });
+ });
+
+ logout = asyncHandler(async (req, res) => {
+ const accessToken = req.headers.authorization?.split(' ')[1]
+ const refreshToken = req.cookies?.refreshToken;
+ await this._logoutUsecase.execute(refreshToken, accessToken);
+ res.clearCookie("refreshToken", REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, authMessages.success.LOGOUT);
+ });
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/admin/admin.bookingController.js b/server/src/presentation/controllers/admin/admin.bookingController.js
new file mode 100644
index 0000000000..98c718873f
--- /dev/null
+++ b/server/src/presentation/controllers/admin/admin.bookingController.js
@@ -0,0 +1,135 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+
+export class AdminBookingController {
+
+ constructor(
+ adminGetAllBookingsUsecase,
+ adminGetBookingByIdUsecase,
+ adminGetBookingStatisticsUsecase
+ ) {
+
+ this._adminGetAllBookingsUsecase =
+ adminGetAllBookingsUsecase;
+
+ this._adminGetBookingByIdUsecase =
+ adminGetBookingByIdUsecase;
+
+ this._adminGetBookingStatisticsUsecase =
+ adminGetBookingStatisticsUsecase;
+
+ }
+
+ getAllBookings = asyncHandler(
+
+ async (req, res) => {
+
+ const {
+
+ search,
+
+ status,
+
+ paymentStatus,
+
+ page,
+
+ limit,
+
+ sortBy,
+
+ bookingDate
+
+ } = req.validatedQuery;
+
+ const result =
+ await this
+ ._adminGetAllBookingsUsecase
+ .execute(
+
+ search,
+
+ status,
+
+ paymentStatus,
+
+ page,
+
+ limit,
+
+ sortBy,
+
+ bookingDate
+
+ );
+
+ return sendSuccess(
+
+ res,
+
+ statusCode.OK,
+
+ "",
+
+ result
+
+ );
+
+ }
+
+ );
+
+ getBookingById = asyncHandler(
+
+ async (req, res) => {
+
+ const booking =
+ await this
+ ._adminGetBookingByIdUsecase
+ .execute(
+ req.params.bookingId
+ );
+
+ return sendSuccess(
+
+ res,
+
+ statusCode.OK,
+
+ "",
+
+ booking
+
+ );
+
+ }
+
+ );
+
+ getBookingStatistics = asyncHandler(
+
+ async (req, res) => {
+
+ const statistics =
+ await this
+ ._adminGetBookingStatisticsUsecase
+ .execute();
+
+ return sendSuccess(
+
+ res,
+
+ statusCode.OK,
+
+ "",
+
+ statistics
+
+ );
+
+ }
+
+ );
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/admin/admin.dashboardController.js b/server/src/presentation/controllers/admin/admin.dashboardController.js
new file mode 100644
index 0000000000..0a362ae12f
--- /dev/null
+++ b/server/src/presentation/controllers/admin/admin.dashboardController.js
@@ -0,0 +1,22 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+
+export class AdminDashboardController {
+ constructor(adminDashboardStatisticsUsecase) {
+ this._adminDashboardStatisticsUsecase =
+ adminDashboardStatisticsUsecase;
+ }
+
+ getDashboardStatistics = asyncHandler(async (req, res) => {
+ const statistics =
+ await this._adminDashboardStatisticsUsecase.execute();
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "",
+ statistics
+ );
+ });
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/admin/admin.paymentController.js b/server/src/presentation/controllers/admin/admin.paymentController.js
new file mode 100644
index 0000000000..d8053809cc
--- /dev/null
+++ b/server/src/presentation/controllers/admin/admin.paymentController.js
@@ -0,0 +1,126 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+
+export class AdminPaymentController {
+
+ constructor(
+
+ adminGetAllPaymentsUsecase,
+
+ adminGetPaymentByIdUsecase,
+
+ adminGetPaymentStatisticsUsecase
+
+ ) {
+
+ this._adminGetAllPaymentsUsecase =
+ adminGetAllPaymentsUsecase;
+
+ this._adminGetPaymentByIdUsecase =
+ adminGetPaymentByIdUsecase;
+
+ this._adminGetPaymentStatisticsUsecase =
+ adminGetPaymentStatisticsUsecase;
+
+ }
+
+ getAllPayments = asyncHandler(
+
+ async (req, res) => {
+
+ const {
+
+ search,
+ paymentStatus,
+ paymentMethod,
+ paymentType,
+ sortBy,
+ page,
+ limit
+
+ } = req.validatedQuery;
+
+ const result =
+ await this
+ ._adminGetAllPaymentsUsecase
+ .execute(
+
+ search,
+ paymentStatus,
+ paymentMethod,
+ paymentType,
+ sortBy,
+ page,
+ limit
+ );
+
+ return sendSuccess(
+
+ res,
+
+ statusCode.OK,
+
+ "",
+
+ result
+
+ );
+
+ }
+
+ );
+
+ getPaymentById = asyncHandler(
+
+ async (req, res) => {
+
+ const payment =
+ await this
+ ._adminGetPaymentByIdUsecase
+ .execute(
+ req.params.paymentId
+ );
+
+ return sendSuccess(
+
+ res,
+
+ statusCode.OK,
+
+ "",
+
+ payment
+
+ );
+
+ }
+
+ );
+
+ getPaymentStatistics = asyncHandler(
+
+ async (req, res) => {
+
+ const statistics =
+ await this
+ ._adminGetPaymentStatisticsUsecase
+ .execute();
+
+ return sendSuccess(
+
+ res,
+
+ statusCode.OK,
+
+ "",
+
+ statistics
+
+ );
+
+ }
+
+ );
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/admin/admin.userController.js b/server/src/presentation/controllers/admin/admin.userController.js
new file mode 100644
index 0000000000..3a1c221d56
--- /dev/null
+++ b/server/src/presentation/controllers/admin/admin.userController.js
@@ -0,0 +1,57 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+import { UserMessage } from "../../../shared/constants/messages/userMessages.js";
+
+export class AdminUserController {
+
+ constructor(
+ adminGetAllUsersUsecase,
+ adminUpdateUserStatusUsecase,
+ ){
+ this._adminGetAllUsersUsecase = adminGetAllUsersUsecase,
+ this._adminUpdateUserStatusUsecase = adminUpdateUserStatusUsecase
+ }
+
+ getAllUsers = asyncHandler(async(req,res) => {
+
+ const {
+ search ,
+ isBlocked,
+ page ,
+ limit
+ } = req.validatedQuery;
+
+ const { data, totalCount, totalPages } =
+ await this._adminGetAllUsersUsecase.execute(
+ search,
+ isBlocked,
+ page,
+ limit
+ )
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.USERS_FETCHED,
+ { data, totalCount, totalPages }
+ )
+ })
+
+ updateUserStatus = asyncHandler(async(req,res) => {
+
+ const { userId } = req.params;
+ const { isBlocked } = req.body;
+
+ const user =
+ await this._adminUpdateUserStatusUsecase.execute(userId, isBlocked)
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ isBlocked?UserMessage.success.USER_BLOCKED:UserMessage.success.USER_UNBLOCKED,
+ user
+ )
+ })
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/admin/admin.vendorController.js b/server/src/presentation/controllers/admin/admin.vendorController.js
new file mode 100644
index 0000000000..a59f9aa4f2
--- /dev/null
+++ b/server/src/presentation/controllers/admin/admin.vendorController.js
@@ -0,0 +1,92 @@
+import { asyncHandler } from '../../../shared/utils/asyncHandler.js'
+import { sendSuccess } from '../../../shared/utils/apiResponse.js'
+import { statusCode } from '../../../shared/constants/enums/statusCode.js'
+import { VendorMessages } from "../../../shared/constants/messages/vendorMessages.js";
+
+export class AdminVendorController {
+ constructor(
+ AdminGetAllVendorsUsecase,
+ AdminGetVendorByIdUsecase,
+ AdminApproveVendorUsecase,
+ AdminRejectVendorUsecase,
+ AdminUpdateVendorStatusUsecase,
+ ) {
+ this._adminGetAllVendorsUsecase = AdminGetAllVendorsUsecase
+ this._adminGetVendorByIdUsecase = AdminGetVendorByIdUsecase
+ this._adminApproveVendorUsecase = AdminApproveVendorUsecase
+ this._adminRejectVendorUsecase = AdminRejectVendorUsecase
+ this._adminUpdateVendorStatusUsecase = AdminUpdateVendorStatusUsecase
+ }
+
+ getAllVendors = asyncHandler(async (req, res) => {
+ console.log("validatedQuery:", req.validatedQuery);
+ const { page, limit, search, status,isBlocked } = req.validatedQuery
+
+ const { data, totalCount, totalPages } =
+ await this._adminGetAllVendorsUsecase.execute(
+ search,
+ status,
+ isBlocked,
+ page,
+ limit
+ )
+
+ return sendSuccess(res, statusCode.OK, '', { data, totalCount, totalPages })
+ })
+
+ getVendorById = asyncHandler(async (req, res) => {
+ const vendorId = req.params.vendorId
+
+ const vendor =
+ await this._adminGetVendorByIdUsecase.execute(vendorId)
+
+ return sendSuccess(res, statusCode.OK, '', vendor)
+ })
+
+ approveVendor = asyncHandler(async (req, res) => {
+ const vendorId = req.params.vendorId
+ const vendor =
+ await this._adminApproveVendorUsecase.execute(vendorId);
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ VendorMessages.success.VENDOR_APPROVED,
+ vendor
+ );
+ });
+
+ rejectVendor = asyncHandler(async (req, res) => {
+ const vendorId = req.params.vendorId
+ const reason = req.body.reason
+ const vendor =
+ await this._adminRejectVendorUsecase.execute(
+ vendorId,
+ reason
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ VendorMessages.success.VENDOR_REJECTED,
+ vendor
+ );
+ });
+
+
+ updateVendorStatus = asyncHandler(async (req, res) => {
+
+ const { vendorId } = req.params;
+ const { isBlocked } = req.body;
+
+ const vendor =
+ await this._adminUpdateVendorStatusUsecase.execute(vendorId, isBlocked)
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ isBlocked ? VendorMessages.success.VENDOR_BLOCKED: VendorMessages.success.VENDOR_UNBLOCKED,
+ vendor
+ )
+ })
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/admin/admin.venueController.js b/server/src/presentation/controllers/admin/admin.venueController.js
new file mode 100644
index 0000000000..f2e9b5ad8a
--- /dev/null
+++ b/server/src/presentation/controllers/admin/admin.venueController.js
@@ -0,0 +1,164 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+
+export class AdminVenueController {
+
+ constructor(
+ adminGetAllVenuesUsecase,
+ adminGetVenueByIdUsecase,
+ adminApproveVenueUsecase,
+ adminRejectVenueUsecase,
+ adminUpdateVenueBlockStatusUsecase
+ ) {
+
+ this._adminGetAllVenuesUsecase =
+ adminGetAllVenuesUsecase;
+
+ this._adminGetVenueByIdUsecase =
+ adminGetVenueByIdUsecase;
+
+ this._adminApproveVenueUsecase =
+ adminApproveVenueUsecase;
+
+ this._adminRejectVenueUsecase =
+ adminRejectVenueUsecase;
+
+ this._adminUpdateVenueBlockStatusUsecase =
+ adminUpdateVenueBlockStatusUsecase;
+ }
+
+ getAllVenues = asyncHandler(
+
+ async (req, res) => {
+
+ const {
+
+ search,
+ category,
+ approvalStatus,
+ isBlocked,
+ page,
+ limit
+
+ } = req.validatedQuery;
+
+ const result =
+ await this
+ ._adminGetAllVenuesUsecase
+ .execute(
+ search,
+ category,
+ approvalStatus,
+ isBlocked,
+ page,
+ limit
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "",
+ result
+ );
+
+ }
+
+ );
+
+ getVenueById = asyncHandler(
+
+ async (req, res) => {
+
+ const venue =
+ await this
+ ._adminGetVenueByIdUsecase
+ .execute(
+ req.params.venueId
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "",
+ venue
+ );
+
+ }
+
+ );
+
+ approveVenue = asyncHandler(
+
+ async (req, res) => {
+
+ const venue =
+ await this
+ ._adminApproveVenueUsecase
+ .execute(
+ req.params.venueId
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "",
+ venue
+ );
+
+ }
+
+ );
+
+ rejectVenue = asyncHandler(
+
+ async (req, res) => {
+
+ const venue =
+ await this
+ ._adminRejectVenueUsecase
+ .execute(
+ req.params.venueId,
+ req.body.reason
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "",
+ venue
+ );
+
+ }
+
+ );
+
+ updateBlockStatus = asyncHandler(
+
+ async (req, res) => {
+
+ const venue =
+ await this
+ ._adminUpdateVenueBlockStatusUsecase
+ .execute({
+
+ venueId:
+ req.params.venueId,
+
+ isBlocked:
+ req.body.isBlocked
+
+ });
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "",
+ venue
+ );
+
+ }
+
+ );
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/common/common.authController.js b/server/src/presentation/controllers/common/common.authController.js
new file mode 100644
index 0000000000..e28d10dcf5
--- /dev/null
+++ b/server/src/presentation/controllers/common/common.authController.js
@@ -0,0 +1,18 @@
+import { statusCode } from "../../../shared/constants/enums/statusCode.js"
+import { sendSuccess } from "../../../shared/utils/apiResponse.js"
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js"
+
+export class UnifiedAuthController {
+ constructor (
+ getMeUsecase
+ ) {
+ this._getMeUsecase = getMeUsecase
+ }
+
+ getMe = asyncHandler ( async (req, res) => {
+ const refreshToken = req.cookies.refreshToken
+ console.log("from unified: ", refreshToken)
+ const {accessToken, user} = await this._getMeUsecase.execute(refreshToken)
+ return sendSuccess(res, statusCode.OK, '', { accessToken, user})
+ })
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/di.js b/server/src/presentation/controllers/di.js
new file mode 100644
index 0000000000..c289be6dbe
--- /dev/null
+++ b/server/src/presentation/controllers/di.js
@@ -0,0 +1,518 @@
+import { redisClient } from '../../infrastructure/config/redis.config.js'
+import { VendorEditVenueUsecase } from '../../application/vendor/usecases/venue/vendor.editVenue.usecase.js'
+import { VendorCreateVenueUsecase } from '../../application/vendor/usecases/venue/vendor.createVenue.usecase.js'
+import { VendorGetVenueByIdUsecase } from '../../application/vendor/usecases/venue/vendor.getVenueById.usecase.js'
+import { VendorDeleteVenueUsecase } from '../../application/vendor/usecases/venue/vendor.deleteVenue.usecase.js'
+import { VendorUpdateVenueStatusUsecase } from '../../application/vendor/usecases/venue/venue.updateVenueStatus.usecase.js'
+import { VendorGetAllVenuesUsecase } from '../../application/vendor/usecases/venue/vendor.getAllVenues.usecase.js'
+import { UserGetAllVenuesUsecase } from '../../application/user/usecases/venue/user.getAllVenue.usecase.js'
+import { UserGetVenueByIdUsecase } from '../../application/user/usecases/venue/user.getVenueById.usecase.js'
+import { RegisterUserUseCase } from '../../application/user/usecases/auth/user.registerUser.useCase.js'
+import LoginUserUseCase from '../../application/user/usecases/auth/user.loginUser.userCase.js'
+import UserLogoutUseCase from '../../application/user/usecases/auth/user.logout.useCase.js'
+import UserRefreshTokenUseCase from '../../application/user/usecases/auth/user.refreshToken.useCase.js'
+import UserVerifyOtpUseCase from '../../application/user/usecases/auth/user.verifyOtp.useCase.js'
+import UserResendOtpUseCase from '../../application/user/usecases/auth/user.resendOtp.useCase.js'
+import UserForgotPasswordUseCase from '../../application/user/usecases/auth/user.forgotPassword.useCase.js'
+import UserResetPasswordUseCase from '../../application/user/usecases/auth/user.resetPassword.useCase.js'
+
+import { AdminGetAllUsersUsecase } from '../../application/admin/usecases/user/admin.getAllUsers.usecase.js'
+import { AdminUpdateUserStatusUsecase } from '../../application/admin/usecases/user/admin.updateUserStatus.usecase.js'
+import { AdminGetAllVendorsUsecase } from '../../application/admin/usecases/vendor/admin.getAllVendors.usecase.js'
+import { AdminGetVendorByIdUsecase } from '../../application/admin/usecases/vendor/admin.getVendorById.usecase.js'
+import { AdminApproveVendorUsecase } from '../../application/admin/usecases/vendor/admin.approveVendor.usecase.js'
+import { AdminRejectVendorUsecase } from '../../application/admin/usecases/vendor/admin.rejectVendor.usecase.js'
+import { AdminUpdateVendorStatusUsecase } from '../../application/admin/usecases/vendor/admin.updateVendorStatus.js'
+import { AdminGetAllVenuesUsecase } from '../../application/admin/usecases/venue/admin.getAllVenues.usecase.js'
+import { AdminGetVenueByIdUsecase } from '../../application/admin/usecases/venue/admin.getVenueById.usecase.js'
+import { AdminApproveVenueUsecase } from '../../application/admin/usecases/venue/admin.approveVenue.usecase.js'
+import { AdminRejectVenueUsecase } from '../../application/admin/usecases/venue/admin.rejectVenue.usecase.js'
+import { AdminUpdateVenueBlockStatusUsecase } from '../../application/admin/usecases/venue/admin.updateVenueStatus.usecase.js'
+import { GetVendorProfileUsecase } from '../../application/vendor/usecases/profile/getVendorProfile.usecase.js'
+import { VendorUpdateProfileUsecase } from '../../application/vendor/usecases/profile/updateVendorProfile.usecase.js'
+import { GetVendorBookingsUsecase } from '../../application/vendor/usecases/booking/getVendorBookingsUsecase.js'
+import { GetBookingByIdUsecase } from '../../application/vendor/usecases/booking/getBookingByIdUsecase.js'
+import { GetDashboardStatsUsecase } from '../../application/vendor/usecases/dashboard/GetDashboardStatsUsecase.js'
+import { AdminGetAllBookingsUsecase } from '../../application/admin/usecases/booking/admin.getAllBookings.usecase.js'
+import { AdminGetBookingByIdUsecase } from '../../application/admin/usecases/booking/admin.getBookingById.usecase.js'
+import { AdminGetBookingStatisticsUsecase } from '../../application/admin/usecases/booking/admin.getBookingStatistics.usecase.js'
+import { AdminGetAllPaymentsUsecase } from "../../application/admin/usecases/payment/admin.getAllPayments.usecase.js";
+import { AdminGetPaymentByIdUsecase } from "../../application/admin/usecases/payment/admin.getPaymentById.usecase.js";
+import { AdminGetPaymentStatisticsUsecase } from "../../application/admin/usecases/payment/admin.getPaymentStatistics.usecase.js";
+import { AdminDashboardStatisticsUsecase } from '../../application/admin/usecases/dashboard/admin.getStatistics.usecase.js'
+import { AdminUserController } from '../controllers/admin/admin.userController.js'
+import { AdminVendorController } from '../controllers/admin/admin.vendorController.js'
+import { AdminVenueController } from './admin/admin.venueController.js'
+import { AdminBookingController } from './admin/admin.bookingController.js'
+import { AdminPaymentController } from "./admin/admin.paymentController.js";
+import { AdminDashboardController } from './admin/admin.dashboardController.js'
+import { VendorVenueController } from '../controllers/vendor/vendor.venueController.js'
+import { UserVenueController } from '../controllers/user/user.venueController.js'
+import { UserAuthController } from '../controllers/user/user.authController.js'
+import { VendorProfileController } from './vendor/vendorProfileController.js'
+import { VendorBookingController } from './vendor/VendorBookingController.js'
+import { VendorDashboardController } from './vendor/VendorDashboardController.js'
+import { VenueRepository } from '../../infrastructure/repositories/venue.repository.js'
+import { UserRepository } from '../../infrastructure/repositories/user.repository.js'
+import VendorRepository from '../../infrastructure/repositories/vendor.repository.js'
+import { BookingRepositoryImpl } from '../../infrastructure/repositories/booking.repository.js'
+import { CloudinaryService } from '../../infrastructure/services/cloudinaryService.js'
+import { HashService } from '../../infrastructure/services/HashService.js'
+import { MailServiceImpl } from '../../infrastructure/services/MailService.js'
+import { TokenService }from '../../infrastructure/services/TokenService.js'
+import { OtpService } from '../../infrastructure/services/OtpService.js'
+import { OtpStoreService } from '../../infrastructure/services/OtpStoreService.js'
+import { UserGetProfileUsecase } from '../../application/user/usecases/profile/user.getProfile.usecase.js';
+import { UserUpdateProfileUsecase } from '../../application/user/usecases/profile/user.updateProfile.usecase.js'
+import { UserProfileController } from './user/user.profileController.js'
+import { RequestEmailChangeOtpUsecase } from '../../application/user/usecases/profile/requestEmailchangeOtp.js'
+import { VerifyEmailChangeOtpUsecase } from '../../application/user/usecases/profile/verifyEmailChangeOtp.usecase.js'
+import { ResendEmailChangeOtpUsecase } from '../../application/user/usecases/profile/resendEmailChangeOtp.usecase.js'
+import { UserUpdateProfileImageUsecase } from '../../application/user/usecases/profile/user.updateProfileImage.usecase.js'
+import { UserRemoveProfileImageUsecase } from '../../application/user/usecases/profile/user.removeProfileImage.usecase.js'
+import { UserAddToWishlistUsecase } from '../../application/user/usecases/wishlist/user.addToWishlist.usecase.js'
+import { UserWishlistController } from './user/user.wishlistController.js'
+import { UserGetWishlistUsecase } from '../../application/user/usecases/wishlist/user.getWishlist.usecase.js'
+import { UserRemoveWishlistUsecase } from '../../application/user/usecases/wishlist/user.removeWishlist.usecase.js'
+import { UserUpdateAccountStatusUsecase } from '../../application/user/usecases/account/user.updateAccountStatus.usecase.js'
+import { UserAccountController } from './user/user.accountController.js'
+import { PaymentRepository } from "../../infrastructure/repositories/payment.repository.js";
+import { UserGetTopVenuesUsecase } from '../../application/user/usecases/venue/user.getTopVenue.usacase.js'
+import { VendorAuthController } from './vendor/vendor.authController.js'
+import { RegisterVendorUsecase } from '../../application/vendor/usecases/auth/vendor.registerVendor.useCase.js'
+import { LoginVendorUsecase } from '../../application/vendor/usecases/auth/vendor.loginVendor.useCase.js'
+import { VendorVerifyOtpUseCase } from '../../application/vendor/usecases/auth/vendor.verifyOtp.usecase.js'
+import VendorrResendOtpUseCase from '../../application/vendor/usecases/auth/vendor.resendOtp.usecase.js'
+import { VendorRefreshTokenUseCase } from '../../application/vendor/usecases/auth/vendor.refreshToken.usecase.js'
+import VendorForgotPasswordUseCase from '../../application/vendor/usecases/auth/vendor.forgotPassword.usecase.js'
+import { VendorResetPasswordUseCase } from '../../application/vendor/usecases/auth/vendor.resetPassword.usecase.js'
+import { VendorLogoutUseCase } from '../../application/vendor/usecases/auth/vendor.logout.usecase.js'
+import { LoginAdminUsecase } from '../../application/admin/usecases/auth/admin.login.usecase.js'
+import { AdminRepository } from '../../infrastructure/repositories/admin.repository.js'
+import { AdminAuthController } from './admin/admin.authController.js'
+import { AdminLogoutUseCase } from '../../application/admin/usecases/auth/admin.logOut.usecase.js'
+import { AdminRefreshTokenUseCase } from '../../application/admin/usecases/auth/admin.refreshToken.usecase.js'
+import { ChangeVendorPasswordUsecase } from '../../application/vendor/usecases/profile/changeVendorPassword.usecase.js'
+import { UserChangePasswordUsecase } from '../../application/user/usecases/profile/user.changePassword.usecase.js'
+import { UserGetSimilarVenuesUsecase } from '../../application/user/usecases/venue/user.getSimilarVenues.usecase.js'
+import { UserCancelBookingUsecase } from "../../application/user/usecases/booking/user.cancelBooking.usecase.js";
+
+//
+import { ReservationService } from "../../infrastructure/services/reservationService.js";
+import { UserReserveBookingUsecase } from "../../application/user/usecases/booking/user.reserveBooking.usecase.js";
+import { UserConfirmBookingUsecase } from "../../application/user/usecases/booking/user.confirmBooking.usecase.js";
+import { UserBookingController } from "../controllers/user/user.booking.controller.js";
+import { UserGetBookingsUsecase } from "../../application/user/usecases/booking/user.getBookings.usecase.js";
+import { UserGetBookingByIdUsecase } from "../../application/user/usecases/booking/user.getBookingById.usecase.js";
+import { UserPaymentReminderUsecase } from "../../application/user/usecases/booking/user.paymentReminder.usecase.js";
+import { UnifiedGetMeUsecase } from '../../application/common/unified.getMe.usecase.js'
+import { UnifiedAuthController } from './common/common.authController.js'
+//repository
+const iVenueRepository = new VenueRepository();
+const iUserRepository = new UserRepository();
+const iVendorRepository = new VendorRepository();
+const iPaymentRepository = new PaymentRepository();
+const bookingRepository = new BookingRepositoryImpl();
+const iAdminRepository = new AdminRepository()
+
+const repositories = {
+ customer: iUserRepository,
+ vendor: iVendorRepository,
+ admin: iAdminRepository
+}
+
+// --- services ---
+const iCloudinaryService = new CloudinaryService()
+const iMailService = new MailServiceImpl()
+const iHashService = new HashService()
+const iOtpService = new OtpService()
+const iOtpStoreService = new OtpStoreService(redisClient)
+export const iTokenService = new TokenService()
+export const iReservationService =
+ new ReservationService(redisClient);
+
+
+
+const iGetMeUsecase = new UnifiedGetMeUsecase(
+ iTokenService,
+ repositories
+)
+// --- admin auth usecase---
+const iAdminLoginUsecase = new LoginAdminUsecase (
+ iAdminRepository,
+ iHashService,
+ iTokenService
+)
+const iAdminLogoutUsecase = new AdminLogoutUseCase (
+ iAdminRepository,
+ iHashService,
+ iTokenService
+)
+const iAdminRefreshToken = new AdminRefreshTokenUseCase (
+ iAdminRepository,
+ iTokenService,
+ iHashService
+)
+// --- admin user usecases ---
+const iAdminGetAllUsersUsecase = new AdminGetAllUsersUsecase(iUserRepository)
+const iAdminUpdateUserStatusUsecase = new AdminUpdateUserStatusUsecase(iUserRepository)
+
+// --- admin vendor usecases ---
+const iAdminGetAllVendorsUsecase = new AdminGetAllVendorsUsecase(iVendorRepository)
+const iAdminGetVendorByIdUsecase = new AdminGetVendorByIdUsecase(iVendorRepository)
+const iAdminApproveVendorUsecase = new AdminApproveVendorUsecase(iVendorRepository, iMailService)
+const iAdminRejectVendorUsecase = new AdminRejectVendorUsecase(iVendorRepository, iMailService)
+const iAdminUpdateVendorStatusUsecase = new AdminUpdateVendorStatusUsecase(iVendorRepository)
+
+// --- admin venue usecases ---
+const iAdminGetAllVenueUsecase = new AdminGetAllVenuesUsecase(iVenueRepository)
+const iAdminGetVenueByIdUsecase = new AdminGetVenueByIdUsecase(iVenueRepository)
+const iAdminApproveVenueUsecase = new AdminApproveVenueUsecase(iVenueRepository, iMailService)
+const iAdminRejectVenueUsecase = new AdminRejectVenueUsecase(iVenueRepository, iMailService)
+const iAdminUpdateVenueBlockStatusUsecase = new AdminUpdateVenueBlockStatusUsecase(iVenueRepository)
+
+//adminBookingUsecases
+const iAdminGetAllBookingUsecase = new AdminGetAllBookingsUsecase(bookingRepository)
+const iAdminGetBookingByIdUsecase = new AdminGetBookingByIdUsecase(bookingRepository)
+const iAdminBookingStatisticsUsecase = new AdminGetBookingStatisticsUsecase(bookingRepository)
+
+//adminPaymentUsecases
+const iAdminGetAllPaymentUsecase = new AdminGetAllPaymentsUsecase(iPaymentRepository)
+const iAdminGetPaymentByIdUsecase = new AdminGetPaymentByIdUsecase(iPaymentRepository)
+const iAdminPaymentStatisticsUsecase = new AdminGetPaymentStatisticsUsecase(iPaymentRepository)
+
+//adminDashboardUsecases
+const iAdminDashboardStatisticsUsecase = new AdminDashboardStatisticsUsecase(iAdminRepository)
+//user auth usecases
+const iRegisterUserUseCase = new RegisterUserUseCase(
+ iUserRepository,
+ iHashService,
+ iOtpService,
+ iOtpStoreService,
+ iMailService
+)
+const iLoginUserUseCase = new LoginUserUseCase(
+ iUserRepository,
+ iHashService,
+ iTokenService
+)
+const iUserLogoutUseCase = new UserLogoutUseCase(
+ iUserRepository,
+ iHashService,
+ iTokenService
+)
+const iUserRefreshTokenUseCase = new UserRefreshTokenUseCase(
+ iUserRepository,
+ iTokenService,
+ iHashService
+)
+const iUserVerifyOtpUseCase = new UserVerifyOtpUseCase(
+ iUserRepository,
+ iOtpService,
+ iOtpStoreService
+)
+const iUserResendOtpUseCase = new UserResendOtpUseCase(
+ iUserRepository,
+ iOtpService,
+ iOtpStoreService,
+ iMailService
+)
+const iUserForgotPasswordUseCase = new UserForgotPasswordUseCase(
+ iUserRepository,
+ iTokenService,
+ iMailService,
+ iHashService
+)
+const iUserResetPasswordUseCase = new UserResetPasswordUseCase(
+ iUserRepository,
+ iHashService
+)
+
+// --- vendor usecases ---
+const iRegsiterVendor = new RegisterVendorUsecase(
+ iVendorRepository,
+ iHashService,
+ iOtpService,
+ iOtpStoreService,
+ iMailService
+)
+const iLoginVendor = new LoginVendorUsecase (
+ iVendorRepository,
+ iHashService,
+ iTokenService
+)
+const iVerifyVendorOtp = new VendorVerifyOtpUseCase (
+ iVendorRepository,
+ iOtpService,
+ iOtpStoreService
+)
+const iResendVendorOtp = new VendorrResendOtpUseCase (
+ iVendorRepository,
+ iOtpService,
+ iOtpStoreService,
+ iMailService
+)
+const iVendorRefreshToken = new VendorRefreshTokenUseCase(
+ iVendorRepository,
+ iTokenService,
+ iHashService
+)
+const iVendorForgotPassword = new VendorForgotPasswordUseCase (
+ iVendorRepository,
+ iTokenService,
+ iMailService,
+ iHashService
+)
+const iVendorResetPassword = new VendorResetPasswordUseCase (
+ iVendorRepository,
+ iHashService
+)
+const iVendorLogout = new VendorLogoutUseCase (
+ iVendorRepository,
+ iHashService,
+ iTokenService
+)
+const iCreateVenueUsecase = new VendorCreateVenueUsecase(
+ iVenueRepository,
+ iVendorRepository
+)
+const iUpdateVenueUsecase = new VendorEditVenueUsecase(
+ iVenueRepository,
+ iCloudinaryService,
+ iVendorRepository
+)
+const iVendorVenueGetById = new VendorGetVenueByIdUsecase(
+ iVenueRepository,
+ iVendorRepository
+)
+const iVendorGetAllVenues = new VendorGetAllVenuesUsecase(
+ iVenueRepository,
+ iVendorRepository
+)
+const iVendorDeleteVenue = new VendorDeleteVenueUsecase(
+ iVenueRepository,
+ iVendorRepository
+)
+const iUpdatevenueStatus = new VendorUpdateVenueStatusUsecase(
+ iVenueRepository,
+ iVendorRepository
+)
+const iGetVendorProfileUsecase = new GetVendorProfileUsecase(iVendorRepository)
+const iUpdateVendorProfileUsecase = new VendorUpdateProfileUsecase(iVendorRepository)
+const getVendorBookingsUsecase = new GetVendorBookingsUsecase(bookingRepository)
+const getBookingByIdUsecase = new GetBookingByIdUsecase(bookingRepository)
+const getDashboardStatsUsecase = new GetDashboardStatsUsecase(iVenueRepository, bookingRepository)
+const changeVendorPasswordUsecase = new ChangeVendorPasswordUsecase(iVendorRepository, iHashService)
+
+// --- user usecases ---
+const iUserGetAllVenues = new UserGetAllVenuesUsecase(iVenueRepository)
+const iUserGetVenueById = new UserGetVenueByIdUsecase(iVenueRepository)
+const iUserGetTopeVenues = new UserGetTopVenuesUsecase(iVenueRepository)
+const iUserSimilarVenues = new UserGetSimilarVenuesUsecase (
+ iVenueRepository,
+ iUserRepository
+)
+
+const iUserAddToWishlist = new UserAddToWishlistUsecase(
+ iUserRepository,
+ iVenueRepository
+)
+const iUserGetWishlist = new UserGetWishlistUsecase(
+ iUserRepository
+)
+const iUserRemoveWishlist = new UserRemoveWishlistUsecase(
+ iUserRepository,
+ iVenueRepository
+)
+const iUserUpdateAccountStatus = new UserUpdateAccountStatusUsecase(
+ iUserRepository
+)
+
+const iUserGetProfile=new UserGetProfileUsecase(
+ iUserRepository
+)
+const iUserUpdateProfile=new UserUpdateProfileUsecase(
+ iUserRepository
+)
+const iRequestEmailChangeOtp= new RequestEmailChangeOtpUsecase(
+ iUserRepository,
+ iHashService,
+ iOtpService,
+ iMailService
+)
+
+const iVerifyEmailChangeOtp= new VerifyEmailChangeOtpUsecase(
+ iUserRepository,
+ iHashService
+)
+const iResendEmailChangeOtp = new ResendEmailChangeOtpUsecase(
+ iUserRepository,
+ iHashService,
+ iOtpService,
+ iMailService
+)
+const iUserUpdateProfileImage = new UserUpdateProfileImageUsecase(
+ iUserRepository
+)
+const iUserRemoveProfileImage = new UserRemoveProfileImageUsecase(
+ iUserRepository
+)
+const userChangePasswordUsecase = new UserChangePasswordUsecase(iUserRepository, iHashService)
+//userbooking usecase
+const iUserReserveBookingUsecase =
+ new UserReserveBookingUsecase(
+ bookingRepository,
+ iVenueRepository,
+ iReservationService
+ );
+
+const iUserConfirmBookingUsecase =
+ new UserConfirmBookingUsecase(
+ bookingRepository,
+ iReservationService ,
+ iUserRepository,
+ iVenueRepository,
+ iMailService
+ );
+const iUserGetBookingsUsecase =
+ new UserGetBookingsUsecase(bookingRepository);
+
+const iUserGetBookingByIdUsecase =
+ new UserGetBookingByIdUsecase(bookingRepository);
+
+const iUserPaymentReminderUsecase =
+ new UserPaymentReminderUsecase(
+ bookingRepository,
+ iUserRepository,
+ iVenueRepository,
+ iMailService
+);
+const iUserCancelBookingUsecase =
+ new UserCancelBookingUsecase(
+ bookingRepository,
+ iMailService
+ );
+
+// --- controllers ---
+export const iVendorVenueController = new VendorVenueController(
+ iCreateVenueUsecase,
+ iUpdateVenueUsecase,
+ iVendorVenueGetById,
+ iVendorGetAllVenues,
+ iVendorDeleteVenue,
+ iUpdatevenueStatus
+)
+export const iAdminDashboardController = new AdminDashboardController(iAdminDashboardStatisticsUsecase)
+export const iAdminUserController = new AdminUserController(
+ iAdminGetAllUsersUsecase,
+ iAdminUpdateUserStatusUsecase
+)
+export const iAdminVendorController = new AdminVendorController(
+ iAdminGetAllVendorsUsecase,
+ iAdminGetVendorByIdUsecase,
+ iAdminApproveVendorUsecase,
+ iAdminRejectVendorUsecase,
+ iAdminUpdateVendorStatusUsecase
+)
+export const iAdminVenueController = new AdminVenueController(
+ iAdminGetAllVenueUsecase,
+ iAdminGetVenueByIdUsecase,
+ iAdminApproveVenueUsecase,
+ iAdminRejectVenueUsecase,
+ iAdminUpdateVenueBlockStatusUsecase
+)
+export const iUserVenueController = new UserVenueController (
+ iUserGetAllVenues,
+ iUserGetVenueById,
+ iUserGetTopeVenues,
+ iUserSimilarVenues
+)
+export const iVendorProfileController = new VendorProfileController(
+ iGetVendorProfileUsecase,
+ iUpdateVendorProfileUsecase,
+ changeVendorPasswordUsecase
+)
+
+//--
+export const iVendorBookingController = new VendorBookingController(
+ getVendorBookingsUsecase,
+ getBookingByIdUsecase
+)
+export const iUserProfileController = new UserProfileController(
+ iUserGetProfile,
+ iUserUpdateProfile,
+ iRequestEmailChangeOtp,
+ iVerifyEmailChangeOtp,
+ iResendEmailChangeOtp,
+ iUserUpdateProfileImage,
+ iUserRemoveProfileImage,
+ userChangePasswordUsecase
+
+)
+export const iUserWishlistController = new UserWishlistController(
+ iUserAddToWishlist,
+ iUserGetWishlist,
+ iUserRemoveWishlist
+)
+export const iUserAccountController = new UserAccountController(
+ iUserUpdateAccountStatus
+)
+//--
+export const iAdminBookingController = new AdminBookingController(
+ iAdminGetAllBookingUsecase,
+ iAdminGetBookingByIdUsecase,
+ iAdminBookingStatisticsUsecase
+)
+//--
+export const iAdminPaymentController = new AdminPaymentController(
+ iAdminGetAllPaymentUsecase,
+ iAdminGetPaymentByIdUsecase,
+ iAdminPaymentStatisticsUsecase
+);
+
+export const iUserAuthController = new UserAuthController(
+ iRegisterUserUseCase,
+ iLoginUserUseCase,
+ iUserLogoutUseCase,
+ iUserRefreshTokenUseCase,
+ iUserVerifyOtpUseCase,
+ iUserResendOtpUseCase,
+ iUserForgotPasswordUseCase,
+ iUserResetPasswordUseCase,
+)
+
+export const iVendorDashboardController = new VendorDashboardController(
+ getDashboardStatsUsecase
+);
+
+export const iVendorAuthController = new VendorAuthController (
+ iRegsiterVendor,
+ iLoginVendor,
+ iVerifyVendorOtp,
+ iResendVendorOtp,
+ iVendorRefreshToken,
+ iVendorForgotPassword,
+ iVendorResetPassword,
+ iVendorLogout,
+)
+
+export const iAdminAuthController = new AdminAuthController (
+ iAdminLoginUsecase,
+ iAdminLogoutUsecase,
+ iAdminRefreshToken,
+)
+
+export const iUserBookingController =
+ new UserBookingController(
+ iUserReserveBookingUsecase,
+ iUserConfirmBookingUsecase,
+ iUserGetBookingsUsecase,
+ iUserGetBookingByIdUsecase,
+ iUserCancelBookingUsecase
+ );
+
+export { iUserPaymentReminderUsecase };
+
+export const iUnifiedAuthController = new UnifiedAuthController (
+ iGetMeUsecase
+)
\ No newline at end of file
diff --git a/server/src/presentation/controllers/user/.gitkeep b/server/src/presentation/controllers/user/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/presentation/controllers/user/user.accountController.js b/server/src/presentation/controllers/user/user.accountController.js
new file mode 100644
index 0000000000..8752095ce5
--- /dev/null
+++ b/server/src/presentation/controllers/user/user.accountController.js
@@ -0,0 +1,32 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+
+export class UserAccountController {
+
+ constructor(userUpdateAccountStatusUsecase){
+ this._userUpdateAccountStatusUsecase =
+ userUpdateAccountStatusUsecase;
+ }
+
+ updateAccountStatus = asyncHandler(async(req,res)=>{
+
+ const userId = req.user.userId;
+
+ const { isActive } = req.body;
+
+ const updatedUser =
+ await this._userUpdateAccountStatusUsecase.execute(
+ userId,
+ isActive
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "Account status updated successfully",
+ updatedUser
+ );
+ });
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/user/user.authController.js b/server/src/presentation/controllers/user/user.authController.js
new file mode 100644
index 0000000000..151c4fe866
--- /dev/null
+++ b/server/src/presentation/controllers/user/user.authController.js
@@ -0,0 +1,80 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+import { authMessages } from "../../../shared/constants/messages/authMessages.js";
+
+const REFRESH_COOKIE_OPTIONS = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ maxAge: 7 * 24 * 60 * 60 * 1000
+};
+
+export class UserAuthController {
+ constructor(
+ registerUserUseCase,
+ loginUserUseCase,
+ logoutUseCase,
+ refreshTokenUseCase,
+ verifyOtpUseCase,
+ resendOtpUseCase,
+ forgotPasswordUseCase,
+ resetPasswordUseCase,
+ ) {
+ this._registerUserUseCase = registerUserUseCase;
+ this._loginUserUseCase = loginUserUseCase;
+ this._logoutUseCase = logoutUseCase;
+ this._refreshTokenUseCase = refreshTokenUseCase;
+ this._verifyOtpUseCase = verifyOtpUseCase;
+ this._resendOtpUseCase = resendOtpUseCase;
+ this._forgotPasswordUseCase = forgotPasswordUseCase;
+ this._resetPasswordUseCase = resetPasswordUseCase;
+ }
+
+ register = asyncHandler(async (req, res) => {
+ await this._registerUserUseCase.execute({...req.body});
+ return sendSuccess(res, statusCode.CREATED, authMessages.success.REGISTERED);
+ });
+
+ login = asyncHandler(async (req, res) => {
+ const { accessToken, refreshToken, user } = await this._loginUserUseCase.execute({...req.body});
+ res.cookie("refreshToken", refreshToken, REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, authMessages.success.LOGIN, { accessToken, user });
+ });
+
+ refreshToken = asyncHandler(async (req, res) => {
+ const token = req.cookies?.refreshToken;
+ const { accessToken, refreshToken, user } = await this._refreshTokenUseCase.execute(token);
+ res.cookie("refreshToken", refreshToken, REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, authMessages.success.TOKEN_REFRESHED, { accessToken, user });
+ });
+
+ forgotPassword = asyncHandler(async (req, res) => {
+ await this._forgotPasswordUseCase.execute({email: req.body.email});
+ return sendSuccess(res, statusCode.OK, authMessages.success.FORGOT_PASSWORD);
+ });
+
+ resetPassword = asyncHandler(async (req, res) => {
+ const { token, password } = req.body;
+ await this._resetPasswordUseCase.execute(token, password);
+ return sendSuccess(res, statusCode.OK, authMessages.success.RESET_PASSWORD);
+ });
+
+ logout = asyncHandler(async (req, res) => {
+ const accessToken = req.headers.authorization?.split(' ')[1]
+ const refreshToken = req.cookies?.refreshToken;
+ await this._logoutUseCase.execute(refreshToken, accessToken);
+ res.clearCookie("refreshToken", REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, authMessages.success.LOGOUT);
+ });
+
+ verifyOtp = asyncHandler(async (req, res) => {
+ await this._verifyOtpUseCase.execute({...req.body});
+ return sendSuccess(res, statusCode.OK, authMessages.success.OTP_VERIFIED);
+ });
+
+ resendOtp = asyncHandler(async (req, res) => {
+ await this._resendOtpUseCase.execute({email: req.body.email});
+ return sendSuccess(res, statusCode.OK, authMessages.success.OTP_RESENT);
+ });
+}
diff --git a/server/src/presentation/controllers/user/user.booking.controller.js b/server/src/presentation/controllers/user/user.booking.controller.js
new file mode 100644
index 0000000000..ce93303518
--- /dev/null
+++ b/server/src/presentation/controllers/user/user.booking.controller.js
@@ -0,0 +1,133 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+
+export class UserBookingController {
+
+ constructor(
+ userReserveBookingUsecase,
+ userConfirmBookingUsecase,
+ userGetBookingsUsecase,
+ userGetBookingByIdUsecase,
+ userCancelBookingUsecase
+ ) {
+ this._userReserveBookingUsecase = userReserveBookingUsecase;
+ this._userConfirmBookingUsecase = userConfirmBookingUsecase;
+ this._userGetBookingsUsecase = userGetBookingsUsecase;
+ this._userGetBookingByIdUsecase = userGetBookingByIdUsecase;
+ this._userCancelBookingUsecase = userCancelBookingUsecase;
+ }
+
+ reserveBooking = asyncHandler(async (req, res) => {
+
+ const userId = req.user.id;
+
+ const result =
+ await this._userReserveBookingUsecase.execute(
+ userId,
+ req.body
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.CREATED,
+ "Booking reserved successfully.",
+ result
+ );
+
+ });
+
+ confirmBooking = asyncHandler(async (req, res) => {
+
+ const result =
+ await this._userConfirmBookingUsecase.execute({
+ reservationId: req.body.reservationId,
+ venueId: req.body.venueId,
+ bookingDate: req.body.bookingDate
+ });
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "Booking confirmed successfully.",
+ result
+ );
+
+ });
+
+ getBookings = asyncHandler(async (req, res) => {
+
+ const userId = req.user.id;
+
+ const {
+ page,
+ limit,
+ status,
+ search,
+ sortBy
+ } = req.query;
+
+ const result =
+ await this._userGetBookingsUsecase.execute(
+ userId,
+ Number(page) || 1,
+ Number(limit) || 10,
+ status,
+ search,
+ sortBy
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "Bookings fetched successfully.",
+ result
+ );
+
+ });
+ getBookingById = asyncHandler(async (req, res) => {
+
+ const userId = req.user.id;
+
+ const { bookingId } = req.params;
+
+ const result =
+ await this._userGetBookingByIdUsecase.execute(
+ userId,
+ bookingId
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "Booking fetched successfully.",
+ result
+ );
+
+ });
+
+ cancelBooking = asyncHandler(async (req, res) => {
+
+ const userId = req.user.id;
+
+ const { bookingId } = req.params;
+
+ const { cancellationReason } = req.body;
+
+ const result =
+ await this._userCancelBookingUsecase.execute(
+ userId,
+ bookingId,
+ cancellationReason
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ "Booking cancelled successfully.",
+ result
+ );
+
+ });
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/user/user.profileController.js b/server/src/presentation/controllers/user/user.profileController.js
new file mode 100644
index 0000000000..3e1e8b4a84
--- /dev/null
+++ b/server/src/presentation/controllers/user/user.profileController.js
@@ -0,0 +1,143 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+// import { ValidationError } from "../../../domain/errors/ValidationError.js";
+import { UserMessage } from "../../../shared/constants/messages/userMessages.js";
+
+export class UserProfileController {
+ constructor(
+ userGetProfileUsecase,
+ userUpdateProfileUsecase,
+ requestEmailChangeOtpUsecase,
+ verifyEmailChangeOtpUsecase,
+ resendEmailChangeOtpUsecase,
+ userUpdateProfileImageUsecase,
+ userRemoveProfileImageUsecase,
+ userChangePasswordUsecase
+ ) {
+ this._userGetProfileUsecase = userGetProfileUsecase;
+ this._userUpdateProfileUsecase = userUpdateProfileUsecase;
+ this._requestEmailChangeOtpUsecase = requestEmailChangeOtpUsecase;
+ this._verifyEmailChangeOtpUsecase = verifyEmailChangeOtpUsecase;
+ this._resendEmailChangeOtpUsecase = resendEmailChangeOtpUsecase;
+ this._userUpdateProfileImageUsecase = userUpdateProfileImageUsecase;
+ this._userRemoveProfileImageUsecase = userRemoveProfileImageUsecase;
+ this._userChangePasswordUsecase = userChangePasswordUsecase;
+ }
+
+ getProfile = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+ // const userId = "6a5c82d2a4cb28be7d10521f";
+
+ const user = await this._userGetProfileUsecase.execute(userId);
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.PROFILE_FETCHED,
+ user
+ );
+ });
+
+ updateProfile = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+ const { fullName, phone } = req.body;
+ const updatedUser = await this._userUpdateProfileUsecase.execute(
+ userId,
+ fullName,
+ phone
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.PROFILE_UPDATED,
+ updatedUser
+ );
+ });
+
+ requestEmailChangeOtp = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+ const { newEmail } = req.body;
+ const result = await this._requestEmailChangeOtpUsecase.execute(
+ userId,
+ newEmail
+ );
+ return sendSuccess(res, statusCode.OK, result.message);
+ });
+
+ verifyEmailChangeOtp = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+ const { otp } = req.body;
+ const updatedUser = await this._verifyEmailChangeOtpUsecase.execute(
+ userId,
+ otp
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.EMAIL_UPDATED,
+ updatedUser
+ );
+ });
+
+ resendEmailChangeOtp = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+
+ await this._resendEmailChangeOtpUsecase.execute(userId);
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.PROFILE_FETCHED,
+ );
+ });
+
+
+
+ updateProfileImage = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+
+ const profileImage = {
+ publicId: req.file.filename,
+ url: req.file.path,
+ };
+ const updatedUser = await this._userUpdateProfileImageUsecase.execute(
+ userId,
+ profileImage
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.PROFILE_IMAGE_UPDATED,
+ updatedUser
+ );
+ });
+
+ removeProfileImage = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+
+ const updatedUser = await this._userRemoveProfileImageUsecase.execute(
+ userId
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.PROFILE_IMAGE_REMOVED,
+ updatedUser
+ );
+ });
+
+ changePassword = asyncHandler(async (req, res) => {
+ const userId = req.user.id;
+ await this._userChangePasswordUsecase.execute({ userId, ...req.body });
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.PASSWORD_CHANGED
+ );
+ });
+}
diff --git a/server/src/presentation/controllers/user/user.venueController.js b/server/src/presentation/controllers/user/user.venueController.js
new file mode 100644
index 0000000000..e21d02b75e
--- /dev/null
+++ b/server/src/presentation/controllers/user/user.venueController.js
@@ -0,0 +1,44 @@
+import { asyncHandler } from '../../../shared/utils/asyncHandler.js'
+import { sendSuccess } from '../../../shared/utils/apiResponse.js'
+import { statusCode } from '../../../shared/constants/enums/statusCode.js'
+
+
+
+export class UserVenueController {
+ constructor (
+ userGetAllVenuesUsecase,
+ userGetVnueByIdUsecase,
+ userGetTopVenuesUsecase,
+ userGetSimilarVenuesUsecase,
+ ) {
+ this._userGetAllVenues = userGetAllVenuesUsecase
+ this._userGetVenueById = userGetVnueByIdUsecase
+ this._userGetTopVenues = userGetTopVenuesUsecase
+ this._userGetSimilarVenues = userGetSimilarVenuesUsecase
+ }
+
+ getAllVenues = asyncHandler( async (req, res ) => {
+ const { search, category, rating, amenities, capacityType, capacity, priceType, minPrice, maxPrice, page, limit,} = req.validatedQuery
+ const { data, totalPages, totalCount } = await this._userGetAllVenues.execute(search, category, rating, amenities, capacityType, capacity, priceType, minPrice, maxPrice, page, limit)
+ return sendSuccess(res, statusCode.OK, '', { data, totalCount, totalPages })
+ })
+
+ getVenueById = asyncHandler( async (req, res ) => {
+ const venueId = req.params.venueId
+ const venue = await this._userGetVenueById.execute(venueId)
+ return sendSuccess(res, statusCode.OK, '', venue)
+ })
+
+ getTopVenues = asyncHandler( async (req, res) => {
+ const venues = await this._userGetTopVenues.execute()
+ // console.log('venue: ', venues)
+ return sendSuccess(res, statusCode.OK,'', venues)
+ })
+
+ getSimilarVenues = asyncHandler( async (req, res) => {
+ const userId = "6a5c82d2a4cb28be7d10521f";
+ const venueId = req.params.venueId
+ const venues = await this._userGetSimilarVenues.execute(userId, venueId)
+ return sendSuccess(res, statusCode.OK, '', venues)
+ })
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/user/user.wishlistController.js b/server/src/presentation/controllers/user/user.wishlistController.js
new file mode 100644
index 0000000000..46ab215d4e
--- /dev/null
+++ b/server/src/presentation/controllers/user/user.wishlistController.js
@@ -0,0 +1,69 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+import { UserMessage } from "../../../shared/constants/messages/userMessages.js";
+
+export class UserWishlistController {
+
+ constructor(
+ userAddToWishlistUsecase,
+ userGetWishlistUsecase,
+ userRemoveWishlistUsecase
+ ){
+ this._userAddToWishlistUsecase = userAddToWishlistUsecase;
+ this._userGetWishlistUsecase = userGetWishlistUsecase;
+ this._userRemoveWishlistUsecase = userRemoveWishlistUsecase;
+ }
+ addToWishlist = asyncHandler(async(req,res)=>{
+
+ // const userId = req.user.userId;
+ const userId = req.user.id;
+
+ const { venueId } = req.params;
+
+ const wishlist = await this._userAddToWishlistUsecase.execute(
+ userId,
+ venueId
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.WISHLIST_ADDED,
+ wishlist
+ );
+ });
+ getWishlist = asyncHandler(async(req,res)=>{
+
+ const userId = req.user.id;
+
+ const wishlist = await this._userGetWishlistUsecase.execute(
+ userId
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.WISHLIST_FETCHED,
+ wishlist
+ );
+ });
+ removeWishlist = asyncHandler(async(req,res)=>{
+
+ const userId = req.user.id;
+ const { venueId } = req.params;
+
+ const wishlist = await this._userRemoveWishlistUsecase.execute(
+ userId,
+ venueId
+ );
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ UserMessage.success.WISHLIST_REMOVED,
+ wishlist
+ );
+ });
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/vendor/.gitkeep b/server/src/presentation/controllers/vendor/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/presentation/controllers/vendor/VendorBookingController.js b/server/src/presentation/controllers/vendor/VendorBookingController.js
new file mode 100644
index 0000000000..312e56ae83
--- /dev/null
+++ b/server/src/presentation/controllers/vendor/VendorBookingController.js
@@ -0,0 +1,59 @@
+import { asyncHandler } from '../../../shared/utils/asyncHandler.js'
+import { sendSuccess } from '../../../shared/utils/apiResponse.js'
+import { statusCode } from '../../../shared/constants/enums/statusCode.js'
+import { BookingMessages } from '../../../shared/constants/messages/bookingMessages.js'
+
+export class VendorBookingController {
+
+ constructor(
+ getVendorBookingsUsecase,
+ getBookingByIdUsecase
+ ) {
+
+ this._getVendorBookingsUsecase =
+ getVendorBookingsUsecase
+ this._getBookingByIdUsecase =
+ getBookingByIdUsecase
+
+ }
+
+ getBookings = asyncHandler(
+
+ async (req, res) => {
+
+ const vendorId = req.user.id;
+
+
+ const bookings =
+ await this._getVendorBookingsUsecase
+ .execute({vendorId, ...req.query})
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ BookingMessages.success.BOOKING_FETCHED,
+ bookings
+ )
+ }
+ )
+
+
+ getBookingById = asyncHandler(
+
+ async (req, res) => {
+ const { bookingId } = req.params
+
+ const vendorId = req.user.id;
+
+
+ const booking =
+ await this._getBookingByIdUsecase
+ .execute({bookingId, vendorId})
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ BookingMessages.success.BOOKING_FETCHED,
+ booking
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/server/src/presentation/controllers/vendor/VendorDashboardController.js b/server/src/presentation/controllers/vendor/VendorDashboardController.js
new file mode 100644
index 0000000000..7d84e59f33
--- /dev/null
+++ b/server/src/presentation/controllers/vendor/VendorDashboardController.js
@@ -0,0 +1,26 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+
+export class VendorDashboardController {
+ constructor(getDashboardStatsUsecase) {
+ this._getDashboardStatsUsecase = getDashboardStatsUsecase;
+ }
+
+ getDashboard = asyncHandler(async (req, res) => {
+ const vendorId = req.user.id;
+
+
+ const dashboard = await this._getDashboardStatsUsecase.execute(vendorId);
+
+ return sendSuccess(
+ res,
+
+ statusCode.OK,
+
+ "Dashboard fetched successfully",
+
+ dashboard
+ );
+ });
+}
diff --git a/server/src/presentation/controllers/vendor/VendorProfileController.js b/server/src/presentation/controllers/vendor/VendorProfileController.js
new file mode 100644
index 0000000000..903e6dd13b
--- /dev/null
+++ b/server/src/presentation/controllers/vendor/VendorProfileController.js
@@ -0,0 +1,72 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+import { VendorMessages } from "../../../shared/constants/messages/vendorMessages.js";
+
+export class VendorProfileController {
+ constructor(
+ GetVendorProfileUsecase,
+ UpdateVendorProfileUsecase,
+ ChangeVendorPasswordUsecase
+ ) {
+ this._getVendorProfileUsecase = GetVendorProfileUsecase;
+
+ this._updateVendorProfileUsecase = UpdateVendorProfileUsecase;
+
+ this._changeVendorPasswordUsecase = ChangeVendorPasswordUsecase;
+ }
+
+ getProfile = asyncHandler(async (req, res) => {
+ const vendorId = req.user.id;
+ const vendor = await this._getVendorProfileUsecase.execute(vendorId);
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ VendorMessages.success.PROFILE_FETCHED,
+ vendor
+ );
+ });
+
+ updateProfile = asyncHandler(async (req, res) => {
+ const vendorId = req.user.id;
+ let profileImage;
+ if(req.file){
+ profileImage = {
+ publicId: req.file.filename,
+ url: req.file.path,
+ };}
+
+
+ const vendor = await this._updateVendorProfileUsecase.execute({
+ vendorId,profileImage,
+
+ ...req.body,
+ });
+
+ return sendSuccess(
+ res,
+ statusCode.OK,
+ VendorMessages.success.PROFILE_UPDATED,
+ vendor
+ );
+ });
+
+ changePassword = asyncHandler(async (req, res) => {
+ const vendorId = req.user.id;
+
+ await this._changeVendorPasswordUsecase.execute({
+ vendorId,
+
+ ...req.body,
+ });
+
+ return sendSuccess(
+ res,
+
+ statusCode.OK,
+
+ VendorMessages.success.PASSWORD_CHANGED
+ );
+ });
+}
diff --git a/server/src/presentation/controllers/vendor/vendor.authController.js b/server/src/presentation/controllers/vendor/vendor.authController.js
new file mode 100644
index 0000000000..79ae50c1f6
--- /dev/null
+++ b/server/src/presentation/controllers/vendor/vendor.authController.js
@@ -0,0 +1,80 @@
+import { asyncHandler } from "../../../shared/utils/asyncHandler.js";
+import { sendSuccess } from "../../../shared/utils/apiResponse.js";
+import { statusCode } from "../../../shared/constants/enums/statusCode.js";
+import { authMessages } from "../../../shared/constants/messages/authMessages.js";
+
+const REFRESH_COOKIE_OPTIONS = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ maxAge: 7 * 24 * 60 * 60 * 1000
+};
+
+export class VendorAuthController {
+ constructor(
+ registerVendorUseCase,
+ loginVendorUseCase,
+ verifyVendorRegisterOtp,
+ resendVendorOtpUsecase,
+ vendorRefreshTokenUsecase,
+ vendorForgotPasswordUsecase,
+ vendorResetPasswordUsecase,
+ vendorLogoutUsecase,
+ ) {
+ this._registerVendorUseCase = registerVendorUseCase;
+ this._loginVendorUseCase = loginVendorUseCase;
+ this._verifyVendorRegisterOtp = verifyVendorRegisterOtp;
+ this._resendVendorOtp = resendVendorOtpUsecase;
+ this._refreshTokenUsecase = vendorRefreshTokenUsecase;
+ this._forgotPasswordUsecase = vendorForgotPasswordUsecase;
+ this._resetPasswordUseCase = vendorResetPasswordUsecase;
+ this._logoutUseCase = vendorLogoutUsecase;
+ }
+
+ register = asyncHandler(async (req, res) => {
+ await this._registerVendorUseCase.execute({...req.body});
+ return sendSuccess(res, statusCode.CREATED, '');
+ });
+
+ verifyOtp = asyncHandler(async (req, res) => {
+ await this._verifyVendorRegisterOtp.execute({...req.body});
+ return sendSuccess(res, statusCode.OK, authMessages.success.OTP_VERIFIED);
+ });
+
+ login = asyncHandler(async (req, res) => {
+ const { accessToken, refreshToken, user } = await this._loginVendorUseCase.execute({...req.body});
+ res.cookie("refreshToken", refreshToken, REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, '', { accessToken, user });
+ });
+
+ resendOtp = asyncHandler(async (req, res) => {
+ await this._resendVendorOtp.execute({email: req.body.email});
+ return sendSuccess(res, statusCode.OK, authMessages.success.OTP_RESENT);
+ });
+
+ refreshToken = asyncHandler(async (req, res) => {
+ const token = req.cookies?.refreshToken;
+ const { accessToken, refreshToken, vendor } = await this._refreshTokenUsecase.execute(token);
+ res.cookie("refreshToken", refreshToken, REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, authMessages.success.TOKEN_REFRESHED, { accessToken, vendor });
+ });
+
+ forgotPassword = asyncHandler(async (req, res) => {
+ await this._forgotPasswordUsecase.execute({email: req.body.email});
+ return sendSuccess(res, statusCode.OK, authMessages.success.FORGOT_PASSWORD);
+ });
+
+ resetPassword = asyncHandler(async (req, res) => {
+ const { token, password } = req.body;
+ await this._resetPasswordUseCase.execute(token, password);
+ return sendSuccess(res, statusCode.OK, authMessages.success.RESET_PASSWORD);
+ });
+
+ logout = asyncHandler(async (req, res) => {
+ const accessToken = req.headers.authorization?.split(' ')[1]
+ const refreshToken = req.cookies?.refreshToken;
+ await this._logoutUseCase.execute(refreshToken, accessToken);
+ res.clearCookie("refreshToken", REFRESH_COOKIE_OPTIONS);
+ return sendSuccess(res, statusCode.OK, authMessages.success.LOGOUT);
+ });
+}
diff --git a/server/src/presentation/controllers/vendor/vendor.venueController.js b/server/src/presentation/controllers/vendor/vendor.venueController.js
new file mode 100644
index 0000000000..23a16f7cba
--- /dev/null
+++ b/server/src/presentation/controllers/vendor/vendor.venueController.js
@@ -0,0 +1,91 @@
+import { asyncHandler } from '../../../shared/utils/asyncHandler.js'
+import { sendSuccess } from '../../../shared/utils/apiResponse.js'
+import { statusCode } from '../../../shared/constants/enums/statusCode.js'
+import { VenueMessages } from '../../../shared/constants/messages/venueMessages.js'
+
+
+export class VendorVenueController {
+ constructor (
+ VendorCreateVenueUsecase,
+ VendorEditVenueUsecase,
+ VendorGetVenueByIdUsecase,
+ VendorGetAllVenuesUsecase,
+ VendorDeleteVenueUsecase,
+ VendorUpdateVenueStatusUsecase,
+ ){
+ this._vendorCreateVenueUsecase = VendorCreateVenueUsecase
+ this._vendorEditVenueUsecase = VendorEditVenueUsecase
+ this._vendorGetVenueByIdUsecase = VendorGetVenueByIdUsecase
+ this._vendorGetAllVenuesUsecase = VendorGetAllVenuesUsecase
+ this._vendorDeleteVenueUsecase = VendorDeleteVenueUsecase
+ this._vendorUpdateVenueStatusUsecase = VendorUpdateVenueStatusUsecase
+ }
+
+ createVenue = asyncHandler( async (req, res) => {
+ const vendorId = req.user.id
+
+ // console.log('files', req.files)
+ const images = (req.files.images || []).map(file => ({
+ publicId: file.filename,
+ url: file.path
+ }))
+ const license = (req.files.license || []).map(file => ({
+ publicId: file.filename,
+ url: file.path
+ }))
+ const venue = await this._vendorCreateVenueUsecase.execute({vendorId,...req.body, images, license})
+ return sendSuccess(res, statusCode.OK, VenueMessages.success.VENUE_CREATED, venue)
+ })
+
+ updateVenue = asyncHandler( async(req, res) => {
+ const vendorId = req.user.id
+
+ const venueId = req.params.venueId
+ const newImages = (req.files.images || []).map(file => ({
+ publicId: file.filename,
+ url: file.path
+ }))
+
+ const newLicense = (req.files.license || []).map(file => ({
+ publicId: file.filename,
+ url: file.path
+ }))
+
+ // console.log("from controller: ", req.body)
+ const venue = await this._vendorEditVenueUsecase.execute({vendorId, venueId, newImages, newLicense, ...req.body})
+ return sendSuccess(res, statusCode.OK, '', venue)
+ })
+
+ getById = asyncHandler( async (req, res) => {
+ const vendorId = req.user.id
+
+ const venueId = req.params.venueId
+ const venue = await this._vendorGetVenueByIdUsecase.execute(vendorId,venueId)
+ return sendSuccess(res, statusCode.OK, '', venue)
+ })
+
+ getAllVenues = asyncHandler( async (req, res) => {
+ const vendorId = req.user.id
+
+ const { page, limit, category, search, status, price} = req.validatedQuery
+ const { data, totalCount, totalPages }= await this._vendorGetAllVenuesUsecase.execute(vendorId, page, limit, category, search, status, price)
+ return sendSuccess(res, statusCode.OK, '', {data, totalCount, totalPages})
+ })
+
+ deleteVenue = asyncHandler( async (req, res) => {
+ const vendorId = req.user.id
+
+ const venueId = req.params.venueId
+ await this._vendorDeleteVenueUsecase.execute(vendorId,venueId)
+ return sendSuccess(res, statusCode.OK, '')
+ })
+
+ updateVenueStatus = asyncHandler( async (req, res) => {
+ const vendorId = req.user.id
+
+ const venueId = req.params.venueId
+ await this._vendorUpdateVenueStatusUsecase.execute({vendorId, venueId, status: req.body.status})
+ return sendSuccess(res, statusCode.OK, '')
+ })
+
+}
\ No newline at end of file
diff --git a/server/src/presentation/middlewares/.gitkeep b/server/src/presentation/middlewares/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/presentation/middlewares/auth.middleware.js b/server/src/presentation/middlewares/auth.middleware.js
new file mode 100644
index 0000000000..40e5658847
--- /dev/null
+++ b/server/src/presentation/middlewares/auth.middleware.js
@@ -0,0 +1,42 @@
+import jwt from "jsonwebtoken";
+import { UnauthorizedError } from "../../domain/errors/UnauthorizedError.js";
+import { authMessages } from "../../shared/constants/messages/authMessages.js";
+
+export const authHandler = (tokenService) => {
+ return async (req, res, next) => {
+
+ const token = req.headers?.authorization?.split(" ")[1];
+
+ console.log("header:",req.headers.authorization)
+
+ if (!token) {
+ throw new UnauthorizedError(authMessages.error.UNAUTHORIZED);
+ }
+
+ try {
+ const isBlackListed = await tokenService.isTokenBlacklisted(token)
+ if(isBlackListed){
+ throw new UnauthorizedError(authMessages.error.UNAUTHORIZED)
+ }
+ const decoded = tokenService.verifyAccessToken(token);
+ req.user = decoded; // { userId, role }
+ console.log('decoded user: ', decoded)
+ next();
+ } catch (error) {
+ if(error instanceof jwt.TokenExpiredError){
+ throw new UnauthorizedError(authMessages.error.ACCESS_TOKEN_EXPIRED)
+ }else{
+ throw new UnauthorizedError(authMessages.error.UNAUTHORIZED)
+ }
+ }
+ }
+};
+
+// export const authorizeRoles = (...roles) => {
+// return (req, res, next) => {
+// if (!roles.includes(req.user?.role)) {
+// return next(new UnauthorizedError("You do not have permission to access this resource"));
+// }
+// next();
+// };
+// };
diff --git a/server/src/presentation/middlewares/cloudinaryUpload.js b/server/src/presentation/middlewares/cloudinaryUpload.js
new file mode 100644
index 0000000000..18e16382b7
--- /dev/null
+++ b/server/src/presentation/middlewares/cloudinaryUpload.js
@@ -0,0 +1,19 @@
+import multer from "multer";
+import { CloudinaryStorage } from "multer-storage-cloudinary";
+import cloudinary from "../../infrastructure/config/cloudinary.config.js";
+
+const cloudinaryUpload = (folderName) => {
+ const storage = new CloudinaryStorage({
+ cloudinary,
+ params: {
+ folder: folderName,
+ allowed_formats: ["jpg", "jpeg", "png", "webp", "pdf"],
+ },
+ });
+
+ return multer({
+ storage,
+ });
+};
+
+export default cloudinaryUpload;
\ No newline at end of file
diff --git a/server/src/presentation/middlewares/errorHandler.js b/server/src/presentation/middlewares/errorHandler.js
new file mode 100644
index 0000000000..73047987c5
--- /dev/null
+++ b/server/src/presentation/middlewares/errorHandler.js
@@ -0,0 +1,10 @@
+export const errorHandler = (err, req, res, next) => {
+ console.error("From error handler:", err);
+
+ const statusCode = err.statusCode || 500;
+
+ res.status(statusCode).json({
+ success: false,
+ message: err.message || "Internal Server Error",
+ });
+};
\ No newline at end of file
diff --git a/server/src/presentation/middlewares/otpRateLimiter.js b/server/src/presentation/middlewares/otpRateLimiter.js
new file mode 100644
index 0000000000..c86bae1b33
--- /dev/null
+++ b/server/src/presentation/middlewares/otpRateLimiter.js
@@ -0,0 +1,63 @@
+/**
+ * Rate limiting middleware to prevent brute force attacks on OTP verification
+ * Limits OTP verification attempts to 5 per email per 15 minutes
+ */
+
+const otpAttempts = new Map();
+
+const MAX_ATTEMPTS = 5;
+const TIME_WINDOW = 15 * 60 * 1000; // 15 minutes
+
+export const otpRateLimiter = (req, res, next) => {
+ const { email } = req.body;
+
+ if (!email) {
+ return res.status(400).json({
+ status: false,
+ message: "Email is required"
+ });
+ }
+
+ const now = Date.now();
+ const key = `otp_${email}`;
+
+ // Get or initialize attempt record
+ const attempt = otpAttempts.get(key) || { count: 0, resetTime: now + TIME_WINDOW };
+
+ // Reset counter if time window has passed
+ if (now > attempt.resetTime) {
+ attempt.count = 0;
+ attempt.resetTime = now + TIME_WINDOW;
+ }
+
+ // Check if max attempts exceeded
+ if (attempt.count >= MAX_ATTEMPTS) {
+ const remainingTime = Math.ceil((attempt.resetTime - now) / 1000);
+ return res.status(429).json({
+ status: false,
+ message: `Too many OTP verification attempts. Please try again in ${remainingTime} seconds.`,
+ retryAfter: remainingTime
+ });
+ }
+
+ // Increment attempt counter
+ attempt.count++;
+ otpAttempts.set(key, attempt);
+
+ next();
+};
+
+/**
+ * Cleanup function to remove old entries (call periodically)
+ */
+export const cleanupOtpAttempts = () => {
+ const now = Date.now();
+ for (const [key, attempt] of otpAttempts.entries()) {
+ if (now > attempt.resetTime) {
+ otpAttempts.delete(key);
+ }
+ }
+};
+
+// Run cleanup every 30 minutes
+setInterval(cleanupOtpAttempts, 30 * 60 * 1000);
diff --git a/server/src/presentation/middlewares/validator.js b/server/src/presentation/middlewares/validator.js
new file mode 100644
index 0000000000..7b5de11dfc
--- /dev/null
+++ b/server/src/presentation/middlewares/validator.js
@@ -0,0 +1,20 @@
+import { AppError } from '../../domain/errors/app.error.js'
+import { statusCode } from '../../shared/constants/enums/statusCode.js'
+
+
+export const validate = (schema, target = 'body') => {
+ return (req, res, next) => {
+ console.log("RAW BODY:", req.body);
+ const result = schema.safeParse(req[target])
+ if(!result.success){
+ const errors = result.error.issues.map(issue => issue.message)
+ return next(new AppError(errors.join(", "), statusCode.BAD_REQUEST))
+ }
+ if(target === 'query'){
+ req.validatedQuery = result.data
+ }else{
+ req[target] = result.data
+ }
+ next()
+ }
+}
\ No newline at end of file
diff --git a/server/src/presentation/routes/.gitkeep b/server/src/presentation/routes/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/presentation/routes/index.js b/server/src/presentation/routes/index.js
new file mode 100644
index 0000000000..88f1f0ecb0
--- /dev/null
+++ b/server/src/presentation/routes/index.js
@@ -0,0 +1,8 @@
+import Express from 'express'
+import v1Routes from './v1/index.js'
+const router = Express.Router()
+
+
+router.use('/v1', v1Routes)
+
+export default router
\ No newline at end of file
diff --git a/server/src/presentation/routes/v1/adminRoutes.js b/server/src/presentation/routes/v1/adminRoutes.js
new file mode 100644
index 0000000000..fae063eeaa
--- /dev/null
+++ b/server/src/presentation/routes/v1/adminRoutes.js
@@ -0,0 +1,108 @@
+import Express from 'express'
+import { iAdminBookingController, iAdminUserController,iAdminPaymentController, iAdminDashboardController,iTokenService ,iAdminVendorController} from '../../controllers/di.js'
+import { ROUTES } from '../../../shared/constants/routes.js'
+import { getAllUsersQuerySchema, updateUserStatusSchema } from '../../validators/adminUser.validator.js'
+import { validate } from '../../middlewares/validator.js'
+import { getAllVendorsQuerySchema, rejectVendorBodySchema, updateVendorStatusSchema } from '../../validators/adminVendor.validator.js'
+import {
+ getAllVenuesQuerySchema,
+ venueIdParamSchema,
+ rejectVenueSchema,
+ updateVenueBlockStatusSchema
+} from "../../validators/adminVenue.validator.js";
+import { iAdminVenueController } from '../../controllers/di.js'
+import { adminGetAllBookingsSchema, adminGetBookingByIdSchema } from '../../validators/adminBooking.validator.js'
+import { adminGetAllPaymentsSchema, adminGetPaymentByIdSchema} from '../../validators/adminPayment.validator.js'
+import { authHandler } from '../../middlewares/auth.middleware.js'
+
+const router = Express.Router()
+router.use(authHandler(iTokenService))
+//Dashboard
+router.get(ROUTES.ADMIN.DASHBOARD.GET_STATISTICS,authHandler(iTokenService),iAdminDashboardController.getDashboardStatistics)
+//User
+router.get(
+ ROUTES.ADMIN.USER.GET_ALL,authHandler(iTokenService), validate(getAllUsersQuerySchema, 'query'),
+ iAdminUserController.getAllUsers
+)
+
+router.patch(
+ ROUTES.ADMIN.USER.UPDATE_STATUS,authHandler(iTokenService), validate(updateUserStatusSchema, 'body'),
+ iAdminUserController.updateUserStatus
+)
+//vendor
+router.get(ROUTES.ADMIN.VENDOR.GET_ALL, authHandler(iTokenService),validate(getAllVendorsQuerySchema, 'query'), iAdminVendorController.getAllVendors)
+router.get(ROUTES.ADMIN.VENDOR.GET_BY_ID,authHandler(iTokenService), iAdminVendorController.getVendorById)
+router.patch(ROUTES.ADMIN.VENDOR.APPROVE_VENDOR,authHandler(iTokenService), iAdminVendorController.approveVendor)
+router.patch(ROUTES.ADMIN.VENDOR.REJECT_VENDOR,authHandler(iTokenService), validate(rejectVendorBodySchema, 'body'), iAdminVendorController.rejectVendor)
+router.patch(ROUTES.ADMIN.VENDOR.UPDATE_STATUS,authHandler(iTokenService), validate(updateVendorStatusSchema, 'body'), iAdminVendorController.updateVendorStatus)
+
+//venue
+
+router.get(
+ ROUTES.ADMIN.VENUE.GET_ALL,authHandler(iTokenService),
+ validate(getAllVenuesQuerySchema, 'query'),
+ iAdminVenueController.getAllVenues
+);
+
+router.get(
+ ROUTES.ADMIN.VENUE.GET_BY_ID,authHandler(iTokenService),
+ validate(venueIdParamSchema, 'params'),
+ iAdminVenueController.getVenueById
+);
+
+router.patch(
+ ROUTES.ADMIN.VENUE.APPROVE_VENUE,
+ validate(venueIdParamSchema, 'params'),
+ iAdminVenueController.approveVenue
+);
+
+router.patch(
+ ROUTES.ADMIN.VENUE.REJECT_VENUE,
+ validate(venueIdParamSchema, 'params', rejectVenueSchema, 'body'),
+ iAdminVenueController.rejectVenue
+);
+
+router.patch(
+ ROUTES.ADMIN.VENUE.UPDATE_STATUS,
+ validate(venueIdParamSchema, 'params', updateVenueBlockStatusSchema, 'body'),
+ iAdminVenueController.updateBlockStatus
+);
+
+//booking
+router.get(
+ ROUTES.ADMIN.BOOKING.GET_ALL,
+ validate(adminGetAllBookingsSchema, 'query'),
+ iAdminBookingController.getAllBookings
+);
+
+router.get(
+ ROUTES.ADMIN.BOOKING.GET_STATISTICS,
+ iAdminBookingController.getBookingStatistics
+);
+
+router.get(
+ ROUTES.ADMIN.BOOKING.GET_BY_ID,
+ validate(adminGetBookingByIdSchema, 'params'),
+ iAdminBookingController.getBookingById
+);
+
+//payment
+router.get(
+ ROUTES.ADMIN.PAYMENT.GET_ALL,
+ validate(adminGetAllPaymentsSchema, 'query'),
+ iAdminPaymentController.getAllPayments
+);
+
+router.get(
+ ROUTES.ADMIN.PAYMENT.GET_STATISTICS,
+ iAdminPaymentController.getPaymentStatistics
+);
+
+router.get(
+ ROUTES.ADMIN.PAYMENT.GET_BY_ID,
+ validate(adminGetPaymentByIdSchema, 'params'),
+ iAdminPaymentController.getPaymentById
+);
+
+export default router
+
diff --git a/server/src/presentation/routes/v1/authRoutes.js b/server/src/presentation/routes/v1/authRoutes.js
new file mode 100644
index 0000000000..379f4b6110
--- /dev/null
+++ b/server/src/presentation/routes/v1/authRoutes.js
@@ -0,0 +1,37 @@
+import Express from 'express'
+import { iAdminAuthController, iUnifiedAuthController, iUserAuthController, iVendorAuthController } from '../../controllers/di.js'
+import { validate } from '../../middlewares/validator.js'
+import { registerSchema, verifyOtpSchema, loginSchema, forgotPasswordSchema, resetPasswordSchema, resendOtpSchema} from '../../validators/auth.validator.js'
+import { ROUTES } from '../../../shared/constants/routes.js'
+const router = Express.Router()
+
+
+router.get(ROUTES.AUTH.GETME, iUnifiedAuthController.getMe)
+//user
+router.post(ROUTES.USER.AUTH.REGISTER, validate(registerSchema, 'body'), iUserAuthController.register)
+router.post(ROUTES.USER.AUTH.VERIFY_OTP, validate(verifyOtpSchema, 'body'), iUserAuthController.verifyOtp)
+router.post(ROUTES.USER.AUTH.RESEND_OTP, validate(resendOtpSchema, 'body'), iUserAuthController.resendOtp)
+router.post(ROUTES.USER.AUTH.LOGIN, validate(loginSchema, 'body'), iUserAuthController.login)
+router.post(ROUTES.USER.AUTH.REFRESH, iUserAuthController.refreshToken)
+router.post(ROUTES.USER.AUTH.LOGOUT, iUserAuthController.logout)
+router.post(ROUTES.USER.AUTH.FORGOT_PASSWORD, validate(forgotPasswordSchema, 'body'), iUserAuthController.forgotPassword)
+router.post(ROUTES.USER.AUTH.RESET_PASSWORD, validate(resetPasswordSchema, 'body'), iUserAuthController.resetPassword)
+
+//vendor
+router.post(ROUTES.OWNER.AUTH.REGISTER, validate(registerSchema, 'body'), iVendorAuthController.register)
+router.post(ROUTES.OWNER.AUTH.LOGIN, validate(loginSchema, 'body'), iVendorAuthController.login)
+router.post(ROUTES.OWNER.AUTH.VERIFY_OTP, validate(verifyOtpSchema, 'body'), iVendorAuthController.verifyOtp)
+router.post(ROUTES.OWNER.AUTH.RESEND_OTP, validate(resendOtpSchema, 'body'), iVendorAuthController.resendOtp)
+router.post(ROUTES.OWNER.AUTH.REFRESH, iVendorAuthController.refreshToken)
+router.post(ROUTES.OWNER.AUTH.FORGOT_PASSWORD, validate(forgotPasswordSchema, 'body'), iVendorAuthController.forgotPassword)
+router.post(ROUTES.OWNER.AUTH.RESET_PASSWORD, validate(resetPasswordSchema, 'body'), iVendorAuthController.resetPassword)
+router.post(ROUTES.OWNER.AUTH.LOGOUT, iVendorAuthController.logout)
+
+
+//admin
+router.post(ROUTES.ADMIN.AUTH.LOGIN, validate(loginSchema, 'body'), iAdminAuthController.login)
+router.post(ROUTES.ADMIN.AUTH.LOGOUT, iAdminAuthController.logout)
+router.post(ROUTES.ADMIN.AUTH.REFRESH, iAdminAuthController.refreshToken)
+
+
+export default router
\ No newline at end of file
diff --git a/server/src/presentation/routes/v1/index.js b/server/src/presentation/routes/v1/index.js
new file mode 100644
index 0000000000..25280f47dd
--- /dev/null
+++ b/server/src/presentation/routes/v1/index.js
@@ -0,0 +1,14 @@
+import { Router } from "express";
+import AdminRoutes from './adminRoutes.js'
+import VendorRoutes from './vendorRoutes.js'
+import UserRoutes from './userRoutes.js'
+import AuthRoutes from './authRoutes.js'
+const router = Router();
+
+router.use('/auth', AuthRoutes)
+router.use('/admin', AdminRoutes)
+router.use('/vendor', VendorRoutes)
+router.use('/user', UserRoutes)
+
+
+export default router;
\ No newline at end of file
diff --git a/server/src/presentation/routes/v1/userRoutes.js b/server/src/presentation/routes/v1/userRoutes.js
new file mode 100644
index 0000000000..b85614601c
--- /dev/null
+++ b/server/src/presentation/routes/v1/userRoutes.js
@@ -0,0 +1,137 @@
+import Express from 'express'
+import { ROUTES } from '../../../shared/constants/routes.js'
+import { VenueParamsSchema, VenueQuerySchema } from '../../validators/VenderVenue.validator.js'
+import { iUserVenueController, iUserProfileController, iTokenService } from '../../controllers/di.js'
+import { validate } from '../../middlewares/validator.js'
+import { updateProfileSchema, userChangePasswordSchema } from '../../validators/UserProfie.validator.js'
+import { RequestEmailChangeOtpSchema,verifyEmailOtpSchema } from '../../validators/UserProfie.validator.js'
+import { WishlistParamsSchema } from "../../validators/UserWishlist.validator.js";
+import { iUserWishlistController } from "../../controllers/di.js";
+import { UpdateAccountStatusSchema } from "../../validators/UserAccount.validator.js";
+import { iUserAccountController } from "../../controllers/di.js";
+import cloudinaryUpload from "../../middlewares/cloudinaryUpload.js";
+import { iUserBookingController } from "../../controllers/di.js"
+import { authHandler } from '../../middlewares/auth.middleware.js'
+import {ReserveBookingSchema, CancelBookingSchema} from "../../validators/userBooking.validator.js";
+
+
+const router = Express.Router()
+
+
+//venue
+router.get(ROUTES.USER.VENUE.GET_ALL, validate(VenueQuerySchema, 'query'), iUserVenueController.getAllVenues)
+router.get(ROUTES.USER.VENUE.GET_BY_ID, validate(VenueParamsSchema, 'params'), iUserVenueController.getVenueById)
+router.get(ROUTES.USER.VENUE.TOP_VENUES, iUserVenueController.getTopVenues)
+router.get(ROUTES.USER.VENUE.SIMILAR_VENUES, validate(VenueParamsSchema, 'params'), iUserVenueController.getSimilarVenues)
+
+
+//profile
+router.get(
+ ROUTES.USER.PROFILE.PROFILE, authHandler(iTokenService),
+ iUserProfileController.getProfile
+)
+
+router.patch(
+
+ ROUTES.USER.PROFILE.PROFILE,
+ authHandler(iTokenService),
+ validate(updateProfileSchema),
+ iUserProfileController.updateProfile
+)
+router.post(
+
+ ROUTES.USER.PROFILE.REQUEST_EMAIL_CHANGE_OTP,
+ authHandler(iTokenService),
+ validate(RequestEmailChangeOtpSchema,'body'),
+ iUserProfileController.requestEmailChangeOtp
+)
+router.post(
+ ROUTES.USER.PROFILE.VERIFY_EMAIL_CHANGE_OTP,
+ authHandler(iTokenService),
+ validate(verifyEmailOtpSchema,'body'),
+ iUserProfileController.verifyEmailChangeOtp
+)
+router.post(
+
+ ROUTES.USER.PROFILE.RESEND_EMAIL_CHANGE_OTP,
+ authHandler(iTokenService),
+ iUserProfileController.resendEmailChangeOtp
+)
+router.patch(
+ ROUTES.USER.PROFILE.PROFILE_IMAGE,
+ authHandler(iTokenService),
+ cloudinaryUpload("profile-images").single("profileImage"),
+ iUserProfileController.updateProfileImage
+)
+router.delete(
+ ROUTES.USER.PROFILE.PROFILE_IMAGE,
+ authHandler(iTokenService),
+ iUserProfileController.removeProfileImage
+)
+router.patch(
+ ROUTES.USER.PROFILE.CHANGE_PASSWORD,
+ authHandler(iTokenService),
+ validate(userChangePasswordSchema, "body"),
+ iUserProfileController.changePassword
+)
+
+//wishlist
+router.post(
+ ROUTES.USER.WISHLIST.WISHLIST,
+ authHandler(iTokenService),
+ validate(WishlistParamsSchema,"params"),
+ iUserWishlistController.addToWishlist
+)
+router.get(
+ ROUTES.USER.WISHLIST.GET,
+ authHandler(iTokenService),
+ iUserWishlistController.getWishlist
+)
+router.delete(
+ ROUTES.USER.WISHLIST.WISHLIST,
+ authHandler(iTokenService),
+ validate(WishlistParamsSchema,'params'),
+ iUserWishlistController.removeWishlist
+)
+//account
+router.patch(
+ ROUTES.USER.ACCOUNT.UPDATE_STATUS,
+ authHandler(iTokenService),
+ validate(UpdateAccountStatusSchema,"body"),
+ iUserAccountController.updateAccountStatus
+)
+
+
+
+// Booking
+router.post(
+ ROUTES.USER.BOOKING.RESERVE,
+ authHandler(iTokenService),
+ iUserBookingController.reserveBooking
+);
+
+router.post(
+ ROUTES.USER.BOOKING.CONFIRM,
+ authHandler(iTokenService),
+ iUserBookingController.confirmBooking
+);
+router.get(
+ ROUTES.USER.BOOKING.GET_ALL,
+ authHandler(iTokenService),
+ iUserBookingController.getBookings
+);
+
+router.get(
+
+ ROUTES.USER.BOOKING.GET_BY_ID,
+ authHandler(iTokenService),
+ iUserBookingController.getBookingById
+);
+router.patch(
+ ROUTES.USER.BOOKING.CANCEL,
+ authHandler(iTokenService),
+ validate(CancelBookingSchema, "body"),
+ iUserBookingController.cancelBooking
+);
+
+export default router
\ No newline at end of file
diff --git a/server/src/presentation/routes/v1/vendorRoutes.js b/server/src/presentation/routes/v1/vendorRoutes.js
new file mode 100644
index 0000000000..9f71b7b14d
--- /dev/null
+++ b/server/src/presentation/routes/v1/vendorRoutes.js
@@ -0,0 +1,127 @@
+import Express from "express";
+import { ROUTES } from "../../../shared/constants/routes.js";
+import cloudinaryUpload from "../../middlewares/cloudinaryUpload.js";
+import {
+ iVendorVenueController,
+ iVendorProfileController,
+ iVendorBookingController,
+ iVendorDashboardController,
+ iTokenService,
+} from "../../controllers/di.js";
+import { validate } from "../../middlewares/validator.js";
+import {
+ createVenueSchema,
+ editVenueSchema,
+ VenueParamsSchema,
+ VenueQuerySchema,
+ VenueUpdateStatusSchema,
+} from "../../validators/VenderVenue.validator.js";
+
+import {
+ UpdateVendorProfileSchema,
+ ChangeVendorPasswordSchema,
+} from "../../validators/vendorProfile.validator.js";
+
+import {
+ BookingParamsSchema,
+ BookingQuerySchema,
+} from "../../validators/vendorBooking.validator.js";
+import { authHandler } from "../../middlewares/auth.middleware.js";
+
+const router = Express.Router();
+
+const uploadVenue = cloudinaryUpload("venues");
+// const uploadVenueLicense = cloudinaryUpload("venueLicense")
+
+//venue
+router.get(
+ ROUTES.OWNER.DASHBOARD,
+ authHandler(iTokenService),
+ iVendorDashboardController.getDashboard
+);
+
+router.post(
+ ROUTES.OWNER.VENUE.CREATE,
+ authHandler(iTokenService),
+ uploadVenue.fields([
+ { name: "images", maxCount: 10 },
+ { name: "license", maxCount: 1 },
+ ]),
+ validate(createVenueSchema, "body"),
+ iVendorVenueController.createVenue
+);
+router.patch(
+ ROUTES.OWNER.VENUE.EDIT,
+ authHandler(iTokenService),
+ uploadVenue.fields([
+ { name: "images", maxCount: 10 },
+ { name: "license", maxCount: 5 },
+ ]),
+ validate(editVenueSchema, "body"),
+ validate(VenueParamsSchema, "params"),
+ iVendorVenueController.updateVenue
+);
+router.get(
+ ROUTES.OWNER.VENUE.GET_BY_ID,
+ authHandler(iTokenService),
+ validate(VenueParamsSchema, "params"),
+ iVendorVenueController.getById
+);
+router.get(
+ ROUTES.OWNER.VENUE.GET_ALL,
+ authHandler(iTokenService),
+ validate(VenueQuerySchema, "query"),
+ iVendorVenueController.getAllVenues
+);
+router.delete(
+ ROUTES.OWNER.VENUE.DELETE,
+ authHandler(iTokenService),
+ validate(VenueParamsSchema, "params"),
+ iVendorVenueController.deleteVenue
+);
+router.patch(
+ ROUTES.OWNER.VENUE.UPDATE_STATUS,
+ authHandler(iTokenService),
+ validate(VenueParamsSchema, "params"),
+ validate(VenueUpdateStatusSchema, "body"),
+ iVendorVenueController.updateVenueStatus
+);
+
+//vendor profile
+router.get
+(ROUTES.OWNER.PROFILE.GET,
+authHandler(iTokenService),
+ iVendorProfileController.getProfile);
+router.patch(
+ ROUTES.OWNER.PROFILE.UPDATE,
+ authHandler(iTokenService),
+ cloudinaryUpload("profile-images").single("profileImage"),
+ validate(UpdateVendorProfileSchema, "body"),
+ iVendorProfileController.updateProfile
+);
+
+
+router.patch(
+ ROUTES.OWNER.PROFILE.CHANGE_PASSWORD,
+ authHandler(iTokenService),
+ validate(ChangeVendorPasswordSchema, "body"),
+ iVendorProfileController.changePassword
+);
+
+// booking
+
+router.get(
+ ROUTES.OWNER.BOOKING.GET_ALL,
+ authHandler(iTokenService),
+ validate(BookingQuerySchema, "query"),
+ iVendorBookingController.getBookings
+);
+
+router.get(
+ ROUTES.OWNER.BOOKING.GET_BY_ID,
+ authHandler(iTokenService),
+ validate(BookingParamsSchema, "params"),
+ iVendorBookingController.getBookingById
+);
+
+export default router;
diff --git a/server/src/presentation/validators/.gitkeep b/server/src/presentation/validators/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/presentation/validators/UserAccount.validator.js b/server/src/presentation/validators/UserAccount.validator.js
new file mode 100644
index 0000000000..66941365db
--- /dev/null
+++ b/server/src/presentation/validators/UserAccount.validator.js
@@ -0,0 +1,8 @@
+import { z } from "zod";
+
+export const UpdateAccountStatusSchema = z.object({
+ isActive: z.boolean({
+ required_error: "isActive is required",
+ invalid_type_error: "isActive must be a boolean"
+ })
+});
\ No newline at end of file
diff --git a/server/src/presentation/validators/UserBooking.validator.js b/server/src/presentation/validators/UserBooking.validator.js
new file mode 100644
index 0000000000..5b1ef0bf8c
--- /dev/null
+++ b/server/src/presentation/validators/UserBooking.validator.js
@@ -0,0 +1,42 @@
+import { z } from "zod";
+import { BookingType } from "../../domain/enums/Booking.enum.js";
+
+export const ReserveBookingSchema = z.object({
+
+ venueId: z
+ .string()
+ .min(1, "Venue ID is required"),
+
+ bookingDate: z
+ .string()
+ .min(1, "Booking date is required"),
+
+ startTime: z
+ .string()
+ .min(1, "Start time is required"),
+
+ endTime: z
+ .string()
+ .min(1, "End time is required"),
+
+ guestCount: z
+ .number()
+ .int()
+ .positive(),
+
+ bookingType: z.enum([
+ BookingType.HOURLY,
+ BookingType.FULL_DAY
+ ]),
+
+
+});
+export const CancelBookingSchema = z.object({
+
+ cancellationReason: z
+ .string()
+ .trim()
+ .min(1, "Cancellation reason is required")
+ .max(500, "Cancellation reason is too long")
+
+});
\ No newline at end of file
diff --git a/server/src/presentation/validators/UserProfie.validator.js b/server/src/presentation/validators/UserProfie.validator.js
new file mode 100644
index 0000000000..46272dd50a
--- /dev/null
+++ b/server/src/presentation/validators/UserProfie.validator.js
@@ -0,0 +1,71 @@
+import{z} from "zod"
+
+export const updateProfileSchema=z.object({
+ fullName:z.string()
+ .trim()
+ .min(3,"Full nam emust be 3 charecters")
+ .max(50,"Full name cannot exceed 50 charecters")
+ .optional(),
+
+ phone:z
+ .string()
+ .trim()
+ .regex(/^[0-9]{10}$/,"phone number must be 10 digits")
+ .optional()
+})
+.refine(
+ data=>data.fullName !==undefined || data.phone !==undefined,
+ {
+ message:"At least one field must be provided"
+ }
+)
+
+export const UserProfileParamsSchema=z.object({
+ userId:z.string().regex(
+ /^[0-9a-fA-F]{24}$/,
+ "invalid user ID"
+ )
+})
+
+export const RequestEmailChangeOtpSchema=z.object({
+ newEmail:z.string()
+ .trim()
+ .email("invalid email address")
+})
+
+export const verifyEmailOtpSchema=z.object({
+ otp:z.string()
+ .trim()
+ .regex(/^\d{6}$/,"OTP must be 6 digits")
+})
+
+export const userChangePasswordSchema = z.object({
+
+ currentPassword: z
+ .string()
+ .min(6, "Current password is required"),
+
+ newPassword: z
+ .string()
+ .min(6, "Password must contain at least 6 characters")
+ .regex(
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
+ "Password must contain uppercase, lowercase, number, and special character"
+ ),
+
+ confirmPassword: z
+ .string()
+
+}).refine(
+
+ data => data.newPassword === data.confirmPassword,
+
+ {
+
+ message: "Passwords do not match",
+
+ path: ["confirmPassword"]
+
+ }
+
+);
\ No newline at end of file
diff --git a/server/src/presentation/validators/UserWishlist.validator.js b/server/src/presentation/validators/UserWishlist.validator.js
new file mode 100644
index 0000000000..866136e141
--- /dev/null
+++ b/server/src/presentation/validators/UserWishlist.validator.js
@@ -0,0 +1,8 @@
+import { z } from "zod";
+
+export const WishlistParamsSchema = z.object({
+ venueId: z.string().regex(
+ /^[0-9a-fA-F]{24}$/,
+ "Invalid venue ID"
+ )
+});
\ No newline at end of file
diff --git a/server/src/presentation/validators/VenderVenue.validator.js b/server/src/presentation/validators/VenderVenue.validator.js
new file mode 100644
index 0000000000..a63d7ae4d2
--- /dev/null
+++ b/server/src/presentation/validators/VenderVenue.validator.js
@@ -0,0 +1,304 @@
+import { z } from "zod";
+import { Amenities, VenueCategory, VenueStatus } from '../../domain/enums/Venue.enum.js'
+
+
+export const createVenueSchema = z.object({
+ name: z
+ .string()
+ .trim()
+ .min(3, "Venue name must be at least 3 characters")
+ .max(100, "Venue name cannot exceed 100 characters"),
+
+ description: z
+ .string()
+ .trim()
+ .min(10, "Description must be at least 10 characters")
+ .max(2000, "Description cannot exceed 2000 characters"),
+
+ category: z.nativeEnum(VenueCategory),
+ // vendorId: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid owner ID'),
+
+ websiteUrl: z
+ .string()
+ .url("Invalid website URL")
+ .optional()
+ .or(z.literal("")),
+
+ addressLine1: z
+ .string()
+ .trim()
+ .min(5, "Address is required"),
+
+ city: z
+ .string()
+ .trim()
+ .min(2, "City is required"),
+
+ state: z
+ .string()
+ .trim()
+ .min(2, "State is required"),
+
+ country: z
+ .string()
+ .trim()
+ .min(2, "Country is required"),
+
+ phone: z
+ .string()
+ .trim()
+ .regex(/^[0-9]{10,15}$/, "Invalid phone number"),
+
+ pincode: z
+ .string()
+ .trim()
+ .regex(/^[0-9]{4,10}$/, "Invalid pincode"),
+
+ googleMapLink: z
+ .string()
+ .url("Invalid Google Map URL")
+ .optional()
+ .or(z.literal("")),
+
+ seatingCapacity: z
+ .coerce
+ .number()
+ .min(0, "Seating capacity cannot be negative"),
+
+ standingCapacity: z
+ .coerce
+ .number()
+ .min(0, "Standing capacity cannot be negative"),
+
+ pricePerHour: z
+ .coerce
+ .number()
+ .optional(),
+ // .min(0, "Price per hour cannot be negative"),
+
+ pricePerDay: z
+ .coerce
+ .number()
+ .min(0, "Price per day cannot be negative"),
+
+ securityDeposit: z
+ .coerce
+ .number()
+ .min(0, "Security deposit cannot be negative"),
+
+ weekendSurcharge: z
+ .coerce
+ .number()
+ .min(0, "Weekend surcharge cannot be negative"),
+
+ minimumBookingHours: z
+ .coerce
+ .number()
+ .min(0, "Minimum booking hours cannot be negative"),
+
+ availabilityRules: z
+ .record(z.any())
+ .optional(),
+
+amenities: z.preprocess(
+ (value) => {
+ if (typeof value === "string") {
+ try {
+ return JSON.parse(value);
+ } catch {
+ return value;
+ }
+ }
+
+ return value;
+ },
+ z.array(z.string()).optional()
+),
+})
+
+export const VenueParamsSchema = z.object({
+ venueId: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid venue ID'),
+ // ownerId: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid venue ID')
+})
+
+export const editVenueSchema = z.object({
+ name: z
+ .string()
+ .trim()
+ .min(3, "Venue name must be at least 3 characters")
+ .max(100, "Venue name cannot exceed 100 characters"),
+ description: z
+ .string()
+ .trim()
+ .min(10, "Description must be at least 10 characters")
+ .max(2000, "Description cannot exceed 2000 characters"),
+ category: z.nativeEnum(VenueCategory),
+ vendorId: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid owner ID'),
+ websiteUrl: z
+ .string()
+ .url("Invalid website URL")
+ .optional()
+ .or(z.literal("")),
+
+ addressLine1: z
+ .string()
+ .trim()
+ .min(5, "Address is required"),
+
+ city: z
+ .string()
+ .trim()
+ .min(2, "City is required"),
+
+ state: z
+ .string()
+ .trim()
+ .min(2, "State is required"),
+
+ country: z
+ .string()
+ .trim()
+ .min(2, "Country is required"),
+
+ phone: z
+ .string()
+ .trim()
+ .regex(/^[0-9]{10,15}$/, "Invalid phone number"),
+
+ pincode: z
+ .string()
+ .trim()
+ .regex(/^[0-9]{4,10}$/, "Invalid pincode"),
+
+ googleMapLink: z
+ .string()
+ .url("Invalid Google Map URL")
+ .optional()
+ .or(z.literal("")),
+
+ seatingCapacity: z
+ .coerce
+ .number()
+ .min(0, "Seating capacity cannot be negative"),
+
+ standingCapacity: z
+ .coerce
+ .number()
+ .min(0, "Standing capacity cannot be negative"),
+
+ pricePerHour: z
+ .coerce
+ .number()
+ .min(0, "Price per hour cannot be negative"),
+
+ pricePerDay: z
+ .coerce
+ .number()
+ .min(0, "Price per day cannot be negative"),
+
+ securityDeposit: z
+ .coerce
+ .number()
+ .min(0, "Security deposit cannot be negative"),
+
+ weekendSurcharge: z
+ .coerce
+ .number()
+ .min(0, "Weekend surcharge cannot be negative"),
+
+ minimumBookingHours: z
+ .coerce
+ .number()
+ .min(0, "Minimum booking hours cannot be negative"),
+
+ availabilityRules: z
+ .record(z.any())
+ .optional(),
+
+ amenities: z.preprocess(
+ (value) => {
+ if (typeof value === "string") {
+ try {
+ return JSON.parse(value);
+ } catch {
+ return value;
+ }
+ }
+
+ return value;
+ },
+ z.array(z.string()).optional()
+),
+ deletedImages: z
+ .string()
+ .transform(value => JSON.parse(value))
+ .pipe(z.array(z.string()))
+ .optional()
+})
+
+export const VenueQuerySchema = z.object({
+ vendorId: z
+ .string()
+ .regex(/^[0-9a-fA-F]{24}$/, 'Invalid venue ID')
+ .optional(),
+ capacityType: z
+ .string()
+ .optional(),
+ capacity: z
+ .coerce
+ .number()
+ .optional(),
+
+ priceType: z
+ .string()
+ .optional(),
+
+ minPrice: z.coerce
+ .number()
+ .optional(),
+
+ maxPrice: z.coerce
+ .number()
+ .optional(),
+
+ category: z
+ .nativeEnum(VenueCategory)
+ .optional(),
+
+ rating: z.coerce
+ .number()
+ .min(0)
+ .max(5)
+ .optional(),
+
+ amenities: z
+ .union([
+ z.nativeEnum(Amenities),
+ z.array(z.nativeEnum(Amenities))
+ ])
+ .optional(),
+
+ search: z
+ .string()
+ .optional(),
+
+ status: z
+ .nativeEnum(VenueStatus)
+ .optional(),
+
+ price: z.coerce
+ .number()
+ .optional(),
+
+ page: z.coerce
+ .number()
+ .default(1),
+
+ limit: z.coerce
+ .number()
+ .default(10)
+})
+
+export const VenueUpdateStatusSchema = z.object({
+ status: z.nativeEnum(VenueStatus)
+})
\ No newline at end of file
diff --git a/server/src/presentation/validators/adminBooking.validator.js b/server/src/presentation/validators/adminBooking.validator.js
new file mode 100644
index 0000000000..f9f76b85a7
--- /dev/null
+++ b/server/src/presentation/validators/adminBooking.validator.js
@@ -0,0 +1,42 @@
+import { z } from "zod";
+import { BookingStatus } from "../../domain/enums/Booking.enum.js";
+import { PaymentStatus } from "../../domain/enums/Payment.enum.js";
+
+export const adminGetAllBookingsSchema = z.object({
+
+ search: z.string().optional(),
+
+ status: z
+ .enum(Object.values(BookingStatus))
+ .optional(),
+
+ paymentStatus: z
+ .enum(Object.values(PaymentStatus))
+ .optional(),
+
+ bookingDate: z.string().optional(),
+
+sortBy: z
+ .enum(["asc", "desc"])
+ .default("desc"),
+
+ page: z.coerce
+ .number()
+ .int()
+ .min(1)
+ .default(1),
+
+ limit: z.coerce
+ .number()
+ .int()
+ .min(1)
+ .max(100)
+ .default(10)
+
+});
+
+export const adminGetBookingByIdSchema = z.object({
+
+ bookingId:z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid booking ID'),
+
+});
\ No newline at end of file
diff --git a/server/src/presentation/validators/adminPayment.validator.js b/server/src/presentation/validators/adminPayment.validator.js
new file mode 100644
index 0000000000..f1903258cd
--- /dev/null
+++ b/server/src/presentation/validators/adminPayment.validator.js
@@ -0,0 +1,62 @@
+import { z } from "zod";
+
+import { PaymentStatus } from "../../domain/enums/Payment.enum.js";
+import { PaymentMethod } from "../../domain/enums/PaymentMethod.enum.js";
+import { PaymentType } from "../../domain/enums/PaymentType.enum.js";
+
+export const adminGetAllPaymentsSchema = z.object({
+
+ search: z.string().optional(),
+
+ paymentStatus:
+
+ z.enum(Object.values(PaymentStatus))
+
+ .optional(),
+
+ paymentMethod:
+
+ z.enum(Object.values(PaymentMethod))
+
+ .optional(),
+
+ paymentType:
+
+ z.enum(Object.values(PaymentType))
+
+ .optional(),
+
+sortBy: z
+ .enum(["asc", "desc"])
+ .default("desc"),
+
+
+ page:
+
+ z.coerce.number()
+
+ .min(1)
+
+ .default(1),
+
+ limit:
+
+ z.coerce.number()
+
+ .min(1)
+
+ .max(100)
+
+ .default(10)
+
+});
+
+export const adminGetPaymentByIdSchema = z.object({
+
+ paymentId:
+
+ z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid payment ID'),
+
+
+
+});
\ No newline at end of file
diff --git a/server/src/presentation/validators/adminUser.validator.js b/server/src/presentation/validators/adminUser.validator.js
new file mode 100644
index 0000000000..3d53799ee8
--- /dev/null
+++ b/server/src/presentation/validators/adminUser.validator.js
@@ -0,0 +1,28 @@
+import { z } from "zod";
+
+export const getAllUsersQuerySchema = z.object({
+ search: z.string().optional(),
+
+ isBlocked: z
+ .enum(["true", "false"])
+ .optional(),
+
+ page: z
+ .coerce
+ .number()
+ .min(1)
+ .default(1),
+
+ limit: z
+ .coerce
+ .number()
+ .min(1)
+ .max(100)
+ .default(10)
+});
+
+export const updateUserStatusSchema = z.object({
+
+ isBlocked: z.boolean()
+
+});
\ No newline at end of file
diff --git a/server/src/presentation/validators/adminVendor.validator.js b/server/src/presentation/validators/adminVendor.validator.js
new file mode 100644
index 0000000000..6e4e6cf0aa
--- /dev/null
+++ b/server/src/presentation/validators/adminVendor.validator.js
@@ -0,0 +1,38 @@
+import { z } from "zod"
+import { VendorApprovalStatus } from '../../domain/enums/VendorApprovalStatus.enum.js'
+
+export const getAllVendorsQuerySchema =
+ z.object({
+
+ search:
+ z.string().optional(),
+
+ status:
+ z.nativeEnum(VendorApprovalStatus)
+ .optional(),
+ isBlocked: z
+ .enum(["true", "false"])
+ .optional(),
+ page:
+ z.coerce.number()
+ .min(1)
+ .default(1),
+
+ limit:
+ z.coerce.number()
+ .min(1)
+ .default(10)
+ })
+
+export const rejectVendorBodySchema = z.object({
+ reason: z
+ .string()
+ .trim()
+ .min(1, "Rejection reason is required")
+});
+
+export const updateVendorStatusSchema = z.object({
+
+ isBlocked: z.boolean()
+
+});
\ No newline at end of file
diff --git a/server/src/presentation/validators/adminVenue.validator.js b/server/src/presentation/validators/adminVenue.validator.js
new file mode 100644
index 0000000000..8c84139895
--- /dev/null
+++ b/server/src/presentation/validators/adminVenue.validator.js
@@ -0,0 +1,47 @@
+import { z } from "zod";
+import { VenueStatus } from "../../domain/enums/Venue.enum.js";
+
+export const getAllVenuesQuerySchema = z.object({
+
+ page: z.coerce.number().default(1),
+
+ limit: z.coerce.number().default(10),
+
+ search: z.string().optional(),
+
+ category: z.string().optional(),
+
+ approvalStatus: z
+ .nativeEnum(VenueStatus)
+ .optional(),
+
+ isBlocked: z
+ .enum([
+ "true",
+ "false"
+ ])
+ .optional()
+
+});
+export const updateVenueBlockStatusSchema =
+ z.object({
+
+ isBlocked:
+ z.boolean()
+
+ });
+
+export const rejectVenueSchema = z.object({
+
+ reason: z
+ .string()
+ .trim()
+ .min(1)
+
+});
+
+export const venueIdParamSchema = z.object({
+
+ venueId:z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid owner ID'),
+
+});
\ No newline at end of file
diff --git a/server/src/presentation/validators/auth.validator.js b/server/src/presentation/validators/auth.validator.js
new file mode 100644
index 0000000000..b35f931a6f
--- /dev/null
+++ b/server/src/presentation/validators/auth.validator.js
@@ -0,0 +1,83 @@
+import { z } from 'zod'
+
+export const registerSchema = z.object({
+ fullName: z
+ .string()
+ .trim()
+ .min(2, "Full name must be at least 2 characters"),
+
+ email: z
+ .string()
+ .trim()
+ .email("Valid email is required"),
+
+ phone: z
+ .string()
+ .trim()
+ .regex(/^[\d\s]{7,}$/, "Valid phone number is required"),
+
+ password: z
+ .string()
+ .min(8, "Password must be at least 8 characters")
+ .regex(
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
+ "Password must contain uppercase, lowercase, number, and special character"
+ )
+})
+
+export const verifyOtpSchema = z.object({
+ email: z
+ .string()
+ .trim()
+ .email("Valid email is required"),
+
+ otpCode: z
+ .string()
+ .trim()
+ .regex(/^\d{6}$/, 'OTP must contain only numbers')
+ .min(6, "OTP must be exactly 6 digit")
+})
+
+export const loginSchema = z.object({
+ email: z
+ .string()
+ .trim()
+ .email("Valid email is required"),
+
+ password: z
+ .string()
+ .min(1, "Password is required")
+})
+
+export const forgotPasswordSchema = z.object({
+ email: z
+ .string()
+ .trim()
+ .email("Valid email is required")
+})
+
+export const resetPasswordSchema = z.object({
+ // email: z
+ // .string()
+ // .trim()
+ // .email("Valid email is required"),
+
+ token: z
+ .string()
+ .min(1, "Reset token is required"),
+
+ password: z
+ .string()
+ .min(8, "Password must be at least 8 characters")
+ .regex(
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
+ "Password must contain uppercase, lowercase, number, and special character"
+ )
+})
+
+export const resendOtpSchema = z.object({
+ email: z
+ .string()
+ .trim()
+ .email("Valid email is required")
+})
diff --git a/server/src/presentation/validators/vendorBooking.validator.js b/server/src/presentation/validators/vendorBooking.validator.js
new file mode 100644
index 0000000000..7c71dd9262
--- /dev/null
+++ b/server/src/presentation/validators/vendorBooking.validator.js
@@ -0,0 +1,34 @@
+import { z } from 'zod'
+import { BookingStatus } from '../../domain/enums/Booking.enum.js'
+
+export const BookingParamsSchema = z.object({
+
+ bookingId: z
+ .string()
+ .min(1, 'Booking id is required')
+
+})
+
+export const BookingQuerySchema = z.object({
+
+ page: z.coerce
+ .number()
+ .min(1)
+ .default(1),
+
+ limit: z.coerce
+ .number()
+ .min(1)
+ .max(50)
+ .default(10),
+
+ status: z
+ .enum(Object.values(BookingStatus))
+ .optional(),
+
+ search: z
+ .string()
+ .trim()
+ .optional()
+
+})
diff --git a/server/src/presentation/validators/vendorProfile.validator.js b/server/src/presentation/validators/vendorProfile.validator.js
new file mode 100644
index 0000000000..2324c79a08
--- /dev/null
+++ b/server/src/presentation/validators/vendorProfile.validator.js
@@ -0,0 +1,55 @@
+import { z } from 'zod'
+
+export const UpdateVendorProfileSchema = z.object({
+
+ fullName: z.string().trim().min(3).optional(),
+
+ phone: z.string()
+ .regex(/^[6-9]\d{9}$/)
+ .optional(),
+
+ companyName: z.string().trim().min(2).optional(),
+
+ address: z.object({
+ addressLine1: z.string(),
+ city: z.string(),
+ state: z.string(),
+ pincode: z.string()
+ }).optional(),
+
+ bio: z.string().min(10).max(300).optional()
+
+})
+
+export const ChangeVendorPasswordSchema = z.object({
+
+ currentPassword: z
+ .string()
+ .min(6, "Current password is required"),
+
+ newPassword: z
+ .string()
+ .min(6, "Password must contain at least 6 characters")
+ .regex(
+ /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
+ "Password must contain uppercase, lowercase, number, and special character"
+ ),
+
+ confirmPassword: z
+ .string()
+
+}).refine(
+
+ data => data.newPassword === data.confirmPassword,
+
+ {
+
+ message: "Passwords do not match",
+
+ path: ["confirmPassword"]
+
+ }
+
+);
+
+export const GetVendorProfileSchema = z.object({})
\ No newline at end of file
diff --git a/server/src/server.js b/server/src/server.js
new file mode 100644
index 0000000000..c08340d679
--- /dev/null
+++ b/server/src/server.js
@@ -0,0 +1,59 @@
+import dotenv from 'dotenv'
+dotenv.config()
+
+import express from 'express';
+import cookieParser from 'cookie-parser'
+import cors from 'cors'
+import routes from './presentation/routes/index.js'
+import { connectDB } from './infrastructure/config/mongo.config.js';
+import cloudinaryUpload from "./presentation/middlewares/cloudinaryUpload.js";
+import { errorHandler } from './presentation/middlewares/errorHandler.js';
+import "./infrastructure/schedulers/paymentReminder.scheduler.js";
+
+const app = express()
+
+app.use(express.json())
+app.use(cookieParser())
+app.use(express.urlencoded({ extended: true }))
+
+
+app.use(cors({
+ origin: 'http://localhost:5173',
+ credentials: true
+}))
+
+app.use((req, res, next) => {
+ console.log(`recieving ${req.method} from ${req.url}`)
+ next()
+})
+
+connectDB()
+
+app.post(
+ "/test-upload",
+ cloudinaryUpload("bookmyvenue/test").single("image"),
+ (req, res) => {
+ res.status(200).json({
+ success: true,
+ file: req.file,
+ });
+ }
+);
+
+
+app.get('/test', (req, res) => {
+ res.status(200).json({
+ status: true,
+ message: "Test route hit"
+ })
+})
+
+app.use('/api', routes)
+app.use(errorHandler)
+
+
+const PORT = process.env.PORT || 4000
+
+app.listen(PORT, () => {
+ console.log('Server connected')
+})
\ No newline at end of file
diff --git a/server/src/shared/constants/.gitkeep b/server/src/shared/constants/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/shared/constants/enums/statusCode.js b/server/src/shared/constants/enums/statusCode.js
new file mode 100644
index 0000000000..a87085fb21
--- /dev/null
+++ b/server/src/shared/constants/enums/statusCode.js
@@ -0,0 +1,12 @@
+export const statusCode = Object.freeze({
+ OK: 200,
+ CREATED: 201,
+ NO_CONTENT: 204,
+ BAD_REQUEST: 400,
+ UNAUTHORIZED: 401,
+ FORBIDDEN: 403,
+ NOT_FOUND: 404,
+ CONFLICT: 409,
+ UNPROCESSABLE_ENTITY: 422,
+ SERVER_ERROR: 500,
+});
diff --git a/server/src/shared/constants/messages/authMessages.js b/server/src/shared/constants/messages/authMessages.js
new file mode 100644
index 0000000000..ca3d9ada82
--- /dev/null
+++ b/server/src/shared/constants/messages/authMessages.js
@@ -0,0 +1,40 @@
+export const authMessages = {
+ success: {
+ OTP_VERIFIED: 'OTP verified successfully',
+ REGISTERED: 'User registered successfully. OTP sent to email.',
+ LOGIN: 'Login successful',
+ LOGOUT: 'Logged out successfully',
+ TOKEN_REFRESHED: 'Token refreshed',
+ OTP_RESENT: 'New OTP sent successfully. Check your email.',
+ FORGOT_PASSWORD: 'Password reset link sent to your email. Check your inbox.',
+ RESET_PASSWORD: 'Password reset successfully. You can now login with your new password.',
+ ADMIN_LOGIN: 'Admin login successful',
+ },
+ error: {
+ ADMIN_NOT_FOUND: 'Admin not found',
+ USER_NOT_FOUND: 'User not found',
+ VENDOR_NOT_FOUND: 'Vendor not found',
+ USER_NOT_FOUND_WITH_EMAIL: 'User not found with this email',
+ OWNER_NOT_FOUND: 'Owner not found',
+ UNAUTHORIZED: 'Unauthorized',
+ INVALID_CREDENTIALS: 'Invalid credentials',
+ OTP_VERIFICATION_REQUIRED: 'OTP verification required',
+ ALREADY_OTP_VERIFIED: 'User is already OTP verified',
+ OTP_NOT_GENERATED: 'OTP not generated or already used',
+ OTP_EXPIRED: 'OTP code has expired',
+ INVALID_OTP: 'Invalid OTP code',
+ OTP_VERIFY_FAILED: 'Unable to verify OTP',
+ NO_REFRESH_TOKEN: 'No refresh token provided',
+ INVALID_REFRESH_TOKEN: 'Invalid refresh token',
+ INVALID_ACCESS_TOKEN: 'Invalid access token',
+ REFRESH_TOKEN_REVOKED: 'Refresh token is invalid or has been revoked',
+ INVALID_ADMIN_CREDENTIALS: 'Invalid admin credentials',
+ PASSWORD_TOO_SHORT: 'Password must be at least 8 characters long',
+ NO_RESET_REQUEST: 'No password reset request found for this email',
+ INVALID_RESET_TOKEN: 'Invalid reset token',
+ RESET_TOKEN_EXPIRED: 'Password reset link has expired',
+ RESET_PASSWORD_FAILED: 'Failed to reset password',
+ EMAIL_ALREADY_EXISTS: 'Email already exists',
+ ACCESS_TOKEN_EXPIRED: 'Access token expired',
+ }
+}
diff --git a/server/src/shared/constants/messages/bookingMessages.js b/server/src/shared/constants/messages/bookingMessages.js
new file mode 100644
index 0000000000..0efb2afa6c
--- /dev/null
+++ b/server/src/shared/constants/messages/bookingMessages.js
@@ -0,0 +1,105 @@
+export const BookingMessages = {
+
+ success: {
+
+ BOOKING_FETCHED:
+ "Booking fetched successfully",
+
+ BOOKINGS_FETCHED:
+ "Bookings fetched successfully",
+
+ BOOKING_RESERVED:
+ "Booking reserved successfully",
+
+ BOOKING_CREATED:
+ "Booking created successfully",
+
+ BOOKING_ACCEPTED:
+ "Booking accepted successfully",
+
+ BOOKING_REJECTED:
+ "Booking rejected successfully",
+
+ BOOKING_CANCELLED:
+ "Booking cancelled successfully",
+
+ PAYMENT_SUCCESS:
+ "Payment completed successfully",
+
+ PAYMENT_FAILED:
+ "Payment failed"
+
+ },
+
+ error: {
+
+ BOOKING_NOT_FOUND:
+ "Booking not found",
+
+ VENUE_NOT_FOUND:
+ "Venue not found",
+
+ VENUE_BLOCKED:
+ "This venue is currently blocked",
+
+ VENUE_NOT_APPROVED:
+ "This venue is not available for booking",
+
+ VENUE_DELETED:
+ "This venue is no longer available",
+
+ BOOKING_DATE_INVALID:
+ "Booking date is invalid",
+
+ BOOKING_TIME_INVALID:
+ "Booking time is invalid",
+
+ VENUE_CLOSED:
+ "Venue is closed on the selected date",
+
+ VENUE_CLOSED_TIME:
+ "Selected time is outside venue working hours",
+
+ MINIMUM_BOOKING_HOURS:
+ "Minimum booking hours not satisfied",
+
+ CAPACITY_EXCEEDED:
+ "Guest count exceeds venue capacity",
+
+ SLOT_ALREADY_BOOKED:
+ "Selected slot is already booked",
+
+ SLOT_TEMPORARILY_RESERVED:
+ "Selected slot is temporarily reserved",
+
+ PAYMENT_REQUIRED:
+ "Payment is required",
+
+ FORBIDDEN:
+ "You are not authorized to access this booking",
+
+ BOOKING_ALREADY_CONFIRMED:
+ "Booking is already confirmed",
+
+ BOOKING_ALREADY_REJECTED:
+ "Booking is already rejected",
+
+ BOOKING_ALREADY_CANCELLED:
+ "Booking is already cancelled",
+
+ BOOKING_ACCEPT_FAILED:
+ "Failed to accept booking",
+
+ BOOKING_REJECT_FAILED:
+ "Failed to reject booking",
+ RESERVATION_NOT_FOUND:
+ "Reservation not found or expired.",
+ BOOKING_CANNOT_BE_CANCELLED:
+ "Booking cannot be cancelled within 3 days of the booking date",
+
+ USER_NOT_FOUND:
+ "User not found"
+
+ }
+
+};
\ No newline at end of file
diff --git a/server/src/shared/constants/messages/paymentMessages.js b/server/src/shared/constants/messages/paymentMessages.js
new file mode 100644
index 0000000000..84f3c13840
--- /dev/null
+++ b/server/src/shared/constants/messages/paymentMessages.js
@@ -0,0 +1,13 @@
+export const PaymentMessages = {
+ success: {
+ PAYMENTS_FETCHED: "Payments fetched successfully.",
+
+ PAYMENT_FETCHED: "Payment fetched successfully."
+
+ },
+ error: {
+ PAYMENT_NOT_FOUND: 'Payment not found',
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/shared/constants/messages/userMessages.js b/server/src/shared/constants/messages/userMessages.js
new file mode 100644
index 0000000000..472eaf2f61
--- /dev/null
+++ b/server/src/shared/constants/messages/userMessages.js
@@ -0,0 +1,49 @@
+export const UserMessage = {
+ success: {
+ USER_FETCHED: "Users fetched successfully",
+ USER_BLOCKED: "User blocked successfully",
+ USER_UNBLOCKED: "User unblocked successfully",
+
+ OTP_SENT: "OTP sent successfully",
+ OTP_RESENT: "OTP resent successfully",
+ EMAIL_UPDATED: "Email updated successfully",
+ PROFILE_UPDATED: "Profile updated successfully",
+ PROFILE_IMAGE_UPDATED: "Profile image updated successfully",
+ PROFILE_IMAGE_REMOVED: "Profile image removed successfully",
+ ACCOUNT_DEACTIVATED: "Account deactivated successfully",
+ ACCOUNT_ACTIVATED: "Account activated successfully",
+ WISHLIST_ADDED: "Venue added to wishlist successfully",
+ WISHLIST_REMOVED: "Venue removed from wishlist successfully",
+ PROFILE_FETCHED: "Profile fetched successfully",
+ WISHLIST_FETCHED: "Wishlist fetched successfully",
+ PASSWORD_CHANGED:"Password changed successfully",
+ },
+
+ error: {
+ USER_NOT_FOUND: "User not found",
+ USER_ALREADY_BLOCKED: "User already blocked",
+ USER_ALREADY_ACTIVE: "User already active",
+
+ USER_BLOCKED_EMAIL_CHANGE: "Blocked users cannot change email",
+ EMAIL_ALREADY_EXISTS: "Email already exists",
+ EMAIL_SAME_AS_CURRENT: "New email cannot be the same as the current email",
+ EMAIL_CHANGE_REQUEST_NOT_FOUND: "No email change request found",
+ OTP_NOT_FOUND: "OTP not found",
+ OTP_EXPIRED: "OTP has expired",
+ INVALID_OTP: "Invalid OTP",
+ USER_ACCOUNT_BLOCKED: "User account is blocked",
+ USER_BLOCKED_REMOVE_PROFILE_IMAGE: "Blocked users cannot remove profile image",
+ PROFILE_IMAGE_NOT_FOUND: "No profile image found",
+ USER_BLOCKED_UPDATE_PROFILE_IMAGE:"Blocked users cannot update profile image",
+ PROFILE_IMAGE_REQUIRED:"Profile image is required",
+ WISHLIST_ALREADY_EXISTS:"Venue already exists in wishlist",
+ WISHLIST_NOT_FOUND: "Venue not found in wishlist",
+ USER_BLOCKED_UPDATE_ACCOUNT_STATUS:"Blocked users cannot update account status",
+ ACCOUNT_ALREADY_ACTIVE: "Account is already active",
+ ACCOUNT_ALREADY_INACTIVE: "Account is already inactive",
+ INVALID_CURRENT_PASSWORD:"Current password is incorrect",
+ PASSWORD_MISMATCH:"New password and confirm password do not match",
+ SAME_PASSWORD:"New password cannot be the same as the current password",
+
+ }
+};
\ No newline at end of file
diff --git a/server/src/shared/constants/messages/vendorMessages.js b/server/src/shared/constants/messages/vendorMessages.js
new file mode 100644
index 0000000000..e3a410e416
--- /dev/null
+++ b/server/src/shared/constants/messages/vendorMessages.js
@@ -0,0 +1,26 @@
+export const VendorMessages = {
+ success: {
+ PROFILE_FETCHED: 'Vendor profile fetched successfully',
+ PROFILE_UPDATED: 'Vendor profile updated successfully',
+ LICENSE_UPLOADED: 'License uploaded successfully',
+ VENDOR_BLOCKED:"Vendor blocked successfully",
+ VENDOR_UNBLOCKED:"Vendor unblocked successfully",
+ VENDOR_APPROVED:"Vendor approved successfully",
+ VENDOR_REJECTED:"Vendor rejected successfully",
+ PASSWORD_CHANGED:"Password changed successfully",
+ },
+
+ error: {
+ VENDOR_NOT_FOUND: 'Vendor not found',
+ PROFILE_UPDATE_FAILED: 'Failed to update profile',
+ LICENSE_UPLOAD_FAILED: 'Failed to upload license',
+ PHONE_ALREADY_EXISTS: 'Phone number alredy exist',
+ REJECTION_REASON_REQUIRED:'Rejection reason is required',
+ VENDOR_NOT_APPROVED_FOR_BLOCK_ACTION:"Only approved vendor can be blocked or unblocked",
+ VENDOR_ALREADY_APPROVED:"Vendor already approved",
+ VENDOR_ALREADY_REJECTED:"Vendor already rejected",
+ INVALID_CURRENT_PASSWORD:"Current password is incorrect",
+ PASSWORD_MISMATCH:"New password and confirm password do not match",
+ SAME_PASSWORD:"New password cannot be the same as the current password",
+ }
+};
diff --git a/server/src/shared/constants/messages/venueMessages.js b/server/src/shared/constants/messages/venueMessages.js
new file mode 100644
index 0000000000..b9625ba1da
--- /dev/null
+++ b/server/src/shared/constants/messages/venueMessages.js
@@ -0,0 +1,25 @@
+export const VenueMessages = {
+ success: {
+ VENUE_CREATED: 'Venue created successfully',
+ VENUE_UPDATED:"Venue updated Successfully"
+
+ },
+ error: {
+ VENUE_NOT_FOUND: 'Venue not found',
+ CANNOT_ADD_VENUE: 'Only venuw owners can add venue',
+ REQUIRE_ATLEAST_THREE_IMAGES: 'You should upload atleast 3 images',
+ ALREADY_EXISTING: 'Venue with same name already existing',
+ FORBIDDEN: 'Forbidden',
+ CANNOT_UPDATE_DELETED_VENUE: 'You cannot update deleted venue',
+ CANNOT_UPDATE_INACTIVE_VENUE: 'You cannot update inactive venue',
+ DELETED_VENUE: 'The venue is Deleted',
+ STATUS_ALREADY_SET: 'Status alredy set',
+ NOT_ACTIVE_VENUE: 'Current venue is inactivated',
+ NOT_ADMIN_VERIFIED: 'Venue is not verified by admin',
+ VENUE_ALREADY_APPROVED:"Venue already approved",
+ VENUE_ALREADY_REJECTED:"Venue already rejected",
+ REJECTION_REASON_REQUIRED:"Rejection reason is required",
+ ONLY_APPROVED_VENUE_CAN_BE_BLOCKED:"Only approved venues ca be blocked or unblocked",
+ VENUE_LICENSE_REQUIRED: 'Venue license is required',
+ }
+}
\ No newline at end of file
diff --git a/server/src/shared/constants/routes.js b/server/src/shared/constants/routes.js
new file mode 100644
index 0000000000..c56942cd9b
--- /dev/null
+++ b/server/src/shared/constants/routes.js
@@ -0,0 +1,125 @@
+export const ROUTES = {
+ AUTH: {
+ LOGOUT: '/logout',
+ GETME: '/getme'
+ },
+ ADMIN: {
+ DASHBOARD:{
+ GET_STATISTICS:'/dashboard/statistics'
+
+ },
+ USER:{
+ GET_ALL:'/users',
+ UPDATE_STATUS: '/users/:userId/status',
+ },
+ VENDOR:{
+ GET_ALL:'/vendors',
+ GET_BY_ID:'/vendors/:vendorId',
+ APPROVE_VENDOR:"/vendors/:vendorId/approve",
+ REJECT_VENDOR:"/vendors/:vendorId/reject",
+ UPDATE_STATUS:'/vendors/:vendorId/status'
+ },
+ VENUE:{
+ GET_ALL:'/venues',
+ GET_BY_ID:'/venues/:venueId',
+ APPROVE_VENUE:'/venues/:venueId/approve',
+ REJECT_VENUE:'/venues/:venueId/reject',
+ UPDATE_STATUS:'/venues/:venueId/status'
+ },
+ BOOKING:{
+ GET_ALL:'/bookings',
+ GET_BY_ID:'/bookings/:bookingId',
+ GET_STATISTICS:'/bookings/statistics',
+ },
+ PAYMENT:{
+ GET_ALL:'/payments',
+ GET_BY_ID:'/payments/:paymentId',
+ GET_STATISTICS:'/payments/statistics',
+ },
+ AUTH: {
+ LOGIN: "/admin/login",
+ REFRESH: '/admin/refresh',
+ LOGOUT: '/admin/logout'
+ },
+
+ },
+ OWNER: {
+ AUTH: {
+ LOGIN: "/vendor/login",
+ REGISTER: "/vendor/register",
+ VERIFY_OTP: "/vendor/verifyotp",
+ GOOGLE: "/vendor/googlelogin",
+ RESEND_OTP: "/vendor/resendotp",
+ FORGOT_PASSWORD: '/vendor/forgotpassword',
+ RESET_PASSWORD: '/vendor/resetpassword',
+ // VERIFY_EMAIL: "/vendor/verifyemail",
+ // VERIFY_OTP_RESET: '/vendor/verifyotpforforgotpassword',
+ REFRESH: '/vendor/refresh',
+ LOGOUT: '/vendor/logout'
+ },
+ VENUE: {
+ CREATE: '/venue',
+ EDIT: '/venue/:venueId',
+ GET_BY_ID: '/venues/:venueId',
+ GET_ALL: '/venues',
+ DELETE: '/venue/:venueId',
+ UPDATE_STATUS: '/venue/:venueId/status'
+ },
+ PROFILE: {
+ GET: '/profile',
+ UPDATE: '/profile',
+ CHANGE_PASSWORD: '/change-password'
+ },
+ BOOKING: {
+
+ GET_ALL: '/bookings',
+ GET_BY_ID: '/bookings/:bookingId'
+ },
+ DASHBOARD: '/dashboard'
+ },
+ USER: {
+ AUTH: {
+ LOGIN: "/user/login",
+ REGISTER: "/user/register",
+ VERIFY_OTP: "/user/verifyotp",
+ GOOGLE: "/user/googlelogin",
+ RESEND_OTP: "/user/resendotp",
+ FORGOT_PASSWORD: '/user/forgotpassword',
+ RESET_PASSWORD: '/user/resetpassword',
+ // VERIFY_EMAIL: "/user/verifyemail",
+ // VERIFY_OTP_RESET: '/user/verifyotpforforgotpassword',
+ REFRESH: '/user/refresh',
+ LOGOUT: '/user/logout'
+ },
+ VENUE: {
+ GET_ALL: "/venues",
+ GET_BY_ID: "/venue/:venueId",
+ TOP_VENUES: "/top-venues",
+ SIMILAR_VENUES: "/similar-venues/:venueId",
+ },
+ PROFILE: {
+ PROFILE: "/profile",
+ REQUEST_EMAIL_CHANGE_OTP: "/profile/email/request-otp",
+ VERIFY_EMAIL_CHANGE_OTP: "/profile/email/verify-otp",
+ RESEND_EMAIL_CHANGE_OTP: "/profile/email/resend-otp",
+ PROFILE_IMAGE: "/profile/image",
+ CHANGE_PASSWORD: "/profile/change-password"
+ },
+ WISHLIST: {
+ WISHLIST: "/wishlist/:venueId",
+ GET: "/wishlist",
+ },
+ ACCOUNT: {
+ UPDATE_STATUS: "/account/status",
+ },
+ BOOKING: {
+ RESERVE: "/booking/reserve",
+ CONFIRM: "/booking/confirm",
+ GET_ALL: "/booking",
+ GET_BY_ID: "/booking/:bookingId",
+ CANCEL: "/bookings/:bookingId/cancel",
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/server/src/shared/utils/.gitkeep b/server/src/shared/utils/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/src/shared/utils/apiResponse.js b/server/src/shared/utils/apiResponse.js
new file mode 100644
index 0000000000..515d1d5043
--- /dev/null
+++ b/server/src/shared/utils/apiResponse.js
@@ -0,0 +1,8 @@
+export const sendSuccess = (res, statusCode, message, data) => {
+ const response = {
+ success: true,
+ message,
+ ...(data !== undefined && { data })
+ }
+ return res.status(statusCode).json(response)
+}
\ No newline at end of file
diff --git a/server/src/shared/utils/asyncHandler.js b/server/src/shared/utils/asyncHandler.js
new file mode 100644
index 0000000000..78a57dff7d
--- /dev/null
+++ b/server/src/shared/utils/asyncHandler.js
@@ -0,0 +1,5 @@
+export const asyncHandler = (fn) => {
+ return (req, res, next) => {
+ Promise.resolve(fn(req, res, next)).catch(next)
+ }
+}
\ No newline at end of file