Merge pull request #1 from Oloodi/integration/base44-update-2025-11-11

feat(scripts/prepare-export.js): convert script to ES module syntax
This commit is contained in:
Boris-Wilfried
2025-11-11 07:27:06 -05:00
committed by GitHub
25 changed files with 7786 additions and 2884 deletions

View File

@@ -1,12 +1,16 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// Replicate __dirname functionality in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, '..');
// --- Fonctions de Patch ---
function applyPatch(filePath, patches) {
function applyPatch(filePath, patchInfo) {
const fullPath = path.join(projectRoot, filePath);
if (!fs.existsSync(fullPath)) {
console.warn(`🟡 Fichier non trouvé, patch ignoré : ${filePath}`);
@@ -14,22 +18,18 @@ function applyPatch(filePath, patches) {
}
let content = fs.readFileSync(fullPath, 'utf8');
let changed = false;
patches.forEach(patch => {
if (content.includes(patch.new_string)) {
console.log(`✅ Patch déjà appliqué dans ${filePath} (recherche de '${patch.search_string}').`);
} else if (content.includes(patch.old_string)) {
content = content.replace(patch.old_string, patch.new_string);
changed = true;
console.log(`🟢 Patch appliqué dans ${filePath} (remplacement de '${patch.search_string}').`);
} else {
console.error(`🔴 Impossible d'appliquer le patch dans ${filePath}. Chaîne non trouvée : '${patch.search_string}'.`);
}
});
if (changed) {
if (content.includes(patchInfo.new_string)) {
console.log(`✅ Patch déjà appliqué dans ${filePath} (recherche de '${patchInfo.search_string}').`);
return;
}
if (content.includes(patchInfo.old_string)) {
content = content.replace(patchInfo.old_string, patchInfo.new_string);
fs.writeFileSync(fullPath, content, 'utf8');
console.log(`🟢 Patch appliqué dans ${filePath} (remplacement de '${patchInfo.search_string}').`);
} else {
console.error(`🔴 Impossible d'appliquer le patch dans ${filePath}. Chaîne non trouvée : '${patchInfo.search_string}'.`);
}
}
@@ -66,7 +66,8 @@ export const base44 = {
},
{
file: 'src/main.jsx',
search_string: `ReactDOM.createRoot(document.getElementById('root')).render(`,
search_string:
`ReactDOM.createRoot(document.getElementById('root')).render(`,
old_string: `import React from 'react'
import ReactDOM from 'react-dom/client'
import App from '@/App.jsx'
@@ -110,7 +111,8 @@ ReactDOM.createRoot(document.getElementById('root')).render(
email: "dev@example.com",
user_role: "admin", // You can change this to 'procurement', 'operator', 'client', etc. to test different navigation menus
profile_picture: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
};`
};
`
},
{
file: 'src/pages/Layout.jsx',
@@ -119,8 +121,6 @@ ReactDOM.createRoot(document.getElementById('root')).render(
queryKey: ['unread-notifications', user?.id],
queryFn: async () => {
if (!user?.id) return 0;
// Assuming ActivityLog entity is used for user notifications
// and has user_id and is_read fields.
const notifications = await base44.entities.ActivityLog.filter({
user_id: user?.id,
is_read: false
@@ -129,10 +129,74 @@ ReactDOM.createRoot(document.getElementById('root')).render(
},
enabled: !!user?.id,
initialData: 0,
refetchInterval: 10000, // Refresh every 10 seconds
refetchInterval: 10000,
});`,
new_string: ` // Get unread notification count
// const { data: unreadCount = 0 } = useQuery({ ... });
// const { data: unreadCount = 0 } = useQuery({
const unreadCount = 0; // Mocked value`
},
{
file: 'src/pages/Layout.jsx',
search_string: 'import { Badge } from \"./components/ui/badge\"',
old_string: 'import { Badge } from \"./components/ui/badge\"',
new_string: 'import { Badge } from \"@/components/ui/badge\"',
},
{
file: 'src/pages/Layout.jsx',
search_string: 'import ChatBubble from \"./components/chat/ChatBubble\"',
old_string: 'import ChatBubble from \"./components/chat/ChatBubble\"',
new_string: 'import ChatBubble from \"@/components/chat/ChatBubble\"',
}
];
];
// --- Global Import Fix ---
function fixComponentImports(directory) {
const entries = fs.readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
fixComponentImports(fullPath);
} else if (entry.isFile() && entry.name.endsWith('.jsx')) {
let content = fs.readFileSync(fullPath, 'utf8');
const originalContent = content;
// Regex to find all imports from "./components/" (with single or double quotes)
const importRegex = /from\s+(['"])\.\/components\//g;
content = content.replace(importRegex, 'from $1@/components/');
// This specifically handles the badge import which might be different
content = content.replace('from "./components/ui/badge"', 'from "@/components/ui/badge"');
content = content.replace('from "./components/chat/ChatBubble"', 'from "@/components/chat/ChatBubble"');
content = content.replace('from "./components/dev/RoleSwitcher"', 'from "@/components/dev/RoleSwitcher"');
content = content.replace('from "./components/notifications/NotificationPanel"', 'from "@/components/notifications/NotificationPanel"');
content = content.replace('from "./components/ui/toaster"', 'from "@/components/ui/toaster"');
if (content !== originalContent) {
console.log(` Glogal import fix appliqué dans ${fullPath}`);
fs.writeFileSync(fullPath, content, 'utf8');
}
}
}
}
// --- Exécution ---
function main() {
console.log('--- Application des patchs pour l\'environnement local ---');
patches.forEach(patchInfo => {
applyPatch(patchInfo.file, patchInfo);
});
console.log('--- Correction globale des imports de composants ---');
fixComponentImports(path.join(projectRoot, 'src'));
console.log('--- Fin de l\'application des patchs ---');
}
main();

View File

@@ -1,19 +1,10 @@
// import { createClient } from '@base44/sdk';
// // import { getAccessToken } from '@base44/sdk/utils/auth-utils';
// // Create a client with authentication required
// export const base44 = createClient({
// appId: "68fc6cf01386035c266e7a5d",
// requiresAuth: true // Ensure authentication is required for all operations
// });
// --- MIGRATION MOCK ---
// This mock completely disables the Base44 SDK to allow for local development.
// It prevents redirection to the Base44 login page.
export const base44 = {
auth: {
me: () => Promise.resolve(null), // Mock the function that checks the current user
me: () => Promise.resolve(null),
logout: () => {},
},
entities: {
@@ -21,5 +12,5 @@ export const base44 = {
filter: () => Promise.resolve([]),
},
},
// Add other mocked functions as needed during development
};

View File

@@ -1,3 +1,4 @@
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -20,6 +21,7 @@ import { Save, X } from "lucide-react";
export default function CreateBusinessModal({ open, onOpenChange, onSubmit, isSubmitting }) {
const [formData, setFormData] = useState({
business_name: "",
company_logo: "", // Added company_logo field
contact_name: "",
phone: "",
email: "",
@@ -43,6 +45,7 @@ export default function CreateBusinessModal({ open, onOpenChange, onSubmit, isSu
// Reset form after submission
setFormData({
business_name: "",
company_logo: "", // Reset company_logo field
contact_name: "",
phone: "",
email: "",
@@ -67,7 +70,7 @@ export default function CreateBusinessModal({ open, onOpenChange, onSubmit, isSu
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6 mt-4">
{/* Business Name & Primary Contact */}
{/* Business Name & Company Logo */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="business_name" className="text-slate-700 font-medium">
@@ -84,20 +87,35 @@ export default function CreateBusinessModal({ open, onOpenChange, onSubmit, isSu
</div>
<div className="space-y-2">
<Label htmlFor="contact_name" className="text-slate-700 font-medium">
Primary Contact <span className="text-red-500">*</span>
<Label htmlFor="company_logo" className="text-slate-700 font-medium">
Company Logo URL
</Label>
<Input
id="contact_name"
value={formData.contact_name}
onChange={(e) => handleChange('contact_name', e.target.value)}
placeholder="Contact name"
required
id="company_logo"
value={formData.company_logo}
onChange={(e) => handleChange('company_logo', e.target.value)}
placeholder="https://example.com/logo.png"
className="border-slate-300"
/>
<p className="text-xs text-slate-500">Optional: URL to company logo image</p>
</div>
</div>
{/* Primary Contact (Moved from first grid to its own section) */}
<div className="space-y-2">
<Label htmlFor="contact_name" className="text-slate-700 font-medium">
Primary Contact <span className="text-red-500">*</span>
</Label>
<Input
id="contact_name"
value={formData.contact_name}
onChange={(e) => handleChange('contact_name', e.target.value)}
placeholder="Contact name"
required
className="border-slate-300"
/>
</div>
{/* Contact Number & Email */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
@@ -239,7 +257,7 @@ export default function CreateBusinessModal({ open, onOpenChange, onSubmit, isSu
<SelectTrigger className="border-slate-300">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectContent>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Inactive">Inactive</SelectItem>
<SelectItem value="Pending">Pending</SelectItem>
@@ -271,4 +289,4 @@ export default function CreateBusinessModal({ open, onOpenChange, onSubmit, isSu
</DialogContent>
</Dialog>
);
}
}

View File

@@ -0,0 +1,632 @@
import React, { useState, useEffect, useRef } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Sparkles, Send, Mic, MicOff, Upload, FileText, Clock,
MapPin, Users, Calendar, DollarSign, Zap, Brain,
TrendingUp, CheckCircle2, Loader2, Eye, MessageSquare,
Image as ImageIcon, Wand2, AlertCircle, ChevronRight
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { useToast } from "@/components/ui/use-toast";
import { format } from "date-fns";
export default function AIOrderAssistant({ onOrderDataExtracted, onClose }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [isListening, setIsListening] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [extractedData, setExtractedData] = useState(null);
const [showSuggestions, setShowSuggestions] = useState(true);
const [voiceEnabled, setVoiceEnabled] = useState(true);
const messagesEndRef = useRef(null);
const { toast } = useToast();
const { data: recentEvents } = useQuery({
queryKey: ['recent-events-ai'],
queryFn: () => base44.entities.Event.list('-created_date', 5),
initialData: [],
});
const { data: businesses } = useQuery({
queryKey: ['businesses-ai'],
queryFn: () => base44.entities.Business.list(),
initialData: [],
});
useEffect(() => {
const greeting = {
id: Date.now(),
role: "assistant",
content: "👋 **AI Order Assistant 2030**\n\nI'm your intelligent workforce ordering assistant. Just tell me what you need naturally - I understand context, history, and can create complete orders instantly.\n\n**Try saying:**\n• \"I need 5 servers for Friday night\"\n• \"Repeat last week's order but add 2 bartenders\"\n• \"Same as Event #1234 but different date\"\n\nYou can also upload files, speak naturally, or let me analyze your patterns.",
timestamp: new Date().toISOString(),
type: "greeting"
};
setMessages([greeting]);
}, []);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// Smart suggestions based on context
const smartSuggestions = [
{
icon: RefreshCw,
label: "Repeat Last Order",
query: "Repeat my last order for next Friday",
color: "bg-purple-500"
},
{
icon: TrendingUp,
label: "Based on History",
query: "Create order based on my usual Friday pattern",
color: "bg-blue-500"
},
{
icon: Zap,
label: "Quick Template",
query: "I need 3 servers and 2 bartenders for Saturday 6pm-midnight",
color: "bg-green-500"
},
{
icon: Brain,
label: "AI Predict",
query: "What staff will I need for a 200-person corporate event?",
color: "bg-orange-500"
}
];
const handleSendMessage = async (text = input) => {
if (!text.trim() && !isListening) return;
const userMessage = {
id: Date.now(),
role: "user",
content: text,
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, userMessage]);
setInput("");
setIsProcessing(true);
setShowSuggestions(false);
try {
// Simulate AI processing with intelligent response
await new Promise(resolve => setTimeout(resolve, 1500));
// Enhanced AI prompt with context awareness
const prompt = `You are an advanced AI workforce ordering assistant in 2030. Analyze this request and extract structured order data.
User Request: "${text}"
Recent Order History:
${recentEvents.slice(0, 3).map((e, idx) => `${idx + 1}. ${e.event_name} - ${e.business_name} - ${e.requested || 0} staff - ${format(new Date(e.date || Date.now()), 'MMM d, yyyy')}`).join('\n')}
Available Businesses: ${businesses.slice(0, 5).map(b => b.business_name).join(', ')}
Instructions:
- If user references "last order" or "repeat", use the most recent event data
- If user says "usual" or "typical", analyze patterns from history
- Be intelligent about inferring missing details (dates, times, quantities)
- Suggest optimal staff counts based on event type
- Return ONLY valid JSON in this exact format:
{
"event_name": "string",
"business_name": "string (from available businesses)",
"hub": "string",
"date": "YYYY-MM-DD",
"order_type": "one_time|rapid|recurring",
"shifts": [{
"shift_name": "Shift 1",
"roles": [{
"role": "string (Server, Bartender, Chef, etc)",
"count": number,
"start_time": "HH:MM",
"end_time": "HH:MM",
"break_minutes": 30
}]
}],
"notes": "string with any additional context",
"ai_confidence": number (0-100),
"ai_reasoning": "string explaining the order creation logic"
}`;
const response = await base44.integrations.Core.InvokeLLM({
prompt: prompt,
add_context_from_internet: false,
response_json_schema: {
type: "object",
properties: {
event_name: { type: "string" },
business_name: { type: "string" },
hub: { type: "string" },
date: { type: "string" },
order_type: { type: "string" },
shifts: {
type: "array",
items: {
type: "object",
properties: {
shift_name: { type: "string" },
roles: {
type: "array",
items: {
type: "object",
properties: {
role: { type: "string" },
count: { type: "number" },
start_time: { type: "string" },
end_time: { type: "string" },
break_minutes: { type: "number" }
}
}
}
}
}
},
notes: { type: "string" },
ai_confidence: { type: "number" },
ai_reasoning: { type: "string" }
}
}
});
const orderData = response;
setExtractedData(orderData);
const assistantMessage = {
id: Date.now() + 1,
role: "assistant",
content: `✨ **Order Created Instantly**\n\n${orderData.ai_reasoning}\n\n**Confidence:** ${orderData.ai_confidence}%\n\nReview the order details below and click "Use This Order" to proceed.`,
timestamp: new Date().toISOString(),
type: "success",
data: orderData
};
setMessages(prev => [...prev, assistantMessage]);
toast({
title: "🎯 AI Order Generated",
description: `Created order with ${orderData.shifts?.[0]?.roles?.reduce((sum, r) => sum + (r.count || 0), 0) || 0} staff members`,
});
} catch (error) {
const errorMessage = {
id: Date.now() + 1,
role: "assistant",
content: `❌ I couldn't process that request. Please try rephrasing or providing more details.\n\nError: ${error.message}`,
timestamp: new Date().toISOString(),
type: "error"
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsProcessing(false);
}
};
const handleVoiceInput = () => {
if (!voiceEnabled) {
toast({
title: "Voice Not Supported",
description: "Your browser doesn't support voice input",
variant: "destructive"
});
return;
}
if (isListening) {
setIsListening(false);
// Stop voice recognition
} else {
setIsListening(true);
// Start voice recognition
toast({
title: "🎤 Listening...",
description: "Speak naturally - I understand context and nuance",
});
// Simulate voice input after 3 seconds
setTimeout(() => {
setIsListening(false);
const voiceText = "I need 5 servers and 3 bartenders for this Friday from 6pm to midnight at our downtown location";
setInput(voiceText);
toast({
title: "✅ Voice Captured",
description: "Processing your request...",
});
}, 3000);
}
};
const handleFileUpload = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
toast({
title: "📄 Analyzing Document...",
description: "AI is extracting order details from your file",
});
try {
const { file_url } = await base44.integrations.Core.UploadFile({ file });
const extractResult = await base44.integrations.Core.ExtractDataFromUploadedFile({
file_url: file_url,
json_schema: {
type: "object",
properties: {
event_name: { type: "string" },
date: { type: "string" },
location: { type: "string" },
staff_needs: {
type: "array",
items: {
type: "object",
properties: {
role: { type: "string" },
count: { type: "number" },
hours: { type: "string" }
}
}
}
}
}
});
if (extractResult.status === "success") {
const message = {
id: Date.now(),
role: "assistant",
content: `📄 **Document Analyzed**\n\nI've extracted the following order details:\n\n${JSON.stringify(extractResult.output, null, 2)}\n\nShall I create an order based on this?`,
timestamp: new Date().toISOString(),
type: "file",
data: extractResult.output
};
setMessages(prev => [...prev, message]);
}
} catch (error) {
toast({
title: "Failed to Process File",
description: error.message,
variant: "destructive"
});
}
};
const handleUseSuggestion = (query) => {
setInput(query);
handleSendMessage(query);
};
const handleUseOrder = () => {
if (extractedData) {
onOrderDataExtracted(extractedData);
toast({
title: "✅ Order Data Applied",
description: "Review and edit the order details in the form",
});
}
};
return (
<div className="bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 rounded-2xl shadow-2xl border-2 border-indigo-200 overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 p-6 relative overflow-hidden">
<div className="absolute inset-0 bg-black/10"></div>
<div className="relative z-10 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white/20 backdrop-blur-xl rounded-2xl flex items-center justify-center shadow-lg">
<Brain className="w-8 h-8 text-white animate-pulse" />
</div>
<div>
<h2 className="text-2xl font-bold text-white mb-1 flex items-center gap-2">
AI Order Assistant 2030
<Badge className="bg-white/20 text-white border-white/40">
<Sparkles className="w-3 h-3 mr-1" />
Neural
</Badge>
</h2>
<p className="text-white/90 text-sm">Instant workforce ordering powered by advanced AI</p>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="text-white hover:bg-white/20"
>
</Button>
</div>
{/* Real-time Stats Bar */}
<div className="mt-4 flex items-center gap-4 text-xs text-white/80">
<div className="flex items-center gap-1">
<Zap className="w-4 h-4" />
<span>3.2s avg response</span>
</div>
<div className="flex items-center gap-1">
<CheckCircle2 className="w-4 h-4" />
<span>98.7% accuracy</span>
</div>
<div className="flex items-center gap-1">
<TrendingUp className="w-4 h-4" />
<span>5.2x faster than forms</span>
</div>
</div>
</div>
{/* Smart Suggestions - Only show initially */}
<AnimatePresence>
{showSuggestions && messages.length <= 1 && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="p-4 bg-white/60 backdrop-blur-sm border-b border-indigo-100"
>
<p className="text-xs font-semibold text-slate-600 mb-3 flex items-center gap-2">
<Wand2 className="w-4 h-4" />
INSTANT ACTIONS
</p>
<div className="grid grid-cols-2 gap-2">
{smartSuggestions.map((suggestion, idx) => (
<button
key={idx}
onClick={() => handleUseSuggestion(suggestion.query)}
className="flex items-center gap-3 p-3 bg-white rounded-xl border border-slate-200 hover:border-indigo-300 hover:shadow-lg transition-all group"
>
<div className={`w-10 h-10 ${suggestion.color} rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform`}>
<suggestion.icon className="w-5 h-5 text-white" />
</div>
<div className="flex-1 text-left">
<p className="text-sm font-semibold text-slate-900">{suggestion.label}</p>
<p className="text-xs text-slate-500 truncate">{suggestion.query.substring(0, 30)}...</p>
</div>
<ChevronRight className="w-4 h-4 text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity" />
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Messages Area */}
<div className="h-96 overflow-y-auto p-6 space-y-4 bg-white/40 backdrop-blur-sm">
<AnimatePresence>
{messages.map((message) => (
<motion.div
key={message.id}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className={`flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
{message.role === 'assistant' && (
<Avatar className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 flex-shrink-0">
<AvatarFallback className="bg-transparent">
<Brain className="w-5 h-5 text-white" />
</AvatarFallback>
</Avatar>
)}
<div className={`max-w-2xl ${message.role === 'user' ? 'order-1' : 'order-2'}`}>
<div
className={`rounded-2xl p-4 shadow-lg ${
message.role === 'user'
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: message.type === 'error'
? 'bg-red-50 border-2 border-red-200'
: 'bg-white border-2 border-indigo-100'
}`}
>
<div className="prose prose-sm max-w-none">
{message.content.split('\n').map((line, idx) => (
<p key={idx} className={`mb-1 last:mb-0 ${message.role === 'user' ? 'text-white' : 'text-slate-700'}`}>
{line}
</p>
))}
</div>
{/* Order Preview Card */}
{message.type === 'success' && message.data && (
<div className="mt-4 p-4 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl border-2 border-green-200">
<div className="flex items-center justify-between mb-3">
<p className="font-bold text-green-900 flex items-center gap-2">
<CheckCircle2 className="w-5 h-5" />
Order Preview
</p>
<Badge className="bg-green-600 text-white">
{message.data.ai_confidence}% Confidence
</Badge>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-green-600" />
<span className="text-slate-700">{message.data.date || 'TBD'}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-green-600" />
<span className="text-slate-700">
{message.data.shifts?.[0]?.roles?.reduce((sum, r) => sum + (r.count || 0), 0) || 0} Staff
</span>
</div>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-green-600" />
<span className="text-slate-700">{message.data.hub || message.data.business_name || 'Location TBD'}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-green-600" />
<span className="text-slate-700">
{message.data.shifts?.[0]?.roles?.[0]?.start_time || '09:00'} - {message.data.shifts?.[0]?.roles?.[0]?.end_time || '17:00'}
</span>
</div>
</div>
<Button
onClick={handleUseOrder}
className="w-full mt-4 bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white font-semibold"
>
<CheckCircle2 className="w-4 h-4 mr-2" />
Use This Order
</Button>
</div>
)}
</div>
<p className="text-xs text-slate-500 mt-1 px-2">
{format(new Date(message.timestamp), 'h:mm a')}
</p>
</div>
{message.role === 'user' && (
<Avatar className="w-10 h-10 bg-gradient-to-br from-slate-500 to-slate-700 flex-shrink-0">
<AvatarFallback className="bg-transparent text-white font-bold">
U
</AvatarFallback>
</Avatar>
)}
</motion.div>
))}
</AnimatePresence>
{isProcessing && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex gap-3"
>
<Avatar className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600">
<AvatarFallback className="bg-transparent">
<Brain className="w-5 h-5 text-white animate-pulse" />
</AvatarFallback>
</Avatar>
<div className="bg-white rounded-2xl p-4 shadow-lg border-2 border-indigo-100">
<div className="flex items-center gap-3">
<Loader2 className="w-5 h-5 text-indigo-600 animate-spin" />
<span className="text-sm text-slate-600">
AI is analyzing and creating your order...
</span>
</div>
</div>
</motion.div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area - Enhanced */}
<div className="p-4 bg-white border-t-2 border-indigo-100">
<div className="flex items-center gap-2 mb-3">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !e.shiftKey && handleSendMessage()}
placeholder="Type, speak, or upload... AI understands natural language 🧠"
className="flex-1 border-2 border-indigo-200 focus:border-indigo-400 rounded-xl h-12 px-4 text-sm"
disabled={isProcessing}
/>
{/* Voice Button */}
<Button
onClick={handleVoiceInput}
size="icon"
variant={isListening ? "default" : "outline"}
className={`h-12 w-12 rounded-xl ${
isListening
? 'bg-red-500 hover:bg-red-600 animate-pulse'
: 'border-2 border-indigo-200 hover:border-indigo-400'
}`}
disabled={isProcessing}
>
{isListening ? (
<MicOff className="w-5 h-5" />
) : (
<Mic className="w-5 h-5" />
)}
</Button>
{/* File Upload */}
<label>
<input
type="file"
accept=".pdf,.doc,.docx,.txt,.csv"
onChange={handleFileUpload}
className="hidden"
disabled={isProcessing}
/>
<Button
as="span"
size="icon"
variant="outline"
className="h-12 w-12 rounded-xl border-2 border-indigo-200 hover:border-indigo-400 cursor-pointer"
disabled={isProcessing}
>
<Upload className="w-5 h-5" />
</Button>
</label>
{/* Send Button */}
<Button
onClick={() => handleSendMessage()}
size="icon"
className="h-12 w-12 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700"
disabled={isProcessing || !input.trim()}
>
<Send className="w-5 h-5" />
</Button>
</div>
{/* Quick Examples */}
<div className="flex items-center gap-2 text-xs text-slate-500">
<Sparkles className="w-3 h-3" />
<span className="font-semibold">Try:</span>
<button
onClick={() => setInput("I need 3 servers for Friday night 6-11pm")}
className="text-indigo-600 hover:underline"
>
"3 servers Friday night"
</button>
<span></span>
<button
onClick={() => setInput("Repeat last week's order")}
className="text-indigo-600 hover:underline"
>
"Repeat last order"
</button>
</div>
</div>
{/* AI Capabilities Footer */}
<div className="px-4 py-3 bg-gradient-to-r from-indigo-50 to-purple-50 border-t border-indigo-100">
<div className="flex items-center justify-between text-xs text-slate-600">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1">
<Brain className="w-3 h-3 text-indigo-600" />
<span>Context-aware</span>
</div>
<div className="flex items-center gap-1">
<MessageSquare className="w-3 h-3 text-purple-600" />
<span>Natural language</span>
</div>
<div className="flex items-center gap-1">
<Zap className="w-3 h-3 text-pink-600" />
<span>Instant generation</span>
</div>
</div>
<Badge variant="outline" className="text-xs">
Powered by Neural AI v3.0
</Badge>
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,915 @@
import React, { useState, useEffect } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Calendar, Zap, RefreshCw, Users, Building2, Shield,
Plus, Minus, Trash2, Search, X, FileText, Save
} from "lucide-react";
import { format } from "date-fns";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
const UNIFORM_TYPES = ["Type 1", "Type 2", "Type 3", "All Black", "Business Casual", "Chef Whites"];
export default function EventFormWizard({ event, onSubmit, isSubmitting, currentUser, onCancel }) {
const [formData, setFormData] = useState(event || {
event_name: "",
order_type: "one_time",
recurrence_type: "single",
recurrence_start_date: "",
recurrence_end_date: "",
scatter_dates: [],
recurring_days: [],
recurring_frequency: "weekly",
business_id: "",
business_name: "",
hub: "",
department: "",
po_reference: "",
status: "Draft",
date: "",
include_backup: false,
backup_staff_count: 0,
shifts: [{
shift_name: "Shift 1",
location_address: "",
same_as_billing: true,
roles: [{
role: "",
department: "",
count: 1,
start_time: "09:00",
end_time: "17:00",
hours: 8,
uniform: "Type 1",
break_minutes: 30,
rate_per_hour: 0,
total_value: 0
}]
}],
notes: "",
total: 0
});
const [roleSearchOpen, setRoleSearchOpen] = useState({});
const { data: user } = useQuery({
queryKey: ['current-user-form'],
queryFn: () => base44.auth.me(),
enabled: !currentUser,
});
const currentUserData = currentUser || user;
const userRole = currentUserData?.user_role || currentUserData?.role || "admin";
const isVendor = userRole === "vendor";
const { data: businesses = [] } = useQuery({
queryKey: ['businesses'],
queryFn: () => base44.entities.Business.list(),
initialData: [],
});
const { data: allRates = [] } = useQuery({
queryKey: ['vendor-rates-all'],
queryFn: () => base44.entities.VendorRate.list(),
initialData: [],
});
const availableRoles = [...new Set(allRates.map(r => r.role_name))].sort();
useEffect(() => {
if (event) {
setFormData(event);
}
}, [event]);
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleBusinessChange = (businessId) => {
const selectedBusiness = businesses.find(b => b.id === businessId);
if (selectedBusiness) {
setFormData(prev => ({
...prev,
business_id: businessId,
business_name: selectedBusiness.business_name || "",
shifts: prev.shifts.map(shift => ({
...shift,
location_address: shift.same_as_billing ? selectedBusiness.address || shift.location_address : shift.location_address
}))
}));
}
};
const calculateHours = (startTime, endTime, breakMinutes = 0) => {
if (!startTime || !endTime) return 0;
const [startHour, startMin] = startTime.split(':').map(Number);
const [endHour, endMin] = endTime.split(':').map(Number);
const startMinutes = startHour * 60 + startMin;
const endMinutes = endHour * 60 + endMin;
let totalMinutes = endMinutes - startMinutes;
if (totalMinutes < 0) totalMinutes += 24 * 60; // Handle overnight shifts
totalMinutes -= (breakMinutes || 0); // Subtract break
return Math.max(0, totalMinutes / 60);
};
const getRateForRole = (roleName) => {
const rate = allRates.find(r => r.role_name === roleName && r.is_active);
return rate ? parseFloat(rate.client_rate || 0) : 0;
};
const handleRoleChange = (shiftIndex, roleIndex, field, value) => {
setFormData(prev => {
const newShifts = [...prev.shifts];
const role = newShifts[shiftIndex].roles[roleIndex];
role[field] = value;
if (field === 'role') {
const rate = getRateForRole(value);
role.rate_per_hour = rate;
}
if (field === 'start_time' || field === 'end_time' || field === 'break_minutes') {
role.hours = calculateHours(role.start_time, role.end_time, role.break_minutes);
}
role.total_value = (role.rate_per_hour || 0) * (role.hours || 0) * (role.count || 1);
return { ...prev, shifts: newShifts };
});
updateGrandTotal();
};
const updateGrandTotal = () => {
setTimeout(() => {
setFormData(prev => {
const total = prev.shifts.reduce((sum, shift) => {
const shiftTotal = shift.roles.reduce((roleSum, role) => roleSum + (role.total_value || 0), 0);
return sum + shiftTotal;
}, 0);
return { ...prev, total };
});
}, 0);
};
const handleAddRole = (shiftIndex) => {
setFormData(prev => {
const newShifts = [...prev.shifts];
newShifts[shiftIndex].roles.push({
role: "",
department: "",
count: 1,
start_time: "09:00",
end_time: "17:00",
hours: 8,
uniform: "Type 1",
break_minutes: 30, // Default to 30 min non-payable
rate_per_hour: 0,
total_value: 0
});
return { ...prev, shifts: newShifts };
});
};
const handleRemoveRole = (shiftIndex, roleIndex) => {
setFormData(prev => {
const newShifts = [...prev.shifts];
newShifts[shiftIndex].roles.splice(roleIndex, 1);
return { ...prev, shifts: newShifts };
});
updateGrandTotal();
};
const handleOrderTypeChange = (type) => {
setFormData(prev => ({
...prev,
order_type: type,
date: "",
recurrence_start_date: "",
recurrence_end_date: "",
scatter_dates: [],
recurring_days: [],
recurring_frequency: "weekly",
recurrence_type: type === "recurring" ? "date_range" : "single"
}));
};
const handleScatterDateSelect = (dates) => {
setFormData(prev => ({
...prev,
scatter_dates: dates?.map(d => format(d, 'yyyy-MM-dd')).sort() || []
}));
};
const handleDayToggle = (day) => {
setFormData(prev => {
const days = prev.recurring_days || [];
const exists = days.includes(day);
return {
...prev,
recurring_days: exists ? days.filter(d => d !== day) : [...days, day].sort((a,b) => a-b)
};
});
};
const handleSubmit = (isDraft = false) => {
const status = isDraft ? "Draft" :
formData.order_type === "rapid" ? "Active" : "Pending";
onSubmit({ ...formData, status });
};
return (
<>
<style>{`
/* 🎨 CALENDAR STYLING - Matching Events Page Design */
/* Base day cell styling */
.rdp-day {
font-size: 0.875rem !important;
min-width: 36px !important;
height: 36px !important;
border-radius: 50% !important;
transition: all 0.2s ease !important;
font-weight: 500 !important;
position: relative !important;
}
/* Regular unselected dates */
.rdp-day button {
width: 100% !important;
height: 100% !important;
border-radius: 50% !important;
}
/* Selected range start date - Solid blue filled circle */
.rdp-day_range_start {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
/* Selected range end date - Solid blue filled circle */
.rdp-day_range_end {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
/* Single selected date - Solid blue filled circle */
.rdp-day_selected:not(.rdp-day_range_start):not(.rdp-day_range_end) {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
/* Multiple selected dates - Solid blue filled circles */
.rdp-day_selected {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
/* Range middle dates - Light lavender/blue background */
.rdp-day_range_middle {
background-color: #e0e7ff !important;
color: #4f46e5 !important;
font-weight: 600 !important;
border-radius: 0 !important;
}
/* When start and end are the same day */
.rdp-day_range_start.rdp-day_range_end {
border-radius: 50% !important;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
}
/* Hover effect - Light blue background */
.rdp-day:hover:not(.rdp-day_selected):not(.rdp-day_disabled):not(.rdp-day_range_start):not(.rdp-day_range_end):not(.rdp-day_range_middle) {
background-color: #eff6ff !important;
color: #2563eb !important;
border-radius: 50% !important;
}
/* Today indicator - Pink/magenta dot at bottom */
.rdp-day_today:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::after {
content: '' !important;
position: absolute !important;
bottom: 4px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 4px !important;
height: 4px !important;
background-color: #ec4899 !important;
border-radius: 50% !important;
}
/* Today with selection - adjust styling */
.rdp-day_today.rdp-day_selected,
.rdp-day_today.rdp-day_range_start,
.rdp-day_today.rdp-day_range_end {
color: white !important;
}
/* Disabled/outside dates - gray and muted */
.rdp-day_outside {
color: #cbd5e1 !important;
opacity: 0.5 !important;
}
.rdp-day_disabled {
opacity: 0.3 !important;
cursor: not-allowed !important;
}
/* Keep selected dates always visible */
.rdp-day_selected,
.rdp-day_range_start,
.rdp-day_range_end,
.rdp-day_range_middle {
opacity: 1 !important;
visibility: visible !important;
z-index: 5 !important;
}
/* Calendar header - weekday names */
.rdp-head_cell {
color: #64748b !important;
font-weight: 600 !important;
font-size: 0.75rem !important;
text-transform: uppercase !important;
padding: 8px 0 !important;
}
/* Month/Year navigation */
.rdp-caption_label {
font-size: 1rem !important;
font-weight: 700 !important;
color: #0f172a !important;
}
/* Navigation arrows */
.rdp-nav_button {
width: 32px !important;
height: 32px !important;
border-radius: 6px !important;
transition: all 0.2s ease !important;
}
.rdp-nav_button:hover {
background-color: #eff6ff !important;
color: #2563eb !important;
}
/* Calendar spacing */
.rdp-months {
gap: 2rem !important;
}
.rdp-month {
padding: 0.75rem !important;
}
.rdp-table {
border-spacing: 0 !important;
margin-top: 1rem !important;
}
/* Cell spacing */
.rdp-cell {
padding: 2px !important;
}
`}</style>
<div className="grid grid-cols-12 gap-4 max-h-[calc(100vh-200px)]">
{/* Left Column - Order Type & Details */}
<div className="col-span-4 space-y-4 overflow-y-auto pr-2">
{/* Order Type */}
<Card className="border-slate-200">
<CardContent className="p-4">
<Label className="text-xs font-bold mb-3 block">Order Type</Label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => handleOrderTypeChange('rapid')}
className={`p-3 rounded-lg border transition-all ${
formData.order_type === 'rapid'
? 'border-red-500 bg-red-50'
: 'border-slate-200 hover:border-red-300'
}`}
>
<Zap className={`w-6 h-6 mx-auto mb-1 ${formData.order_type === 'rapid' ? 'text-red-600' : 'text-slate-400'}`} />
<p className={`text-xs font-bold ${formData.order_type === 'rapid' ? 'text-red-600' : 'text-slate-600'}`}>RAPID</p>
</button>
<button
type="button"
onClick={() => handleOrderTypeChange('one_time')}
className={`p-3 rounded-lg border transition-all ${
formData.order_type === 'one_time'
? 'border-[#0A39DF] bg-blue-50'
: 'border-slate-200 hover:border-[#0A39DF]/30'
}`}
>
<Calendar className={`w-6 h-6 mx-auto mb-1 ${formData.order_type === 'one_time' ? 'text-[#0A39DF]' : 'text-slate-400'}`} />
<p className={`text-xs font-bold ${formData.order_type === 'one_time' ? 'text-[#0A39DF]' : 'text-slate-600'}`}>One-Time</p>
</button>
<button
type="button"
onClick={() => handleOrderTypeChange('recurring')}
className={`p-3 rounded-lg border transition-all ${
formData.order_type === 'recurring'
? 'border-purple-500 bg-purple-50'
: 'border-slate-200 hover:border-purple-300'
}`}
>
<RefreshCw className={`w-6 h-6 mx-auto mb-1 ${formData.order_type === 'recurring' ? 'text-purple-600' : 'text-slate-400'}`} />
<p className={`text-xs font-bold ${formData.order_type === 'recurring' ? 'text-purple-600' : 'text-slate-600'}`}>Recurring</p>
</button>
<button
type="button"
onClick={() => handleOrderTypeChange('permanent')}
className={`p-3 rounded-lg border transition-all ${
formData.order_type === 'permanent'
? 'border-green-500 bg-green-50'
: 'border-slate-200 hover:border-green-300'
}`}
>
<Users className={`w-6 h-6 mx-auto mb-1 ${formData.order_type === 'permanent' ? 'text-green-600' : 'text-slate-400'}`} />
<p className={`text-xs font-bold ${formData.order_type === 'permanent' ? 'text-green-600' : 'text-slate-600'}`}>Permanent</p>
</button>
</div>
{/* Recurring Options - Enhanced with Calendar */}
{formData.order_type === 'recurring' && (
<div className="mt-3 p-3 bg-slate-50 rounded-lg space-y-3">
<div className="flex gap-1">
{['weekly', 'monthly', 'custom'].map(freq => (
<button
key={freq}
type="button"
onClick={() => handleChange('recurring_frequency', freq)}
className={`flex-1 p-1.5 rounded text-xs capitalize ${
formData.recurring_frequency === freq
? 'bg-[#0A39DF] text-white'
: 'bg-white border border-slate-200'
}`}
>
{freq}
</button>
))}
</div>
{/* Weekly: Day Selector */}
{formData.recurring_frequency === 'weekly' && (
<div>
<Label className="text-xs mb-2 block">Select Days:</Label>
<div className="flex gap-1 justify-center">
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, idx) => {
const isSelected = (formData.recurring_days || []).includes(idx);
return (
<button
key={idx}
type="button"
onClick={() => handleDayToggle(idx)}
className={`w-7 h-7 rounded-full text-xs font-medium ${
isSelected
? 'bg-[#0A39DF] text-white'
: 'bg-white border border-slate-200'
}`}
>
{day}
</button>
);
})}
</div>
</div>
)}
{/* Monthly: Date Range Picker */}
{formData.recurring_frequency === 'monthly' && (
<div className="space-y-2">
<Label className="text-xs">Select Date Range:</Label>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-xs text-slate-600">Start Date</Label>
<Input
type="date"
value={formData.recurrence_start_date || ""}
onChange={(e) => handleChange('recurrence_start_date', e.target.value)}
className="h-8 text-xs"
/>
</div>
<div>
<Label className="text-xs text-slate-600">End Date</Label>
<Input
type="date"
value={formData.recurrence_end_date || ""}
onChange={(e) => handleChange('recurrence_end_date', e.target.value)}
className="h-8 text-xs"
/>
</div>
</div>
<div className="text-xs text-slate-600 bg-white p-2 rounded border border-slate-200">
This order will repeat monthly between the selected dates
</div>
</div>
)}
{/* Custom: Multiple Date Picker */}
{formData.recurring_frequency === 'custom' && (
<div className="space-y-2">
<Label className="text-xs">Select Specific Dates:</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full h-8 text-xs justify-start">
<Calendar className="w-3 h-3 mr-2" />
{formData.scatter_dates && formData.scatter_dates.length > 0
? `${formData.scatter_dates.length} dates selected`
: "Pick dates..."}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarComponent
mode="multiple"
selected={formData.scatter_dates?.map(d => new Date(d))}
onSelect={(dates) => handleScatterDateSelect(dates)}
numberOfMonths={2}
className="rounded-md"
/>
</PopoverContent>
</Popover>
{formData.scatter_dates && formData.scatter_dates.length > 0 && (
<div className="text-xs bg-white p-2 rounded border border-slate-200">
<p className="font-medium mb-1">Selected dates:</p>
<div className="flex flex-wrap gap-1">
{formData.scatter_dates.slice(0, 3).map((date, idx) => (
<Badge key={idx} variant="outline" className="text-xs">
{format(new Date(date), 'MMM d')}
</Badge>
))}
{formData.scatter_dates.length > 3 && (
<Badge variant="outline" className="text-xs">
+{formData.scatter_dates.length - 3} more
</Badge>
)}
</div>
</div>
)}
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Event Details - REORDERED */}
<Card className="border-slate-200">
<CardContent className="p-4 space-y-3">
<Label className="text-xs font-bold">Event Details</Label>
{/* 1. Hub (first) */}
<div>
<Label className="text-xs">Hub *</Label>
<Input
value={formData.hub || ""}
onChange={(e) => handleChange('hub', e.target.value)}
placeholder="Hub location"
className="h-8 text-sm"
required
/>
</div>
{/* 2. Department (new field) */}
<div>
<Label className="text-xs">Department</Label>
<Input
value={formData.department || ""}
onChange={(e) => handleChange('department', e.target.value)}
placeholder="Department name"
className="h-8 text-sm"
/>
</div>
{/* 3. Date (required - only for non-recurring) */}
{formData.order_type !== 'recurring' && (
<div>
<Label className="text-xs">Date *</Label>
<Input
type="date"
value={formData.date || ""}
onChange={(e) => handleChange('date', e.target.value)}
className="h-8 text-sm"
required
/>
</div>
)}
{/* 4. Event Name (now optional) */}
<div>
<Label className="text-xs">Event Name</Label>
<Input
value={formData.event_name || ""}
onChange={(e) => handleChange('event_name', e.target.value)}
placeholder="Event name (optional)"
className="h-8 text-sm"
/>
</div>
{/* 5. PO Reference (optional) */}
<div>
<Label className="text-xs">PO Reference</Label>
<Input
value={formData.po_reference || ""}
onChange={(e) => handleChange('po_reference', e.target.value)}
placeholder="Purchase order number (optional)"
className="h-8 text-sm"
/>
</div>
{isVendor && (
<div>
<Label className="text-xs">Client</Label>
<Select value={formData.business_id || ""} onValueChange={handleBusinessChange}>
<SelectTrigger className="h-8 text-sm">
<SelectValue placeholder="Select client" />
</SelectTrigger>
<SelectContent>
{businesses.map((business) => (
<SelectItem key={business.id} value={business.id} className="text-sm">
{business.business_name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="flex items-center gap-2 p-2 bg-green-50 rounded">
<Checkbox
id="backup"
checked={formData.include_backup}
onCheckedChange={(checked) => handleChange('include_backup', checked)}
/>
<Label htmlFor="backup" className="text-xs cursor-pointer">Backup Staff</Label>
</div>
</CardContent>
</Card>
{/* Grand Total */}
<Card className="border-2 border-[#0A39DF]">
<CardContent className="p-4">
<div className="text-center">
<p className="text-xs text-slate-600">Total</p>
<p className="text-2xl font-bold text-[#0A39DF]">${(formData.total || 0).toFixed(2)}</p>
</div>
</CardContent>
</Card>
{/* Actions */}
<div className="space-y-2">
<Button
onClick={() => handleSubmit(false)}
disabled={isSubmitting}
className="w-full bg-gradient-to-r from-[#0A39DF] to-[#1C323E] h-9"
>
<Save className="w-4 h-4 mr-2" />
Submit Order
</Button>
<Button
variant="outline"
onClick={() => handleSubmit(true)}
disabled={isSubmitting}
className="w-full h-8 text-sm"
>
Save as Draft
</Button>
<Button
variant="ghost"
onClick={onCancel}
className="w-full h-8 text-sm"
>
Cancel
</Button>
</div>
</div>
{/* Right Column - Shifts & Roles */}
<div className="col-span-8 space-y-4 overflow-y-auto pr-2">
<Label className="text-sm font-bold">Shifts & Roles</Label>
{formData.shifts.map((shift, shiftIdx) => (
<Card key={shiftIdx} className="border-slate-200">
<CardContent className="p-4 space-y-3">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-sm">{shift.shift_name}</h4>
</div>
{shift.roles.map((role, roleIdx) => (
<div key={roleIdx} className="bg-slate-50 rounded-lg p-3 space-y-3">
{/* Row 1: Role, Count, Times, Hours */}
<div className="grid grid-cols-12 gap-2 items-end">
{/* Role */}
<div className="col-span-3">
<Label className="text-xs">Role</Label>
<Popover open={roleSearchOpen[`${shiftIdx}-${roleIdx}`]} onOpenChange={(open) => setRoleSearchOpen(prev => ({ ...prev, [`${shiftIdx}-${roleIdx}`]: open }))}>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full h-8 text-xs justify-between">
{role.role || "Select..."}
<Search className="w-3 h-3" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search..." className="h-8" />
<CommandEmpty>No role found.</CommandEmpty>
<CommandGroup className="max-h-40 overflow-auto">
{availableRoles.map((roleName) => (
<CommandItem
key={roleName}
onSelect={() => {
handleRoleChange(shiftIdx, roleIdx, 'role', roleName);
setRoleSearchOpen(prev => ({ ...prev, [`${shiftIdx}-${roleIdx}`]: false }));
}}
className="text-xs"
>
{roleName}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
{/* Count - More Visible */}
<div className="col-span-2">
<Label className="text-xs">Count</Label>
<div className="flex gap-1 items-center">
<Button
size="icon"
variant="outline"
className="h-8 w-8"
onClick={() => handleRoleChange(shiftIdx, roleIdx, 'count', Math.max(1, role.count - 1))}
>
<Minus className="w-3 h-3" />
</Button>
<div className="flex-1 h-8 bg-white border border-slate-300 rounded flex items-center justify-center font-bold text-[#0A39DF] text-base">
{role.count || 1}
</div>
<Button
size="icon"
variant="outline"
className="h-8 w-8"
onClick={() => handleRoleChange(shiftIdx, roleIdx, 'count', role.count + 1)}
>
<Plus className="w-3 h-3" />
</Button>
</div>
</div>
{/* Start Time */}
<div className="col-span-2">
<Label className="text-xs">Start</Label>
<Input
type="time"
value={role.start_time}
onChange={(e) => handleRoleChange(shiftIdx, roleIdx, 'start_time', e.target.value)}
className="h-8 text-xs"
/>
</div>
{/* End Time */}
<div className="col-span-2">
<Label className="text-xs">End</Label>
<Input
type="time"
value={role.end_time}
onChange={(e) => handleRoleChange(shiftIdx, roleIdx, 'end_time', e.target.value)}
className="h-8 text-xs"
/>
</div>
{/* Hours */}
<div className="col-span-2">
<Label className="text-xs">Hours</Label>
<div className="flex items-center justify-center h-8">
<Badge className="bg-[#0A39DF] text-xs">{role.hours || 0}h</Badge>
</div>
</div>
{/* Delete */}
<div className="col-span-1 flex justify-end">
{shift.roles.length > 1 && (
<Button
size="icon"
variant="ghost"
onClick={() => handleRemoveRole(shiftIdx, roleIdx)}
className="h-8 w-8 text-red-600"
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</div>
{/* Row 2: Break Selection */}
<div className="flex items-center gap-2">
<Label className="text-xs font-semibold">Break:</Label>
<div className="flex gap-2">
<button
type="button"
onClick={() => handleRoleChange(shiftIdx, roleIdx, 'break_minutes', 15)}
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
role.break_minutes === 15
? 'bg-green-500 text-white shadow-md'
: 'bg-white border border-slate-300 text-slate-700 hover:border-green-400'
}`}
>
15 Min Payable
</button>
<button
type="button"
onClick={() => handleRoleChange(shiftIdx, roleIdx, 'break_minutes', 30)}
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
role.break_minutes === 30
? 'bg-amber-500 text-white shadow-md'
: 'bg-white border border-slate-300 text-slate-700 hover:border-amber-400'
}`}
>
30 Min Non-Payable
</button>
<button
type="button"
onClick={() => handleRoleChange(shiftIdx, roleIdx, 'break_minutes', 0)}
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
role.break_minutes === 0 || !role.break_minutes
? 'bg-slate-500 text-white shadow-md'
: 'bg-white border border-slate-300 text-slate-700 hover:border-slate-400'
}`}
>
No Break
</button>
</div>
</div>
{/* Rate & Total */}
<div className="flex items-center justify-between text-xs pt-2 border-t border-slate-200">
<span className="text-slate-600">Rate: ${(role.rate_per_hour || 0).toFixed(2)}/hr</span>
<span className="font-bold text-[#0A39DF] text-sm">Total: ${(role.total_value || 0).toFixed(2)}</span>
</div>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleAddRole(shiftIdx)}
className="w-full h-8 text-xs"
>
<Plus className="w-3 h-3 mr-2" />
Add Role
</Button>
</CardContent>
</Card>
))}
{/* Notes */}
<Card className="border-slate-200">
<CardContent className="p-4">
<Label className="text-xs mb-2 block">Additional Notes</Label>
<Textarea
value={formData.notes || ""}
onChange={(e) => handleChange('notes', e.target.value)}
placeholder="Special instructions..."
rows={2}
className="text-sm"
/>
</CardContent>
</Card>
</div>
</div>
</>
);
}

View File

@@ -1,3 +1,4 @@
import React, { useRef, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
@@ -5,6 +6,18 @@ import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { FileText } from "lucide-react";
// Safe date formatter
const safeFormatDate = (dateString, formatStr) => {
if (!dateString) return "";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return "";
return format(date, formatStr);
} catch {
return "";
}
};
export default function MessageThread({ messages, currentUserId }) {
const messagesEndRef = useRef(null);
@@ -73,7 +86,7 @@ export default function MessageThread({ messages, currentUserId }) {
</Card>
<span className="text-xs text-slate-500 mt-1">
{message.created_date && format(new Date(message.created_date), "MMM d, h:mm a")}
{safeFormatDate(message.created_date, "MMM d, h:mm a")}
</span>
</div>
</div>
@@ -83,4 +96,4 @@ export default function MessageThread({ messages, currentUserId }) {
<div ref={messagesEndRef} />
</div>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
import React from "react";
import { Link } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { MapPin, Users, Calendar, Clock } from "lucide-react";
import { format, differenceInHours, parseISO } from "date-fns";
function UpcomingOrderItem({ order }) {
const assignedCount = order.assigned_staff?.length || 0;
const requestedCount = order.requested || 0;
const fillPercentage = requestedCount > 0 ? Math.round((assignedCount / requestedCount) * 100) : 0;
// Calculate ETA (hours until event)
const eventDate = order.date ? parseISO(order.date) : new Date();
const hoursUntil = differenceInHours(eventDate, new Date());
const eta = hoursUntil > 24 ? `${Math.round(hoursUntil / 24)}d` : `${hoursUntil}h`;
// Determine status
let status = "On track";
let statusColor = "bg-emerald-100 text-emerald-700 border-emerald-200";
if (fillPercentage < 60) {
status = "At risk";
statusColor = "bg-red-100 text-red-700 border-red-200";
} else if (fillPercentage < 90) {
status = "Attention";
statusColor = "bg-amber-100 text-amber-700 border-amber-200";
}
// Progress bar color based on status
let progressColor = "bg-[#1C323E]";
if (fillPercentage < 60) progressColor = "bg-red-500";
else if (fillPercentage < 90) progressColor = "bg-amber-500";
return (
<div className="p-5 rounded-2xl bg-white border-2 border-slate-200 hover:border-[#0A39DF] transition-all shadow-sm hover:shadow-md">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-bold text-[#1C323E] mb-1">
{order.business_name || "Client"} {order.event_name}
</h3>
</div>
<Badge className={`${statusColor} border font-semibold`}>
{status}
</Badge>
</div>
{/* Staff Info */}
<div className="flex items-center gap-2 text-slate-600 mb-3">
<Users className="w-4 h-4" />
<span className="text-sm font-medium">
{assignedCount} × {order.shifts?.[0]?.roles?.[0]?.role || "Staff"}
{requestedCount > assignedCount && (
<span className="text-slate-400 ml-1">/ {requestedCount} needed</span>
)}
</span>
</div>
{/* Date/Time */}
<div className="flex items-center gap-2 text-slate-600 mb-4">
<Calendar className="w-4 h-4" />
<span className="text-sm">
{order.date ? format(parseISO(order.date), "EEE, HH:mm") : "Date TBD"}
{order.shifts?.[0]?.roles?.[0]?.end_time && (
<span>{order.shifts[0].roles[0].end_time}</span>
)}
</span>
</div>
{/* Progress Bar */}
<div className="mb-4">
<div className="flex items-center justify-between text-xs text-slate-600 mb-2">
<span className="font-semibold">{fillPercentage}%</span>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span className="font-medium">ETA {eta}</span>
</div>
</div>
<div className="w-full h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className={`h-full ${progressColor} transition-all duration-500 rounded-full`}
style={{ width: `${fillPercentage}%` }}
/>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-3">
<Link to={createPageUrl(`EventDetail?id=${order.id}`)} className="flex-1">
<Button
variant="outline"
className="w-full rounded-xl border-slate-300 hover:bg-slate-50"
>
View
</Button>
</Link>
<Link to={createPageUrl(`EventDetail?id=${order.id}`)} className="flex-1">
<Button className="w-full bg-[#1C323E] hover:bg-[#1C323E]/90 text-white rounded-xl font-semibold">
Smart Assign
</Button>
</Link>
</div>
</div>
);
}
export default function UpcomingOrdersCard({ orders }) {
if (!orders || orders.length === 0) {
return (
<Card className="bg-white border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<CardTitle className="text-base flex items-center gap-2">
<MapPin className="w-5 h-5 text-[#0A39DF]" />
Upcoming Orders
</CardTitle>
</CardHeader>
<CardContent className="p-8 text-center">
<Calendar className="w-12 h-12 mx-auto text-slate-300 mb-3" />
<p className="text-sm text-slate-500 font-medium">No upcoming orders</p>
<p className="text-xs text-slate-400 mt-1">New orders will appear here</p>
</CardContent>
</Card>
);
}
return (
<Card className="bg-white border-slate-200 shadow-sm">
<CardHeader className="border-b border-slate-100 pb-4">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<MapPin className="w-5 h-5 text-[#0A39DF]" />
Upcoming Orders
</CardTitle>
<Badge variant="outline" className="font-semibold">
{orders.length} {orders.length === 1 ? 'order' : 'orders'}
</Badge>
</div>
</CardHeader>
<CardContent className="p-5 space-y-4">
{orders.map((order) => (
<UpcomingOrderItem key={order.id} order={order} />
))}
</CardContent>
</Card>
);
}

View File

@@ -12,4 +12,5 @@ ReactDOM.createRoot(document.getElementById('root')).render(
<App />
</QueryClientProvider>
</React.StrictMode>,
)
)

View File

@@ -38,6 +38,19 @@ const colorMap = {
purple: "bg-purple-100 text-purple-600",
};
// Safe date formatter
const safeFormatDistanceToNow = (dateString) => {
if (!dateString) return "Unknown time";
try {
const date = new Date(dateString);
// Check for valid date object
if (isNaN(date.getTime())) return "Unknown time";
return formatDistanceToNow(date, { addSuffix: true });
} catch {
return "Unknown time";
}
};
export default function ActivityLog() {
const [activeTab, setActiveTab] = useState("all");
const [searchTerm, setSearchTerm] = useState("");
@@ -122,7 +135,7 @@ export default function ActivityLog() {
<div className="flex items-start justify-between mb-2">
<h3 className="font-bold text-[#1C323E] text-lg">{activity.title}</h3>
<span className="text-sm text-slate-500 whitespace-nowrap ml-4">
{formatDistanceToNow(new Date(activity.created_date), { addSuffix: true })}
{safeFormatDistanceToNow(activity.created_date)}
</span>
</div>
<p className="text-slate-600 mb-4">{activity.description}</p>

File diff suppressed because it is too large Load Diff

View File

@@ -1,165 +1,21 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Calendar as CalendarIcon, MapPin, Users, Clock, DollarSign, FileText, Plus, RefreshCw } from "lucide-react";
import { Calendar as CalendarIcon, MapPin, Users, Clock, DollarSign, FileText, Plus, RefreshCw, Zap } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { format, addDays } from "date-fns";
import { useToast } from "@/components/ui/use-toast";
// Imports for QuickReorderModal components (assuming shadcn/ui components)
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar"; // Shadcn Calendar component
import { cn } from "@/lib/utils"; // Utility for merging classNames
// Dummy QuickReorderModal component definition for functionality
// In a real application, this would likely be in its own file (e.g., @/components/modals/QuickReorderModal.jsx)
const QuickReorderModal = ({ event, open, onOpenChange }) => {
const navigate = useNavigate();
const { toast } = useToast();
const [newDate, setNewDate] = useState(event?.date ? new Date(event.date) : new Date());
const [eventName, setEventName] = useState(event?.event_name || "");
const [eventLocation, setEventLocation] = useState(event?.event_location || "");
React.useEffect(() => {
if (event) {
setNewDate(event.date ? new Date(event.date) : addDays(new Date(), 7)); // Suggest a week later if no original date
setEventName(event.event_name || "");
setEventLocation(event.event_location || "");
}
}, [event]);
const handleConfirmReorder = async () => {
if (!newDate) {
toast({
title: "Date Required",
description: "Please select a date for your reordered event.",
variant: "destructive",
});
return;
}
if (!eventName.trim()) {
toast({
title: "Event Name Required",
description: "Please enter a name for your reordered event.",
variant: "destructive",
});
return;
}
const reorderData = {
...event, // Copy all existing event data
event_name: eventName.trim(),
event_location: eventLocation.trim(),
date: newDate.toISOString(), // New date
status: "Pending", // New orders start as pending
id: undefined, // Ensure a new ID is generated by the backend
created_at: undefined,
updated_at: undefined,
// Clear specific fields that might not be relevant for a reorder
assigned: 0,
total: 0,
// Any other fields that should be reset or modified for a new order
};
sessionStorage.setItem('reorderData', JSON.stringify(reorderData));
toast({
title: "Event Reordered",
description: `Successfully prepared reorder for "${eventName}". Redirecting to creation page.`,
});
onOpenChange(false); // Close modal
navigate(createPageUrl("CreateEvent") + "?reorder=true");
};
if (!event) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Quick Reorder: {event.event_name}</DialogTitle>
<DialogDescription>
Confirm details for your new order based on this event. You can modify these further on the next page.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="eventName" className="text-right">
Event Name
</Label>
<Input
id="eventName"
value={eventName}
onChange={(e) => setEventName(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="eventLocation" className="text-right">
Location
</Label>
<Input
id="eventLocation"
value={eventLocation}
onChange={(e) => setEventLocation(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="date" className="text-right">
Date
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"col-span-3 justify-start text-left font-normal",
!newDate && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{newDate ? format(newDate, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={newDate}
onSelect={setNewDate}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button onClick={handleConfirmReorder}>
<RefreshCw className="w-4 h-4 mr-2" />
Reorder Event
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
import QuickReorderModal from "../components/events/QuickReorderModal";
export default function ClientOrders() {
const navigate = useNavigate();
const [statusFilter, setStatusFilter] = useState("all");
const [reorderModalOpen, setReorderModalOpen] = useState(false); // New state
const [selectedEvent, setSelectedEvent] = useState(null); // New state
const [reorderModalOpen, setReorderModalOpen] = useState(false);
const [selectedEvent, setSelectedEvent] = useState(null);
const { toast } = useToast();
const { data: user } = useQuery({
@@ -180,11 +36,16 @@ export default function ClientOrders() {
const filteredEvents = statusFilter === "all"
? clientEvents
: clientEvents.filter(e => e.status?.toLowerCase() === statusFilter);
: clientEvents.filter(e => {
if (statusFilter === "rapid_request") return e.is_rapid_request;
if (statusFilter === "pending") return e.status?.toLowerCase() === "pending" || e.status?.toLowerCase() === "draft";
return e.status?.toLowerCase() === statusFilter;
});
const getStatusColor = (status) => {
const colors = {
'pending': 'bg-yellow-100 text-yellow-700',
'draft': 'bg-gray-100 text-gray-700',
'confirmed': 'bg-green-100 text-green-700',
'active': 'bg-blue-100 text-blue-700',
'completed': 'bg-slate-100 text-slate-700',
@@ -196,14 +57,12 @@ export default function ClientOrders() {
const stats = {
total: clientEvents.length,
pending: clientEvents.filter(e => e.status === 'Pending').length,
rapidRequest: clientEvents.filter(e => e.is_rapid_request).length,
pending: clientEvents.filter(e => e.status === 'Pending' || e.status === 'Draft').length,
confirmed: clientEvents.filter(e => e.status === 'Confirmed').length,
completed: clientEvents.filter(e => e.status === 'Completed').length,
};
// Removed the old handleReorder function as per the outline
// const handleReorder = (event) => { /* ... existing logic ... */ };
const handleQuickReorder = (event) => {
setSelectedEvent(event);
setReorderModalOpen(true);
@@ -227,7 +86,7 @@ export default function ClientOrders() {
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="grid grid-cols-1 md:grid-cols-5 gap-6 mb-8">
<Card className="border-slate-200">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
@@ -238,6 +97,16 @@ export default function ClientOrders() {
</CardContent>
</Card>
<Card className="border-slate-200 bg-gradient-to-br from-red-50 to-white">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
<Zap className="w-8 h-8 text-red-600" />
</div>
<p className="text-sm text-slate-500">Rapid Requests</p>
<p className="text-3xl font-bold text-red-600">{stats.rapidRequest}</p>
</CardContent>
</Card>
<Card className="border-slate-200">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
@@ -270,7 +139,7 @@ export default function ClientOrders() {
</div>
{/* Filter Tabs */}
<div className="flex gap-2 mb-6">
<div className="flex gap-2 mb-6 flex-wrap">
<Button
variant={statusFilter === "all" ? "default" : "outline"}
onClick={() => setStatusFilter("all")}
@@ -278,6 +147,14 @@ export default function ClientOrders() {
>
All
</Button>
<Button
variant={statusFilter === "rapid_request" ? "default" : "outline"}
onClick={() => setStatusFilter("rapid_request")}
className={statusFilter === "rapid_request" ? "bg-red-600 hover:bg-red-700" : ""}
>
<Zap className="w-4 h-4 mr-2" />
Rapid Request
</Button>
<Button
variant={statusFilter === "pending" ? "default" : "outline"}
onClick={() => setStatusFilter("pending")}
@@ -314,6 +191,17 @@ export default function ClientOrders() {
<Badge className={getStatusColor(event.status)}>
{event.status}
</Badge>
{event.is_rapid_request && (
<Badge className="bg-red-100 text-red-700 border-red-200 border">
<Zap className="w-3 h-3 mr-1" />
Rapid Request
</Badge>
)}
{event.include_backup && (
<Badge className="bg-green-100 text-green-700 border-green-200 border">
🛡 {event.backup_staff_count || 0} Backup Staff
</Badge>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm text-slate-600 mb-4">
<div className="flex items-center gap-2">
@@ -322,7 +210,7 @@ export default function ClientOrders() {
</div>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
<span>{event.event_location || 'Location TBD'}</span>
<span>{event.event_location || event.hub || 'Location TBD'}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
@@ -391,4 +279,4 @@ export default function ClientOrders() {
</div>
</div>
);
}
}

View File

@@ -1,120 +1,148 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import EventFormWizard from "../components/events/EventFormWizard";
import AIOrderAssistant from "../components/events/AIOrderAssistant";
import { useToast } from "@/components/ui/use-toast";
import { Button } from "@/components/ui/button";
import { ArrowLeft, RefreshCw, Lock } from "lucide-react";
import EventForm from "../components/events/EventForm";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Sparkles, FileText, X } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
export default function CreateEvent() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [reorderData, setReorderData] = useState(null);
const [isReorder, setIsReorder] = useState(false);
const { toast } = useToast();
const [useAI, setUseAI] = useState(false);
const [aiExtractedData, setAiExtractedData] = useState(null);
// Get current user
const { data: user } = useQuery({
const { data: currentUser } = useQuery({
queryKey: ['current-user-create-event'],
queryFn: () => base44.auth.me(),
});
const userRole = user?.user_role || user?.role || "admin";
const isClient = userRole === "client";
const isVendor = userRole === "vendor";
const canCreateEvent = isClient || isVendor;
// Check if this is a reorder
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const reorder = urlParams.get('reorder');
if (reorder === 'true') {
const storedData = sessionStorage.getItem('reorderData');
if (storedData) {
try {
const data = JSON.parse(storedData);
setReorderData(data);
setIsReorder(true);
sessionStorage.removeItem('reorderData');
} catch (e) {
console.error('Failed to parse reorder data:', e);
}
}
}
}, []);
const createEventMutation = useMutation({
mutationFn: (eventData) => base44.entities.Event.create(eventData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['events'] });
toast({
title: "✅ Event Created",
description: "Your event has been created successfully.",
});
navigate(createPageUrl("Events"));
},
onError: (error) => {
toast({
title: "❌ Failed to Create Event",
description: error.message || "There was an error creating the event.",
variant: "destructive",
});
},
});
const handleSubmit = (eventData) => {
createEventMutation.mutate(eventData);
};
// If user doesn't have permission, show access denied
if (!canCreateEvent) {
return (
<div className="p-4 md:p-8">
<div className="max-w-4xl mx-auto">
<Alert className="bg-red-50 border-red-200">
<Lock className="w-4 h-4 text-red-600" />
<AlertDescription className="text-red-800">
<strong>Access Denied:</strong> Event creation is only available for Client and Vendor users.
</AlertDescription>
</Alert>
<div className="mt-6 text-center">
<Button onClick={() => navigate(createPageUrl("Events"))} variant="outline">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Events
const handleAIDataExtracted = (extractedData) => {
setAiExtractedData(extractedData);
setUseAI(false);
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<div className="max-w-7xl mx-auto p-4 md:p-8">
{/* Header with AI Toggle */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-[#1C323E]">Create New Order</h1>
<p className="text-slate-600 mt-1">
{useAI ? "Use AI to create your order naturally" : "Fill out the form to create your order"}
</p>
</div>
<div className="flex gap-2">
<Button
variant={useAI ? "default" : "outline"}
onClick={() => setUseAI(true)}
className={useAI ? "bg-gradient-to-r from-[#0A39DF] to-purple-600" : ""}
>
<Sparkles className="w-4 h-4 mr-2" />
AI Assistant
</Button>
<Button
variant={!useAI ? "default" : "outline"}
onClick={() => setUseAI(false)}
className={!useAI ? "bg-[#1C323E]" : ""}
>
<FileText className="w-4 h-4 mr-2" />
Form
</Button>
<Button
variant="ghost"
onClick={() => navigate(createPageUrl("Events"))}
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
</div>
);
}
return (
<div className="p-4 md:p-8">
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<Button
variant="ghost"
onClick={() => navigate(createPageUrl("Events"))}
className="mb-4 hover:bg-slate-100"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Events
</Button>
{isReorder && (
<Alert className="mb-4 bg-green-50 border-green-200">
<RefreshCw className="w-4 h-4 text-green-600" />
<AlertDescription className="text-green-800">
<strong>Reordering Event:</strong> Details from your previous order have been pre-filled. Update the date and any other details as needed.
</AlertDescription>
</Alert>
{/* AI Assistant Interface */}
<AnimatePresence>
{useAI && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
>
<AIOrderAssistant
onOrderDataExtracted={handleAIDataExtracted}
onClose={() => setUseAI(false)}
/>
</motion.div>
)}
</AnimatePresence>
<h1 className="text-3xl md:text-4xl font-bold text-slate-900 mb-2">
{isReorder ? "Reorder Event" : "Create New Event"}
</h1>
<p className="text-slate-600">
{isReorder ? "Review and update the details for your new order" : "Fill in the details to create a new event"}
</p>
</div>
{/* Wizard Form */}
{!useAI && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{aiExtractedData && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="w-5 h-5 text-green-600" />
<span className="font-semibold text-green-900">AI Pre-filled Data</span>
</div>
<p className="text-sm text-green-700 mb-3">
The form has been pre-filled with information from your conversation. Review and edit as needed.
</p>
<Button
variant="outline"
size="sm"
onClick={() => {
setAiExtractedData(null);
setUseAI(true);
}}
className="border-green-300 text-green-700 hover:bg-green-100"
>
<Sparkles className="w-4 h-4 mr-2" />
Chat with AI Again
</Button>
</div>
)}
<EventForm
event={reorderData}
onSubmit={handleSubmit}
isSubmitting={createEventMutation.isPending}
currentUser={user}
/>
<EventFormWizard
event={aiExtractedData}
onSubmit={handleSubmit}
isSubmitting={createEventMutation.isPending}
currentUser={currentUser}
onCancel={() => navigate(createPageUrl("Events"))}
/>
</motion.div>
)}
</div>
</div>
);

View File

@@ -1,3 +1,4 @@
import React from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
@@ -26,6 +27,7 @@ export default function EditBusiness() {
const [formData, setFormData] = React.useState({
business_name: "",
company_logo: "",
contact_name: "",
email: "",
phone: "",
@@ -37,6 +39,7 @@ export default function EditBusiness() {
if (business) {
setFormData({
business_name: business.business_name || "",
company_logo: business.company_logo || "",
contact_name: business.contact_name || "",
email: business.email || "",
phone: business.phone || "",
@@ -117,6 +120,32 @@ export default function EditBusiness() {
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="company_logo" className="text-slate-700 font-medium">Company Logo URL</Label>
<Input
id="company_logo"
value={formData.company_logo}
onChange={(e) => handleChange('company_logo', e.target.value)}
className="border-slate-200"
placeholder="https://example.com/logo.png"
/>
<p className="text-xs text-slate-500">Optional: URL to company logo image</p>
{formData.company_logo && (
<div className="mt-2 p-3 bg-slate-50 rounded-lg border border-slate-200">
<p className="text-xs text-slate-600 mb-2">Preview:</p>
<img
src={formData.company_logo}
alt="Company logo preview"
className="w-16 h-16 object-contain"
onError={(e) => {
e.target.style.display = 'none';
e.target.parentElement.innerHTML = '<p class="text-xs text-red-500">Failed to load image</p>';
}}
/>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="contact_name" className="text-slate-700 font-medium">Contact Person</Label>
<Input
@@ -210,4 +239,4 @@ export default function EditBusiness() {
</div>
</div>
);
}
}

View File

@@ -28,6 +28,18 @@ const statusColors = {
Canceled: "bg-red-100 text-red-800" // Added Canceled status for completeness
};
// Safe date formatter
const safeFormatDate = (dateString, formatStr) => {
if (!dateString) return "-";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return "-";
return format(date, formatStr);
} catch {
return "-";
}
};
export default function EventDetail() {
const navigate = useNavigate();
const queryClient = useQueryClient();
@@ -124,7 +136,7 @@ export default function EventDetail() {
</div>
<div>
<p className="text-xs text-slate-500">Data</p>
<p className="font-medium">{event.date ? format(new Date(event.date), "dd.MM.yyyy") : "-"}</p>
<p className="font-medium">{safeFormatDate(event.date, "dd.MM.yyyy")}</p>
</div>
<div>
<p className="text-xs text-slate-500">Status</p>

View File

@@ -1,4 +1,3 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
@@ -32,6 +31,127 @@ const statusColors = {
Cancelled: "bg-red-100 text-red-800"
};
// Date range presets
const datePresets = [
{
label: "Today",
getValue: () => {
const today = new Date();
return { from: startOfDay(today), to: endOfDay(today) };
}
},
{
label: "This week",
getValue: () => {
const today = new Date();
const start = startOfDay(new Date(today.setDate(today.getDate() - today.getDay())));
const end = endOfDay(new Date(today.setDate(today.getDate() - today.getDay() + 6)));
return { from: start, to: end };
}
},
{
label: "This month",
getValue: () => {
const today = new Date();
const start = startOfDay(new Date(today.getFullYear(), today.getMonth(), 1));
const end = endOfDay(new Date(today.getFullYear(), today.getMonth() + 1, 0));
return { from: start, to: end };
}
},
{
label: "This quarter",
getValue: () => {
const today = new Date();
const quarter = Math.floor(today.getMonth() / 3);
const start = startOfDay(new Date(today.getFullYear(), quarter * 3, 1));
const end = endOfDay(new Date(today.getFullYear(), quarter * 3 + 3, 0));
return { from: start, to: end };
}
},
{
label: "This year",
getValue: () => {
const today = new Date();
const start = startOfDay(new Date(today.getFullYear(), 0, 1));
const end = endOfDay(new Date(today.getFullYear(), 11, 31));
return { from: start, to: end };
}
},
{
label: "Last week",
getValue: () => {
const today = new Date();
const lastWeekStart = new Date(today);
lastWeekStart.setDate(today.getDate() - today.getDay() - 7);
const lastWeekEnd = new Date(today);
lastWeekEnd.setDate(today.getDate() - today.getDay() - 1);
return { from: startOfDay(lastWeekStart), to: endOfDay(lastWeekEnd) };
}
},
{
label: "Last month",
getValue: () => {
const today = new Date();
const start = startOfDay(new Date(today.getFullYear(), today.getMonth() - 1, 1));
const end = endOfDay(new Date(today.getFullYear(), today.getMonth(), 0));
return { from: start, to: end };
}
},
{
label: "Last quarter",
getValue: () => {
const today = new Date();
const quarter = Math.floor(today.getMonth() / 3);
const prevQuarterMonth = quarter * 3 - 3;
const year = prevQuarterMonth < 0 ? today.getFullYear() - 1 : today.getFullYear();
const adjustedMonth = prevQuarterMonth < 0 ? prevQuarterMonth + 12 : prevQuarterMonth;
const start = startOfDay(new Date(year, adjustedMonth, 1));
const end = endOfDay(new Date(year, adjustedMonth + 3, 0));
return { from: start, to: end };
}
},
{
label: "Last year",
getValue: () => {
const today = new Date();
const start = startOfDay(new Date(today.getFullYear() - 1, 0, 1));
const end = endOfDay(new Date(today.getFullYear() - 1, 11, 31));
return { from: start, to: end };
}
},
{
label: "Last 30 days",
getValue: () => {
const today = new Date();
const start = startOfDay(new Date(today.setDate(today.getDate() - 30)));
const end = endOfDay(new Date());
return { from: start, to: end };
}
},
{
label: "Last 90 days",
getValue: () => {
const today = new Date();
const start = startOfDay(new Date(today.setDate(today.getDate() - 90)));
const end = endOfDay(new Date());
return { from: start, to: end };
}
},
{
label: "Last 365 days",
getValue: () => {
const today = new Date();
const start = startOfDay(new Date(today.setDate(today.getDate() - 365)));
const end = endOfDay(new Date());
return { from: start, to: end };
}
},
{
label: "All time",
getValue: () => null
},
];
// Helper function to safely parse dates
const safeParseDate = (dateString) => {
if (!dateString) return null;
@@ -54,6 +174,18 @@ const safeFormatDate = (dateString, formatStr) => {
}
};
// Safely format date range for presets
const safeFormatDateRange = (date, formatStr) => {
if (!date) return "";
try {
const validDate = date instanceof Date ? date : new Date(date);
if (!isValid(validDate)) return "";
return format(validDate, formatStr);
} catch {
return "";
}
};
export default function Events() {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("all");
@@ -64,6 +196,8 @@ export default function Events() {
const [calendarOpen, setCalendarOpen] = useState(false);
const [showAlert, setShowAlert] = useState(true);
const { toast } = useToast();
const [selectedPreset, setSelectedPreset] = useState(null);
const [compareMode, setCompareMode] = useState(false);
const { data: events, isLoading } = useQuery({
queryKey: ['events'],
@@ -202,19 +336,33 @@ export default function Events() {
const clearDates = () => {
setSelectedDates([]);
setDateRange(null);
setSelectedPreset(null);
setShowAlert(true);
};
const getDateSelectionText = () => {
try {
if (selectedPreset) {
if (selectedPreset === "All time") return "All time";
const preset = datePresets.find(p => p.label === selectedPreset);
if (preset) {
const range = preset.getValue();
if (range?.from && range?.to) {
return `${safeFormatDateRange(range.from, 'MMM d')} - ${safeFormatDateRange(range.to, 'MMM d, yyyy')}`;
} else if (range?.from) {
return safeFormatDateRange(range.from, 'MMM d, yyyy');
}
}
}
if (selectionMode === "range" && dateRange?.from) {
if (dateRange.to) {
return `${format(dateRange.from, 'MMM d')} - ${format(dateRange.to, 'MMM d, yyyy')}`;
return `${safeFormatDateRange(dateRange.from, 'MMM d')} - ${safeFormatDateRange(dateRange.to, 'MMM d, yyyy')}`;
}
return format(dateRange.from, 'MMM d, yyyy');
return safeFormatDateRange(dateRange.from, 'MMM d, yyyy');
} else if (selectionMode === "multiple" && selectedDates.length > 0) {
if (selectedDates.length === 1) {
return format(selectedDates[0], 'MMM d, yyyy');
return safeFormatDateRange(selectedDates[0], 'MMM d, yyyy');
}
return `${selectedDates.length} dates selected`;
}
@@ -236,17 +384,46 @@ export default function Events() {
}).length;
};
const handlePresetSelect = (preset) => {
setSelectedPreset(preset.label);
setCalendarOpen(false);
if (preset.label === "All time") {
setDateRange(null);
setSelectedDates([]);
setSelectionMode("range");
} else {
const range = preset.getValue();
setDateRange(range);
setSelectedDates([]);
setSelectionMode("range");
}
setShowAlert(true);
};
const getPresetDateRange = (preset) => {
if (preset.label === "All time") return "All dates";
const range = preset.getValue();
if (!range?.from) return "";
try {
if (range.to) {
return `${safeFormatDateRange(range.from, 'd MMM')} - ${safeFormatDateRange(range.to, 'd MMM, yyyy')}`;
}
return safeFormatDateRange(range.from, 'd MMM, yyyy');
} catch {
return "";
}
};
React.useEffect(() => {
if (showAlert && (filteredEvents.length > 0 && (selectedDates.length > 0 || dateRange?.from))) {
if (showAlert && (filteredEvents.length > 0 && (selectedDates.length > 0 || dateRange?.from || selectedPreset === "All time"))) {
const timer = setTimeout(() => {
setShowAlert(false);
}, 5000);
return () => clearTimeout(timer);
}
}, [showAlert, filteredEvents.length, selectedDates.length, dateRange]);
}, [showAlert, filteredEvents.length, selectedDates.length, dateRange, selectedPreset]);
const handleReorder = (event) => {
// Create a clean copy of the event for reordering
const reorderData = {
event_name: event.event_name,
business_id: event.business_id,
@@ -274,6 +451,189 @@ export default function Events() {
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
<style>{`
/* 🎨 CALENDAR STYLING - Based on Reference Design */
/* Base day cell styling */
.rdp-day {
font-size: 0.875rem !important;
min-width: 36px !important;
height: 36px !important;
border-radius: 50% !important;
transition: all 0.2s ease !important;
font-weight: 500 !important;
position: relative !important;
}
/* Regular unselected dates */
.rdp-day button {
width: 100% !important;
height: 100% !important;
border-radius: 50% !important;
}
/* Selected range start date - Solid blue filled circle */
.rdp-day_range_start {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
/* Selected range end date - Solid blue filled circle */
.rdp-day_range_end {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
/* Single selected date - Solid blue filled circle */
.rdp-day_selected:not(.rdp-day_range_start):not(.rdp-day_range_end) {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
/* Range middle dates - Light lavender/blue background */
.rdp-day_range_middle {
background-color: #e0e7ff !important;
color: #4f46e5 !important;
font-weight: 600 !important;
border-radius: 0 !important;
}
/* When start and end are the same day */
.rdp-day_range_start.rdp-day_range_end {
border-radius: 50% !important;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
}
/* Hover effect - Light blue background */
.rdp-day:hover:not(.rdp-day_selected):not(.rdp-day_disabled):not(.rdp-day_range_start):not(.rdp-day_range_end):not(.rdp-day_range_middle) {
background-color: #eff6ff !important;
color: #2563eb !important;
border-radius: 50% !important;
}
/* Today indicator - Pink/magenta dot at bottom */
.rdp-day_today:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::after {
content: '' !important;
position: absolute !important;
bottom: 4px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 4px !important;
height: 4px !important;
background-color: #ec4899 !important;
border-radius: 50% !important;
}
/* Today with selection - adjust styling */
.rdp-day_today.rdp-day_selected,
.rdp-day_today.rdp-day_range_start,
.rdp-day_today.rdp-day_range_end {
color: white !important;
}
/* Disabled/outside dates - gray and muted */
.rdp-day_outside {
color: #cbd5e1 !important;
opacity: 0.5 !important;
}
.rdp-day_disabled {
opacity: 0.3 !important;
cursor: not-allowed !important;
}
/* Keep selected dates always visible */
.rdp-day_selected,
.rdp-day_range_start,
.rdp-day_range_end,
.rdp-day_range_middle {
opacity: 1 !important;
visibility: visible !important;
z-index: 5 !important;
}
/* Calendar header - weekday names */
.rdp-head_cell {
color: #64748b !important;
font-weight: 600 !important;
font-size: 0.75rem !important;
text-transform: uppercase !important;
padding: 8px 0 !important;
}
/* Month/Year navigation */
.rdp-caption_label {
font-size: 1rem !important;
font-weight: 700 !important;
color: #0f172a !important;
}
/* Navigation arrows */
.rdp-nav_button {
width: 32px !important;
height: 32px !important;
border-radius: 6px !important;
transition: all 0.2s ease !important;
}
.rdp-nav_button:hover {
background-color: #eff6ff !important;
color: #2563eb !important;
}
/* Event indicator dots */
.rdp-day.has-events:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::before {
content: '' !important;
position: absolute !important;
top: 4px !important;
right: 4px !important;
width: 4px !important;
height: 4px !important;
background-color: #2563eb !important;
border-radius: 50% !important;
}
/* Selected dates with events - white dot */
.rdp-day_selected.has-events::before,
.rdp-day_range_start.has-events::before,
.rdp-day_range_end.has-events::before {
background-color: white !important;
}
/* Middle range dates with events - blue dot */
.rdp-day_range_middle.has-events::before {
background-color: #4f46e5 !important;
}
/* Calendar spacing */
.rdp-months {
gap: 2rem !important;
}
.rdp-month {
padding: 0.75rem !important;
}
.rdp-table {
border-spacing: 0 !important;
margin-top: 1rem !important;
}
/* Cell spacing */
.rdp-cell {
padding: 2px !important;
}
`}</style>
<div className="max-w-7xl mx-auto">
<PageHeader
title="Events Management"
@@ -281,7 +641,7 @@ export default function Events() {
showUnpublished={true}
actions={
<Link to={createPageUrl("CreateEvent")}>
<Button className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0A39DF]/90 hover:to-[#1C323E]/90 text-white shadow-lg">
<Button className="bg-gradient-to-r from-[#2563eb] to-[#1C323E] hover:from-[#2563eb]/90 hover:to-[#1C323E]/90 text-white shadow-lg">
<Plus className="w-5 h-5 mr-2" />
Create Event
</Button>
@@ -301,8 +661,9 @@ export default function Events() {
onClick={() => {
setSelectionMode("multiple");
setDateRange(null);
setSelectedPreset(null);
}}
className={selectionMode === "multiple" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90 text-white" : "text-slate-600 hover:text-slate-900 hover:bg-white"}
className={selectionMode === "multiple" ? "bg-[#2563eb] hover:bg-[#2563eb]/90 text-white" : "text-slate-600 hover:text-slate-900 hover:bg-white"}
>
Multiple
</Button>
@@ -312,8 +673,9 @@ export default function Events() {
onClick={() => {
setSelectionMode("range");
setSelectedDates([]);
setSelectedPreset(null);
}}
className={selectionMode === "range" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90 text-white" : "text-slate-600 hover:text-slate-900 hover:bg-white"}
className={selectionMode === "range" ? "bg-[#2563eb] hover:bg-[#2563eb]/90 text-white" : "text-slate-600 hover:text-slate-900 hover:bg-white"}
>
Range
</Button>
@@ -332,70 +694,126 @@ export default function Events() {
<PopoverTrigger asChild>
<Button
variant="outline"
className="border-[#0A39DF] text-[#0A39DF] hover:bg-[#0A39DF]/5 hover:border-[#0A39DF] font-medium min-w-[200px]"
className="border-[#2563eb] text-[#2563eb] hover:bg-[#2563eb]/5 hover:border-[#2563eb] font-medium min-w-[200px]"
>
<CalendarIcon className="w-4 h-4 mr-2" />
{getDateSelectionText()}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<div className="bg-white rounded-lg shadow-xl border border-slate-200">
<div className="p-4 border-b border-slate-200 bg-slate-50">
<p className="font-semibold text-slate-900">
{selectionMode === "range" ? "Select Date Range" : "Select Multiple Dates"}
</p>
<p className="text-xs text-slate-500 mt-1">
{selectionMode === "range"
? "Click start date, then end date"
: "Click dates to select/deselect"}
</p>
<div className="bg-white rounded-lg shadow-xl border border-slate-200 flex">
{/* Date Presets Sidebar */}
<div className="w-64 border-r border-slate-200 bg-slate-50 p-4 rounded-l-lg">
<h4 className="font-semibold text-sm text-slate-700 mb-3 uppercase tracking-wide">DATE RANGE</h4>
<div className="space-y-1">
{datePresets.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => handlePresetSelect(preset)}
className={`w-full text-left px-3 py-2 rounded-lg transition-all text-sm ${
selectedPreset === preset.label
? 'bg-[#2563eb] text-white font-medium shadow-sm'
: 'hover:bg-white text-slate-700 hover:shadow-sm'
}`}
>
<div className="flex items-center justify-between">
<span>{preset.label}</span>
</div>
{selectedPreset === preset.label && preset.label !== "All time" && (
<div className="text-xs mt-1 opacity-90">
{getPresetDateRange(preset)}
</div>
)}
</button>
))}
</div>
{/* Compare Toggle */}
<div className="mt-6 pt-4 border-t border-slate-200">
<button
type="button"
onClick={() => setCompareMode(!compareMode)}
className={`w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all ${
compareMode
? 'bg-purple-100 text-purple-700 font-medium'
: 'text-slate-600 hover:bg-white'
}`}
>
<RefreshCw className="w-4 h-4" />
Compare
</button>
</div>
</div>
<Calendar
mode={selectionMode === "range" ? "range" : "multiple"}
selected={selectionMode === "range" ? dateRange : selectedDates}
onSelect={selectionMode === "range" ? handleRangeSelect : handleDateSelect}
numberOfMonths={2}
modifiers={{
hasEvents: (date) => getEventCountForDate(date) > 0
}}
modifiersStyles={{
hasEvents: {
fontWeight: 'bold',
textDecoration: 'underline',
color: '#0A39DF'
}
}}
className="rounded-md border-0 p-4"
/>
<div className="p-4 border-t border-slate-200 bg-slate-50 flex items-center justify-between">
<p className="text-xs text-slate-500">
<span className="font-bold text-[#0A39DF]">Bold underlined dates</span> have events
</p>
<Button
size="sm"
onClick={() => setCalendarOpen(false)}
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
>
Done
</Button>
{/* Calendar Section */}
<div className="p-4">
<div className="mb-3">
<p className="font-semibold text-slate-900 text-sm">
{selectionMode === "range" ? "Select Date Range" : "Select Multiple Dates"}
</p>
<p className="text-xs text-slate-500 mt-1">
{selectionMode === "range"
? "Click start date, then end date"
: "Click dates to select/deselect"}
</p>
</div>
<Calendar
mode={selectionMode === "range" ? "range" : "multiple"}
selected={selectionMode === "range" ? dateRange : selectedDates}
onSelect={selectionMode === "range" ? handleRangeSelect : handleDateSelect}
numberOfMonths={2}
modifiers={{
hasEvents: (date) => getEventCountForDate(date) > 0
}}
modifiersClassNames={{
hasEvents: 'has-events'
}}
className="rounded-md border-0"
/>
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-200">
<p className="text-xs text-slate-500">
Dates with <span className="font-semibold text-[#2563eb]">dot indicators</span> have events
</p>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
setCalendarOpen(false);
clearDates();
}}
className="border-slate-300"
>
Cancel
</Button>
<Button
size="sm"
onClick={() => setCalendarOpen(false)}
className="bg-[#2563eb] hover:bg-[#2563eb]/90"
>
Confirm
</Button>
</div>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
{(selectedDates.length > 0 || dateRange?.from) && showAlert && filteredEvents.length > 0 && (
<Alert className="bg-[#0A39DF]/5 border-[#0A39DF]/20 relative mt-4">
{(selectedDates.length > 0 || dateRange?.from || selectedPreset === "All time") && showAlert && filteredEvents.length > 0 && (
<Alert className="bg-[#2563eb]/5 border-[#2563eb]/20 relative mt-4">
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 h-6 w-6 text-[#0A39DF] hover:bg-[#0A39DF]/10"
className="absolute top-2 right-2 h-6 w-6 text-[#2563eb] hover:bg-[#2563eb]/10"
onClick={() => setShowAlert(false)}
>
<X className="w-4 h-4" />
</Button>
<CalendarIcon className="h-4 w-4 text-[#0A39DF]" />
<AlertDescription className="text-[#0A39DF] font-medium pr-8">
<CalendarIcon className="h-4 w-4 text-[#2563eb]" />
<AlertDescription className="text-[#2563eb] font-medium pr-8">
{filteredEvents.length} event{filteredEvents.length !== 1 ? 's' : ''} found for selected date{selectionMode === "multiple" && selectedDates.length > 1 ? 's' : ''}
</AlertDescription>
</Alert>
@@ -404,22 +822,22 @@ export default function Events() {
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
<TabsList className="bg-white border border-slate-200 h-auto p-1 shadow-sm">
<TabsTrigger value="all" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
<TabsTrigger value="all" className="data-[state=active]:bg-[#2563eb] data-[state=active]:text-white">
Total Events <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("all")}</span>
</TabsTrigger>
<TabsTrigger value="last_minute" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
<TabsTrigger value="last_minute" className="data-[state=active]:bg-[#2563eb] data-[state=active]:text-white">
Last Minute <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("last_minute")}</span>
</TabsTrigger>
<TabsTrigger value="upcoming" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
<TabsTrigger value="upcoming" className="data-[state=active]:bg-[#2563eb] data-[state=active]:text-white">
Upcoming <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("upcoming")}</span>
</TabsTrigger>
<TabsTrigger value="active" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
<TabsTrigger value="active" className="data-[state=active]:bg-[#2563eb] data-[state=active]:text-white">
Active <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("active")}</span>
</TabsTrigger>
<TabsTrigger value="canceled" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
<TabsTrigger value="canceled" className="data-[state=active]:bg-[#2563eb] data-[state=active]:text-white">
Canceled <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("canceled")}</span>
</TabsTrigger>
<TabsTrigger value="past" className="data-[state=active]:bg-[#0A39DF] data-[state=active]:text-white">
<TabsTrigger value="past" className="data-[state=active]:bg-[#2563eb] data-[state=active]:text-white">
Past <span className="ml-2 px-2 py-0.5 rounded-full bg-slate-100 data-[state=active]:bg-white/20 text-slate-700 data-[state=active]:text-white text-xs font-medium">{getTabCount("past")}</span>
</TabsTrigger>
</TabsList>
@@ -501,7 +919,7 @@ export default function Events() {
<TableCell className="text-center font-semibold">{event.requested || 0}</TableCell>
<TableCell className="text-center">
<QuickAssignPopover event={event}>
<button className={`hover:text-[#0A39DF] font-semibold ${
<button className={`hover:text-[#2563eb] font-semibold ${
assignedCount >= event.requested && event.requested > 0 ? 'text-green-600' : 'text-orange-600'
}`}>
{assignedCount}
@@ -517,7 +935,7 @@ export default function Events() {
e.stopPropagation();
navigate(createPageUrl(`EventDetail?id=${event.id}`));
}}
className="hover:text-[#0A39DF] hover:bg-[#0A39DF]/10"
className="hover:text-[#2563eb] hover:bg-[#2563eb]/10"
title="View Details"
>
<Eye className="w-4 h-4" />
@@ -529,7 +947,7 @@ export default function Events() {
e.stopPropagation();
navigate(createPageUrl(`EditEvent?id=${event.id}`));
}}
className="hover:text-[#0A39DF] hover:bg-[#0A39DF]/10"
className="hover:text-[#2563eb] hover:bg-[#2563eb]/10"
title="Edit Event"
>
<Edit className="w-4 h-4" />
@@ -558,4 +976,4 @@ export default function Events() {
</div>
</div>
);
}
}

View File

@@ -1,5 +1,4 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { createPageUrl } from "@/utils";
@@ -28,12 +27,13 @@ import {
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import ChatBubble from "@/components/chat/ChatBubble";
import RoleSwitcher from "@/components/dev/RoleSwitcher";
import NotificationPanel from "@/components/notifications/NotificationPanel";
import { Toaster } from "@/components/ui/toaster";
// Navigation items for each role
// Navigation items for each role (removed Control Tower)
const roleNavigationMap = {
admin: [
{ title: "Dashboard", url: createPageUrl("Dashboard"), icon: LayoutDashboard },
@@ -60,7 +60,6 @@ const roleNavigationMap = {
{ title: "Sectors", url: createPageUrl("SectorManagement"), icon: MapPin },
{ title: "Partners", url: createPageUrl("PartnerManagement"), icon: Briefcase },
{ title: "Vendors", url: createPageUrl("VendorManagement"), icon: Package },
{ title: "Operators", url: createPageUrl("Business"), icon: Briefcase },
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Compliance", url: createPageUrl("WorkforceCompliance"), icon: Shield },
{ title: "Orders", url: createPageUrl("Events"), icon: Clipboard },
@@ -135,6 +134,8 @@ const roleNavigationMap = {
],
};
// ... keep all existing helper functions (getRoleName, etc.) ...
const getRoleName = (role) => {
const names = {
admin: "KROW Admin",
@@ -231,6 +232,8 @@ function NavigationMenu({ location, userRole, closeSheet }) {
}
export default function Layout({ children }) {
// ... keep ALL existing Layout code (state, queries, handlers) ...
const location = useLocation();
const [showNotifications, setShowNotifications] = React.useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
@@ -248,33 +251,16 @@ export default function Layout({ children }) {
profile_picture: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
};
// Sample avatar if user doesn't have one
const sampleAvatar = "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop";
const userAvatar = user?.profile_picture || sampleAvatar;
// Get unread notification count
// const { data: unreadCount = 0 } = useQuery({
// queryKey: ['unread-notifications', user?.id],
// queryFn: async () => {
// if (!user?.id) return 0;
// // Assuming ActivityLog entity is used for user notifications
// // and has user_id and is_read fields.
// const notifications = await base44.entities.ActivityLog.filter({
// user_id: user?.id,
// is_read: false
// });
// return notifications.length;
// },
// enabled: !!user?.id,
// initialData: 0,
// refetchInterval: 10000, // Refresh every 10 seconds
// });
const unreadCount = 0; // Mocked value
const userRole = user?.user_role || user?.role || "admin";
const userName = user?.full_name || user?.email || "User";
const userInitial = userName.charAt(0).toUpperCase();
const roleDescription = getRoleDescription(userRole);
const handleLogout = () => {
base44.auth.logout();
@@ -286,6 +272,7 @@ export default function Layout({ children }) {
return (
<div className="min-h-screen flex flex-col w-full bg-slate-50">
{/* ... keep ALL existing Layout structure (header, sidebar, main, footer) ... */}
<style>{`
:root {
--primary: 10 57 223;
@@ -294,14 +281,206 @@ export default function Layout({ children }) {
--accent: 28 50 62;
--muted: 241 245 249;
}
/* Calendar styling kept as is */
.rdp * {
border-color: transparent !important;
}
.rdp-day {
font-size: 0.875rem !important;
min-width: 36px !important;
height: 36px !important;
border-radius: 50% !important;
transition: all 0.2s ease !important;
font-weight: 500 !important;
position: relative !important;
}
.rdp-day button {
width: 100% !important;
height: 100% !important;
border-radius: 50% !important;
background-color: transparent !important;
}
.rdp-day_range_start,
.rdp-day_range_start > button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
.rdp-day_range_end,
.rdp-day_range_end > button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
.rdp-day_selected:not(.rdp-day_range_start):not(.rdp-day_range_end),
.rdp-day_selected:not(.rdp-day_range_start):not(.rdp-day_range_end) > button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
.rdp-day_selected,
.rdp-day_selected > button {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
color: white !important;
font-weight: 700 !important;
border-radius: 50% !important;
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3) !important;
}
.rdp-day_range_middle,
.rdp-day_range_middle > button {
background-color: #dbeafe !important;
background: #dbeafe !important;
color: #2563eb !important;
font-weight: 600 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.rdp-day_range_start.rdp-day_range_end,
.rdp-day_range_start.rdp-day_range_end > button {
border-radius: 50% !important;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
}
.rdp-day:hover:not(.rdp-day_selected):not(.rdp-day_disabled):not(.rdp-day_range_start):not(.rdp-day_range_end):not(.rdp-day_range_middle) > button {
background-color: #eff6ff !important;
background: #eff6ff !important;
color: #2563eb !important;
border-radius: 50% !important;
}
.rdp-day_today:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::after {
content: '' !important;
position: absolute !important;
bottom: 4px !important;
left: 50% !important;
transform: translateX(-50%) !important;
width: 4px !important;
height: 4px !important;
background-color: #ec4899 !important;
border-radius: 50% !important;
z-index: 10 !important;
}
.rdp-day_today.rdp-day_selected,
.rdp-day_today.rdp-day_range_start,
.rdp-day_today.rdp-day_range_end {
color: white !important;
}
.rdp-day_today.rdp-day_selected > button,
.rdp-day_today.rdp-day_range_start > button,
.rdp-day_today.rdp-day_range_end > button {
color: white !important;
}
.rdp-day_outside,
.rdp-day_outside > button {
color: #cbd5e1 !important;
opacity: 0.5 !important;
}
.rdp-day_disabled,
.rdp-day_disabled > button {
opacity: 0.3 !important;
cursor: not-allowed !important;
}
.rdp-day_selected,
.rdp-day_range_start,
.rdp-day_range_end,
.rdp-day_range_middle {
opacity: 1 !important;
visibility: visible !important;
z-index: 5 !important;
}
.rdp-head_cell {
color: #64748b !important;
font-weight: 600 !important;
font-size: 0.75rem !important;
text-transform: uppercase !important;
padding: 8px 0 !important;
}
.rdp-caption_label {
font-size: 1rem !important;
font-weight: 700 !important;
color: #0f172a !important;
}
.rdp-nav_button {
width: 32px !important;
height: 32px !important;
border-radius: 6px !important;
transition: all 0.2s ease !important;
}
.rdp-nav_button:hover {
background-color: #eff6ff !important;
color: #2563eb !important;
}
.rdp-day.has-events:not(.rdp-day_selected):not(.rdp-day_range_start):not(.rdp-day_range_end)::before {
content: '' !important;
position: absolute !important;
top: 4px !important;
right: 4px !important;
width: 4px !important;
height: 4px !important;
background-color: #2563eb !important;
border-radius: 50% !important;
}
.rdp-day_selected.has-events::before,
.rdp-day_range_start.has-events::before,
.rdp-day_range_end.has-events::before {
background-color: white !important;
}
.rdp-day_range_middle.has-events::before {
background-color: #2563eb !important;
}
.rdp-months {
gap: 2rem !important;
}
.rdp-month {
padding: 0.75rem !important;
}
.rdp-table {
border-spacing: 0 !important;
margin-top: 1rem !important;
}
.rdp-cell {
padding: 2px !important;
}
.rdp-day[style*="background"] {
background: transparent !important;
}
`}</style>
{/* Unified Top Header - Always Visible */}
<header className="bg-white border-b border-slate-200 shadow-sm sticky top-0 z-30">
<div className="px-4 md:px-6 py-3 flex items-center justify-between gap-4">
{/* Left Section - Menu + Logo + Search */}
<div className="flex items-center gap-4 flex-1">
{/* Mobile Menu Toggle */}
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="lg:hidden hover:bg-slate-100">
@@ -349,7 +528,6 @@ export default function Layout({ children }) {
</SheetContent>
</Sheet>
{/* Logo & Company Name */}
<Link to={getDashboardUrl(userRole)} className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="w-8 h-8 flex items-center justify-center">
<img
@@ -363,7 +541,6 @@ export default function Layout({ children }) {
</div>
</Link>
{/* Search Bar - Desktop */}
<div className="hidden md:flex flex-1 max-w-xl">
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
@@ -376,9 +553,7 @@ export default function Layout({ children }) {
</div>
</div>
{/* Right Section - Icons */}
<div className="flex items-center gap-2">
{/* Unpublished Changes Indicator */}
<button
onClick={handleRefresh}
className="flex items-center gap-2 px-3 py-2 text-slate-500 hover:text-[#0A39DF] hover:bg-blue-50 rounded-lg transition-all group"
@@ -388,7 +563,6 @@ export default function Layout({ children }) {
<span className="hidden lg:inline text-sm font-medium">Unpublished changes</span>
</button>
{/* Search Icon - Mobile */}
<Button
variant="ghost"
size="icon"
@@ -398,7 +572,6 @@ export default function Layout({ children }) {
<Search className="w-5 h-5 text-slate-600" />
</Button>
{/* Notification Bell */}
<button
onClick={() => setShowNotifications(true)}
className="relative p-2 hover:bg-slate-100 rounded-lg transition-colors"
@@ -412,28 +585,24 @@ export default function Layout({ children }) {
)}
</button>
{/* Home */}
<Link to={getDashboardUrl(userRole)} title="Home">
<Button variant="ghost" size="icon" className="hover:bg-slate-100">
<Home className="w-5 h-5 text-slate-600" />
</Button>
</Link>
{/* Messages */}
<Link to={createPageUrl("Messages")} title="Messages">
<Button variant="ghost" size="icon" className="hover:bg-slate-100">
<MessageSquare className="w-5 h-5 text-slate-600" />
</Button>
</Link>
{/* Help */}
<Link to={createPageUrl("Support")} title="Help & Support">
<Button variant="ghost" size="icon" className="hover:bg-slate-100">
<HelpCircle className="w-5 h-5 text-slate-600" />
</Button>
</Link>
{/* More Menu (3 dots) */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="hover:bg-slate-100" title="More options">
@@ -461,7 +630,6 @@ export default function Layout({ children }) {
</DropdownMenuContent>
</DropdownMenu>
{/* User Menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 hover:bg-slate-100 rounded-lg p-1.5 transition-colors" title={`${userName} - ${getRoleName(userRole)}`}>
@@ -498,22 +666,18 @@ export default function Layout({ children }) {
</div>
</header>
{/* Main Layout with Sidebar */}
<div className="flex flex-1 overflow-hidden">
{/* Desktop Sidebar */}
<aside className="hidden lg:flex lg:flex-col w-64 bg-white border-r border-slate-200 shadow-sm overflow-y-auto">
<div className="p-3">
<NavigationMenu location={location} userRole={userRole} closeSheet={() => {}} />
</div>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-auto pb-16">
{children}
</main>
</div>
{/* Layer Identifier - Bottom Bar */}
<div className="fixed bottom-0 left-0 right-0 z-20 bg-white border-t-2 border-slate-200 shadow-lg">
<div className="px-4 py-2 flex items-center justify-center gap-3">
<span className="text-xs text-slate-500 font-medium">Current:</span>
@@ -523,21 +687,14 @@ export default function Layout({ children }) {
</div>
</div>
{/* Notification Panel */}
<NotificationPanel
isOpen={showNotifications}
onClose={() => setShowNotifications(false)}
/>
{/* Live Chat Bubble */}
<ChatBubble />
{/* Role Switcher (Development Tool) */}
<RoleSwitcher />
{/* Toast Notifications */}
<Toaster />
</div>
);
}

View File

@@ -1,54 +1,205 @@
import React, { useState } from "react";
import React, { useState, useMemo } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Briefcase, Plus, Search, MapPin, DollarSign, Edit } from "lucide-react";
import { Briefcase, Plus, Search, MapPin, DollarSign, Edit, Building2, TrendingUp, AlertTriangle, CheckCircle2, Users, Target, LayoutGrid, List } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
export default function PartnerManagement() {
const [searchTerm, setSearchTerm] = useState("");
const [viewMode, setViewMode] = useState("grid"); // New state for view mode
const { data: partners = [], isLoading } = useQuery({
const { data: partners = [], isLoading: partnersLoading } = useQuery({
queryKey: ['partners'],
queryFn: () => base44.entities.Partner.list('-created_date'),
initialData: [],
});
const { data: businesses = [], isLoading: businessesLoading } = useQuery({
queryKey: ['businesses'],
queryFn: () => base44.entities.Business.list('-created_date'),
initialData: [],
});
const { data: sectors = [] } = useQuery({
queryKey: ['sectors'],
queryFn: () => base44.entities.Sector.list('-created_date'),
initialData: [],
});
const { data: enterprises = [] } = useQuery({
queryKey: ['enterprises'],
queryFn: () => base44.entities.Enterprise.list('-created_date'),
initialData: [],
});
// Consolidate businesses by company name
const consolidatedBusinesses = useMemo(() => {
const grouped = {};
businesses.forEach(business => {
let companyName = business.business_name;
// Extract company name (remove hub suffix if present)
const dashIndex = companyName.indexOf(' - ');
if (dashIndex > 0) {
companyName = companyName.substring(0, dashIndex).trim();
}
if (!grouped[companyName]) {
grouped[companyName] = {
company_name: companyName,
partner_type: "Corporate",
hubs: [],
primary_contact: business.contact_name,
primary_email: business.email,
primary_phone: business.phone,
sector: business.sector || business.area || '',
total_hubs: 0,
company_logo: business.company_logo || null,
};
}
grouped[companyName].hubs.push({
id: business.id,
hub_name: business.business_name,
contact_name: business.contact_name,
email: business.email,
phone: business.phone,
address: business.address,
city: business.city,
area: business.area,
rate_group: business.rate_group,
company_logo: business.company_logo,
});
grouped[companyName].total_hubs++;
// Use the first hub's logo if available
if (business.company_logo && !grouped[companyName].company_logo) {
grouped[companyName].company_logo = business.company_logo;
}
});
return Object.values(grouped);
}, [businesses]);
// Operator coverage data
const operatorMetrics = {
totalCoverage: 94,
activeIncidents: 2,
clientSatisfaction: 4.8,
forecastAccuracy: 91
};
const hubCoverageData = [
{ hub: 'San Jose', enterprise: 'Compass', sector: 'Bon Appétit', coverage: 97, incidents: 0, satisfaction: 4.9, partners: 12 },
{ hub: 'San Francisco', enterprise: 'Compass', sector: 'Eurest', coverage: 92, incidents: 1, satisfaction: 4.7, partners: 8 },
{ hub: 'Oakland', enterprise: 'Compass', sector: 'Bon Appétit', coverage: 89, incidents: 2, satisfaction: 4.5, partners: 6 },
{ hub: 'Sacramento', enterprise: 'Compass', sector: 'Chartwells', coverage: 95, incidents: 1, satisfaction: 4.8, partners: 10 },
];
const filteredPartners = partners.filter(p =>
!searchTerm ||
p.partner_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
p.partner_number?.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredBusinesses = consolidatedBusinesses.filter(b =>
!searchTerm ||
b.company_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
b.primary_contact?.toLowerCase().includes(searchTerm.toLowerCase())
);
const totalPartnerCount = filteredPartners.length + filteredBusinesses.length;
const totalHubCount = filteredBusinesses.reduce((sum, b) => sum + b.total_hubs, 0) +
filteredPartners.reduce((sum, p) => sum + (p.sites?.length || 0), 0);
const isLoading = partnersLoading || businessesLoading;
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
<div className="max-w-7xl mx-auto">
<PageHeader
title="Partner Management"
subtitle={`${filteredPartners.length} partners • Clients across all sectors`}
title="Partners & Operators"
subtitle={`${totalPartnerCount} partners • ${totalHubCount} total hubs • ${sectors.length} sectors • ${enterprises.length} enterprises`}
actions={
<Link to={createPageUrl("AddPartner")}>
<Button className="bg-[#0A39DF] hover:bg-[#0A39DF]/90">
<Plus className="w-4 h-4 mr-2" />
Add Partner
</Button>
</Link>
<div className="flex gap-2">
<Link to={createPageUrl("AddPartner")}>
<Button className="bg-[#0A39DF] hover:bg-[#0A39DF]/90">
<Plus className="w-4 h-4 mr-2" />
Add Partner
</Button>
</Link>
<Link to={createPageUrl("Business")}>
<Button variant="outline" className="border-slate-300">
<Building2 className="w-4 h-4 mr-2" />
Business Directory
</Button>
</Link>
</div>
}
/>
{/* Operator Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<Card className="border-slate-200 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
<Target className="w-8 h-8 text-[#0A39DF]" />
<Badge className="bg-emerald-100 text-emerald-700">+5%</Badge>
</div>
<p className="text-sm text-slate-500">Coverage Rate</p>
<p className="text-3xl font-bold text-[#1C323E]">{operatorMetrics.totalCoverage}%</p>
</CardContent>
</Card>
<Card className="border-slate-200 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
<AlertTriangle className="w-8 h-8 text-amber-600" />
<Badge className="bg-green-100 text-green-700">Low</Badge>
</div>
<p className="text-sm text-slate-500">Active Incidents</p>
<p className="text-3xl font-bold text-[#1C323E]">{operatorMetrics.activeIncidents}</p>
</CardContent>
</Card>
<Card className="border-slate-200 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
<CheckCircle2 className="w-8 h-8 text-emerald-600" />
<Badge className="bg-blue-100 text-blue-700">{operatorMetrics.clientSatisfaction}/5.0</Badge>
</div>
<p className="text-sm text-slate-500">Client Satisfaction</p>
<p className="text-3xl font-bold text-[#1C323E]">{operatorMetrics.clientSatisfaction}</p>
</CardContent>
</Card>
<Card className="border-slate-200 shadow-lg">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
<TrendingUp className="w-8 h-8 text-purple-600" />
<Badge className="bg-purple-100 text-purple-700">{operatorMetrics.forecastAccuracy}%</Badge>
</div>
<p className="text-sm text-slate-500">Forecast Accuracy</p>
<p className="text-3xl font-bold text-[#1C323E]">{operatorMetrics.forecastAccuracy}%</p>
</CardContent>
</Card>
</div>
{/* Search */}
<Card className="mb-6 border-slate-200">
<CardContent className="p-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<Input
placeholder="Search partners..."
placeholder="Search partners or businesses..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
@@ -57,90 +208,437 @@ export default function PartnerManagement() {
</CardContent>
</Card>
{/* Partners Grid */}
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-64 bg-slate-100 animate-pulse rounded-xl" />
))}
</div>
) : filteredPartners.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredPartners.map((partner) => (
<Card key={partner.id} className="border-2 border-slate-200 hover:border-[#0A39DF] hover:shadow-xl transition-all">
<CardContent className="p-6">
<div className="flex items-start gap-4 mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-green-700 rounded-xl flex items-center justify-center text-white font-bold">
<Briefcase className="w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="font-bold text-lg text-[#1C323E] mb-1">
{partner.partner_name}
</h3>
<p className="text-sm text-slate-500">{partner.partner_number}</p>
</div>
<Link to={createPageUrl(`EditPartner?id=${partner.id}`)}>
<Button variant="ghost" size="icon" className="text-slate-400 hover:text-[#0A39DF] hover:bg-blue-50">
<Edit className="w-4 h-4" />
</Button>
</Link>
</div>
<div className="space-y-3 mb-4">
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Type</span>
<Badge variant="outline">{partner.partner_type}</Badge>
</div>
{partner.sector_name && (
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Sector</span>
<span className="font-semibold text-sm text-[#1C323E]">{partner.sector_name}</span>
{/* Live Operator Coverage Map */}
<Card className="border-slate-200 shadow-lg mb-8">
<CardHeader className="bg-gradient-to-br from-blue-50 to-white border-b border-slate-100">
<CardTitle className="text-base flex items-center gap-2">
<MapPin className="w-5 h-5 text-[#0A39DF]" />
Live Operator Coverage Map
</CardTitle>
<p className="text-sm text-slate-500 mt-1">Real-time coverage across all hubs and sectors</p>
</CardHeader>
<CardContent className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{hubCoverageData.map((hub, index) => (
<div
key={index}
className="p-6 rounded-xl border-2 border-slate-200 hover:border-[#0A39DF] hover:shadow-lg transition-all bg-white"
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${
hub.coverage >= 95 ? 'bg-emerald-100' :
hub.coverage >= 90 ? 'bg-blue-100' :
'bg-amber-100'
}`}>
<MapPin className={`w-6 h-6 ${
hub.coverage >= 95 ? 'text-emerald-600' :
hub.coverage >= 90 ? 'text-blue-600' :
'text-amber-600'
}`} />
</div>
)}
{partner.sites && partner.sites.length > 0 && (
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600 flex items-center gap-1">
<MapPin className="w-3 h-3" />
Sites
</span>
<span className="font-semibold text-[#1C323E]">{partner.sites.length}</span>
<div>
<h4 className="font-bold text-[#1C323E]">{hub.hub}</h4>
<p className="text-xs text-slate-500">{hub.enterprise} {hub.sector}</p>
</div>
)}
{partner.payment_terms && (
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600 flex items-center gap-1">
<DollarSign className="w-3 h-3" />
Terms
</span>
<span className="font-semibold text-[#1C323E]">{partner.payment_terms}</span>
</div>
)}
</div>
<div className="pt-4 border-t border-slate-200">
<Badge className={partner.is_active ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"}>
{partner.is_active ? "Active" : "Inactive"}
</div>
<Badge className={`${
hub.coverage >= 95 ? 'bg-emerald-100 text-emerald-700' :
hub.coverage >= 90 ? 'bg-blue-100 text-blue-700' :
'bg-amber-100 text-amber-700'
} text-lg px-3 py-1 font-bold`}>
{hub.coverage}%
</Badge>
</div>
</CardContent>
</Card>
))}
<div className="grid grid-cols-3 gap-4 text-sm mb-4">
<div className="p-3 bg-slate-50 rounded-lg">
<p className="text-xs text-slate-500 mb-1">Partners</p>
<p className="font-bold text-[#0A39DF]">{hub.partners}</p>
</div>
<div className="p-3 bg-slate-50 rounded-lg">
<p className="text-xs text-slate-500 mb-1">Incidents</p>
<p className={`font-bold ${hub.incidents > 0 ? 'text-red-600' : 'text-emerald-600'}`}>
{hub.incidents}
</p>
</div>
<div className="p-3 bg-slate-50 rounded-lg">
<p className="text-xs text-slate-500 mb-1">Rating</p>
<p className="font-bold text-amber-600">{hub.satisfaction}/5.0</p>
</div>
</div>
<div className="w-full bg-slate-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${
hub.coverage >= 95 ? 'bg-gradient-to-r from-emerald-500 to-emerald-600' :
hub.coverage >= 90 ? 'bg-gradient-to-r from-blue-500 to-blue-600' :
'bg-gradient-to-r from-amber-500 to-amber-600'
}`}
style={{ width: `${hub.coverage}%` }}
/>
</div>
</div>
))}
</div>
{/* Organizational Hierarchy */}
<div className="mt-8 pt-6 border-t border-slate-200">
<h3 className="text-base font-bold text-[#1C323E] mb-4 flex items-center gap-2">
<Building2 className="w-5 h-5 text-[#0A39DF]" />
Enterprise & Sector Overview
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="p-5 bg-gradient-to-br from-indigo-50 to-white rounded-xl border-2 border-slate-200">
<div className="flex items-center gap-2 mb-3">
<div className="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center">
<Building2 className="w-5 h-5 text-indigo-600" />
</div>
<div>
<p className="text-xs text-slate-500">Enterprises</p>
<p className="text-2xl font-bold text-[#1C323E]">{enterprises.length}</p>
</div>
</div>
<Link to={createPageUrl("EnterpriseManagement")}>
<Button variant="outline" size="sm" className="w-full text-xs border-slate-300">
Manage Enterprises
</Button>
</Link>
</div>
<div className="p-5 bg-gradient-to-br from-purple-50 to-white rounded-xl border-2 border-slate-200">
<div className="flex items-center gap-2 mb-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<MapPin className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-xs text-slate-500">Sectors</p>
<p className="text-2xl font-bold text-[#1C323E]">{sectors.length}</p>
</div>
</div>
<Link to={createPageUrl("SectorManagement")}>
<Button variant="outline" size="sm" className="w-full text-xs border-slate-300">
Manage Sectors
</Button>
</Link>
</div>
<div className="p-5 bg-gradient-to-br from-green-50 to-white rounded-xl border-2 border-slate-200">
<div className="flex items-center gap-2 mb-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<Briefcase className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-xs text-slate-500">Partners</p>
<p className="text-2xl font-bold text-[#1C323E]">{totalPartnerCount}</p>
</div>
</div>
<Button
variant="outline"
size="sm"
className="w-full text-xs border-slate-300"
onClick={() => {
const partnersSection = document.getElementById('partners-section');
partnersSection?.scrollIntoView({ behavior: 'smooth' });
}}
>
View Partners
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Partners Grid/List */}
<div id="partners-section">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-[#1C323E] flex items-center gap-2">
<Briefcase className="w-6 h-6 text-[#0A39DF]" />
Partner Directory
</h2>
<div className="flex items-center gap-2">
<Button
variant={viewMode === "grid" ? "default" : "outline"}
size="icon"
onClick={() => setViewMode("grid")}
className={viewMode === "grid" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90" : "border-slate-300 text-slate-500 hover:text-[#0A39DF]"}
>
<LayoutGrid className="w-5 h-5" />
</Button>
<Button
variant={viewMode === "list" ? "default" : "outline"}
size="icon"
onClick={() => setViewMode("list")}
className={viewMode === "list" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90" : "border-slate-300 text-slate-500 hover:text-[#0A39DF]"}
>
<List className="w-5 h-5" />
</Button>
</div>
</div>
) : (
<Card className="border-slate-200">
<CardContent className="p-12 text-center">
<Briefcase className="w-16 h-16 mx-auto text-slate-300 mb-4" />
<h3 className="text-xl font-semibold text-slate-700 mb-2">No Partners Found</h3>
<p className="text-slate-500 mb-6">Add your first partner client</p>
<Link to={createPageUrl("AddPartner")}>
<Button className="bg-[#0A39DF] hover:bg-[#0A39DF]/90">
<Plus className="w-4 h-4 mr-2" />
Add First Partner
</Button>
</Link>
</CardContent>
</Card>
)}
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-64 bg-slate-100 animate-pulse rounded-xl" />
))}
</div>
) : (filteredPartners.length > 0 || filteredBusinesses.length > 0) ? (
<>
{/* Grid View */}
{viewMode === "grid" && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Display Businesses from Business Directory */}
{filteredBusinesses.map((business, idx) => (
<Card key={`business-${idx}`} className="border-2 border-slate-200 hover:border-[#0A39DF] hover:shadow-xl transition-all">
<CardContent className="p-6">
<div className="flex items-start gap-4 mb-4">
<div className="w-14 h-14 rounded-xl flex items-center justify-center overflow-hidden bg-white border-2 border-slate-200 flex-shrink-0">
{business.company_logo ? (
<img
src={business.company_logo}
alt={business.company_name}
className="w-full h-full object-contain p-2"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-green-500 to-green-700 rounded-lg flex items-center justify-center text-white font-bold text-xl">
{business.company_name?.charAt(0) || 'B'}
</div>
)}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-xl text-[#1C323E] mb-1 truncate">
{business.company_name}
</h3>
<p className="text-sm text-slate-500">PN-{String(idx + 1000).padStart(4, '0')}</p>
</div>
<Link to={createPageUrl("Business")}>
<Button variant="ghost" size="icon" className="text-slate-400 hover:text-[#0A39DF] hover:bg-blue-50 flex-shrink-0">
<Edit className="w-4 h-4" />
</Button>
</Link>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between py-1">
<span className="text-sm text-slate-600">Type</span>
<span className="font-semibold text-sm text-[#1C323E]">{business.partner_type}</span>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-sm text-slate-600">Sector</span>
<span className="font-semibold text-sm text-[#1C323E]">{business.sector || '—'}</span>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-sm text-slate-600 flex items-center gap-1">
<MapPin className="w-3 h-3" />
Sites
</span>
<span className="font-semibold text-[#1C323E]">{business.total_hubs}</span>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-sm text-slate-600 flex items-center gap-1">
<DollarSign className="w-3 h-3" />
Terms
</span>
<span className="font-semibold text-[#1C323E]">Net 30</span>
</div>
</div>
<div className="pt-3 mt-3 border-t border-slate-200">
<Badge className="bg-green-100 text-green-700">
Active
</Badge>
</div>
</CardContent>
</Card>
))}
{/* Display Traditional Partners */}
{filteredPartners.map((partner) => (
<Card key={partner.id} className="border-2 border-slate-200 hover:border-[#0A39DF] hover:shadow-xl transition-all">
<CardContent className="p-6">
<div className="flex items-start gap-4 mb-4">
<div className="w-14 h-14 bg-gradient-to-br from-green-500 to-green-700 rounded-xl flex items-center justify-center text-white flex-shrink-0">
<Briefcase className="w-7 h-7" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-xl text-[#1C323E] mb-1 truncate">
{partner.partner_name}
</h3>
<p className="text-sm text-slate-500">{partner.partner_number}</p>
</div>
<Link to={createPageUrl(`EditPartner?id=${partner.id}`)}>
<Button variant="ghost" size="icon" className="text-slate-400 hover:text-[#0A39DF] hover:bg-blue-50 flex-shrink-0">
<Edit className="w-4 h-4" />
</Button>
</Link>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between py-1">
<span className="text-sm text-slate-600">Type</span>
<span className="font-semibold text-sm text-[#1C323E]">{partner.partner_type}</span>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-sm text-slate-600">Sector</span>
<span className="font-semibold text-sm text-[#1C323E]">{partner.sector_name || '—'}</span>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-sm text-slate-600 flex items-center gap-1">
<MapPin className="w-3 h-3" />
Sites
</span>
<span className="font-semibold text-[#1C323E]">{partner.sites?.length || 0}</span>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-sm text-slate-600 flex items-center gap-1">
<DollarSign className="w-3 h-3" />
Terms
</span>
<span className="font-semibold text-[#1C323E]">{partner.payment_terms || 'Net 30'}</span>
</div>
</div>
<div className="pt-3 mt-3 border-t border-slate-200">
<Badge className={partner.is_active ? "bg-green-100 text-green-700" : "bg-slate-100 text-slate-700"}>
{partner.is_active ? "Active" : "Inactive"}
</Badge>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* List View */}
{viewMode === "list" && (
<Card className="border-slate-200 shadow-lg">
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 border-b-2 border-slate-200">
<tr>
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Partner Name</th>
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Type</th>
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Sector</th>
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Hubs/Sites</th>
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Contact</th>
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Status</th>
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Actions</th>
</tr>
</thead>
<tbody>
{/* Display Businesses from Business Directory */}
{filteredBusinesses.map((business, idx) => (
<tr key={`business-${idx}`} className="border-b border-slate-100 hover:bg-slate-50 transition-colors">
<td className="py-4 px-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg flex items-center justify-center overflow-hidden bg-white border-2 border-slate-200 flex-shrink-0">
{business.company_logo ? (
<img
src={business.company_logo}
alt={business.company_name}
className="w-full h-full object-contain p-1"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-blue-500 to-blue-700 rounded-lg flex items-center justify-center text-white font-bold text-sm">
{business.company_name?.charAt(0) || 'B'}
</div>
)}
</div>
<div>
<p className="font-semibold text-[#1C323E]">{business.company_name}</p>
<Badge variant="outline" className="text-xs mt-1">From Business Directory</Badge>
</div>
</div>
</td>
<td className="py-4 px-4 text-sm text-slate-700">{business.partner_type}</td>
<td className="py-4 px-4 text-sm text-slate-700">{business.sector || '—'}</td>
<td className="py-4 px-4 text-center">
<Badge className="bg-blue-100 text-blue-700 font-semibold">
{business.total_hubs}
</Badge>
</td>
<td className="py-4 px-4 text-sm text-slate-700">{business.primary_contact || '—'}</td>
<td className="py-4 px-4 text-center">
<Badge className="bg-green-100 text-green-700">Active</Badge>
</td>
<td className="py-4 px-4 text-center">
<Link to={createPageUrl("Business")}>
<Button variant="outline" size="sm">
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
</Link>
</td>
</tr>
))}
{/* Display Traditional Partners */}
{filteredPartners.map((partner) => (
<tr key={partner.id} className="border-b border-slate-100 hover:bg-slate-50 transition-colors">
<td className="py-4 px-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-green-500 to-green-700 rounded-lg flex items-center justify-center text-white flex-shrink-0">
<Briefcase className="w-5 h-5" />
</div>
<div>
<p className="font-semibold text-[#1C323E]">{partner.partner_name}</p>
<p className="text-xs text-slate-500">{partner.partner_number}</p>
</div>
</div>
</td>
<td className="py-4 px-4 text-sm text-slate-700">{partner.partner_type}</td>
<td className="py-4 px-4 text-sm text-slate-700">{partner.sector_name || '—'}</td>
<td className="py-4 px-4 text-center">
<Badge className="bg-purple-100 text-purple-700 font-semibold">
{partner.sites?.length || 0}
</Badge>
</td>
<td className="py-4 px-4 text-sm text-slate-700">{partner.primary_contact_name || '—'}</td>
<td className="py-4 px-4 text-center">
<Badge className={partner.is_active ? "bg-green-100 text-green-700" : "bg-slate-100 text-slate-700"}>
{partner.is_active ? "Active" : "Inactive"}
</Badge>
</td>
<td className="py-4 px-4 text-center">
<Link to={createPageUrl(`EditPartner?id=${partner.id}`)}>
<Button variant="outline" size="sm">
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)}
</>
) : (
<Card className="border-slate-200">
<CardContent className="p-12 text-center">
<Briefcase className="w-16 h-16 mx-auto text-slate-300 mb-4" />
<h3 className="text-xl font-semibold text-slate-700 mb-2">No Partners Found</h3>
<p className="text-slate-500 mb-6">Add your first partner client</p>
<Link to={createPageUrl("AddPartner")}>
<Button className="bg-[#0A39DF] hover:bg-[#0A39DF]/90">
<Plus className="w-4 h-4 mr-2" />
Add First Partner
</Button>
</Link>
</CardContent>
</Card>
)}
</div>
</div>
</div>
);

File diff suppressed because it is too large Load Diff

View File

@@ -63,6 +63,18 @@ const MARKET_AVERAGES = {
const COLORS = ['#0A39DF', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#ef4444', '#3b82f6'];
// Safe date formatter
const safeFormatDate = (dateString, formatStr) => {
if (!dateString) return "N/A";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return "N/A";
return format(date, formatStr);
} catch {
return "N/A";
}
};
export default function Reports() {
const [greeting, setGreeting] = useState("");
const [reviewVendor, setReviewVendor] = useState(null);
@@ -649,9 +661,16 @@ export default function Reports() {
// Monthly spending trend
const monthlySpendMap = filteredEvents.reduce((acc, event) => {
const monthYear = format(new Date(event.date), 'MMM yyyy');
if (!acc[monthYear]) acc[monthYear] = 0;
acc[monthYear] += event.total || 0;
if (!event.date) return acc;
try {
const date = new Date(event.date);
if (isNaN(date.getTime())) return acc; // Skip invalid dates
const monthYear = format(date, 'MMM yyyy');
if (!acc[monthYear]) acc[monthYear] = 0;
acc[monthYear] += event.total || 0;
} catch {
// Skip invalid dates
}
return acc;
}, {});
@@ -992,7 +1011,7 @@ export default function Reports() {
<div>
<h1 className="text-2xl font-bold text-slate-900">{greeting}</h1>
<p className="text-sm text-slate-500 flex items-center gap-2">
Data as of {format(new Date(), 'MMM dd, yyyy, h:mm a (z)')}
Data as of {safeFormatDate(new Date(), 'MMM dd, yyyy, h:mm a')}
</p>
</div>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Plus, Search, Filter, Download, LayoutGrid, List, Eye, Edit, Trash2, MoreHorizontal, Users, UserPlus, RefreshCw, Copy, Calendar as CalendarIcon, ArrowUpDown, Check, Bell, Send, FileText } from "lucide-react";
import { Plus, Search, Filter, Download, LayoutGrid, List, Eye, Edit, Trash2, MoreHorizontal, Users, UserPlus, RefreshCw, Copy, Calendar as CalendarIcon, ArrowUpDown, Check, Bell, Send, FileText, Zap } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
@@ -28,6 +29,11 @@ import { createPageUrl } from "@/utils";
import EventAssignmentModal from "../components/events/EventAssignmentModal";
const getStatusColor = (order) => {
// Check for Rapid Request first
if (order.is_rapid_request) {
return "bg-red-500 text-white";
}
if (!order.shifts_data || order.shifts_data.length === 0) {
return "bg-orange-500 text-white";
}
@@ -53,6 +59,8 @@ const getStatusColor = (order) => {
};
const getStatusText = (order) => {
if (order.is_rapid_request) return "Rapid Request";
if (!order.shifts_data || order.shifts_data.length === 0) return "Pending";
let totalNeeded = 0;
@@ -299,7 +307,18 @@ Legendary Event Staffing Team
const matchesSearch = order.event_name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.client_business?.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.manager?.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = filters.status === 'all' || getStatusText(order) === filters.status;
// Updated status filtering to include "Rapid Request" and "Partially Filled"
let matchesStatus = filters.status === 'all';
if (!matchesStatus) {
const statusText = getStatusText(order);
if (filters.status === 'Rapid Request') {
matchesStatus = order.is_rapid_request;
} else {
matchesStatus = statusText === filters.status;
}
}
const matchesHub = filters.hub === 'all' || order.hub_location === filters.hub;
const matchesDate = !selectedDate || order.event_date === format(selectedDate, 'yyyy-MM-dd');
return matchesSearch && matchesStatus && matchesHub && matchesDate;
@@ -342,9 +361,10 @@ Legendary Event Staffing Team
}, 0);
}, 0);
const rapidRequestToday = todaysOrders.filter(o => o.is_rapid_request).length;
const pendingToday = todaysOrders.filter(o => getStatusText(o) === 'Pending').length;
const partiallyFilledToday = todaysOrders.filter(o => getStatusText(o) === 'Partially Filled').length;
const fullyStaffedToday = todaysOrders.filter(o => getStatusText(o) === 'Fully Staffed').length;
const completedToday = todaysOrders.filter(o => o.status === 'completed').length;
return (
<div className="p-6 md:p-8 space-y-6 bg-slate-50 min-h-screen">
@@ -366,7 +386,7 @@ Legendary Event Staffing Team
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
<Card className="bg-gradient-to-br from-blue-500 to-blue-600 border-0 text-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
@@ -379,6 +399,20 @@ Legendary Event Staffing Team
</CardContent>
</Card>
<Card className="bg-white border-slate-200 bg-gradient-to-br from-red-50 to-white">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500 mb-2">Rapid Request</p>
<p className="text-3xl font-bold text-red-600">{rapidRequestToday}</p>
</div>
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
<Zap className="w-6 h-6 text-red-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white border-slate-200">
<CardContent className="p-6">
<div className="flex items-center justify-between">
@@ -397,11 +431,11 @@ Legendary Event Staffing Team
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500 mb-2">Fully Staffed</p>
<p className="text-3xl font-bold text-green-600">{fullyStaffedToday}</p>
<p className="text-sm text-slate-500 mb-2">Partially Filled</p>
<p className="text-3xl font-bold text-blue-600">{partiallyFilledToday}</p>
</div>
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-green-600" />
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600" />
</div>
</div>
</CardContent>
@@ -411,11 +445,11 @@ Legendary Event Staffing Team
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500 mb-2">Completed</p>
<p className="text-3xl font-bold text-slate-900">{completedToday}</p>
<p className="text-sm text-slate-500 mb-2">Fully Staffed</p>
<p className="text-3xl font-bold text-green-600">{fullyStaffedToday}</p>
</div>
<div className="w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center">
<Check className="w-6 h-6 text-slate-700" />
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<Check className="w-6 h-6 text-green-600" />
</div>
</div>
</CardContent>
@@ -467,6 +501,12 @@ Legendary Event Staffing Team
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="Rapid Request">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-red-600" />
Rapid Request
</div>
</SelectItem>
<SelectItem value="Pending">Pending</SelectItem>
<SelectItem value="Partially Filled">Partially Filled</SelectItem>
<SelectItem value="Fully Staffed">Fully Staffed</SelectItem>
@@ -573,9 +613,16 @@ Legendary Event Staffing Team
</div>
</td>
<td className="px-4 py-3">
<Badge className={`${getStatusColor(order)} rounded-full px-3 text-xs`}>
{statusText}
</Badge>
<div className="flex items-center gap-2">
<Badge className={`${getStatusColor(order)} rounded-full px-3 text-xs`}>
{statusText}
</Badge>
{order.include_backup && order.backup_staff_count > 0 && (
<Badge className="bg-green-100 text-green-700 text-xs">
🛡 {order.backup_staff_count}
</Badge>
)}
</div>
</td>
<td className="px-4 py-3 text-sm text-slate-600">
{order.event_date ? format(new Date(order.event_date), 'MM/dd/yy') : 'N/A'}
@@ -733,9 +780,16 @@ Legendary Event Staffing Team
{order.client_business}
</p>
</div>
<Badge className={`${getStatusColor(order)} rounded-full px-3`}>
{statusText}
</Badge>
<div className="flex flex-col gap-2">
<Badge className={`${getStatusColor(order)} rounded-full px-3`}>
{statusText}
</Badge>
{order.include_backup && order.backup_staff_count > 0 && (
<Badge className="bg-green-100 text-green-700">
🛡 {order.backup_staff_count} Backup
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-2 text-slate-700">
@@ -806,4 +860,4 @@ Legendary Event Staffing Team
/>
</div>
);
}
}

View File

@@ -1,3 +1,4 @@
import React from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
@@ -7,6 +8,18 @@ import { Button } from "@/components/ui/button";
import { Calendar, MapPin, Clock, DollarSign, CheckCircle2, AlertCircle } from "lucide-react";
import { format } from "date-fns";
// Safe date formatter
const safeFormatDate = (dateString, formatStr) => {
if (!dateString) return "Date TBD";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return "Date TBD";
return format(date, formatStr);
} catch {
return "Date TBD";
}
};
export default function WorkforceShifts() {
const { data: user } = useQuery({
queryKey: ['current-user'],
@@ -85,7 +98,7 @@ export default function WorkforceShifts() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm text-slate-600">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
{shift.date ? format(new Date(shift.date), 'PPP') : 'Date TBD'}
{safeFormatDate(shift.date, 'PPP')}
</div>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
@@ -121,4 +134,4 @@ export default function WorkforceShifts() {
</div>
</div>
);
}
}