From 401011b407d10705ab2212c76f78214cfdd124a6 Mon Sep 17 00:00:00 2001 From: ajay Date: Wed, 11 Mar 2026 12:20:48 +0530 Subject: [PATCH 1/4] feat:admindashboard - user verification done --- apps/core-api/package.json | 1 + .../src/controllers/admin.controllers.ts | 17 +- .../src/controllers/auth.controller.ts | 1 + .../src/models/pendingSignup.models.ts | 33 +- .../src/repositories/Signup.repository.ts | 28 +- .../src/repositories/user.repository.ts | 41 +- apps/core-api/src/routes/admin.route.ts | 9 +- apps/core-api/src/services/auth.service.ts | 64 +- .../src/services/verification.service.ts | 42 +- .../core-api/src/types/pendingSignup.types.ts | 15 + apps/web/app/(admin)/adminAuth/login/page.tsx | 66 + .../web/app/(admin)/adminAuth/signup/page.tsx | 63 + .../admindashboard/bookings-audit/page.tsx | 101 + .../admindashboard/disputes-reports/page.tsx | 90 + .../admindashboard/gig-moderation/page.tsx | 113 ++ .../web/app/(admin)/admindashboard/layout.tsx | 68 + apps/web/app/(admin)/admindashboard/page.tsx | 85 + .../admindashboard/user-verification/page.tsx | 158 ++ apps/web/app/(auth)/login/page.tsx | 2 +- apps/web/app/gig-list/page.tsx | 592 +++--- apps/web/app/gig/create/page.tsx | 2 +- apps/web/app/home/page.tsx | 46 +- apps/web/app/page.tsx | 7 +- apps/web/app/profile/page.tsx | 82 +- apps/web/app/signup/page.tsx | 12 +- apps/web/components/adminAuth/AuthForm.tsx | 93 + apps/web/components/adminAuth/RoleToggle.tsx | 35 + .../admindashboard/ActivityTable.tsx | 109 ++ .../admindashboard/BookingsTable.tsx | 128 ++ .../admindashboard/BookingsTabs.tsx | 33 + .../DeleteConfirmationModal.tsx | 107 + .../admindashboard/DisputeInvestigation.tsx | 124 ++ .../admindashboard/DisputeTable.tsx | 111 ++ .../components/admindashboard/DisputeTabs.tsx | 33 + .../components/admindashboard/GigsTable.tsx | 142 ++ .../components/admindashboard/GigsTabs.tsx | 41 + .../admindashboard/InvestigationView.tsx | 105 + .../components/admindashboard/MetricCard.tsx | 54 + .../web/components/admindashboard/Sidebar.tsx | 117 ++ .../components/admindashboard/StatsChart.tsx | 76 + .../admindashboard/SystemHealth.tsx | 38 + .../admindashboard/UserDetailsDrawer.tsx | 190 ++ .../admindashboard/VerificationTable.tsx | 149 ++ .../admindashboard/VerificationTabs.tsx | 37 + apps/web/components/rbac/Guards.tsx | 22 + apps/web/components/rbac/RoleGuard.tsx | 65 + apps/web/eslint-report.txt | 132 ++ apps/web/package.json | 1 + pnpm-lock.yaml | 1734 +++++++++++++++-- 49 files changed, 4849 insertions(+), 565 deletions(-) create mode 100644 apps/core-api/src/types/pendingSignup.types.ts create mode 100644 apps/web/app/(admin)/adminAuth/login/page.tsx create mode 100644 apps/web/app/(admin)/adminAuth/signup/page.tsx create mode 100644 apps/web/app/(admin)/admindashboard/bookings-audit/page.tsx create mode 100644 apps/web/app/(admin)/admindashboard/disputes-reports/page.tsx create mode 100644 apps/web/app/(admin)/admindashboard/gig-moderation/page.tsx create mode 100644 apps/web/app/(admin)/admindashboard/layout.tsx create mode 100644 apps/web/app/(admin)/admindashboard/page.tsx create mode 100644 apps/web/app/(admin)/admindashboard/user-verification/page.tsx create mode 100644 apps/web/components/adminAuth/AuthForm.tsx create mode 100644 apps/web/components/adminAuth/RoleToggle.tsx create mode 100644 apps/web/components/admindashboard/ActivityTable.tsx create mode 100644 apps/web/components/admindashboard/BookingsTable.tsx create mode 100644 apps/web/components/admindashboard/BookingsTabs.tsx create mode 100644 apps/web/components/admindashboard/DeleteConfirmationModal.tsx create mode 100644 apps/web/components/admindashboard/DisputeInvestigation.tsx create mode 100644 apps/web/components/admindashboard/DisputeTable.tsx create mode 100644 apps/web/components/admindashboard/DisputeTabs.tsx create mode 100644 apps/web/components/admindashboard/GigsTable.tsx create mode 100644 apps/web/components/admindashboard/GigsTabs.tsx create mode 100644 apps/web/components/admindashboard/InvestigationView.tsx create mode 100644 apps/web/components/admindashboard/MetricCard.tsx create mode 100644 apps/web/components/admindashboard/Sidebar.tsx create mode 100644 apps/web/components/admindashboard/StatsChart.tsx create mode 100644 apps/web/components/admindashboard/SystemHealth.tsx create mode 100644 apps/web/components/admindashboard/UserDetailsDrawer.tsx create mode 100644 apps/web/components/admindashboard/VerificationTable.tsx create mode 100644 apps/web/components/admindashboard/VerificationTabs.tsx create mode 100644 apps/web/components/rbac/Guards.tsx create mode 100644 apps/web/components/rbac/RoleGuard.tsx create mode 100644 apps/web/eslint-report.txt diff --git a/apps/core-api/package.json b/apps/core-api/package.json index e2348d9..23a38a5 100644 --- a/apps/core-api/package.json +++ b/apps/core-api/package.json @@ -16,6 +16,7 @@ "license": "ISC", "packageManager": "pnpm@10.27.0", "dependencies": { + "@aws-sdk/client-s3": "^3.1004.0", "amqplib": "^0.10.9", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", diff --git a/apps/core-api/src/controllers/admin.controllers.ts b/apps/core-api/src/controllers/admin.controllers.ts index 6d4af38..497668b 100644 --- a/apps/core-api/src/controllers/admin.controllers.ts +++ b/apps/core-api/src/controllers/admin.controllers.ts @@ -1,6 +1,6 @@ -import type { Request, Response, NextFunction } from "express"; +import type { Request, Response, NextFunction } from "express"; -import { approveSignupService, rejectSignupService } from "../services/verification.service.js"; +import { approveSignupService, getAllPendingSignupService, rejectSignupService } from "../services/verification.service.js"; export const approveSignupController = async ( req: Request, @@ -33,3 +33,16 @@ export const rejectSignupController = async ( next(error); } }; + +export const getAllpendingSignupController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const requests = await getAllPendingSignupService(req.query); + res.status(200).json({ success: true, data: requests }); + } catch (error) { + next(error); + } +}; diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index 2fbd591..7c9a22c 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -22,6 +22,7 @@ export const signupController = async ( next: NextFunction ) => { try { + const { email, password, role, documents: businessInfo } = req.body; if (!email || !password || !role) { diff --git a/apps/core-api/src/models/pendingSignup.models.ts b/apps/core-api/src/models/pendingSignup.models.ts index d2767d1..472ac30 100644 --- a/apps/core-api/src/models/pendingSignup.models.ts +++ b/apps/core-api/src/models/pendingSignup.models.ts @@ -2,22 +2,27 @@ import { Schema, model } from "mongoose"; const PendingSignupSchema = new Schema( { - email: { - type: String, - required: true, - unique: true + email: { + type: String, + required: true, + unique: true }, - passwordHash: { - type: String, - required: true + passwordHash: { + type: String, + required: true }, role: { type: String, - enum: ["INFLUENCER", "BRAND"], + enum: ["INFLUENCER", "BRAND", "ADMIN"], required: true, }, + adminLevel: { + type: String, + enum: ["SUPER", "NORMAL"], + default: null, + }, documents: { type: String }, @@ -29,12 +34,12 @@ const PendingSignupSchema = new Schema( // OTP FIELDS START HERE - emailOtpHash: { - type: String, - select: false, - default: null, + emailOtpHash: { + type: String, + select: false, + default: null, -}, + }, emailOtpExpiresAt: { @@ -59,7 +64,7 @@ const PendingSignupSchema = new Schema( otpLockedUntil: { type: Date, - default: null, + default: null, }, isEmailVerified: { diff --git a/apps/core-api/src/repositories/Signup.repository.ts b/apps/core-api/src/repositories/Signup.repository.ts index a525570..8e85864 100644 --- a/apps/core-api/src/repositories/Signup.repository.ts +++ b/apps/core-api/src/repositories/Signup.repository.ts @@ -1,11 +1,12 @@ import { PendingSignup } from "../models/pendingSignup.models.js"; - +import type { PendingSignupFilter } from "../types/pendingSignup.types.js"; interface CreatePendingSignupInput { email: string; passwordHash: string; documents: string; - role: "INFLUENCER" | "BRAND"; + role: "INFLUENCER" | "BRAND" | "ADMIN"; + adminLevel?: "SUPER" | "NORMAL"; status: "PENDING" | "APPROVED" | "REJECTED"; @@ -19,18 +20,25 @@ interface CreatePendingSignupInput { isEmailVerified?: boolean; } + + class PendingSignupRepository { // ================= CREATE ================= create(data: CreatePendingSignupInput) { return PendingSignup.create(data); } + //==================GET ALL PENDING SIGNUPS================== + getAllPendingSignups(filter:PendingSignupFilter={}) { + return PendingSignup.find(filter).sort({ createdAt: -1 }); + } + // ================= FIND ================= - findByEmail(email: string) { - return PendingSignup - .findOne({ email }) - .select("+emailOtpHash"); -} + findByEmail(email: string) { + return PendingSignup + .findOne({ email }) + .select("+emailOtpHash"); + } // ================= UPDATE STATUS ================= @@ -48,9 +56,9 @@ class PendingSignupRepository { } // ================= DELETE MANY (FOR CLEANUP) ================= - deleteMany(filter: Record): Promise { - return PendingSignup.deleteMany(filter); -} + deleteMany(filter: Record): Promise { + return PendingSignup.deleteMany(filter); + } } diff --git a/apps/core-api/src/repositories/user.repository.ts b/apps/core-api/src/repositories/user.repository.ts index b9d093b..f57a6a6 100644 --- a/apps/core-api/src/repositories/user.repository.ts +++ b/apps/core-api/src/repositories/user.repository.ts @@ -2,17 +2,17 @@ import { User } from "../models/user.model.js"; class UserRepository { - + // FIND USER WITH PASSWORD - + async findEmailWithPassword(email: string) { const normalizedEmail = email.trim().toLowerCase(); return User.findOne({ email: normalizedEmail }).select("+password"); } - + // FIND USER WITH RESET FIELDS - + async findByEmailWithResetFields(email: string) { const normalizedEmail = email.trim().toLowerCase(); @@ -20,25 +20,25 @@ class UserRepository { .select("+password +resetSessionExpiry +resetSessionToken"); } - + // NORMAL FIND BY EMAIL - + async findByEmail(email: string) { const normalizedEmail = email.trim().toLowerCase(); return User.findOne({ email: normalizedEmail }); } // FIND BY ID - + async findById(userId: string) { return User.findById(userId).select("+refreshToken"); } - + // SAVE REFRESH TOKEN - + async saveRefreshToken(userId: string, refreshToken: string) { - + return User.findByIdAndUpdate( userId, { refreshToken }, @@ -46,9 +46,9 @@ class UserRepository { ); } - + // SAVE RESET OTP - + async saveResetOtp( userId: string, hashedOtp: string, @@ -64,9 +64,9 @@ class UserRepository { ); } - + // SAVE RESET SESSION TOKEN - + async saveResetSession( userId: string, token: string, @@ -84,9 +84,9 @@ class UserRepository { ); } - + // UPDATE PASSWORD (Production-Safe) - + async updatePassword(userId: string, hashedPassword: string) { return User.findByIdAndUpdate( userId, @@ -101,9 +101,9 @@ class UserRepository { ); } - + // CLEAR RESET SESSION - + async clearResetSession(userId: string) { return User.findByIdAndUpdate( userId, @@ -115,13 +115,14 @@ class UserRepository { ); } - + // CREATE USER - + async create(data: { email: string; password: string; role: string; + adminLevel?: string; isEmailVerified: boolean; status: string; }) { diff --git a/apps/core-api/src/routes/admin.route.ts b/apps/core-api/src/routes/admin.route.ts index 5d771a3..57dc047 100644 --- a/apps/core-api/src/routes/admin.route.ts +++ b/apps/core-api/src/routes/admin.route.ts @@ -1,11 +1,14 @@ -import { Router } from "express"; +import { Router } from "express"; import { authenticate } from "../middlewares/auth.middleware.js"; -import { approveSignupController, rejectSignupController } from "../controllers/admin.controllers.js"; +import { approveSignupController, getAllpendingSignupController, rejectSignupController } from "../controllers/admin.controllers.js"; const router: Router = Router(); -router.post("/signup/approve", authenticate, approveSignupController); +router.get("/signup/", authenticate, getAllpendingSignupController); +router.post("/signup/approve", + // authenticate, + approveSignupController); router.post("/signup/reject", authenticate, rejectSignupController); export default router; diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index 4583efc..2a02a23 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -38,8 +38,7 @@ export const signupService = async (data: SignupInput) => { // Generate OTP const otp = Math.floor(100000 + Math.random() * 900000).toString(); - await sendOtpEmail(data.email, otp); - // const hashedOtp = await bcrypt.hash(otp, 10); + // Hash OTP before saving const hashedOtp = await bcrypt.hash(otp, 10); @@ -64,6 +63,7 @@ export const signupService = async (data: SignupInput) => { await sendOtpEmail(data.email, otp); return { message: "OTP sent to your email" }; + }; @@ -73,19 +73,20 @@ interface LoginResult { accessToken: string, refreshToken: string, user: { - id: string, - role: string, + id: string, + email: string, + role: string, adminLevel: string | null } } -export const loginService = async( - email : string, - password : string +export const loginService = async ( + email: string, + password: string ): Promise => { - + const user = await userRepository.findEmailWithPassword(email) - logger.info(`EMAIL: ${email}`); + logger.info(`EMAIL: ${email}`); if (!user) { throw createHttpError("Invalid credentials", 409); @@ -108,21 +109,22 @@ export const loginService = async( } const payload = { - userId : user._id.toString(), - role : user.role, - adminLevel : user.adminLevel ?? null + userId: user._id.toString(), + role: user.role, + adminLevel: user.adminLevel ?? null } const accessToken = signAccessToken(payload) const refreshToken = signRefreshToken(payload) - await userRepository.saveRefreshToken(user._id.toString(),refreshToken) + await userRepository.saveRefreshToken(user._id.toString(), refreshToken) return { accessToken, refreshToken, user: { id: user._id.toString(), + email: user.email, role: user.role, adminLevel: user.adminLevel ?? null } @@ -205,7 +207,7 @@ export const refreshTokenService = async ( refreshToken: string ): Promise => { if (!refreshToken) { - + throw createHttpError("Refresh token required", 409); } @@ -213,7 +215,7 @@ export const refreshTokenService = async ( try { payload = verifyRefreshToken(refreshToken); } catch { - + throw createHttpError("Invalid refresh token", 409); } @@ -221,14 +223,14 @@ export const refreshTokenService = async ( // FIRST check user existence if (!user || !user.refreshToken) { - + throw createHttpError("Refresh token mismatch", 409); } // Compare after narrowing if (user.refreshToken.trim() !== refreshToken.trim()) { - + throw createHttpError("Refresh token mismatch", 409); } @@ -261,7 +263,7 @@ export const refreshTokenService = async ( export const logoutService = async (userId: string) => { if (!userId) { - + throw createHttpError("User not authenticated", 409); } @@ -281,22 +283,22 @@ export const verifySignupOtpService = async ( const pending = await pendingSignupRepository.findByEmail(email); if (!pending) { - + throw createHttpError("Signup request not found", 409); } if (pending.isEmailVerified) { - + throw createHttpError("Email already verified", 409); } if (!pending.emailOtpHash || !pending.emailOtpExpiresAt) { - + throw createHttpError("OTP not found", 409); } if (pending.emailOtpExpiresAt < new Date()) { - + throw createHttpError("OTP expired", 409); } @@ -306,7 +308,7 @@ export const verifySignupOtpService = async ( ); if (!isMatch) { - + throw createHttpError("Invalid OTP", 409); } @@ -325,10 +327,10 @@ export const verifySignupOtpService = async ( export const forgotPasswordService = async (email: string) => { const user = await userRepository.findEmailWithPassword(email); - if (!user) return; + if (!user) return; const otp = Math.floor(100000 + Math.random() * 900000).toString(); - console.log("RESET OTP:", otp); + console.log("RESET OTP:", otp); const hashedOtp = await bcrypt.hash(otp, 10); @@ -355,19 +357,19 @@ export const verifyOtpService = async ( const user = await userRepository.findByEmailWithResetFields(email); if (!user || !user.resetOtp || !user.resetOtpExpiry) { - + throw createHttpError("Invalid request", 409); } if (user.resetOtpExpiry < new Date()) { - + throw createHttpError("OTP expired", 409); } const isMatch = await bcrypt.compare(otp, user.resetOtp); if (!isMatch) { - + throw createHttpError("Invalid OTP", 409); } @@ -399,17 +401,17 @@ export const resetPasswordService = async ( !user.resetSessionToken || !user.resetSessionExpiry ) { - + throw createHttpError("Invalid request", 409); } if (user.resetSessionToken !== resetSessionToken) { - + throw createHttpError("Invalid session", 409); } if (user.resetSessionExpiry < new Date()) { - + throw createHttpError("Session expired", 409); } diff --git a/apps/core-api/src/services/verification.service.ts b/apps/core-api/src/services/verification.service.ts index 886c920..f585bc3 100644 --- a/apps/core-api/src/services/verification.service.ts +++ b/apps/core-api/src/services/verification.service.ts @@ -4,6 +4,7 @@ import type { HttpError } from "../modules/auth/http-error.js"; import { pendingSignupRepository } from "../repositories/Signup.repository.js"; import { userRepository } from "../repositories/user.repository.js"; import { profileRepository } from "../repositories/profile.repository.js"; +import type { PendingSignupFilter, PendingSignupQuery } from "../types/pendingSignup.types.js"; // ================= VERIFY OTP ================= export const verifyOtpService = async ( @@ -141,10 +142,20 @@ export const approveSignupService = async (email: string) => { throw err; } + // Check if user already exists + const existingUser = await userRepository.findByEmail(pending.email); + if (existingUser) { + const err: HttpError = new Error("User with this email already exists"); + err.statusCode = 409; + throw err; + } + const user = await userRepository.create({ email: pending.email, password: pending.passwordHash, role: pending.role, + // @ts-expect-error - adminLevel not in user create type yet + adminLevel: pending.adminLevel || null, isEmailVerified: true, status: "ACTIVE", }); @@ -164,9 +175,9 @@ export const approveSignupService = async (email: string) => { if (user.role === "BRAND") { await profileRepository.createBrand({ userId: user._id, - companyName: "", - industry: "", - contactPersonName: "", + companyName: "Pending Setup", + industry: "Not Specified", + contactPersonName: user.email?.split("@")[0] || "Pending", contactEmail: user.email, documents: [], isProfileComplete: false, @@ -175,7 +186,7 @@ export const approveSignupService = async (email: string) => { } - await pendingSignupRepository.deleteByEmail(email); + await pendingSignupRepository.updateStatus(email, "APPROVED"); return { message: "Signup approved successfully" }; }; @@ -193,16 +204,35 @@ export const rejectSignupService = async ( throw err; } - await pendingSignupRepository.deleteByEmail(email); + await pendingSignupRepository.updateStatus(email, "REJECTED"); return { message: "Signup rejected successfully" }; }; export const cleanupExpiredSignups = async () => { - const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); + const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); await pendingSignupRepository.deleteMany({ isEmailVerified: false, createdAt: { $lt: cutoff }, }); }; + + +export const getAllPendingSignupService = async (query: PendingSignupQuery = {}) => { + const { search, role, status = "PENDING" } = query; + const filter: PendingSignupFilter = { status }; + + if (role) { + filter.role = role.toUpperCase(); + } + + if (search) { + filter.$or = [ + { email: { $regex: search, $options: "i" } }, + { documents: { $regex: search, $options: "i" } }, + ]; + } + + return pendingSignupRepository.getAllPendingSignups(filter); +}; \ No newline at end of file diff --git a/apps/core-api/src/types/pendingSignup.types.ts b/apps/core-api/src/types/pendingSignup.types.ts new file mode 100644 index 0000000..3346ec2 --- /dev/null +++ b/apps/core-api/src/types/pendingSignup.types.ts @@ -0,0 +1,15 @@ +export interface PendingSignupQuery{ + search?:string; + role?:string; + status?:string; + +} + +export interface PendingSignupFilter{ + status?:string; + role?:string; + $or?:Array<{ + email?:{$regex:string,$options:string}; + documents?:{$regex:string,$options:string}; + }>; +} \ No newline at end of file diff --git a/apps/web/app/(admin)/adminAuth/login/page.tsx b/apps/web/app/(admin)/adminAuth/login/page.tsx new file mode 100644 index 0000000..1b81ac4 --- /dev/null +++ b/apps/web/app/(admin)/adminAuth/login/page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; + +import api from "@/lib/axios.client"; +import { useAuthStore } from "@/store/auth.store"; +import AuthForm from "@/components/adminAuth/AuthForm"; + +export default function AdminLoginPage() { + const router = useRouter(); + const setAuth = useAuthStore((state) => state.setAuth); + + const [role, setRole] = useState<"ADMIN" | "SUPER_ADMIN">("ADMIN"); + const [formData, setFormData] = useState({ email: "", password: "" }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + try { + const response = await api.post("/auth/login", formData); + const { accessToken, user } = response.data.data; + + if (accessToken) { + // 1. Role Check: Must be ADMIN + if (user.role !== "ADMIN") { + setError("Access denied: You do not have admin privileges."); + return; + } + + // 2. Admin Level Check: If SUPER_ADMIN selected, verify it + if (role === "SUPER_ADMIN" && user.adminLevel !== "SUPER") { + setError("Access denied: You do not have super admin privileges."); + return; + } + + setAuth(accessToken, user); + router.push("/admindashboard"); + } else { + setError("Login failed: No access token returned."); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + const msg = err.response?.data?.message || err.message || "Login failed. Please try again."; + setError(msg); + } finally { + setLoading(false); + } + }; + + return ( + + ); +} diff --git a/apps/web/app/(admin)/adminAuth/signup/page.tsx b/apps/web/app/(admin)/adminAuth/signup/page.tsx new file mode 100644 index 0000000..59b775c --- /dev/null +++ b/apps/web/app/(admin)/adminAuth/signup/page.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; + +import api from "@/lib/axios.client"; +import { useAuthStore } from "@/store/auth.store"; +import AuthForm from "@/components/adminAuth/AuthForm"; + +export default function AdminSignupPage() { + const router = useRouter(); + const setAuth = useAuthStore((state) => state.setAuth); + + const [role, setRole] = useState<"ADMIN" | "SUPER_ADMIN">("ADMIN"); + const [formData, setFormData] = useState({ email: "", password: "", confirmPassword: "" }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (formData.password !== formData.confirmPassword) { + setError("Passwords do not match."); + return; + } + setLoading(true); + setError(""); + try { + const payload = { + email: formData.email, + password: formData.password, + role: "ADMIN", + adminLevel: role === "SUPER_ADMIN" ? "SUPER" : "NORMAL", + documents: "" // Required by backend validator + }; + const response = await api.post("/auth/signup", payload); + const { accessToken, user } = response.data.data; + if (accessToken) { + setAuth(accessToken, user); + router.push("/admindashboard"); + } else { + setError("Signup failed: No token returned."); + } + } catch (err) { + const msg = err instanceof Error ? err.message : "Signup failed. Please try again."; + setError(msg); + } finally { + setLoading(false); + } + }; + + return ( + + ); +} diff --git a/apps/web/app/(admin)/admindashboard/bookings-audit/page.tsx b/apps/web/app/(admin)/admindashboard/bookings-audit/page.tsx new file mode 100644 index 0000000..df2a93f --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/bookings-audit/page.tsx @@ -0,0 +1,101 @@ +"use client"; +import React, { useState } from "react"; +import { + Search, + Bell, + Download, + Menu, + CheckCircle, + Clock, + DollarSign, + RefreshCw, + Filter +} from "lucide-react"; + +import Sidebar from "@/components/admindashboard/Sidebar"; +import MetricCard from "@/components/admindashboard/MetricCard"; +import BookingsTabs from "@/components/admindashboard/BookingsTabs"; +import BookingsTable from "@/components/admindashboard/BookingsTable"; +import InvestigationView from "@/components/admindashboard/InvestigationView"; +import { AdminGuard } from "@/components/rbac/Guards"; + +export default function BookingsAuditPage() { + return ( + +
+ {/* Left Column: Metrics & Table */} +
+ {/* Welcome Header */} +
+

Bookings & Payments Audit

+

Investigate transaction history and booking lifecycles. Read-only access.

+
+ + {/* Metrics Grid */} +
+ + + + +
+ + {/* Moderation Controls */} +
+ +
+
+ + +
+ +
+
+ + {/* Bookings Table */} + +
+ + {/* Right Column: Investigation View */} +
+ +
+
+
+ ); +} diff --git a/apps/web/app/(admin)/admindashboard/disputes-reports/page.tsx b/apps/web/app/(admin)/admindashboard/disputes-reports/page.tsx new file mode 100644 index 0000000..1e3a174 --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/disputes-reports/page.tsx @@ -0,0 +1,90 @@ +"use client"; +import React, { useState } from "react"; +import { + Search, + Bell, + Download, + Menu, + Clock, + CheckCircle2, + Users, + Filter +} from "lucide-react"; + +import Sidebar from "@/components/admindashboard/Sidebar"; +import MetricCard from "@/components/admindashboard/MetricCard"; +import DisputeTabs from "@/components/admindashboard/DisputeTabs"; +import DisputeTable from "@/components/admindashboard/DisputeTable"; +import DisputeInvestigation from "@/components/admindashboard/DisputeInvestigation"; +import { AdminGuard } from "@/components/rbac/Guards"; + +export default function DisputesReportsPage() { + return ( + +
+ {/* Left Column: Metrics & Table */} +
+ {/* Welcome Header */} +
+

Disputes & Reports

+

Resolve conflicts safely and transparently.

+
+ + {/* Metrics Grid */} +
+ + + +
+ + {/* Search & Filter Controls */} +
+ +
+
+ + +
+ +
+
+ + {/* Disputes Table */} + +
+ + {/* Right Column: Investigation View */} +
+ +
+
+
+ ); +} diff --git a/apps/web/app/(admin)/admindashboard/gig-moderation/page.tsx b/apps/web/app/(admin)/admindashboard/gig-moderation/page.tsx new file mode 100644 index 0000000..52f6e5a --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/gig-moderation/page.tsx @@ -0,0 +1,113 @@ +"use client"; +import React, { useState } from "react"; +import { + Bell, + Download, + Plus, + Menu, + Briefcase, + AlertCircle, + PauseCircle, + DollarSign, + Filter, + Search +} from "lucide-react"; + +import Sidebar from "@/components/admindashboard/Sidebar"; +import MetricCard from "@/components/admindashboard/MetricCard"; +import GigsTabs from "@/components/admindashboard/GigsTabs"; +import GigsTable from "@/components/admindashboard/GigsTable"; +import DeleteConfirmationModal from "@/components/admindashboard/DeleteConfirmationModal"; + +export default function GigsModerationPage() { + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedGig, setSelectedGig] = useState(null); + + const handleDeleteClick = (gig: unknown) => { + setSelectedGig(gig); + setIsDeleteModalOpen(true); + }; + + const handleConfirmDelete = () => { + if (selectedGig && typeof selectedGig === 'object' && 'id' in selectedGig) { + console.log("Deleting gig:", selectedGig.id); + } + // Add actual delete logic here + }; + + return ( + <> + {/* Welcome Header */} +
+

Gig Moderation

+

Review and manage influencer service listings to ensure platform compliance.

+
+ + {/* Metrics Grid */} +
+ + + + +
+ + {/* Moderation Controls */} +
+ +
+
+ + +
+ +
+
+ + {/* Gigs Table */} + + + {/* Delete Confirmation Modal */} + setIsDeleteModalOpen(false)} + onConfirm={handleConfirmDelete} + /> + + ); +} diff --git a/apps/web/app/(admin)/admindashboard/layout.tsx b/apps/web/app/(admin)/admindashboard/layout.tsx new file mode 100644 index 0000000..e865916 --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/layout.tsx @@ -0,0 +1,68 @@ +"use client"; + +import React, { useState } from "react"; +import { Menu, Search, Bell, Download, Plus } from "lucide-react"; + +import { AdminGuard } from "@/components/rbac/Guards"; +import Sidebar from "@/components/admindashboard/Sidebar"; + +export default function AdminDashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + return ( + +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Topbar */} +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ + {/* Page Content */} +
+ {children} +
+
+
+
+ ); +} diff --git a/apps/web/app/(admin)/admindashboard/page.tsx b/apps/web/app/(admin)/admindashboard/page.tsx new file mode 100644 index 0000000..50ca421 --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/page.tsx @@ -0,0 +1,85 @@ +"use client"; +import React, { useState } from "react"; +import { + Users, + UserCheck, + Briefcase, + DollarSign, + Search, + Bell, + Download, + Plus, + Menu +} from "lucide-react"; + +import Sidebar from "@/components/admindashboard/Sidebar"; +import MetricCard from "@/components/admindashboard/MetricCard"; +import ActivityTable from "@/components/admindashboard/ActivityTable"; +import StatsChart from "@/components/admindashboard/StatsChart"; +import SystemHealth from "@/components/admindashboard/SystemHealth"; + +export default function DashboardPage() { + return ( + <> + {/* Welcome Header */} +
+

Platform Overview

+

Welcome back, here's what's happening today.

+
+ + {/* Metrics Grid */} +
+ + + + +
+ + {/* Main Grid: Activity and Trends */} +
+
+ +
+ +
+
+ +
+
+ +
+
+
+ + ); +} diff --git a/apps/web/app/(admin)/admindashboard/user-verification/page.tsx b/apps/web/app/(admin)/admindashboard/user-verification/page.tsx new file mode 100644 index 0000000..5bda128 --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/user-verification/page.tsx @@ -0,0 +1,158 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { + Users, + UserCheck, + Briefcase, + DollarSign, + Search, + Bell, + Download, + Plus, + Menu +} from "lucide-react"; + +import Sidebar from "@/components/admindashboard/Sidebar"; +import MetricCard from "@/components/admindashboard/MetricCard"; +import VerificationTable from "@/components/admindashboard/VerificationTable"; +import VerificationTabs from "@/components/admindashboard/VerificationTabs"; +import UserDetailsDrawer from "@/components/admindashboard/UserDetailsDrawer"; +import api from "@/lib/axios.client"; + +export default function VerificationPage() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [selectedUser, setSelectedUser] = useState(null); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [requests, setRequests] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchRequests = async () => { + setLoading(true); + try { + const response = await api.get("/admin/signup/"); + if (response.data.success) { + setRequests(response.data.data); + } + } catch (error) { + console.error("Failed to fetch requests:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchRequests(); + }, []); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleSelectUser = (user: any) => { + setSelectedUser(user); + setIsDrawerOpen(true); + }; + + const handleApprove = async (email: string) => { + try { + const response = await api.post("/admin/signup/approve", { email }); + if (response.data.success) { + // Refresh list + fetchRequests(); + } + } catch (error) { + console.error("Failed to approve:", error); + } + }; + + const handleReject = async (email: string) => { + try { + const response = await api.post("/admin/signup/reject", { email }); + if (response.data.success) { + // Refresh list + fetchRequests(); + } + } catch (error) { + console.error("Failed to reject:", error); + } + }; + + return ( + <> + {/* Welcome Header */} +
+

User Verification

+

Review and manage pending influencer and brand applications.

+
+ + {/* Metrics Grid */} +
+ + + + +
+ + {/* Verification Controls */} +
+ +
+
+ + +
+
+
+ + {/* Verification Table */} + + + {/* User Details Drawer */} + setIsDrawerOpen(false)} + user={selectedUser} + onApprove={handleApprove} + onReject={handleReject} + /> + + ); +} diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index 92a63ca..93fbcd6 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -45,7 +45,7 @@ export default function LoginPage() { if (response.data.data?.accessToken) { const {accessToken, user} = response.data.data setAuth(accessToken, user) - router.push("/home") ; + router.push("/home"); ; } } catch (error) { diff --git a/apps/web/app/gig-list/page.tsx b/apps/web/app/gig-list/page.tsx index 5336043..25426ab 100644 --- a/apps/web/app/gig-list/page.tsx +++ b/apps/web/app/gig-list/page.tsx @@ -1,6 +1,9 @@ "use client"; import { useState, useEffect, useCallback } from "react"; +import RoleGuard from "@/components/rbac/RoleGuard"; +import { useAuthStore } from "@/store/auth.store"; + // ─── Types ─────────────────────────────────────────────────────────────────── type Platform = "IG" | "YT" | "TT" | "PT"; @@ -219,13 +222,13 @@ function GigCard({ gig, view }: { gig: Gig; view: ViewMode }) {
{platforms.length > 0 ? platforms.map((p) => ( - - {platformIcons[p]} {p} - - )) + + {platformIcons[p]} {p} + + )) : ( - - )} + + )}
@@ -266,6 +269,7 @@ function GigCard({ gig, view }: { gig: Gig; view: ViewMode }) { const MAX_PRICE_LIMIT = 50000; export default function ExploreGigs() { + const { user } = useAuthStore(); const [search, setSearch] = useState(""); const [activeCategory, setActiveCategory] = useState(null); const [activePlatform, setActivePlatform] = useState(null); @@ -385,333 +389,329 @@ export default function ExploreGigs() { }; return ( -
- {/* Navbar */} - + -
- {/* Sidebar */} -
- {/* Category */} -
-

Category

-
    - {categories.map((c) => ( -
  • - -
  • - ))} -
-
- - {/* Price Range */} -
-

Price Range

- -
- ₹1k - - {maxPrice >= MAX_PRICE_LIMIT ? "₹50k+" : `₹${(maxPrice / 1000).toFixed(0)}k`} - + }`} + > + {c} + + + ))} +
-
- {/* Available toggle */} -
- - Available this week -
- - {/* Active filters summary */} - {(activeCategory || activePlatform || maxPrice < MAX_PRICE_LIMIT) && ( + {/* Price Range */}
-
-

Active Filters

- -
-
- {activeCategory && ( - - {activeCategory.split(" & ")[0]} - - - )} - {activePlatform && ( - - {activePlatform} - - - )} - {maxPrice < MAX_PRICE_LIMIT && ( - - ≤₹{(maxPrice / 1000).toFixed(0)}k - - - )} +

Price Range

+ +
+ ₹1k + + {maxPrice >= MAX_PRICE_LIMIT ? "₹50k+" : `₹${(maxPrice / 1000).toFixed(0)}k`} +
- )} - - - {/* Main */} -
- {/* Header */} -
-

Explore Influencer Gigs

-

Browse verified creators and book based on real availability.

-
- {/* Search */} -
- - - - setSearch(e.target.value)} - onKeyDown={handleSearchKeyDown} - onBlur={handleSearchBlur} - className="w-full pl-10 pr-10 py-3 bg-white border border-gray-200 rounded-xl text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent shadow-sm" - /> - {search && ( + {/* Available toggle */} +
+ Available this week +
+ + {/* Active filters summary */} + {(activeCategory || activePlatform || maxPrice < MAX_PRICE_LIMIT) && ( +
+
+

Active Filters

+ +
+
+ {activeCategory && ( + + {activeCategory.split(" & ")[0]} + + + )} + {activePlatform && ( + + {activePlatform} + + + )} + {maxPrice < MAX_PRICE_LIMIT && ( + + ≤₹{(maxPrice / 1000).toFixed(0)}k + + + )} +
+
)} -
+ + + {/* Main */} +
+ {/* Header */} +
+

Explore Influencer Gigs

+

Browse verified creators and book based on real availability.

+
- {/* Toolbar */} -
-

- {loading ? ( - - ) : pagination ? ( - <> - Showing{" "} - {gigs.length}{" "} - of{" "} - {pagination.total}{" "} - verified gigs - - ) : null} -

-
- {/* Grid/List toggle */} -
- + {/* Search */} +
+ + + + setSearch(e.target.value)} + onKeyDown={handleSearchKeyDown} + onBlur={handleSearchBlur} + className="w-full pl-10 pr-10 py-3 bg-white border border-gray-200 rounded-xl text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent shadow-sm" + /> + {search && ( + )} +
+ + {/* Toolbar */} +
+
+ {loading ? ( + + ) : pagination ? ( + <> + Showing{" "} + {gigs.length}{" "} + of{" "} + {pagination.total}{" "} + verified gigs + + ) : null}
+
+ {/* Grid/List toggle */} +
+ + +
- + +
-
- {/* Error state */} - {error && ( -
- {error} - -
- )} + {/* Error state */} + {error && ( +
+ {error} + +
+ )} - {/* Cards */} -
- {loading - ? Array.from({ length: LIMIT }).map((_, i) => ) - : gigs.map((gig) => ( + }`} + > + {loading + ? Array.from({ length: LIMIT }).map((_, i) => ) + : gigs.map((gig) => ( ))} -
- - {/* Empty state */} - {!loading && !error && gigs.length === 0 && ( -
-
🔍
-

No gigs found

-

Try adjusting your filters or search query.

-
- )} - {/* Pagination */} - {!loading && totalPages > 1 && ( -
- + {/* Empty state */} + {!loading && !error && gigs.length === 0 && ( +
+
🔍
+

No gigs found

+

Try adjusting your filters or search query.

+ +
+ )} - {pageNumbers().map((n, i) => - n === "…" ? ( - - … - - ) : ( - + + {pageNumbers().map((n, i) => + n === "…" ? ( + + … + + ) : ( + - ) - )} + }`} + > + {n} + + ) + )} - -
- )} -
+ +
+ )} + +
- + ); } \ No newline at end of file diff --git a/apps/web/app/gig/create/page.tsx b/apps/web/app/gig/create/page.tsx index 6fe98ad..8bed05d 100644 --- a/apps/web/app/gig/create/page.tsx +++ b/apps/web/app/gig/create/page.tsx @@ -84,7 +84,7 @@ export default function GigCreatePage() { Your gig is now live and visible to brands. You can manage it from your dashboard.

+ +
+

Dashboard

+

Welcome {user?.email} ({user?.role})

+ +
+ + + +
- +
); - } diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 87a7b7c..41d0e45 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -5,6 +5,9 @@ export default function HomePage() { const router = useRouter(); const handleLoginClick = () => { router.push("/login"); + }; + const handleSignupClick = () => { + router.push("/signup"); } return (
@@ -28,10 +31,10 @@ export default function HomePage() {
- -
diff --git a/apps/web/app/profile/page.tsx b/apps/web/app/profile/page.tsx index e0edf4c..2abed7f 100644 --- a/apps/web/app/profile/page.tsx +++ b/apps/web/app/profile/page.tsx @@ -1,52 +1,70 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import api from "@/lib/axios.client"; +import { useAuthStore } from "@/store/auth.store"; +import RoleGuard from "@/components/rbac/RoleGuard"; export default function ProfilePage() { const router = useRouter(); -interface ProfileResponse { - email?: string; - username?: string; - fullName?: string; - companyName?: string; - bio?: string; - location?: string; - industry?: string; - website?: string; - isProfileComplete?: boolean; - isVerified?: boolean; -} + const { user } = useAuthStore(); + + interface ProfileResponse { + email?: string; + username?: string; + fullName?: string; + companyName?: string; + bio?: string; + location?: string; + industry?: string; + website?: string; + isProfileComplete?: boolean; + isVerified?: boolean; + } -const [profile, setProfile] = useState(null); + const [profile, setProfile] = useState(null); useEffect(() => { const fetchProfile = async () => { - const res = await api.get("/profile/get_profile"); - setProfile(res.data.data); + try { + const res = await api.get("/profile/get_profile"); + setProfile(res.data.data); + } catch (err) { + console.error("Failed to fetch profile", err); + } }; fetchProfile(); }, []); - if (!profile) return

Loading...

; - return ( -
-

My Profile

- -
-        {JSON.stringify(profile, null, 2)}
-      
- - -
+ +
+

My Profile

+

Account Role: {user?.role}

+ + {profile ? ( + <> +
+              {JSON.stringify(profile, null, 2)}
+            
+ + + + ) : ( +
+
+ Loading profile data... +
+ )} +
+ ); } diff --git a/apps/web/app/signup/page.tsx b/apps/web/app/signup/page.tsx index c6b6a3c..46bab44 100644 --- a/apps/web/app/signup/page.tsx +++ b/apps/web/app/signup/page.tsx @@ -2,6 +2,7 @@ "use client"; import { useState } from "react"; +import { useRouter } from "next/navigation"; import api from "@/lib/axios.client"; @@ -17,6 +18,7 @@ export default function LoginPage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(false); + const router=useRouter(); const isValidBusinessInfo = (info: string) => { const gstRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/; @@ -63,15 +65,21 @@ export default function LoginPage() { const response = await api.post("/auth/signup", payload); + if(response.data.role==="BRAND"){ + router.push("/gig-list") + }else{ + router.push("/") + } + if (response.data.accessToken) { // localStorage.setItem("accessToken", response.data.accessToken); - // window.location.href = "/dashboard"; + // window.location.href = "/admindashboard"; setFormData({ fullName: "", email: "", password: "", confirmPassword: "", - businessInfo: "", + businessInfo: "" }); } setSuccess(true); diff --git a/apps/web/components/adminAuth/AuthForm.tsx b/apps/web/components/adminAuth/AuthForm.tsx new file mode 100644 index 0000000..a1e7161 --- /dev/null +++ b/apps/web/components/adminAuth/AuthForm.tsx @@ -0,0 +1,93 @@ +"use client"; + +import React, { ChangeEvent, FormEvent } from "react"; + +import RoleToggle from "./RoleToggle"; + +interface AuthFormProps { + type: "login" | "signup"; + role: "ADMIN" | "SUPER_ADMIN"; + setRole: (role: "ADMIN" | "SUPER_ADMIN") => void; + formData: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setFormData: React.Dispatch>; + loading: boolean; + error: string; + onSubmit: (e: FormEvent) => Promise; +} + +export default function AuthForm({ + type, + role, + setRole, + formData, + setFormData, + loading, + error, + onSubmit, +}: AuthFormProps) { + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setFormData({ ...formData, [name]: value }); + }; + + return ( +
+
+

+ {type === "login" ? "Admin Login" : "Admin Sign Up"} +

+ {error &&
{error}
} + +
+ {/* Email */} +
+ + +
+ {/* Password */} +
+ + +
+ {/* Confirm password for signup */} + {type === "signup" && ( +
+ + +
+ )} + +
+
+
+ ); +} diff --git a/apps/web/components/adminAuth/RoleToggle.tsx b/apps/web/components/adminAuth/RoleToggle.tsx new file mode 100644 index 0000000..33c4fbc --- /dev/null +++ b/apps/web/components/adminAuth/RoleToggle.tsx @@ -0,0 +1,35 @@ +"use client"; + +import React from "react"; + +interface RoleToggleProps { + role: "ADMIN" | "SUPER_ADMIN"; + setRole: (role: "ADMIN" | "SUPER_ADMIN") => void; +} + +export default function RoleToggle({ role, setRole }: RoleToggleProps) { + return ( +
+ + +
+ ); +} diff --git a/apps/web/components/admindashboard/ActivityTable.tsx b/apps/web/components/admindashboard/ActivityTable.tsx new file mode 100644 index 0000000..30bed3b --- /dev/null +++ b/apps/web/components/admindashboard/ActivityTable.tsx @@ -0,0 +1,109 @@ +import React from "react"; + +import { cn } from "@/lib/utils"; + +const activities = [ + { + icon: "👤", + title: "New Signup", + entity: { name: "Alex Rivera", avatar: "https://i.pravatar.cc/150?u=alex" }, + status: "Pending Verification", + time: "2 mins ago", + statusColor: "bg-orange-50 text-orange-600 border-orange-100", + }, + { + icon: "📅", + title: "New Booking", + entity: { name: "Summer Campaign" }, + status: "Confirmed", + time: "14 mins ago", + statusColor: "bg-emerald-50 text-emerald-600 border-emerald-100", + }, + { + icon: "🎬", + title: "New Gig Published", + entity: { name: "UGC Video Review" }, + status: "Active", + time: "45 mins ago", + statusColor: "bg-blue-50 text-blue-600 border-blue-100", + }, + { + icon: "⚠️", + title: "Dispute Raised", + entity: { name: "Order #8821" }, + status: "Urgent", + time: "1 hour ago", + statusColor: "bg-red-50 text-red-600 border-red-100", + }, + { + icon: "💰", + title: "Payout Processed", + entity: { name: "$1,200.00" }, + status: "Completed", + time: "3 hours ago", + statusColor: "bg-gray-50 text-gray-600 border-gray-100", + }, +]; + +export default function ActivityTable() { + return ( +
+
+

Recent Platform Activity

+ +
+ +
+ + + + + + + + + + + {activities.map((activity, idx) => ( + + + + + + + ))} + +
ActivityEntityStatusTime
+
+
+ {activity.icon} +
+ {activity.title} +
+
+
+ {activity.entity.avatar && ( + {activity.entity.name} + )} + {activity.entity.name} +
+
+ + {activity.status} + + + {activity.time} +
+
+
+ ); +} diff --git a/apps/web/components/admindashboard/BookingsTable.tsx b/apps/web/components/admindashboard/BookingsTable.tsx new file mode 100644 index 0000000..23de0fe --- /dev/null +++ b/apps/web/components/admindashboard/BookingsTable.tsx @@ -0,0 +1,128 @@ +"use client"; +import React from "react"; +import { Eye, CheckCircle2, Clock, AlertTriangle } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const bookings = [ + { + id: "#BK-99210", + brand: "Nike Global", + brandLogo: "https://logo.clearbit.com/nike.com", + influencer: "Sarah Jenkins", + influencerAvatar: "https://i.pravatar.cc/150?u=sarah", + amount: "$1,250.00", + paymentStatus: "PAID", + paymentStatusColor: "bg-emerald-50 text-emerald-600 border-emerald-100", + bookingStatus: "In Progress", + bookingStatusColor: "bg-blue-50 text-blue-600 border-blue-100", + }, + { + id: "#BK-88200", + brand: "Samsung Mobile", + brandLogo: "https://logo.clearbit.com/samsung.com", + influencer: "Marcus Chen", + influencerAvatar: "https://i.pravatar.cc/150?u=marcus", + amount: "$4,800.00", + paymentStatus: "ESCROW", + paymentStatusColor: "bg-orange-50 text-orange-600 border-orange-100", + bookingStatus: "Pending Approval", + bookingStatusColor: "bg-gray-100 text-gray-600 border-gray-200", + }, + { + id: "#BK-99195", + brand: "Coca-Cola", + brandLogo: "https://logo.clearbit.com/cocacola.com", + influencer: "Elena Rodriguez", + influencerAvatar: "https://i.pravatar.cc/150?u=elena", + amount: "$950.00", + paymentStatus: "DISPUTED", + paymentStatusColor: "bg-red-50 text-red-600 border-red-100", + bookingStatus: "On Hold", + bookingStatusColor: "bg-red-50 text-red-600 border-red-100", + }, +]; + +export default function BookingsTable() { + return ( +
+
+ + + + + + + + + + + + + + {bookings.map((booking) => ( + + + + + + + + + + ))} + +
Booking IDBrandInfluencerAmountPayment StatusBooking StatusAudit
+ {booking.id} + +
+
+ { + (e.target as HTMLImageElement).parentElement!.innerText = booking.brand[0]; + }} /> +
+ {booking.brand} +
+
+
+
+ +
+ {booking.influencer} +
+
+ {booking.amount} + +
+ {booking.paymentStatus === "PAID" && } + {booking.paymentStatus === "ESCROW" && } + {booking.paymentStatus === "DISPUTED" && } + {booking.paymentStatus} +
+
+ + {booking.bookingStatus} + + + +
+
+ +
+

Showing 1 to 10 of 1,284 entries

+
+ +
+ + + +
+ +
+
+
+ ); +} diff --git a/apps/web/components/admindashboard/BookingsTabs.tsx b/apps/web/components/admindashboard/BookingsTabs.tsx new file mode 100644 index 0000000..4288ddd --- /dev/null +++ b/apps/web/components/admindashboard/BookingsTabs.tsx @@ -0,0 +1,33 @@ +"use client"; +import React, { useState } from "react"; + +import { cn } from "@/lib/utils"; + +const tabs = [ + { id: "all", label: "All Bookings" }, + { id: "flagged", label: "Flagged" }, + { id: "refunded", label: "Refunded" }, +]; + +export default function BookingsTabs() { + const [activeTab, setActiveTab] = useState("all"); + + return ( +
+ {tabs.map((tab) => ( + + ))} +
+ ); +} diff --git a/apps/web/components/admindashboard/DeleteConfirmationModal.tsx b/apps/web/components/admindashboard/DeleteConfirmationModal.tsx new file mode 100644 index 0000000..6a174ab --- /dev/null +++ b/apps/web/components/admindashboard/DeleteConfirmationModal.tsx @@ -0,0 +1,107 @@ +"use client"; +import React, { useEffect, useState } from "react"; +import { AlertCircle, X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +interface DeleteConfirmationModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title?: string; + description?: string; +} + +export default function DeleteConfirmationModal({ + isOpen, + onClose, + onConfirm, + title = "Remove Gig Listing?", + description = "Are you sure you want to remove? This action cannot be undone and the influencer will be notified", +}: DeleteConfirmationModalProps) { + const [isVisible, setIsVisible] = useState(isOpen); + const [animate, setAnimate] = useState(false); + + // Synchronize isVisible with isOpen state when it becomes true so it renders immediately + if (isOpen && !isVisible) { + setIsVisible(true); + } + + useEffect(() => { + let timer: NodeJS.Timeout; + if (isOpen) { + timer = setTimeout(() => setAnimate(true), 10); + } else { + // Start exit animation and set cascade timeout + timer = setTimeout(() => { + setAnimate(false); + setTimeout(() => setIsVisible(false), 300); + }, 0); + } + return () => clearTimeout(timer); + }, [isOpen]); + + if (!isVisible && !isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal Content */} +
+
+ {/* Icon Container */} +
+ +
+ +

+ {title} +

+

+ {description} +

+
+ + {/* Footer Actions */} +
+ + +
+ + {/* Optional Close Button */} + +
+
+ ); +} diff --git a/apps/web/components/admindashboard/DisputeInvestigation.tsx b/apps/web/components/admindashboard/DisputeInvestigation.tsx new file mode 100644 index 0000000..7534700 --- /dev/null +++ b/apps/web/components/admindashboard/DisputeInvestigation.tsx @@ -0,0 +1,124 @@ +"use client"; +import React from "react"; +import { MessageSquare, ShieldCheck, Info, CornerDownRight, AlertTriangle, ShieldAlert } from "lucide-react"; + +export default function DisputeInvestigation() { + return ( +
+ {/* Header */} +
+
+

Investigation View

+

Report #REP-88291

+
+ +
+ +
+ {/* Issue Description */} +
+

Issue Description

+
+

+ "Influencer failed to include the agreed-upon brand mentions in the final video delivery. Payment was released but content does not meet the brief requirements." +

+
+
+ + {/* Chat History Snippet */} +
+

Chat History Snippet

+
+ {/* Influencer Message */} +
+
+ +
+
+

I've uploaded the video. Can you check?

+
+
+ {/* Brand Message */} +
+
+ +
+
+

Wait, you missed the 30s brand shoutout!

+
+
+
+
+ + {/* Audit Trail */} +
+

Audit Trail

+
+
+ +
+
+ +
+
+

Report Created

+

Oct 24, 2023 • 09:12 AM

+
+
+ +
+
+
+
+
+

Admin Assigned (You)

+

Oct 24, 2023 • 10:45 AM

+
+
+
+
+ + {/* Admin Notes */} +
+
+

Admin Notes

+ *Required for action +
+