diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9ba7f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.DS_Store +server/public +vite.config.ts.* +*.tar.gz \ No newline at end of file diff --git a/.replit b/.replit index e69de29..51f344f 100644 --- a/.replit +++ b/.replit @@ -0,0 +1,36 @@ +modules = ["nodejs-20", "web", "postgresql-16"] +run = "npm run dev" +hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"] + +[nix] +channel = "stable-24_05" + +[deployment] +deploymentTarget = "autoscale" +build = ["npm", "run", "build"] +run = ["npm", "run", "start"] + +[[ports]] +localPort = 5000 +externalPort = 80 + +[workflows] +runButton = "Project" + +[[workflows.workflow]] +name = "Project" +mode = "parallel" +author = "agent" + +[[workflows.workflow.tasks]] +task = "workflow.run" +args = "Start application" + +[[workflows.workflow]] +name = "Start application" +author = "agent" + +[[workflows.workflow.tasks]] +task = "shell.exec" +args = "npm run dev" +waitForPort = 5000 diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..4b4d09e --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..61e53b1 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,110 @@ +import { Switch, Route } from "wouter"; +import { queryClient } from "./lib/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { useAuth } from "@/hooks/useAuth"; +import { useEffect } from "react"; +import { useToast } from "@/hooks/use-toast"; +import { isUnauthorizedError } from "@/lib/authUtils"; + +// Pages +import NotFound from "@/pages/not-found"; +import Landing from "@/pages/Landing"; +import Home from "@/pages/Home"; +import Profile from "@/pages/Profile"; +import Events from "@/pages/Events"; +import Schedule from "@/pages/Schedule"; +import Availability from "@/pages/Availability"; +import AdminDashboard from "@/pages/admin/AdminDashboard"; +import ManageDJs from "@/pages/admin/ManageDJs"; +import EventTypes from "@/pages/admin/EventTypes"; +import Templates from "@/pages/admin/Templates"; +import AssignmentTool from "@/pages/admin/AssignmentTool"; +import PublicDJProfile from "@/pages/PublicDJProfile"; + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { toast } = useToast(); + const { isAuthenticated, isLoading } = useAuth(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + toast({ + title: "Unauthorized", + description: "You are logged out. Logging in again...", + variant: "destructive", + }); + setTimeout(() => { + window.location.href = "/api/login"; + }, 500); + return; + } + }, [isAuthenticated, isLoading, toast]); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + return null; + } + + return <>{children}; +} + +function Router() { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( + + {/* Public routes */} + + + {/* Protected routes */} + {isAuthenticated ? ( + <> + + + + + + + + + + + + ) : ( + + )} + + {/* Fallback */} + + + ); +} + +function App() { + return ( + + + + + + + ); +} + +export default App; diff --git a/client/src/components/admin/AdminDashboard.tsx b/client/src/components/admin/AdminDashboard.tsx new file mode 100644 index 0000000..220722e --- /dev/null +++ b/client/src/components/admin/AdminDashboard.tsx @@ -0,0 +1,90 @@ +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { PendingRequests } from "./PendingRequests"; +import { AssignmentTool } from "./AssignmentTool"; + +export function AdminDashboard() { + const { data: stats, isLoading } = useQuery({ + queryKey: ["/api/stats/admin"], + }); + + if (isLoading) { + return ( +
+
+ {[...Array(4)].map((_, i) => ( + + + + + + ))} +
+
+ + +
+
+ ); + } + + const statCards = [ + { + title: "Total DJs", + value: stats?.totalDJs || 0, + icon: "fas fa-users", + color: "blue", + }, + { + title: "Active Events", + value: stats?.activeEvents || 0, + icon: "fas fa-calendar-check", + color: "green", + }, + { + title: "Pending Requests", + value: stats?.pendingRequests || 0, + icon: "fas fa-exclamation-triangle", + color: "yellow", + }, + { + title: "This Month", + value: stats?.thisMonth || 0, + icon: "fas fa-chart-bar", + color: "purple", + }, + ]; + + return ( +
+
+

Admin Dashboard

+

Manage DJs, events, and system settings

+
+ +
+ {statCards.map((stat) => ( + + +
+
+

{stat.title}

+

{stat.value}

+
+
+ +
+
+
+
+ ))} +
+ +
+ + +
+
+ ); +} diff --git a/client/src/components/admin/AssignmentTool.tsx b/client/src/components/admin/AssignmentTool.tsx new file mode 100644 index 0000000..6c948b3 --- /dev/null +++ b/client/src/components/admin/AssignmentTool.tsx @@ -0,0 +1,170 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Checkbox } from "@/components/ui/checkbox"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import { Wand2 } from "lucide-react"; + +export function AssignmentTool() { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [selectedTemplate, setSelectedTemplate] = useState(""); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [randomize, setRandomize] = useState(true); + const [respectFrequency, setRespectFrequency] = useState(true); + const [checkAvailability, setCheckAvailability] = useState(true); + + const { data: templates } = useQuery({ + queryKey: ["/api/schedule-templates"], + }); + + const generateAssignmentsMutation = useMutation({ + mutationFn: async (data: any) => { + // TODO: Implement assignment generation logic + await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call + return { success: true, message: "Assignments generated successfully" }; + }, + onSuccess: () => { + toast({ + title: "Assignments generated", + description: "DJ assignments have been generated successfully.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/events"] }); + }, + onError: (error) => { + toast({ + title: "Error", + description: "Failed to generate assignments", + variant: "destructive", + }); + }, + }); + + const handleGenerateAssignments = () => { + if (!selectedTemplate || !startDate || !endDate) { + toast({ + title: "Validation Error", + description: "Please fill in all required fields", + variant: "destructive", + }); + return; + } + + const data = { + templateId: parseInt(selectedTemplate), + startDate, + endDate, + options: { + randomize, + respectFrequency, + checkAvailability, + }, + }; + + generateAssignmentsMutation.mutate(data); + }; + + return ( + + + Quick Assignment Tool +

Assign DJs to schedule templates

+
+ +
+
+ + +
+ +
+
+ + setStartDate(e.target.value)} + /> +
+
+ + setEndDate(e.target.value)} + /> +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+
+
+ ); +} diff --git a/client/src/components/admin/DJManagement.tsx b/client/src/components/admin/DJManagement.tsx new file mode 100644 index 0000000..db00391 --- /dev/null +++ b/client/src/components/admin/DJManagement.tsx @@ -0,0 +1,263 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { DataTable } from "@/components/ui/data-table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import { UserPlus, UserCheck, UserX, Shield, ShieldOff } from "lucide-react"; + +export function DJManagement() { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [showInviteModal, setShowInviteModal] = useState(false); + const [inviteName, setInviteName] = useState(""); + const [inviteEmail, setInviteEmail] = useState(""); + + const { data: djs, isLoading } = useQuery({ + queryKey: ["/api/users"], + }); + + const { data: invitations } = useQuery({ + queryKey: ["/api/invitations"], + }); + + const inviteDJMutation = useMutation({ + mutationFn: async (data: { name: string; email: string }) => { + await apiRequest("POST", "/api/invitations", data); + }, + onSuccess: () => { + toast({ + title: "Invitation sent", + description: "DJ invitation has been sent successfully.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/invitations"] }); + setShowInviteModal(false); + setInviteName(""); + setInviteEmail(""); + }, + onError: (error) => { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + }, + }); + + const toggleUserStatusMutation = useMutation({ + mutationFn: async (data: { userId: string; action: "activate" | "deactivate" }) => { + const endpoint = data.action === "activate" ? "reactivate" : "deactivate"; + await apiRequest("POST", `/api/users/${data.userId}/${endpoint}`); + }, + onSuccess: () => { + toast({ + title: "Status updated", + description: "User status has been updated successfully.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/users"] }); + }, + onError: (error) => { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + }, + }); + + const toggleAdminMutation = useMutation({ + mutationFn: async (data: { userId: string; action: "make-admin" | "remove-admin" }) => { + await apiRequest("POST", `/api/users/${data.userId}/${data.action}`); + }, + onSuccess: () => { + toast({ + title: "Admin status updated", + description: "User admin status has been updated successfully.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/users"] }); + }, + onError: (error) => { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + }, + }); + + const handleInvite = () => { + if (!inviteName || !inviteEmail) { + toast({ + title: "Validation Error", + description: "Please fill in all fields", + variant: "destructive", + }); + return; + } + + inviteDJMutation.mutate({ name: inviteName, email: inviteEmail }); + }; + + const columns = [ + { + key: "displayName" as const, + header: "DJ Name", + cell: (dj: any) => ( +
+ {dj.displayName} +
+
{dj.displayName || dj.firstName}
+
{dj.email}
+
+
+ ), + }, + { + key: "role" as const, + header: "Role", + cell: (dj: any) => ( + + {dj.role} + + ), + }, + { + key: "isActive" as const, + header: "Status", + cell: (dj: any) => ( + + {dj.isActive ? "Active" : "Inactive"} + + ), + }, + { + key: "maxEventsPerMonth" as const, + header: "Max Events/Month", + }, + ]; + + const actions = (dj: any) => ( +
+ + + +
+ ); + + return ( +
+
+
+

DJ Management

+

Manage DJ accounts and invitations

+
+ + + + + + + + Invite New DJ + +
+
+ + setInviteName(e.target.value)} + placeholder="Enter DJ name" + /> +
+
+ + setInviteEmail(e.target.value)} + placeholder="Enter email address" + /> +
+
+ + +
+
+
+
+
+ + + + Active DJs + + + + + + + {invitations && invitations.length > 0 && ( + + + Pending Invitations + + +
+ {invitations.map((invitation: any) => ( +
+
+
{invitation.name}
+
{invitation.email}
+
+ Pending +
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/client/src/components/admin/EventTypeManagement.tsx b/client/src/components/admin/EventTypeManagement.tsx new file mode 100644 index 0000000..fbae876 --- /dev/null +++ b/client/src/components/admin/EventTypeManagement.tsx @@ -0,0 +1,251 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { DataTable } from "@/components/ui/data-table"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import { Plus, Edit, Trash2 } from "lucide-react"; + +export function EventTypeManagement() { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [showModal, setShowModal] = useState(false); + const [selectedEventType, setSelectedEventType] = useState(null); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + + const { data: eventTypes, isLoading } = useQuery({ + queryKey: ["/api/event-types"], + }); + + const createEventTypeMutation = useMutation({ + mutationFn: async (data: { name: string; description: string }) => { + await apiRequest("POST", "/api/event-types", data); + }, + onSuccess: () => { + toast({ + title: "Event type created", + description: "Event type has been created successfully.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/event-types"] }); + handleCloseModal(); + }, + onError: (error) => { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + }, + }); + + const updateEventTypeMutation = useMutation({ + mutationFn: async (data: { id: number; name: string; description: string }) => { + await apiRequest("PATCH", `/api/event-types/${data.id}`, { name: data.name, description: data.description }); + }, + onSuccess: () => { + toast({ + title: "Event type updated", + description: "Event type has been updated successfully.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/event-types"] }); + handleCloseModal(); + }, + onError: (error) => { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + }, + }); + + const deleteEventTypeMutation = useMutation({ + mutationFn: async (id: number) => { + await apiRequest("DELETE", `/api/event-types/${id}`); + }, + onSuccess: () => { + toast({ + title: "Event type deleted", + description: "Event type has been deleted successfully.", + }); + queryClient.invalidateQueries({ queryKey: ["/api/event-types"] }); + }, + onError: (error) => { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + }, + }); + + const handleOpenModal = (eventType?: any) => { + setSelectedEventType(eventType); + setName(eventType?.name || ""); + setDescription(eventType?.description || ""); + setShowModal(true); + }; + + const handleCloseModal = () => { + setShowModal(false); + setSelectedEventType(null); + setName(""); + setDescription(""); + }; + + const handleSubmit = () => { + if (!name) { + toast({ + title: "Validation Error", + description: "Event type name is required", + variant: "destructive", + }); + return; + } + + if (selectedEventType) { + updateEventTypeMutation.mutate({ + id: selectedEventType.id, + name, + description, + }); + } else { + createEventTypeMutation.mutate({ name, description }); + } + }; + + const handleDelete = (id: number) => { + if (window.confirm("Are you sure you want to delete this event type?")) { + deleteEventTypeMutation.mutate(id); + } + }; + + const columns = [ + { + key: "name" as const, + header: "Name", + cell: (eventType: any) => ( +
{eventType.name}
+ ), + }, + { + key: "description" as const, + header: "Description", + cell: (eventType: any) => ( +
{eventType.description || "-"}
+ ), + }, + { + key: "isActive" as const, + header: "Status", + cell: (eventType: any) => ( + + {eventType.isActive ? "Active" : "Inactive"} + + ), + }, + ]; + + const actions = (eventType: any) => ( +
+ + +
+ ); + + return ( +
+
+
+

Event Types

+

Manage event categories and types

+
+ + + + + + + + + {selectedEventType ? "Edit Event Type" : "Create Event Type"} + + +
+
+ + setName(e.target.value)} + placeholder="Enter event type name" + /> +
+
+ +