feat(Makefile): install frontend dependencies on dev command
feat(Makefile): patch Layout.jsx queryKey for local development feat(frontend-web): mock base44 client for local development with role switching feat(frontend-web): add event assignment modal with conflict detection and bulk assign feat(frontend-web): add client dashboard with key metrics and quick actions feat(frontend-web): add layout component with role-based navigation feat(frontend-web): update various pages to use "@/components" alias feat(frontend-web): update create event page with ai assistant toggle feat(frontend-web): update dashboard page with new components feat(frontend-web): update events page with quick assign popover feat(frontend-web): update invite vendor page with hover card feat(frontend-web): update messages page with conversation list and message thread feat(frontend-web): update operator dashboard page with new components feat(frontend-web): update partner management page with new components feat(frontend-web): update permissions page with new components feat(frontend-web): update procurement dashboard page with new components feat(frontend-web): update smart vendor onboarding page with new components feat(frontend-web): update staff directory page with new components feat(frontend-web): update teams page with new components feat(frontend-web): update user management page with new components feat(frontend-web): update vendor compliance page with new components feat(frontend-web): update main.jsx to include react query provider feat: add vendor marketplace page feat: add global import fix to prepare-export script feat: add patch-layout-query-key script to fix query key feat: update patch-base44-client script to use a more robust method
This commit is contained in:
4
Makefile
4
Makefile
@@ -16,6 +16,8 @@ install:
|
|||||||
|
|
||||||
# Starts the local development server.
|
# Starts the local development server.
|
||||||
dev:
|
dev:
|
||||||
|
@echo "--> Ensuring web frontend dependencies are installed..."
|
||||||
|
@cd frontend-web && npm install
|
||||||
@echo "--> Starting web frontend development server on http://localhost:5173 ..."
|
@echo "--> Starting web frontend development server on http://localhost:5173 ..."
|
||||||
@cd frontend-web && npm run dev
|
@cd frontend-web && npm run dev
|
||||||
|
|
||||||
@@ -41,6 +43,8 @@ integrate-export:
|
|||||||
@cp ../krow-workforce-export-latest/index.html ./frontend-web/index.html
|
@cp ../krow-workforce-export-latest/index.html ./frontend-web/index.html
|
||||||
@echo " - Patching base44Client.js for local development..."
|
@echo " - Patching base44Client.js for local development..."
|
||||||
@node scripts/patch-base44-client.js
|
@node scripts/patch-base44-client.js
|
||||||
|
@echo " - Patching queryKey in Layout.jsx for local development..."
|
||||||
|
@node scripts/patch-layout-query-key.js
|
||||||
@echo "--> Integration complete. Next step: 'make prepare-export'."
|
@echo "--> Integration complete. Next step: 'make prepare-export'."
|
||||||
|
|
||||||
# Applies all necessary patches to a fresh Base44 export to run it locally.
|
# Applies all necessary patches to a fresh Base44 export to run it locally.
|
||||||
|
|||||||
@@ -2,15 +2,75 @@
|
|||||||
|
|
||||||
// --- MIGRATION MOCK ---
|
// --- MIGRATION MOCK ---
|
||||||
// This mock completely disables the Base44 SDK to allow for local development.
|
// This mock completely disables the Base44 SDK to allow for local development.
|
||||||
|
// It also simulates user roles for the RoleSwitcher component.
|
||||||
|
|
||||||
|
const MOCK_USER_KEY = 'krow_mock_user_role';
|
||||||
|
|
||||||
|
// Default mock user data
|
||||||
|
const DEFAULT_MOCK_USER = {
|
||||||
|
id: "mock-user-123",
|
||||||
|
email: "dev@example.com",
|
||||||
|
full_name: "Dev User",
|
||||||
|
// 'role' is the Base44 default, 'user_role' is our custom field
|
||||||
|
role: "admin",
|
||||||
|
user_role: "admin", // Default role for testing
|
||||||
|
profile_picture: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to get the current mock user state
|
||||||
|
const getMockUser = () => {
|
||||||
|
try {
|
||||||
|
const storedRole = localStorage.getItem(MOCK_USER_KEY);
|
||||||
|
if (storedRole) {
|
||||||
|
return { ...DEFAULT_MOCK_USER, user_role: storedRole, role: storedRole };
|
||||||
|
}
|
||||||
|
// If no role is stored, set the default and return it
|
||||||
|
localStorage.setItem(MOCK_USER_KEY, DEFAULT_MOCK_USER.user_role);
|
||||||
|
return DEFAULT_MOCK_USER;
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage is not available (e.g., in SSR)
|
||||||
|
return DEFAULT_MOCK_USER;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const base44 = {
|
export const base44 = {
|
||||||
auth: {
|
auth: {
|
||||||
me: () => Promise.resolve(null),
|
me: () => Promise.resolve(getMockUser()),
|
||||||
logout: () => {},
|
logout: () => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(MOCK_USER_KEY); // Clear role on logout
|
||||||
|
// Optionally, redirect to login page or reload
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage is not available
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
updateMe: (data) => {
|
||||||
|
try {
|
||||||
|
if (data.user_role) {
|
||||||
|
localStorage.setItem(MOCK_USER_KEY, data.user_role);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage is not available
|
||||||
|
}
|
||||||
|
// Simulate a successful update
|
||||||
|
return Promise.resolve({ ...getMockUser(), ...data });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
entities: {
|
entities: {
|
||||||
ActivityLog: {
|
ActivityLog: {
|
||||||
filter: () => Promise.resolve([]),
|
filter: () => Promise.resolve([]),
|
||||||
},
|
},
|
||||||
|
// Add other entity mocks as needed for the RoleSwitcher to function
|
||||||
|
// For now, the RoleSwitcher only updates the user role, so other entities might not be critical.
|
||||||
},
|
},
|
||||||
|
integrations: {
|
||||||
|
Core: {
|
||||||
|
SendEmail: () => Promise.resolve({ status: "sent" }),
|
||||||
|
UploadFile: () => Promise.resolve({ file_url: "mock-file-url" }),
|
||||||
|
InvokeLLM: () => Promise.resolve({ result: "mock-ai-response" }),
|
||||||
|
// Add other integration mocks if the RoleSwitcher indirectly calls them
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useMemo } from "react";
|
||||||
import { base44 } from "@/api/base44Client";
|
import { base44 } from "@/api/base44Client";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
@@ -10,9 +10,11 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { Calendar, Users, Check, Plus, X, Clock, MapPin, ChevronLeft, ChevronRight } from "lucide-react";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { format } from "date-fns";
|
import { Calendar, Users, Check, Plus, X, Clock, MapPin, ChevronLeft, ChevronRight, AlertTriangle, RefreshCw, Search } from "lucide-react";
|
||||||
|
import { format, parseISO, isWithinInterval } from "date-fns";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
const convertTo12Hour = (time24) => {
|
const convertTo12Hour = (time24) => {
|
||||||
@@ -40,9 +42,32 @@ const avatarColors = [
|
|||||||
'bg-red-500',
|
'bg-red-500',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Helper to check if times overlap
|
||||||
|
const hasTimeConflict = (existingStart, existingEnd, newStart, newEnd, existingDate, newDate) => {
|
||||||
|
// If different dates, no conflict
|
||||||
|
if (existingDate !== newDate) return false;
|
||||||
|
|
||||||
|
if (!existingStart || !existingEnd || !newStart || !newEnd) return false;
|
||||||
|
|
||||||
|
const parseTime = (time) => {
|
||||||
|
const [hours, minutes] = time.split(':').map(Number);
|
||||||
|
return hours * 60 + minutes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingStartMin = parseTime(existingStart);
|
||||||
|
const existingEndMin = parseTime(existingEnd);
|
||||||
|
const newStartMin = parseTime(newStart);
|
||||||
|
const newEndMin = parseTime(newEnd);
|
||||||
|
|
||||||
|
return (newStartMin < existingEndMin && newEndMin > existingStartMin);
|
||||||
|
};
|
||||||
|
|
||||||
export default function EventAssignmentModal({ open, onClose, order, onUpdate }) {
|
export default function EventAssignmentModal({ open, onClose, order, onUpdate }) {
|
||||||
const [selectedShiftIndex, setSelectedShiftIndex] = useState(0);
|
const [selectedShiftIndex, setSelectedShiftIndex] = useState(0);
|
||||||
const [selectedRoleIndex, setSelectedRoleIndex] = useState(0);
|
const [selectedRoleIndex, setSelectedRoleIndex] = useState(0);
|
||||||
|
const [selectedStaffIds, setSelectedStaffIds] = useState([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [swapMode, setSwapMode] = useState(null); // { employeeId, assignmentIndex }
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -52,13 +77,20 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: allOrders = [] } = useQuery({
|
||||||
|
queryKey: ['orders-for-conflict-check'],
|
||||||
|
queryFn: () => base44.entities.Order.list(),
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
const updateOrderMutation = useMutation({
|
const updateOrderMutation = useMutation({
|
||||||
mutationFn: (updatedOrder) => base44.entities.Order.update(order.id, updatedOrder),
|
mutationFn: (updatedOrder) => base44.entities.Order.update(order.id, updatedOrder),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['orders'] });
|
queryClient.invalidateQueries({ queryKey: ['orders'] });
|
||||||
if (onUpdate) onUpdate();
|
if (onUpdate) onUpdate();
|
||||||
|
setSelectedStaffIds([]);
|
||||||
toast({
|
toast({
|
||||||
title: "Staff assigned successfully",
|
title: "✅ Staff assigned successfully",
|
||||||
description: "The order has been updated with new assignments.",
|
description: "The order has been updated with new assignments.",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -73,6 +105,152 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
|
|
||||||
if (!currentRole) return null;
|
if (!currentRole) return null;
|
||||||
|
|
||||||
|
// Check for conflicts
|
||||||
|
const getStaffConflicts = (staffId) => {
|
||||||
|
const conflicts = [];
|
||||||
|
|
||||||
|
allOrders.forEach(existingOrder => {
|
||||||
|
if (existingOrder.id === order.id) return; // Skip current order
|
||||||
|
if (!existingOrder.shifts_data) return;
|
||||||
|
|
||||||
|
existingOrder.shifts_data.forEach(shift => {
|
||||||
|
shift.roles.forEach(role => {
|
||||||
|
if (!role.assignments) return;
|
||||||
|
|
||||||
|
role.assignments.forEach(assignment => {
|
||||||
|
if (assignment.employee_id === staffId) {
|
||||||
|
const hasConflict = hasTimeConflict(
|
||||||
|
role.start_time,
|
||||||
|
role.end_time,
|
||||||
|
currentRole.start_time,
|
||||||
|
currentRole.end_time,
|
||||||
|
existingOrder.event_date,
|
||||||
|
order.event_date
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasConflict) {
|
||||||
|
conflicts.push({
|
||||||
|
orderName: existingOrder.event_name,
|
||||||
|
role: role.service,
|
||||||
|
time: `${convertTo12Hour(role.start_time)} - ${convertTo12Hour(role.end_time)}`,
|
||||||
|
date: existingOrder.event_date
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return conflicts;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Role-based filtering
|
||||||
|
const roleKeywords = {
|
||||||
|
'bartender': ['bartender', 'bar'],
|
||||||
|
'cook': ['cook', 'chef', 'kitchen'],
|
||||||
|
'server': ['server', 'waiter', 'waitress'],
|
||||||
|
'cashier': ['cashier', 'register'],
|
||||||
|
'host': ['host', 'hostess', 'greeter'],
|
||||||
|
'busser': ['busser', 'bus'],
|
||||||
|
'dishwasher': ['dishwasher', 'dish'],
|
||||||
|
'manager': ['manager', 'supervisor'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoleCategory = (roleName) => {
|
||||||
|
const lowerRole = roleName.toLowerCase();
|
||||||
|
for (const [category, keywords] of Object.entries(roleKeywords)) {
|
||||||
|
if (keywords.some(keyword => lowerRole.includes(keyword))) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchesRole = (staffPosition, requiredRole) => {
|
||||||
|
const staffLower = (staffPosition || '').toLowerCase();
|
||||||
|
const requiredLower = (requiredRole || '').toLowerCase();
|
||||||
|
|
||||||
|
// Direct match
|
||||||
|
if (staffLower.includes(requiredLower) || requiredLower.includes(staffLower)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category match
|
||||||
|
const staffCategory = getRoleCategory(staffPosition || '');
|
||||||
|
const requiredCategory = getRoleCategory(requiredRole);
|
||||||
|
|
||||||
|
return staffCategory && requiredCategory && staffCategory === requiredCategory;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkAssign = () => {
|
||||||
|
if (selectedStaffIds.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "No staff selected",
|
||||||
|
description: "Please select staff members to assign.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedOrder = { ...order };
|
||||||
|
const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || [];
|
||||||
|
|
||||||
|
// Check for conflicts
|
||||||
|
const conflictingStaff = [];
|
||||||
|
selectedStaffIds.forEach(staffId => {
|
||||||
|
const conflicts = getStaffConflicts(staffId);
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
const staff = allStaff.find(s => s.id === staffId);
|
||||||
|
conflictingStaff.push(staff?.employee_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conflictingStaff.length > 0) {
|
||||||
|
toast({
|
||||||
|
title: "⚠️ Time Conflict Detected",
|
||||||
|
description: `${conflictingStaff.join(', ')} ${conflictingStaff.length === 1 ? 'is' : 'are'} already assigned to overlapping shifts.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const needed = parseInt(currentRole.count) || 0;
|
||||||
|
const currentAssigned = assignments.length;
|
||||||
|
const remaining = needed - currentAssigned;
|
||||||
|
|
||||||
|
if (selectedStaffIds.length > remaining) {
|
||||||
|
toast({
|
||||||
|
title: "Too many selected",
|
||||||
|
description: `Only ${remaining} more staff ${remaining === 1 ? 'is' : 'are'} needed.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all selected staff
|
||||||
|
const newAssignments = selectedStaffIds.map(staffId => {
|
||||||
|
const staff = allStaff.find(s => s.id === staffId);
|
||||||
|
return {
|
||||||
|
employee_id: staff.id,
|
||||||
|
employee_name: staff.employee_name,
|
||||||
|
position: currentRole.service,
|
||||||
|
shift_date: order.event_date,
|
||||||
|
shift_start: currentRole.start_time,
|
||||||
|
shift_end: currentRole.end_time,
|
||||||
|
location: currentShift.address || order.event_address,
|
||||||
|
hub_location: order.hub_location,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments = [
|
||||||
|
...assignments,
|
||||||
|
...newAssignments
|
||||||
|
];
|
||||||
|
|
||||||
|
updateOrderMutation.mutate(updatedOrder);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAssignStaff = (staffMember) => {
|
const handleAssignStaff = (staffMember) => {
|
||||||
const updatedOrder = { ...order };
|
const updatedOrder = { ...order };
|
||||||
const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || [];
|
const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || [];
|
||||||
@@ -87,6 +265,17 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for conflicts
|
||||||
|
const conflicts = getStaffConflicts(staffMember.id);
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
toast({
|
||||||
|
title: "⚠️ Time Conflict",
|
||||||
|
description: `${staffMember.employee_name} is already assigned to ${conflicts[0].orderName} at ${conflicts[0].time}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add new assignment
|
// Add new assignment
|
||||||
const newAssignment = {
|
const newAssignment = {
|
||||||
employee_id: staffMember.id,
|
employee_id: staffMember.id,
|
||||||
@@ -117,14 +306,63 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
updateOrderMutation.mutate(updatedOrder);
|
updateOrderMutation.mutate(updatedOrder);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSwapStaff = (newStaffMember) => {
|
||||||
|
if (!swapMode) return;
|
||||||
|
|
||||||
|
const updatedOrder = { ...order };
|
||||||
|
const assignments = updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments || [];
|
||||||
|
|
||||||
|
// Check for conflicts
|
||||||
|
const conflicts = getStaffConflicts(newStaffMember.id);
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
toast({
|
||||||
|
title: "⚠️ Time Conflict",
|
||||||
|
description: `${newStaffMember.employee_name} is already assigned to ${conflicts[0].orderName} at ${conflicts[0].time}`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the assignment
|
||||||
|
assignments[swapMode.assignmentIndex] = {
|
||||||
|
employee_id: newStaffMember.id,
|
||||||
|
employee_name: newStaffMember.employee_name,
|
||||||
|
position: currentRole.service,
|
||||||
|
shift_date: order.event_date,
|
||||||
|
shift_start: currentRole.start_time,
|
||||||
|
shift_end: currentRole.end_time,
|
||||||
|
location: currentShift.address || order.event_address,
|
||||||
|
hub_location: order.hub_location,
|
||||||
|
};
|
||||||
|
|
||||||
|
updatedOrder.shifts_data[selectedShiftIndex].roles[selectedRoleIndex].assignments = assignments;
|
||||||
|
updateOrderMutation.mutate(updatedOrder);
|
||||||
|
setSwapMode(null);
|
||||||
|
};
|
||||||
|
|
||||||
const assignments = currentRole.assignments || [];
|
const assignments = currentRole.assignments || [];
|
||||||
const needed = parseInt(currentRole.count) || 0;
|
const needed = parseInt(currentRole.count) || 0;
|
||||||
const assigned = assignments.length;
|
const assigned = assignments.length;
|
||||||
const isFullyStaffed = assigned >= needed;
|
const isFullyStaffed = assigned >= needed;
|
||||||
|
|
||||||
// Filter available staff (not already assigned to this role)
|
// Filter staff by role and exclude already assigned
|
||||||
const assignedIds = new Set(assignments.map(a => a.employee_id));
|
const assignedIds = new Set(assignments.map(a => a.employee_id));
|
||||||
const availableStaff = allStaff.filter(s => !assignedIds.has(s.id));
|
const roleFilteredStaff = allStaff.filter(s =>
|
||||||
|
matchesRole(s.position, currentRole.service) ||
|
||||||
|
matchesRole(s.position_2, currentRole.service)
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableStaff = roleFilteredStaff
|
||||||
|
.filter(s => !assignedIds.has(s.id))
|
||||||
|
.filter(s => {
|
||||||
|
if (!searchTerm) return true;
|
||||||
|
const lowerSearch = searchTerm.toLowerCase();
|
||||||
|
return (
|
||||||
|
s.employee_name?.toLowerCase().includes(lowerSearch) ||
|
||||||
|
s.position?.toLowerCase().includes(lowerSearch) ||
|
||||||
|
s.position_2?.toLowerCase().includes(lowerSearch)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate total assignments across all roles in this shift
|
// Calculate total assignments across all roles in this shift
|
||||||
let totalNeeded = 0;
|
let totalNeeded = 0;
|
||||||
@@ -134,9 +372,27 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
totalAssigned += role.assignments?.length || 0;
|
totalAssigned += role.assignments?.length || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleStaffSelection = (staffId) => {
|
||||||
|
setSelectedStaffIds(prev =>
|
||||||
|
prev.includes(staffId)
|
||||||
|
? prev.filter(id => id !== staffId)
|
||||||
|
: [...prev, staffId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectAll = () => {
|
||||||
|
const remaining = needed - assigned;
|
||||||
|
const selectableStaff = availableStaff.slice(0, remaining);
|
||||||
|
setSelectedStaffIds(selectableStaff.map(s => s.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deselectAll = () => {
|
||||||
|
setSelectedStaffIds([]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onClose}>
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
<DialogHeader className="border-b pb-4">
|
<DialogHeader className="border-b pb-4">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -147,12 +403,22 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
className={`${
|
className={`${
|
||||||
isFullyStaffed
|
totalAssigned >= totalNeeded
|
||||||
? 'bg-green-100 text-green-800 border-green-200'
|
? 'bg-emerald-500 text-white border-0 animate-pulse'
|
||||||
: 'bg-orange-100 text-orange-800 border-orange-200'
|
: 'bg-orange-500 text-white border-0'
|
||||||
} border font-semibold`}
|
} font-semibold px-3 py-1 shadow-md`}
|
||||||
>
|
>
|
||||||
{isFullyStaffed ? 'Fully Staffed' : 'Needs Staff'}
|
{totalAssigned >= totalNeeded ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-3 h-3 mr-1" />
|
||||||
|
Fully Staffed
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<AlertTriangle className="w-3 h-3 mr-1" />
|
||||||
|
Needs Staff
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -181,21 +447,21 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
</div>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-sm font-bold border-2"
|
className={`text-sm font-bold border-2 ${
|
||||||
style={{
|
totalAssigned >= totalNeeded
|
||||||
borderColor: totalAssigned >= totalNeeded ? '#10b981' : '#f97316',
|
? 'border-emerald-500 text-emerald-700 bg-emerald-50'
|
||||||
color: totalAssigned >= totalNeeded ? '#10b981' : '#f97316'
|
: 'border-orange-500 text-orange-700 bg-orange-50'
|
||||||
}}
|
}`}
|
||||||
>
|
>
|
||||||
{totalAssigned} / {totalNeeded}
|
{totalAssigned} / {totalNeeded}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalAssigned >= totalNeeded && (
|
{totalAssigned >= totalNeeded && (
|
||||||
<div className="mb-3 p-3 rounded-lg bg-green-50 border border-green-200">
|
<div className="mb-3 p-3 rounded-lg bg-gradient-to-r from-emerald-50 to-green-50 border-2 border-emerald-200">
|
||||||
<div className="flex items-center gap-2 text-green-700 text-sm font-medium">
|
<div className="flex items-center gap-2 text-emerald-700 text-sm font-semibold">
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-5 h-5" />
|
||||||
Fully staffed
|
✨ Fully staffed - All positions filled!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -207,7 +473,11 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
<label className="text-sm font-semibold text-slate-700 mb-2 block">Select Position:</label>
|
<label className="text-sm font-semibold text-slate-700 mb-2 block">Select Position:</label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedRoleIndex.toString()}
|
value={selectedRoleIndex.toString()}
|
||||||
onValueChange={(value) => setSelectedRoleIndex(parseInt(value))}
|
onValueChange={(value) => {
|
||||||
|
setSelectedRoleIndex(parseInt(value));
|
||||||
|
setSelectedStaffIds([]);
|
||||||
|
setSwapMode(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -216,13 +486,17 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
{currentShift.roles.map((role, idx) => {
|
{currentShift.roles.map((role, idx) => {
|
||||||
const roleAssigned = role.assignments?.length || 0;
|
const roleAssigned = role.assignments?.length || 0;
|
||||||
const roleNeeded = parseInt(role.count) || 0;
|
const roleNeeded = parseInt(role.count) || 0;
|
||||||
|
const roleFilled = roleAssigned >= roleNeeded;
|
||||||
return (
|
return (
|
||||||
<SelectItem key={idx} value={idx.toString()}>
|
<SelectItem key={idx} value={idx.toString()}>
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4 w-full">
|
||||||
<span>{role.service}</span>
|
<span className="font-medium">{role.service}</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant={roleAssigned >= roleNeeded ? "default" : "secondary"}
|
className={`${
|
||||||
className="text-xs"
|
roleFilled
|
||||||
|
? 'bg-emerald-100 text-emerald-700'
|
||||||
|
: 'bg-orange-100 text-orange-700'
|
||||||
|
} text-xs font-bold ml-2`}
|
||||||
>
|
>
|
||||||
{roleAssigned}/{roleNeeded}
|
{roleAssigned}/{roleNeeded}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -236,23 +510,23 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Current Position Details */}
|
{/* Current Position Details */}
|
||||||
<div className="mb-6 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
<div className="mb-6 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border-2 border-blue-200">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h4 className="font-semibold text-slate-900">{currentRole.service}</h4>
|
<h4 className="font-bold text-slate-900 text-lg">{currentRole.service}</h4>
|
||||||
<Badge
|
<Badge
|
||||||
className={`${
|
className={`${
|
||||||
assigned >= needed
|
assigned >= needed
|
||||||
? 'bg-green-100 text-green-700'
|
? 'bg-emerald-500 text-white'
|
||||||
: 'bg-orange-100 text-orange-700'
|
: 'bg-orange-500 text-white'
|
||||||
} font-semibold`}
|
} font-bold px-3 py-1 text-base`}
|
||||||
>
|
>
|
||||||
{assigned}/{needed}
|
{assigned}/{needed}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentRole.start_time && (
|
{currentRole.start_time && (
|
||||||
<div className="flex items-center gap-2 text-sm text-slate-600">
|
<div className="flex items-center gap-2 text-sm text-slate-700 font-medium">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4 text-blue-600" />
|
||||||
<span>
|
<span>
|
||||||
{convertTo12Hour(currentRole.start_time)} - {convertTo12Hour(currentRole.end_time)}
|
{convertTo12Hour(currentRole.start_time)} - {convertTo12Hour(currentRole.end_time)}
|
||||||
</span>
|
</span>
|
||||||
@@ -260,108 +534,282 @@ export default function EventAssignmentModal({ open, onClose, order, onUpdate })
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Swap Mode Banner */}
|
||||||
|
{swapMode && (
|
||||||
|
<div className="mb-4 p-4 bg-purple-50 border-2 border-purple-300 rounded-xl">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<RefreshCw className="w-5 h-5 text-purple-600" />
|
||||||
|
<span className="font-semibold text-purple-900">
|
||||||
|
Swap Mode Active - Select replacement for {assignments[swapMode.assignmentIndex]?.employee_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSwapMode(null)}
|
||||||
|
className="text-purple-600 hover:bg-purple-100"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Assigned Staff List */}
|
{/* Assigned Staff List */}
|
||||||
{assignments.length > 0 && (
|
{assignments.length > 0 && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h4 className="text-sm font-semibold text-slate-700 mb-3">ASSIGNED STAFF:</h4>
|
<h4 className="text-sm font-bold text-slate-700 mb-3 uppercase tracking-wide">✅ Assigned Staff:</h4>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{assignments.map((assignment, idx) => (
|
{assignments.map((assignment, idx) => {
|
||||||
<div
|
const conflicts = getStaffConflicts(assignment.employee_id);
|
||||||
key={idx}
|
return (
|
||||||
className="flex items-center justify-between p-3 bg-white rounded-lg border border-slate-200 hover:border-blue-300 transition-colors"
|
<div
|
||||||
>
|
key={idx}
|
||||||
<div className="flex items-center gap-3">
|
className="flex items-center justify-between p-3 bg-white rounded-xl border-2 border-slate-200 hover:border-blue-300 transition-all shadow-sm"
|
||||||
<Avatar className={`w-10 h-10 ${avatarColors[idx % avatarColors.length]}`}>
|
>
|
||||||
<AvatarFallback className="text-white font-bold">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
{getInitials(assignment.employee_name)}
|
<Avatar className={`w-10 h-10 ${avatarColors[idx % avatarColors.length]}`}>
|
||||||
</AvatarFallback>
|
<AvatarFallback className="text-white font-bold">
|
||||||
</Avatar>
|
{getInitials(assignment.employee_name)}
|
||||||
<div>
|
</AvatarFallback>
|
||||||
<p className="font-semibold text-slate-900">{assignment.employee_name}</p>
|
</Avatar>
|
||||||
<p className="text-xs text-slate-500">{currentRole.service}</p>
|
<div className="flex-1">
|
||||||
|
<p className="font-bold text-slate-900">{assignment.employee_name}</p>
|
||||||
|
<p className="text-xs text-slate-500">{currentRole.service}</p>
|
||||||
|
{conflicts.length > 0 && (
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<AlertTriangle className="w-3 h-3 text-red-500" />
|
||||||
|
<span className="text-xs text-red-600 font-medium">Time conflict detected</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSwapMode({ employeeId: assignment.employee_id, assignmentIndex: idx })}
|
||||||
|
className="text-purple-600 hover:bg-purple-50 border-purple-300"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3 h-3 mr-1" />
|
||||||
|
Swap
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleRemoveStaff(assignment.employee_id)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
);
|
||||||
variant="ghost"
|
})}
|
||||||
size="icon"
|
|
||||||
onClick={() => handleRemoveStaff(assignment.employee_id)}
|
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add Staff Section */}
|
{/* Add Staff Section */}
|
||||||
{assigned < needed && (
|
{(assigned < needed || swapMode) && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-semibold text-slate-700 mb-3">ADD STAFF:</h4>
|
<div className="flex items-center justify-between mb-3">
|
||||||
{availableStaff.length > 0 ? (
|
<h4 className="text-sm font-bold text-slate-700 uppercase tracking-wide">
|
||||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
{swapMode ? '🔄 Select Replacement:' : '➕ Add Staff:'}
|
||||||
{availableStaff.map((staff, idx) => (
|
</h4>
|
||||||
<div
|
{!swapMode && availableStaff.length > 0 && (
|
||||||
key={staff.id}
|
<div className="flex gap-2">
|
||||||
className="flex items-center justify-between p-3 bg-white rounded-lg border border-slate-200 hover:border-blue-300 transition-colors"
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={selectAll}
|
||||||
|
disabled={needed - assigned === 0}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
Select All ({Math.min(availableStaff.length, needed - assigned)})
|
||||||
<Avatar className={`w-10 h-10 ${avatarColors[idx % avatarColors.length]}`}>
|
</Button>
|
||||||
<AvatarFallback className="text-white font-bold">
|
{selectedStaffIds.length > 0 && (
|
||||||
{getInitials(staff.employee_name)}
|
<>
|
||||||
</AvatarFallback>
|
<Button
|
||||||
</Avatar>
|
variant="outline"
|
||||||
<div>
|
size="sm"
|
||||||
<p className="font-semibold text-slate-900">{staff.employee_name}</p>
|
onClick={deselectAll}
|
||||||
<p className="text-xs text-slate-500">{staff.position || 'Staff Member'}</p>
|
>
|
||||||
</div>
|
Clear
|
||||||
</div>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleAssignStaff(staff)}
|
onClick={handleBulkAssign}
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
Assign {selectedStaffIds.length}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search staff by name or position..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 mt-2">
|
||||||
|
Showing {availableStaff.length} {currentRole.service.toLowerCase()}(s)
|
||||||
|
{roleFilteredStaff.length !== allStaff.length && (
|
||||||
|
<span className="text-blue-600 font-medium"> (filtered by role)</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{availableStaff.length > 0 ? (
|
||||||
|
<div className="space-y-2 max-h-80 overflow-y-auto pr-2">
|
||||||
|
{availableStaff.map((staff, idx) => {
|
||||||
|
const isSelected = selectedStaffIds.includes(staff.id);
|
||||||
|
const conflicts = getStaffConflicts(staff.id);
|
||||||
|
const hasConflict = conflicts.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={staff.id}
|
||||||
|
className={`flex items-center justify-between p-3 bg-white rounded-xl border-2 transition-all ${
|
||||||
|
hasConflict
|
||||||
|
? 'border-red-200 bg-red-50'
|
||||||
|
: isSelected
|
||||||
|
? 'border-blue-400 bg-blue-50'
|
||||||
|
: 'border-slate-200 hover:border-blue-300'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 mr-1" />
|
<div className="flex items-center gap-3 flex-1">
|
||||||
Assign
|
{!swapMode && (
|
||||||
</Button>
|
<Checkbox
|
||||||
</div>
|
checked={isSelected}
|
||||||
))}
|
onCheckedChange={() => toggleStaffSelection(staff.id)}
|
||||||
|
disabled={hasConflict}
|
||||||
|
className="border-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Avatar className={`w-10 h-10 ${avatarColors[idx % avatarColors.length]}`}>
|
||||||
|
<AvatarFallback className="text-white font-bold">
|
||||||
|
{getInitials(staff.employee_name)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-bold text-slate-900 truncate">{staff.employee_name}</p>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<p className="text-xs text-slate-500">{staff.position || 'Staff Member'}</p>
|
||||||
|
{staff.position_2 && (
|
||||||
|
<Badge variant="outline" className="text-[10px]">{staff.position_2}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{hasConflict && (
|
||||||
|
<div className="flex items-center gap-1 mt-1">
|
||||||
|
<AlertTriangle className="w-3 h-3 text-red-500" />
|
||||||
|
<span className="text-xs text-red-600 font-medium">
|
||||||
|
Conflict: {conflicts[0].orderName} ({conflicts[0].time})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => swapMode ? handleSwapStaff(staff) : handleAssignStaff(staff)}
|
||||||
|
disabled={hasConflict}
|
||||||
|
className={`${
|
||||||
|
swapMode
|
||||||
|
? 'bg-purple-600 hover:bg-purple-700'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
} ${hasConflict ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
{swapMode ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-1" />
|
||||||
|
Swap
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
Assign
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 text-slate-400">
|
<div className="text-center py-12 bg-slate-50 rounded-xl border-2 border-dashed border-slate-200">
|
||||||
<Users className="w-12 h-12 mx-auto mb-2 opacity-30" />
|
<Users className="w-16 h-16 mx-auto mb-3 text-slate-300" />
|
||||||
<p className="text-sm">All available staff have been assigned</p>
|
<p className="font-medium text-slate-600">
|
||||||
|
{searchTerm
|
||||||
|
? 'No staff match your search'
|
||||||
|
: `No available ${currentRole.service.toLowerCase()}s found`
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-400 mt-1">
|
||||||
|
{searchTerm ? 'Try a different search term' : 'All matching staff have been assigned'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t pt-4 flex items-center justify-between">
|
<div className="border-t pt-4 flex items-center justify-between bg-slate-50 -mx-6 px-6 -mb-6 pb-6">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{selectedShiftIndex > 0 && (
|
{selectedShiftIndex > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setSelectedShiftIndex(selectedShiftIndex - 1)}
|
onClick={() => {
|
||||||
|
setSelectedShiftIndex(selectedShiftIndex - 1);
|
||||||
|
setSelectedRoleIndex(0);
|
||||||
|
setSelectedStaffIds([]);
|
||||||
|
setSwapMode(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-4 h-4 mr-1" />
|
<ChevronLeft className="w-4 h-4 mr-1" />
|
||||||
Previous
|
Previous Shift
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{selectedShiftIndex < order.shifts_data.length - 1 && (
|
{selectedShiftIndex < order.shifts_data.length - 1 && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setSelectedShiftIndex(selectedShiftIndex + 1)}
|
onClick={() => {
|
||||||
|
setSelectedShiftIndex(selectedShiftIndex + 1);
|
||||||
|
setSelectedRoleIndex(0);
|
||||||
|
setSelectedStaffIds([]);
|
||||||
|
setSwapMode(null);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Next
|
Next Shift
|
||||||
<ChevronRight className="w-4 h-4 ml-1" />
|
<ChevronRight className="w-4 h-4 ml-1" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={onClose}>
|
<div className="flex gap-2">
|
||||||
Done
|
<Badge
|
||||||
</Button>
|
variant="outline"
|
||||||
|
className={`px-4 py-2 text-base font-bold ${
|
||||||
|
totalAssigned >= totalNeeded
|
||||||
|
? 'bg-emerald-50 border-emerald-500 text-emerald-700'
|
||||||
|
: 'bg-orange-50 border-orange-500 text-orange-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{totalAssigned}/{totalNeeded} Filled
|
||||||
|
</Badge>
|
||||||
|
<Button onClick={onClose} className="bg-blue-600 hover:bg-blue-700">
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from '@/App.jsx'
|
import App from '@/App.jsx'
|
||||||
import '@/index.css'
|
import '@/index.css'
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -13,4 +14,3 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
|||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
Filter
|
Filter
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
calendar: Calendar,
|
calendar: Calendar,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Building2, ArrowLeft, Check } from "lucide-react";
|
import { Building2, ArrowLeft, Check } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export default function AddEnterprise() {
|
export default function AddEnterprise() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Briefcase, ArrowLeft, Check, Plus, X } from "lucide-react";
|
import { Briefcase, ArrowLeft, Check, Plus, X } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export default function AddPartner() {
|
export default function AddPartner() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { MapPin, ArrowLeft, Check } from "lucide-react";
|
import { MapPin, ArrowLeft, Check } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export default function AddSector() {
|
export default function AddSector() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import StaffForm from "../components/staff/StaffForm";
|
import StaffForm from "@/components/staff/StaffForm";
|
||||||
|
|
||||||
export default function AddStaff() {
|
export default function AddStaff() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Plus, Building2, Mail, Phone, MapPin, Search, Eye, Trash2, ChevronDown, ChevronRight } from "lucide-react";
|
import { Plus, Building2, Mail, Phone, MapPin, Search, Eye, Trash2, ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} from "@/components/ui/collapsible";
|
||||||
import CreateBusinessModal from "../components/business/CreateBusinessModal";
|
import CreateBusinessModal from "@/components/business/CreateBusinessModal";
|
||||||
|
|
||||||
export default function Business() {
|
export default function Business() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import { format, addDays } from "date-fns";
|
import { format, addDays } from "date-fns";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import QuickReorderModal from "../components/events/QuickReorderModal";
|
import QuickReorderModal from "@/components/events/QuickReorderModal";
|
||||||
|
|
||||||
export default function ClientOrders() {
|
export default function ClientOrders() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { base44 } from "@/api/base44Client";
|
|||||||
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import EventFormWizard from "../components/events/EventFormWizard";
|
import EventFormWizard from "@/components/events/EventFormWizard";
|
||||||
import AIOrderAssistant from "../components/events/AIOrderAssistant";
|
import AIOrderAssistant from "@/components/events/AIOrderAssistant";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Sparkles, FileText, X } from "lucide-react";
|
import { Sparkles, FileText, X } from "lucide-react";
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { createPageUrl } from "@/utils";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
import { Users, Building2, UserPlus, TrendingUp, MapPin, Calendar, DollarSign, Award, Target, BarChart3, Shield, Leaf } from "lucide-react";
|
import { Users, Building2, UserPlus, TrendingUp, MapPin, Calendar, DollarSign, Award, Target, BarChart3, Shield, Leaf } from "lucide-react";
|
||||||
import StatsCard from "../components/staff/StatsCard";
|
import StatsCard from "@/components/staff/StatsCard";
|
||||||
import EcosystemWheel from "../components/dashboard/EcosystemWheel";
|
import EcosystemWheel from "@/components/dashboard/EcosystemWheel";
|
||||||
import QuickMetrics from "../components/dashboard/QuickMetrics";
|
import QuickMetrics from "@/components/dashboard/QuickMetrics";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Building2, ArrowLeft, Save, Loader2 } from "lucide-react";
|
import { Building2, ArrowLeft, Save, Loader2 } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export default function EditEnterprise() {
|
export default function EditEnterprise() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft, Loader2 } from "lucide-react";
|
import { ArrowLeft, Loader2 } from "lucide-react";
|
||||||
import EventForm from "../components/events/EventForm";
|
import EventForm from "@/components/events/EventForm";
|
||||||
|
|
||||||
export default function EditEvent() {
|
export default function EditEvent() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Briefcase, ArrowLeft, Save, Plus, X, Loader2 } from "lucide-react";
|
import { Briefcase, ArrowLeft, Save, Plus, X, Loader2 } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export default function EditPartner() {
|
export default function EditPartner() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { MapPin, ArrowLeft, Save, Loader2 } from "lucide-react";
|
import { MapPin, ArrowLeft, Save, Loader2 } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export default function EditSector() {
|
export default function EditSector() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft, Loader2 } from "lucide-react";
|
import { ArrowLeft, Loader2 } from "lucide-react";
|
||||||
import StaffForm from "../components/staff/StaffForm";
|
import StaffForm from "@/components/staff/StaffForm";
|
||||||
|
|
||||||
export default function EditStaff() {
|
export default function EditStaff() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Building2, ArrowLeft, Save, Loader2 } from "lucide-react";
|
import { Building2, ArrowLeft, Save, Loader2 } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export default function EditVendor() {
|
export default function EditVendor() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Building2, Plus, Search, Users, Edit } from "lucide-react";
|
import { Building2, Plus, Search, Users, Edit } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
export default function EnterpriseManagement() {
|
export default function EnterpriseManagement() {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ArrowLeft, Bell, RefreshCw } from "lucide-react";
|
import { ArrowLeft, Bell, RefreshCw } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import ShiftCard from "../components/events/ShiftCard";
|
import ShiftCard from "@/components/events/ShiftCard";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -7,18 +7,18 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Plus, Search, Calendar as CalendarIcon, Eye, Edit, Copy, X, RefreshCw } from "lucide-react";
|
import { Plus, Search, Calendar as CalendarIcon, Eye, Edit, Copy, X, RefreshCw } from "lucide-react";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import StatusCard from "../components/events/StatusCard";
|
import StatusCard from "@/components/events/StatusCard";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { format, isSameDay, parseISO, isWithinInterval, startOfDay, endOfDay, isValid } from "date-fns";
|
import { format, isSameDay, parseISO, isWithinInterval, startOfDay, endOfDay, isValid } from "date-fns";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import EventHoverCard from "../components/events/EventHoverCard";
|
import EventHoverCard from "@/components/events/EventHoverCard";
|
||||||
import QuickAssignPopover from "../components/events/QuickAssignPopover";
|
import QuickAssignPopover from "@/components/events/QuickAssignPopover";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
const statusColors = {
|
const statusColors = {
|
||||||
Draft: "bg-gray-100 text-gray-800",
|
Draft: "bg-gray-100 text-gray-800",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from "@/components/ui/hover-card";
|
} from "@/components/ui/hover-card";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
export default function InviteVendor() {
|
export default function InviteVendor() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { FileText, Plus, DollarSign, Search, Eye, Download } from "lucide-react";
|
import { FileText, Plus, DollarSign, Search, Eye, Download } from "lucide-react";
|
||||||
import { format, parseISO, isPast } from "date-fns";
|
import { format, parseISO, isPast } from "date-fns";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
DollarSign, Award, HelpCircle, BarChart3, Activity, Menu, MessageSquare,
|
DollarSign, Award, HelpCircle, BarChart3, Activity, Menu, MessageSquare,
|
||||||
Package, TrendingUp, Clipboard, LogOut, Shield, MapPin, Bell, CloudOff,
|
Package, TrendingUp, Clipboard, LogOut, Shield, MapPin, Bell, CloudOff,
|
||||||
RefreshCw, User, Search, ShoppingCart, Home, Settings as SettingsIcon, MoreVertical,
|
RefreshCw, User, Search, ShoppingCart, Home, Settings as SettingsIcon, MoreVertical,
|
||||||
Building2, Sparkles, CheckSquare, UserCheck
|
Building2, Sparkles, CheckSquare, UserCheck, Store
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -33,7 +34,7 @@ import RoleSwitcher from "@/components/dev/RoleSwitcher";
|
|||||||
import NotificationPanel from "@/components/notifications/NotificationPanel";
|
import NotificationPanel from "@/components/notifications/NotificationPanel";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
||||||
// Navigation items for each role (removed Control Tower)
|
// Navigation items for each role
|
||||||
const roleNavigationMap = {
|
const roleNavigationMap = {
|
||||||
admin: [
|
admin: [
|
||||||
{ title: "Dashboard", url: createPageUrl("Dashboard"), icon: LayoutDashboard },
|
{ title: "Dashboard", url: createPageUrl("Dashboard"), icon: LayoutDashboard },
|
||||||
@@ -97,9 +98,9 @@ const roleNavigationMap = {
|
|||||||
{ title: "Dashboard", url: createPageUrl("ClientDashboard"), icon: LayoutDashboard },
|
{ title: "Dashboard", url: createPageUrl("ClientDashboard"), icon: LayoutDashboard },
|
||||||
{ title: "My Orders", url: createPageUrl("ClientOrders"), icon: Clipboard },
|
{ title: "My Orders", url: createPageUrl("ClientOrders"), icon: Clipboard },
|
||||||
{ title: "New Order", url: createPageUrl("CreateEvent"), icon: UserPlus },
|
{ title: "New Order", url: createPageUrl("CreateEvent"), icon: UserPlus },
|
||||||
{ title: "Vendors", url: createPageUrl("VendorManagement"), icon: Package },
|
{ title: "Vendor Marketplace", url: createPageUrl("VendorMarketplace"), icon: Store },
|
||||||
|
{ title: "Compare Rates", url: createPageUrl("VendorRateCard"), icon: DollarSign },
|
||||||
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
|
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
|
||||||
{ title: "Partner Rates", url: createPageUrl("VendorRateCard"), icon: DollarSign },
|
|
||||||
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
|
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
|
||||||
{ title: "Invoices", url: createPageUrl("Invoices"), icon: FileText },
|
{ title: "Invoices", url: createPageUrl("Invoices"), icon: FileText },
|
||||||
{ title: "Reports", url: createPageUrl("Reports"), icon: BarChart3 },
|
{ title: "Reports", url: createPageUrl("Reports"), icon: BarChart3 },
|
||||||
@@ -134,8 +135,6 @@ const roleNavigationMap = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ... keep all existing helper functions (getRoleName, etc.) ...
|
|
||||||
|
|
||||||
const getRoleName = (role) => {
|
const getRoleName = (role) => {
|
||||||
const names = {
|
const names = {
|
||||||
admin: "KROW Admin",
|
admin: "KROW Admin",
|
||||||
@@ -232,31 +231,32 @@ function NavigationMenu({ location, userRole, closeSheet }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
export default function Layout({ children }) {
|
||||||
// ... keep ALL existing Layout code (state, queries, handlers) ...
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [showNotifications, setShowNotifications] = React.useState(false);
|
const [showNotifications, setShowNotifications] = React.useState(false);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
||||||
|
|
||||||
// const { data: user } = useQuery({
|
const { data: user } = useQuery({
|
||||||
// queryKey: ['current-user-layout'],
|
queryKey: ['current-user'],
|
||||||
// queryFn: () => base44.auth.me(),
|
queryFn: () => base44.auth.me(),
|
||||||
// });
|
});
|
||||||
|
|
||||||
// Mock user data to prevent redirection and allow local development
|
|
||||||
const user = {
|
|
||||||
full_name: "Dev User",
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
|
|
||||||
const sampleAvatar = "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop";
|
const sampleAvatar = "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop";
|
||||||
const userAvatar = user?.profile_picture || sampleAvatar;
|
const userAvatar = user?.profile_picture || sampleAvatar;
|
||||||
|
|
||||||
// Get unread notification count
|
const { data: unreadCount = 0 } = useQuery({
|
||||||
// const { data: unreadCount = 0 } = useQuery({
|
queryKey: ['unread-notifications', user?.id],
|
||||||
const unreadCount = 0; // Mocked value
|
queryFn: async () => {
|
||||||
|
if (!user?.id) return 0;
|
||||||
|
const notifications = await base44.entities.ActivityLog.filter({
|
||||||
|
user_id: user?.id,
|
||||||
|
is_read: false
|
||||||
|
});
|
||||||
|
return notifications.length;
|
||||||
|
},
|
||||||
|
enabled: !!user?.id,
|
||||||
|
initialData: 0,
|
||||||
|
refetchInterval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
const userRole = user?.user_role || user?.role || "admin";
|
const userRole = user?.user_role || user?.role || "admin";
|
||||||
const userName = user?.full_name || user?.email || "User";
|
const userName = user?.full_name || user?.email || "User";
|
||||||
@@ -272,7 +272,6 @@ export default function Layout({ children }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col w-full bg-slate-50">
|
<div className="min-h-screen flex flex-col w-full bg-slate-50">
|
||||||
{/* ... keep ALL existing Layout structure (header, sidebar, main, footer) ... */}
|
|
||||||
<style>{`
|
<style>{`
|
||||||
:root {
|
:root {
|
||||||
--primary: 10 57 223;
|
--primary: 10 57 223;
|
||||||
@@ -698,3 +697,4 @@ export default function Layout({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { MessageSquare, Plus, Users } from "lucide-react";
|
import { MessageSquare, Plus, Users } from "lucide-react";
|
||||||
import ConversationList from "../components/messaging/ConversationList";
|
import ConversationList from "@/components/messaging/ConversationList";
|
||||||
import MessageThread from "../components/messaging/MessageThread";
|
import MessageThread from "@/components/messaging/MessageThread";
|
||||||
import MessageInput from "../components/messaging/MessageInput";
|
import MessageInput from "@/components/messaging/MessageInput";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -20,7 +20,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
export default function Messages() {
|
export default function Messages() {
|
||||||
const [selectedConversation, setSelectedConversation] = useState(null);
|
const [selectedConversation, setSelectedConversation] = useState(null);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
||||||
|
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
const coverageData = [
|
const coverageData = [
|
||||||
{ hub: 'San Jose', coverage: 97, incidents: 0, satisfaction: 4.9 },
|
{ hub: 'San Jose', coverage: 97, incidents: 0, satisfaction: 4.9 },
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Briefcase, Plus, Search, MapPin, DollarSign, Edit, Building2, TrendingUp, AlertTriangle, CheckCircle2, Users, Target, LayoutGrid, List } 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";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
export default function PartnerManagement() {
|
export default function PartnerManagement() {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Shield, Search, Save, Info, ChevronDown, ChevronRight, Users, Calendar, Package, DollarSign, FileText, Settings as SettingsIcon, BarChart3, MessageSquare, Briefcase, Building2 } from "lucide-react";
|
import { Shield, Search, Save, Info, ChevronDown, ChevronRight, Users, Calendar, Package, DollarSign, FileText, Settings as SettingsIcon, BarChart3, MessageSquare, Briefcase, Building2 } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
|
|||||||
@@ -38,15 +38,15 @@ import {
|
|||||||
Clock, // Added Clock icon for onboarding tab
|
Clock, // Added Clock icon for onboarding tab
|
||||||
X // Added X icon for closing dialog/panel
|
X // Added X icon for closing dialog/panel
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import StatsCard from "../components/staff/StatsCard";
|
import StatsCard from "@/components/staff/StatsCard";
|
||||||
import W9FormViewer from "../components/procurement/W9FormViewer";
|
import W9FormViewer from "@/components/procurement/W9FormViewer";
|
||||||
import COIViewer from "../components/procurement/COIViewer";
|
import COIViewer from "@/components/procurement/COIViewer";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import VendorDetailModal from "../components/procurement/VendorDetailModal";
|
import VendorDetailModal from "@/components/procurement/VendorDetailModal";
|
||||||
import VendorHoverCard from "../components/procurement/VendorHoverCard";
|
import VendorHoverCard from "@/components/procurement/VendorHoverCard";
|
||||||
import VendorScoreHoverCard from "../components/procurement/VendorScoreHoverCard"; // Added this import
|
import VendorScoreHoverCard from "@/components/procurement/VendorScoreHoverCard"; // Added this import
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast"; // Import useToast
|
import { useToast } from "@/components/ui/use-toast"; // Import useToast
|
||||||
|
|
||||||
export default function ProcurementDashboard() {
|
export default function ProcurementDashboard() {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { MapPin, Plus, Search, Building2, Edit } from "lucide-react";
|
import { MapPin, Plus, Search, Building2, Edit } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
export default function SectorManagement() {
|
export default function SectorManagement() {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import DragDropFileUpload from "../components/common/DragDropFileUpload";
|
import DragDropFileUpload from "@/components/common/DragDropFileUpload";
|
||||||
import DocumentViewer from "../components/vendor/DocumentViewer";
|
import DocumentViewer from "@/components/vendor/DocumentViewer";
|
||||||
|
|
||||||
// Google Places Autocomplete Component
|
// Google Places Autocomplete Component
|
||||||
const GoogleAddressInput = ({ value, onChange, placeholder, label, required }) => {
|
const GoogleAddressInput = ({ value, onChange, placeholder, label, required }) => {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { UserPlus, Users, LayoutGrid, List as ListIcon, Phone, MapPin, Calendar, Star } from "lucide-react";
|
import { UserPlus, Users, LayoutGrid, List as ListIcon, Phone, MapPin, Calendar, Star } from "lucide-react";
|
||||||
import FilterBar from "../components/staff/FilterBar";
|
import FilterBar from "@/components/staff/FilterBar";
|
||||||
import StaffCard from "../components/staff/StaffCard";
|
import StaffCard from "@/components/staff/StaffCard";
|
||||||
import EmployeeCard from "../components/staff/EmployeeCard";
|
import EmployeeCard from "@/components/staff/EmployeeCard";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
export default function StaffDirectory() {
|
export default function StaffDirectory() {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Users, Plus, Search, Building2, MapPin, UserCheck, Mail, Edit, Loader2, Trash2, UserX, LayoutGrid, List as ListIcon, RefreshCw, Send, Filter } from "lucide-react";
|
import { Users, Plus, Search, Building2, MapPin, UserCheck, Mail, Edit, Loader2, Trash2, UserX, LayoutGrid, List as ListIcon, RefreshCw, Send, Filter } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Users, UserPlus, Mail, Shield, Building2, Edit, Trash2 } from "lucide-react";
|
import { Users, UserPlus, Mail, Shield, Building2, Edit, Trash2 } from "lucide-react";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import UserPermissionsModal from "../components/permissions/UserPermissionsModal"; // Import the new modal component
|
import UserPermissionsModal from "@/components/permissions/UserPermissionsModal"; // Import the new modal component
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; // Import Avatar components
|
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; // Import Avatar components
|
||||||
|
|
||||||
export default function UserManagement() {
|
export default function UserManagement() {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
TrendingUp, Users, Bell, Zap, Loader2, Sparkles, FolderUp, X
|
TrendingUp, Users, Bell, Zap, Loader2, Sparkles, FolderUp, X
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { differenceInDays, format, isBefore } from "date-fns";
|
import { differenceInDays, format, isBefore } from "date-fns";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
export default function VendorCompliance() {
|
export default function VendorCompliance() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||||
import { ArrowLeft, FileText, Shield, CheckCircle2, Clock, Eye } from "lucide-react";
|
import { ArrowLeft, FileText, Shield, CheckCircle2, Clock, Eye } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import DocumentViewer from "../components/vendor/DocumentViewer";
|
import DocumentViewer from "@/components/vendor/DocumentViewer";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
const ONBOARDING_DOCUMENTS = [
|
const ONBOARDING_DOCUMENTS = [
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import {
|
|||||||
Building2, DollarSign, Mail, CheckCircle2, XCircle, Clock, Eye,
|
Building2, DollarSign, Mail, CheckCircle2, XCircle, Clock, Eye,
|
||||||
Archive, LayoutGrid, List as ListIcon
|
Archive, LayoutGrid, List as ListIcon
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import VendorScoreHoverCard from "../components/procurement/VendorScoreHoverCard";
|
import VendorScoreHoverCard from "@/components/procurement/VendorScoreHoverCard";
|
||||||
import VendorDetailModal from "../components/procurement/VendorDetailModal";
|
import VendorDetailModal from "@/components/procurement/VendorDetailModal";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|||||||
922
frontend-web/src/pages/VendorMarketplace.jsx
Normal file
922
frontend-web/src/pages/VendorMarketplace.jsx
Normal file
@@ -0,0 +1,922 @@
|
|||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import { base44 } from "@/api/base44Client";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { createPageUrl } from "@/utils";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Search, MapPin, Users, Star, DollarSign, TrendingUp, MessageSquare, CheckCircle, Award, Filter, Grid, List, Phone, Mail, Building2, Zap, ArrowRight, ChevronDown, ChevronUp, UserCheck, Briefcase } from "lucide-react";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
export default function VendorMarketplace() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [regionFilter, setRegionFilter] = useState("all");
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState("all");
|
||||||
|
const [sortBy, setSortBy] = useState("rating");
|
||||||
|
const [viewMode, setViewMode] = useState("grid");
|
||||||
|
const [contactModal, setContactModal] = useState({ open: false, vendor: null });
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [expandedVendors, setExpandedVendors] = useState({});
|
||||||
|
|
||||||
|
const { data: user } = useQuery({
|
||||||
|
queryKey: ['current-user-marketplace'],
|
||||||
|
queryFn: () => base44.auth.me(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: vendors = [] } = useQuery({
|
||||||
|
queryKey: ['approved-vendors'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const allVendors = await base44.entities.Vendor.list();
|
||||||
|
return allVendors.filter(v => v.approval_status === 'approved' && v.is_active);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: vendorRates = [] } = useQuery({
|
||||||
|
queryKey: ['vendor-rates-marketplace'],
|
||||||
|
queryFn: () => base44.entities.VendorRate.list(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: staff = [] } = useQuery({
|
||||||
|
queryKey: ['vendor-staff-count'],
|
||||||
|
queryFn: () => base44.entities.Staff.list(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: events = [] } = useQuery({
|
||||||
|
queryKey: ['events-vendor-marketplace'],
|
||||||
|
queryFn: () => base44.entities.Event.list(),
|
||||||
|
initialData: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: businesses = [] } = useQuery({
|
||||||
|
queryKey: ['businesses-vendor-marketplace'],
|
||||||
|
queryFn: () => base44.entities.Business.list(),
|
||||||
|
initialData: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate vendor metrics
|
||||||
|
const vendorsWithMetrics = useMemo(() => {
|
||||||
|
return vendors.map(vendor => {
|
||||||
|
const rates = vendorRates.filter(r => r.vendor_name === vendor.legal_name || r.vendor_id === vendor.id);
|
||||||
|
const vendorStaff = staff.filter(s => s.vendor_name === vendor.legal_name);
|
||||||
|
|
||||||
|
const avgRate = rates.length > 0
|
||||||
|
? rates.reduce((sum, r) => sum + (r.client_rate || 0), 0) / rates.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const minRate = rates.length > 0
|
||||||
|
? Math.min(...rates.map(r => r.client_rate || 999))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const rating = 4.5 + (Math.random() * 0.5);
|
||||||
|
const completedJobs = Math.floor(Math.random() * 200) + 50;
|
||||||
|
|
||||||
|
// Calculate how many clients in user's sector are using this vendor
|
||||||
|
const vendorEvents = events.filter(e =>
|
||||||
|
e.vendor_name === vendor.legal_name ||
|
||||||
|
e.vendor_id === vendor.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const uniqueClients = new Set(
|
||||||
|
vendorEvents.map(e => e.business_name || e.client_email)
|
||||||
|
).size;
|
||||||
|
|
||||||
|
// Calculate sector-specific usage
|
||||||
|
const userSector = user?.sector || user?.company_name;
|
||||||
|
const sectorClients = businesses.filter(b =>
|
||||||
|
b.sector === userSector || b.area === user?.area
|
||||||
|
);
|
||||||
|
|
||||||
|
const clientsInSector = new Set(
|
||||||
|
vendorEvents
|
||||||
|
.filter(e => sectorClients.some(sc =>
|
||||||
|
sc.business_name === e.business_name ||
|
||||||
|
sc.contact_name === e.client_name
|
||||||
|
))
|
||||||
|
.map(e => e.business_name || e.client_email)
|
||||||
|
).size;
|
||||||
|
|
||||||
|
// Group rates by category
|
||||||
|
const ratesByCategory = rates.reduce((acc, rate) => {
|
||||||
|
const category = rate.category || 'Other';
|
||||||
|
if (!acc[category]) {
|
||||||
|
acc[category] = [];
|
||||||
|
}
|
||||||
|
acc[category].push(rate);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...vendor,
|
||||||
|
rates,
|
||||||
|
ratesByCategory,
|
||||||
|
avgRate,
|
||||||
|
minRate,
|
||||||
|
rating,
|
||||||
|
completedJobs,
|
||||||
|
staffCount: vendorStaff.length,
|
||||||
|
responseTime: `${Math.floor(Math.random() * 3) + 1}h`,
|
||||||
|
totalClients: uniqueClients,
|
||||||
|
clientsInSector: clientsInSector,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [vendors, vendorRates, staff, events, businesses, user]);
|
||||||
|
|
||||||
|
// Filtering and sorting
|
||||||
|
const filteredVendors = useMemo(() => {
|
||||||
|
let filtered = vendorsWithMetrics;
|
||||||
|
|
||||||
|
// Search
|
||||||
|
if (searchTerm) {
|
||||||
|
const lower = searchTerm.toLowerCase();
|
||||||
|
filtered = filtered.filter(v =>
|
||||||
|
v.legal_name?.toLowerCase().includes(lower) ||
|
||||||
|
v.doing_business_as?.toLowerCase().includes(lower) ||
|
||||||
|
v.service_specialty?.toLowerCase().includes(lower)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Region filter
|
||||||
|
if (regionFilter !== "all") {
|
||||||
|
filtered = filtered.filter(v => v.region === regionFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category filter
|
||||||
|
if (categoryFilter !== "all") {
|
||||||
|
filtered = filtered.filter(v => v.service_specialty === categoryFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorting
|
||||||
|
filtered.sort((a, b) => {
|
||||||
|
switch (sortBy) {
|
||||||
|
case "rating":
|
||||||
|
return b.rating - a.rating;
|
||||||
|
case "price-low":
|
||||||
|
return a.minRate - b.minRate;
|
||||||
|
case "price-high":
|
||||||
|
return b.avgRate - a.avgRate;
|
||||||
|
case "staff":
|
||||||
|
return b.staffCount - a.staffCount;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}, [vendorsWithMetrics, searchTerm, regionFilter, categoryFilter, sortBy]);
|
||||||
|
|
||||||
|
const uniqueRegions = [...new Set(vendors.map(v => v.region).filter(Boolean))];
|
||||||
|
const uniqueCategories = [...new Set(vendors.map(v => v.service_specialty).filter(Boolean))];
|
||||||
|
|
||||||
|
const handleContactVendor = (vendor) => {
|
||||||
|
setContactModal({ open: true, vendor });
|
||||||
|
setMessage(`Hi ${vendor.legal_name},\n\nI'm interested in your services for an upcoming event. Could you please provide more information about your availability and pricing?\n\nBest regards,\n${user?.full_name || 'Client'}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMessage = async () => {
|
||||||
|
if (!message.trim()) {
|
||||||
|
toast({
|
||||||
|
title: "Message required",
|
||||||
|
description: "Please enter a message to send.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create conversation
|
||||||
|
try {
|
||||||
|
await base44.entities.Conversation.create({
|
||||||
|
participants: [
|
||||||
|
{ id: user?.id, name: user?.full_name, role: "client" },
|
||||||
|
{ id: contactModal.vendor.id, name: contactModal.vendor.legal_name, role: "vendor" }
|
||||||
|
],
|
||||||
|
conversation_type: "client-vendor",
|
||||||
|
is_group: false,
|
||||||
|
subject: `Inquiry from ${user?.full_name || 'Client'}`,
|
||||||
|
last_message: message.substring(0, 100),
|
||||||
|
last_message_at: new Date().toISOString(),
|
||||||
|
status: "active"
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "✅ Message sent!",
|
||||||
|
description: `Your message has been sent to ${contactModal.vendor.legal_name}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setContactModal({ open: false, vendor: null });
|
||||||
|
setMessage("");
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Failed to send message",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateOrder = (vendor) => {
|
||||||
|
sessionStorage.setItem('selectedVendor', JSON.stringify({
|
||||||
|
id: vendor.id,
|
||||||
|
name: vendor.legal_name,
|
||||||
|
rates: vendor.rates
|
||||||
|
}));
|
||||||
|
navigate(createPageUrl("CreateEvent"));
|
||||||
|
toast({
|
||||||
|
title: "Vendor selected",
|
||||||
|
description: `${vendor.legal_name} will be used for this order.`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleVendorRates = (vendorId) => {
|
||||||
|
setExpandedVendors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[vendorId]: !prev[vendorId]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/20 to-indigo-50/30 p-6">
|
||||||
|
<div className="max-w-[1800px] mx-auto space-y-8">
|
||||||
|
|
||||||
|
{/* Hero Header */}
|
||||||
|
<div className="relative overflow-hidden bg-gradient-to-r from-[#0A39DF] via-blue-600 to-indigo-600 rounded-2xl p-6 shadow-xl">
|
||||||
|
<div className="absolute inset-0 opacity-10" style={{
|
||||||
|
backgroundImage: 'radial-gradient(circle at 2px 2px, white 1px, transparent 0)',
|
||||||
|
backgroundSize: '40px 40px'
|
||||||
|
}} />
|
||||||
|
<div className="relative z-10 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
|
||||||
|
<Building2 className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-1">Vendor Marketplace</h1>
|
||||||
|
<p className="text-blue-100 text-sm">Compare rates • See who others trust • Order instantly</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge className="bg-white/20 backdrop-blur-md text-white border-white/30 border px-4 py-2 text-base font-semibold">
|
||||||
|
{filteredVendors.length} Vendors
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Cards */}
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<Card className="bg-gradient-to-br from-[#0A39DF] to-blue-600 text-white border-0 shadow-md hover:shadow-lg transition-all hover:scale-105 overflow-hidden relative group">
|
||||||
|
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12 group-hover:scale-110 transition-transform" />
|
||||||
|
<CardContent className="p-5 relative z-10">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-blue-100 text-xs mb-2 font-medium uppercase tracking-wide">Vendors</p>
|
||||||
|
<p className="text-3xl font-bold mb-0.5">{vendors.length}</p>
|
||||||
|
<p className="text-blue-200 text-xs">Approved</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
|
||||||
|
<Building2 className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gradient-to-br from-emerald-500 to-green-600 text-white border-0 shadow-md hover:shadow-lg transition-all hover:scale-105 overflow-hidden relative group">
|
||||||
|
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12 group-hover:scale-110 transition-transform" />
|
||||||
|
<CardContent className="p-5 relative z-10">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-emerald-100 text-xs mb-2 font-medium uppercase tracking-wide">Staff</p>
|
||||||
|
<p className="text-3xl font-bold mb-0.5">{staff.length}</p>
|
||||||
|
<p className="text-emerald-200 text-xs">Available</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
|
||||||
|
<Users className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gradient-to-br from-indigo-500 to-purple-600 text-white border-0 shadow-md hover:shadow-lg transition-all hover:scale-105 overflow-hidden relative group">
|
||||||
|
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12 group-hover:scale-110 transition-transform" />
|
||||||
|
<CardContent className="p-5 relative z-10">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-indigo-100 text-xs mb-2 font-medium uppercase tracking-wide">Avg Rate</p>
|
||||||
|
<p className="text-3xl font-bold mb-0.5">
|
||||||
|
${Math.round(vendorsWithMetrics.reduce((sum, v) => sum + v.avgRate, 0) / vendorsWithMetrics.length || 0)}
|
||||||
|
</p>
|
||||||
|
<p className="text-indigo-200 text-xs">Per hour</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
|
||||||
|
<DollarSign className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gradient-to-br from-amber-500 to-orange-500 text-white border-0 shadow-md hover:shadow-lg transition-all hover:scale-105 overflow-hidden relative group">
|
||||||
|
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12 group-hover:scale-110 transition-transform" />
|
||||||
|
<CardContent className="p-5 relative z-10">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-amber-100 text-xs mb-2 font-medium uppercase tracking-wide">Rating</p>
|
||||||
|
<div className="flex items-center gap-2 mb-0.5">
|
||||||
|
<p className="text-3xl font-bold">4.7</p>
|
||||||
|
<Star className="w-5 h-5 fill-white" />
|
||||||
|
</div>
|
||||||
|
<p className="text-amber-200 text-xs">Average</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
|
||||||
|
<Award className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced Filters */}
|
||||||
|
<Card className="border-2 border-slate-200 shadow-lg bg-white/80 backdrop-blur-sm">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="grid grid-cols-12 gap-4 items-end">
|
||||||
|
{/* Search - Takes more space */}
|
||||||
|
<div className="col-span-5">
|
||||||
|
<label className="text-xs font-bold text-slate-700 mb-2 block uppercase tracking-wide">
|
||||||
|
Search Vendors
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#0A39DF]" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search by name, specialty, or location..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10 h-11 border-2 border-slate-200 focus:border-[#0A39DF] text-sm rounded-lg shadow-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Region Filter */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="text-xs font-bold text-slate-700 mb-2 block uppercase tracking-wide">
|
||||||
|
Region
|
||||||
|
</label>
|
||||||
|
<Select value={regionFilter} onValueChange={setRegionFilter}>
|
||||||
|
<SelectTrigger className="h-11 border-2 border-slate-200 rounded-lg shadow-sm">
|
||||||
|
<SelectValue placeholder="All Regions" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Regions</SelectItem>
|
||||||
|
{uniqueRegions.map(region => (
|
||||||
|
<SelectItem key={region} value={region}>{region}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="text-xs font-bold text-slate-700 mb-2 block uppercase tracking-wide">
|
||||||
|
Specialty
|
||||||
|
</label>
|
||||||
|
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
||||||
|
<SelectTrigger className="h-11 border-2 border-slate-200 rounded-lg shadow-sm">
|
||||||
|
<SelectValue placeholder="All" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Specialties</SelectItem>
|
||||||
|
{uniqueCategories.map(cat => (
|
||||||
|
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sort */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="text-xs font-bold text-slate-700 mb-2 block uppercase tracking-wide">
|
||||||
|
Sort By
|
||||||
|
</label>
|
||||||
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
|
<SelectTrigger className="h-11 border-2 border-slate-200 rounded-lg shadow-sm">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="rating">⭐ Highest Rated</SelectItem>
|
||||||
|
<SelectItem value="price-low">💰 Lowest Price</SelectItem>
|
||||||
|
<SelectItem value="price-high">💎 Premium</SelectItem>
|
||||||
|
<SelectItem value="staff">👥 Most Staff</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Toggle */}
|
||||||
|
<div className="col-span-1 flex justify-end">
|
||||||
|
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setViewMode("grid")}
|
||||||
|
className={viewMode === "grid" ? "bg-[#0A39DF] hover:bg-[#0A39DF]" : "hover:bg-white"}
|
||||||
|
>
|
||||||
|
<Grid className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === "list" ? "default" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setViewMode("list")}
|
||||||
|
className={viewMode === "list" ? "bg-[#0A39DF] hover:bg-[#0A39DF]" : "hover:bg-white"}
|
||||||
|
>
|
||||||
|
<List className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Vendors Grid/List */}
|
||||||
|
{viewMode === "grid" ? (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{filteredVendors.map((vendor) => {
|
||||||
|
const isExpanded = expandedVendors[vendor.id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={vendor.id} className="bg-white border-2 border-slate-200 hover:border-[#0A39DF] hover:shadow-2xl transition-all group overflow-hidden">
|
||||||
|
{/* Vendor Header */}
|
||||||
|
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50/30 border-b border-slate-200 pb-5">
|
||||||
|
<div className="flex items-start justify-between gap-6">
|
||||||
|
<div className="flex items-center gap-4 flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Avatar className="w-16 h-16 bg-gradient-to-br from-[#0A39DF] to-indigo-600 shadow-lg ring-2 ring-blue-200">
|
||||||
|
<AvatarFallback className="text-white text-xl font-bold">
|
||||||
|
{vendor.legal_name?.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-green-500 rounded-full border-2 border-white flex items-center justify-center shadow-md">
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<CardTitle className="text-xl font-bold text-[#1C323E] group-hover:text-[#0A39DF] transition-colors">
|
||||||
|
{vendor.legal_name}
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex items-center gap-1.5 bg-amber-50 px-3 py-1.5 rounded-full border border-amber-200">
|
||||||
|
<Star className="w-4 h-4 text-amber-600 fill-amber-600" />
|
||||||
|
<span className="text-sm font-bold text-amber-700">{vendor.rating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{vendor.doing_business_as && (
|
||||||
|
<p className="text-xs text-slate-500 mb-3 italic">DBA: {vendor.doing_business_as}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
|
{vendor.service_specialty && (
|
||||||
|
<div className="flex items-center gap-1.5 text-sm bg-blue-50 px-3 py-1.5 rounded-lg border border-blue-200">
|
||||||
|
<Award className="w-3.5 h-3.5 text-[#0A39DF]" />
|
||||||
|
<span className="text-[#1C323E] font-semibold">{vendor.service_specialty}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<MapPin className="w-4 h-4 text-[#0A39DF]" />
|
||||||
|
<span className="text-slate-700 font-medium">{vendor.region || vendor.city}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<Users className="w-4 h-4 text-[#0A39DF]" />
|
||||||
|
<span className="text-slate-700 font-medium">{vendor.staffCount} Staff</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<Zap className="w-4 h-4 text-emerald-600" />
|
||||||
|
<span className="text-slate-700 font-medium">{vendor.responseTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-end gap-3">
|
||||||
|
<div className="p-4 bg-gradient-to-br from-[#0A39DF] to-indigo-600 rounded-xl shadow-lg text-center min-w-[140px]">
|
||||||
|
<p className="text-blue-100 text-[10px] mb-1 font-semibold uppercase tracking-wide">Starting from</p>
|
||||||
|
<p className="text-3xl font-bold text-white mb-1">${vendor.minRate}</p>
|
||||||
|
<p className="text-blue-200 text-xs">per hour</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Proof */}
|
||||||
|
{vendor.clientsInSector > 0 && (
|
||||||
|
<div className="bg-gradient-to-br from-purple-50 to-pink-50 border-2 border-purple-300 rounded-xl px-4 py-3 shadow-md min-w-[140px]">
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-1">
|
||||||
|
<UserCheck className="w-5 h-5 text-purple-700" />
|
||||||
|
<span className="text-2xl font-bold text-purple-700">{vendor.clientsInSector}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-purple-600 font-bold text-center uppercase tracking-wide">
|
||||||
|
clients in your area
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge className="bg-green-50 text-green-700 border-green-200 border px-3 py-1.5 text-xs">
|
||||||
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
|
{vendor.completedJobs} jobs
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="border-slate-300 px-3 py-1.5 text-xs font-semibold">
|
||||||
|
{vendor.rates.length} services
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
{/* Actions Bar */}
|
||||||
|
<div className="px-5 py-4 bg-white border-b border-slate-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Collapsible open={isExpanded} onOpenChange={() => toggleVendorRates(vendor.id)} className="flex-1">
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-auto px-4 py-2 hover:bg-blue-50 rounded-lg group/trigger"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 bg-blue-100 rounded-lg flex items-center justify-center group-hover/trigger:bg-[#0A39DF] transition-colors">
|
||||||
|
<TrendingUp className="w-4 h-4 text-[#0A39DF] group-hover/trigger:text-white transition-colors" />
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<span className="font-bold text-[#1C323E] text-base block">
|
||||||
|
Compare All Rates
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500">
|
||||||
|
{vendor.rates.length} services • Full breakdown
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-slate-400 ml-2" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-slate-400 ml-2" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleContactVendor(vendor)}
|
||||||
|
className="border-2 hover:border-[#0A39DF] hover:bg-blue-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
|
Contact
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleCreateOrder(vendor)}
|
||||||
|
className="bg-gradient-to-r from-emerald-600 to-green-600 hover:from-emerald-700 hover:to-green-700 shadow-md rounded-lg"
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4 mr-2" />
|
||||||
|
Create Order
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rate Comparison Section */}
|
||||||
|
<Collapsible open={isExpanded}>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<CardContent className="p-6 bg-gradient-to-br from-slate-50 to-blue-50/20">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Object.entries(vendor.ratesByCategory).map(([category, categoryRates], catIdx) => (
|
||||||
|
<div key={category} className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all">
|
||||||
|
<div className="bg-gradient-to-r from-[#0A39DF] to-indigo-600 px-5 py-3">
|
||||||
|
<h4 className="font-bold text-white text-sm flex items-center gap-2">
|
||||||
|
<div className="w-7 h-7 bg-white/20 rounded-lg flex items-center justify-center">
|
||||||
|
<Briefcase className="w-4 h-4 text-white" />
|
||||||
|
</div>
|
||||||
|
{category}
|
||||||
|
<Badge className="bg-white/20 text-white border-white/30 border ml-auto px-2 py-1 text-xs">
|
||||||
|
{categoryRates.length}
|
||||||
|
</Badge>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-slate-100">
|
||||||
|
{categoryRates.map((rate, rateIdx) => {
|
||||||
|
const baseWage = rate.employee_wage || 0;
|
||||||
|
const markupAmount = baseWage * ((rate.markup_percentage || 0) / 100);
|
||||||
|
const subtotal = baseWage + markupAmount;
|
||||||
|
const feeAmount = subtotal * ((rate.vendor_fee_percentage || 0) / 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={rate.id} className="p-4 hover:bg-blue-50 transition-all">
|
||||||
|
<div className="flex items-center justify-between gap-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="w-7 h-7 bg-slate-100 rounded-lg flex items-center justify-center font-bold text-slate-600 text-sm">
|
||||||
|
{rateIdx + 1}
|
||||||
|
</div>
|
||||||
|
<h5 className="font-bold text-[#1C323E] text-base">
|
||||||
|
{rate.role_name}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visual Price Breakdown */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="w-24 text-slate-600 font-medium">Base Wage:</span>
|
||||||
|
<div className="flex-1 h-7 bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-lg flex items-center justify-between px-3 text-white font-semibold shadow-sm">
|
||||||
|
<span>${baseWage.toFixed(2)}/hr</span>
|
||||||
|
<span className="text-emerald-100 text-[10px]">Employee</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="w-24 text-slate-600 font-medium">+ Markup:</span>
|
||||||
|
<div className="flex-1 h-7 bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg flex items-center justify-between px-3 text-white font-semibold shadow-sm">
|
||||||
|
<span>{rate.markup_percentage}%</span>
|
||||||
|
<span className="text-blue-100 text-[10px]">+${markupAmount.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="w-24 text-slate-600 font-medium">+ Admin Fee:</span>
|
||||||
|
<div className="flex-1 h-7 bg-gradient-to-r from-purple-500 to-purple-600 rounded-lg flex items-center justify-between px-3 text-white font-semibold shadow-sm">
|
||||||
|
<span>{rate.vendor_fee_percentage}%</span>
|
||||||
|
<span className="text-purple-100 text-[10px]">+${feeAmount.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<p className="text-[10px] text-slate-500 mb-2 font-bold uppercase tracking-wide">You Pay</p>
|
||||||
|
<div className="bg-gradient-to-br from-[#0A39DF] to-indigo-600 rounded-xl px-6 py-4 shadow-lg">
|
||||||
|
<p className="text-3xl font-bold text-white">
|
||||||
|
${rate.client_rate?.toFixed(0)}
|
||||||
|
</p>
|
||||||
|
<p className="text-blue-200 text-xs text-center mt-1">per hour</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Summary Footer */}
|
||||||
|
<div className="mt-5 p-5 bg-white rounded-xl border-2 border-blue-200 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-[#0A39DF] rounded-lg flex items-center justify-center">
|
||||||
|
<DollarSign className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 font-medium">Average Rate</p>
|
||||||
|
<p className="text-2xl font-bold text-[#1C323E]">
|
||||||
|
${Math.round(vendor.avgRate)}/hr
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs text-slate-600 font-medium">Price Range</p>
|
||||||
|
<p className="text-xl font-bold text-[#1C323E]">
|
||||||
|
${vendor.minRate} - ${Math.round(Math.max(...vendor.rates.map(r => r.client_rate || 0)))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="border-2 border-slate-200 shadow-xl">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gradient-to-r from-slate-50 to-blue-50 border-b-2 border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th className="text-left py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Vendor</th>
|
||||||
|
<th className="text-left py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Specialty</th>
|
||||||
|
<th className="text-left py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Location</th>
|
||||||
|
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Rating</th>
|
||||||
|
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Clients</th>
|
||||||
|
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Staff</th>
|
||||||
|
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Min Rate</th>
|
||||||
|
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredVendors.map((vendor) => (
|
||||||
|
<tr key={vendor.id} className="border-b border-slate-100 hover:bg-gradient-to-r hover:from-blue-50 hover:to-transparent transition-all group">
|
||||||
|
<td className="py-5 px-5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Avatar className="w-12 h-12 bg-gradient-to-br from-[#0A39DF] to-indigo-600 ring-2 ring-slate-200 group-hover:ring-[#0A39DF] transition-all shadow-md">
|
||||||
|
<AvatarFallback className="text-white font-bold text-lg">
|
||||||
|
{vendor.legal_name?.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-[#1C323E] text-base">{vendor.legal_name}</p>
|
||||||
|
<p className="text-xs text-slate-500 flex items-center gap-1 mt-1">
|
||||||
|
<CheckCircle className="w-3 h-3 text-green-600" />
|
||||||
|
{vendor.completedJobs} jobs completed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-5">
|
||||||
|
<span className="text-sm text-slate-700 font-medium">{vendor.service_specialty || '—'}</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-5">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-slate-700">
|
||||||
|
<MapPin className="w-4 h-4 text-[#0A39DF]" />
|
||||||
|
<span className="font-medium">{vendor.region}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-5 text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 bg-amber-50 px-4 py-2 rounded-full shadow-sm">
|
||||||
|
<Star className="w-4 h-4 text-amber-500 fill-amber-500" />
|
||||||
|
<span className="font-bold text-base text-amber-700">{vendor.rating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-5 text-center">
|
||||||
|
{vendor.clientsInSector > 0 ? (
|
||||||
|
<Badge className="bg-purple-100 text-purple-700 border-purple-200 border-2 font-bold text-sm px-3 py-1.5">
|
||||||
|
<UserCheck className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
{vendor.clientsInSector}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-slate-400 text-sm">—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-5 text-center">
|
||||||
|
<Badge variant="outline" className="font-bold border-2 text-sm px-3 py-1.5">{vendor.staffCount}</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-5 text-center">
|
||||||
|
<div className="inline-flex flex-col items-center bg-blue-50 px-5 py-3 rounded-xl shadow-sm">
|
||||||
|
<span className="font-bold text-2xl text-[#0A39DF]">${vendor.minRate}</span>
|
||||||
|
<span className="text-xs text-slate-500 font-medium">/hour</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-5 px-5">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleContactVendor(vendor)}
|
||||||
|
className="border-2 hover:border-[#0A39DF] hover:bg-blue-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-1" />
|
||||||
|
Contact
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
toggleVendorRates(vendor.id);
|
||||||
|
setViewMode("grid");
|
||||||
|
}}
|
||||||
|
className="bg-[#0A39DF] hover:bg-blue-700 rounded-lg"
|
||||||
|
>
|
||||||
|
<TrendingUp className="w-4 h-4 mr-1" />
|
||||||
|
Rates
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredVendors.length === 0 && (
|
||||||
|
<div className="text-center py-16 bg-white rounded-2xl border-2 border-dashed border-slate-300">
|
||||||
|
<div className="w-20 h-20 mx-auto mb-4 bg-slate-100 rounded-2xl flex items-center justify-center">
|
||||||
|
<Building2 className="w-10 h-10 text-slate-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 mb-2">No vendors found</h3>
|
||||||
|
<p className="text-slate-600 mb-5">Try adjusting your filters to see more results</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchTerm("");
|
||||||
|
setRegionFilter("all");
|
||||||
|
setCategoryFilter("all");
|
||||||
|
}}
|
||||||
|
className="border-2 hover:border-[#0A39DF] hover:bg-blue-50 rounded-lg"
|
||||||
|
>
|
||||||
|
Clear All Filters
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Modal */}
|
||||||
|
<Dialog open={contactModal.open} onOpenChange={(open) => setContactModal({ open, vendor: null })}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl font-bold text-[#1C323E] mb-1">
|
||||||
|
Contact {contactModal.vendor?.legal_name}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Start a conversation and get staffing help within hours
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-5 py-4">
|
||||||
|
{/* Vendor Info Card */}
|
||||||
|
<div className="flex items-center gap-4 p-5 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200">
|
||||||
|
<Avatar className="w-16 h-16 bg-gradient-to-br from-[#0A39DF] to-indigo-600 ring-2 ring-white shadow-md">
|
||||||
|
<AvatarFallback className="text-white text-xl font-bold">
|
||||||
|
{contactModal.vendor?.legal_name?.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-bold text-[#1C323E] text-lg mb-2">{contactModal.vendor?.legal_name}</h4>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs bg-white px-2 py-1.5 rounded-lg">
|
||||||
|
<MapPin className="w-3.5 h-3.5 text-[#0A39DF]" />
|
||||||
|
<span className="font-medium text-slate-700">{contactModal.vendor?.region}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs bg-white px-2 py-1.5 rounded-lg">
|
||||||
|
<Users className="w-3.5 h-3.5 text-[#0A39DF]" />
|
||||||
|
<span className="font-medium text-slate-700">{contactModal.vendor?.staffCount} staff</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 bg-amber-50 px-2 py-1.5 rounded-lg">
|
||||||
|
<Star className="w-3.5 h-3.5 text-amber-500 fill-amber-500" />
|
||||||
|
<span className="text-xs font-bold text-amber-700">{contactModal.vendor?.rating?.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
{contactModal.vendor?.clientsInSector > 0 && (
|
||||||
|
<Badge className="bg-purple-100 text-purple-700 border-purple-200 border text-xs">
|
||||||
|
<UserCheck className="w-3 h-3 mr-1" />
|
||||||
|
{contactModal.vendor?.clientsInSector} in your area
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message Input */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-bold text-slate-700 mb-2 block flex items-center gap-2">
|
||||||
|
<MessageSquare className="w-4 h-4 text-[#0A39DF]" />
|
||||||
|
Your Message
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
rows={8}
|
||||||
|
placeholder="Enter your message..."
|
||||||
|
className="resize-none border-2 border-slate-200 focus:border-[#0A39DF] text-sm rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-slate-500 mt-2 bg-blue-50 p-2.5 rounded-lg border border-blue-100">
|
||||||
|
💡 <strong>Tip:</strong> Include event date, location, and number of staff needed for faster response
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setContactModal({ open: false, vendor: null })}
|
||||||
|
className="border-2 rounded-lg"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
className="bg-gradient-to-r from-[#0A39DF] to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-md rounded-lg"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
|
Send Message
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
FileCheck,
|
FileCheck,
|
||||||
TrendingUp
|
TrendingUp
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
const ONBOARDING_STEPS = [
|
const ONBOARDING_STEPS = [
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { format, isToday, addDays } from "date-fns";
|
|||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import EventAssignmentModal from "../components/events/EventAssignmentModal";
|
import EventAssignmentModal from "@/components/events/EventAssignmentModal";
|
||||||
|
|
||||||
const getStatusColor = (order) => {
|
const getStatusColor = (order) => {
|
||||||
// Check for Rapid Request first
|
// Check for Rapid Request first
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Download, Search, Building2, MapPin, DollarSign } from "lucide-react";
|
import { Download, Search, Building2, MapPin, DollarSign } from "lucide-react";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
// Define regions - these map to the notes field or can be a new field
|
// Define regions - these map to the notes field or can be a new field
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Users, MapPin, DollarSign, Award, BookOpen, TrendingUp, Star, Clock, ArrowLeft, Calendar } from "lucide-react";
|
import { Users, MapPin, DollarSign, Award, BookOpen, TrendingUp, Star, Clock, ArrowLeft, Calendar } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { createPageUrl } from "@/utils";
|
import { createPageUrl } from "@/utils";
|
||||||
import PageHeader from "../components/common/PageHeader";
|
import PageHeader from "@/components/common/PageHeader";
|
||||||
|
|
||||||
export default function WorkforceDashboard() {
|
export default function WorkforceDashboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ import VendorRates from "./VendorRates";
|
|||||||
|
|
||||||
import VendorDocumentReview from "./VendorDocumentReview";
|
import VendorDocumentReview from "./VendorDocumentReview";
|
||||||
|
|
||||||
|
import VendorMarketplace from "./VendorMarketplace";
|
||||||
|
|
||||||
import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom';
|
import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
const PAGES = {
|
const PAGES = {
|
||||||
@@ -240,6 +242,8 @@ const PAGES = {
|
|||||||
|
|
||||||
VendorDocumentReview: VendorDocumentReview,
|
VendorDocumentReview: VendorDocumentReview,
|
||||||
|
|
||||||
|
VendorMarketplace: VendorMarketplace,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getCurrentPage(url) {
|
function _getCurrentPage(url) {
|
||||||
@@ -385,6 +389,8 @@ function PagesContent() {
|
|||||||
|
|
||||||
<Route path="/VendorDocumentReview" element={<VendorDocumentReview />} />
|
<Route path="/VendorDocumentReview" element={<VendorDocumentReview />} />
|
||||||
|
|
||||||
|
<Route path="/VendorMarketplace" element={<VendorMarketplace />} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,22 +4,6 @@ const path = require('path');
|
|||||||
const projectRoot = path.resolve(__dirname, '..');
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
const clientFilePath = path.join(projectRoot, 'frontend-web', 'src', 'api', 'base44Client.js');
|
const clientFilePath = path.join(projectRoot, 'frontend-web', 'src', 'api', 'base44Client.js');
|
||||||
|
|
||||||
const originalMock = `// import { createClient } from '@base44/sdk';
|
|
||||||
|
|
||||||
// --- MIGRATION MOCK ---
|
|
||||||
// This mock completely disables the Base44 SDK to allow for local development.
|
|
||||||
export const base44 = {
|
|
||||||
auth: {
|
|
||||||
me: () => Promise.resolve(null),
|
|
||||||
logout: () => {},
|
|
||||||
},
|
|
||||||
entities: {
|
|
||||||
ActivityLog: {
|
|
||||||
filter: () => Promise.resolve([]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};`;
|
|
||||||
|
|
||||||
const patchedMock = `// import { createClient } from '@base44/sdk';
|
const patchedMock = `// import { createClient } from '@base44/sdk';
|
||||||
|
|
||||||
// --- MIGRATION MOCK ---
|
// --- MIGRATION MOCK ---
|
||||||
@@ -98,24 +82,17 @@ export const base44 = {
|
|||||||
};`;
|
};`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let content = fs.readFileSync(clientFilePath, 'utf8');
|
const content = fs.readFileSync(clientFilePath, 'utf8');
|
||||||
|
|
||||||
if (content.includes('me: () => Promise.resolve(null),')) {
|
// Check if the file is the original, unpatched version from the export
|
||||||
content = content.replace(originalMock.trim(), patchedMock.trim());
|
if (content.includes("createClient({")) {
|
||||||
fs.writeFileSync(clientFilePath, content, 'utf8');
|
fs.writeFileSync(clientFilePath, patchedMock, 'utf8');
|
||||||
console.log('✅ Successfully patched frontend-web/src/api/base44Client.js');
|
console.log('✅ Successfully patched frontend-web/src/api/base44Client.js');
|
||||||
} else if (content.includes('const MOCK_USER_KEY')) {
|
} else if (content.includes("const MOCK_USER_KEY")) {
|
||||||
console.log('ℹ️ base44Client.js is already patched. Skipping.');
|
console.log('ℹ️ base44Client.js is already patched. Skipping.');
|
||||||
} else {
|
} else {
|
||||||
// Fallback to a simpler, more brittle replacement if the full mock doesn't match
|
console.error('❌ Patching failed: Could not find the expected code in base44Client.js. The export format may have changed.');
|
||||||
const simpleOriginal = 'me: () => Promise.resolve(null),';
|
process.exit(1);
|
||||||
if (content.includes(simpleOriginal)) {
|
|
||||||
fs.writeFileSync(clientFilePath, patchedMock, 'utf8');
|
|
||||||
console.log('✅ Successfully patched frontend-web/src/api/base44Client.js using fallback.');
|
|
||||||
} else {
|
|
||||||
console.error('❌ Patching failed: Could not find the original mock code in base44Client.js.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ An error occurred during patching:', error);
|
console.error('❌ An error occurred during patching:', error);
|
||||||
|
|||||||
26
scripts/patch-layout-query-key.js
Normal file
26
scripts/patch-layout-query-key.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
|
const layoutFilePath = path.join(projectRoot, 'frontend-web', 'src', 'pages', 'Layout.jsx');
|
||||||
|
|
||||||
|
const oldString = ` queryKey: ['current-user-layout'],`;
|
||||||
|
const newString = ` queryKey: ['current-user'],`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(layoutFilePath, 'utf8');
|
||||||
|
|
||||||
|
if (content.includes(oldString)) {
|
||||||
|
const newContent = content.replace(oldString, newString);
|
||||||
|
fs.writeFileSync(layoutFilePath, newContent, 'utf8');
|
||||||
|
console.log('✅ Successfully patched queryKey in frontend-web/src/pages/Layout.jsx');
|
||||||
|
} else if (content.includes(newString)) {
|
||||||
|
console.log('ℹ️ queryKey in Layout.jsx is already patched. Skipping.');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Patching failed: Could not find the expected queryKey in Layout.jsx.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ An error occurred during patching Layout.jsx queryKey:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -8,79 +8,74 @@ const __dirname = path.dirname(__filename);
|
|||||||
|
|
||||||
const projectRoot = path.join(__dirname, '..', 'frontend-web');
|
const projectRoot = path.join(__dirname, '..', 'frontend-web');
|
||||||
|
|
||||||
// --- Fonctions de Patch ---
|
// --- Patching Functions ---
|
||||||
|
|
||||||
function applyPatch(filePath, patchInfo) {
|
function applyPatch(filePath, patchInfo) {
|
||||||
const fullPath = path.join(projectRoot, filePath);
|
const fullPath = path.join(projectRoot, filePath);
|
||||||
if (!fs.existsSync(fullPath)) {
|
if (!fs.existsSync(fullPath)) {
|
||||||
console.warn(`🟡 Fichier non trouvé, patch ignoré : ${filePath}`);
|
console.warn(`🟡 File not found, patch skipped: ${filePath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fs.readFileSync(fullPath, 'utf8');
|
let content = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
|
||||||
if (content.includes(patchInfo.new_string)) {
|
if (patchInfo.new_string && content.includes(patchInfo.new_string)) {
|
||||||
console.log(`✅ Patch déjà appliqué dans ${filePath} (recherche de '${patchInfo.search_string}').`);
|
console.log(`✅ Patch already applied in ${filePath}.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content.includes(patchInfo.old_string)) {
|
if (patchInfo.old_string && content.includes(patchInfo.old_string)) {
|
||||||
content = content.replace(patchInfo.old_string, patchInfo.new_string);
|
content = content.replace(patchInfo.old_string, patchInfo.new_string);
|
||||||
fs.writeFileSync(fullPath, content, 'utf8');
|
fs.writeFileSync(fullPath, content, 'utf8');
|
||||||
console.log(`🟢 Patch appliqué dans ${filePath} (remplacement de '${patchInfo.search_string}').`);
|
console.log(`🟢 Patch applied in ${filePath}.`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`🔴 Impossible d'appliquer le patch dans ${filePath}. Chaîne non trouvée : '${patchInfo.search_string}'.`);
|
console.error(`🔴 Could not apply patch in ${filePath}. String not found.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Définition des Patches ---
|
// --- Global Import Fix ---
|
||||||
|
|
||||||
const patches = [
|
function fixAllComponentImports(directory) {
|
||||||
{
|
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
||||||
file: 'src/api/base44Client.js',
|
|
||||||
search_string: 'createClient',
|
|
||||||
old_string: `import { createClient } from '@base44/sdk';
|
|
||||||
// import { getAccessToken } from '@base44/sdk/utils/auth-utils';
|
|
||||||
|
|
||||||
// Create a client with authentication required
|
for (const entry of entries) {
|
||||||
export const base44 = createClient({
|
const fullPath = path.join(directory, entry.name);
|
||||||
appId: "68fc6cf01386035c266e7a5d",
|
if (entry.isDirectory()) {
|
||||||
requiresAuth: true // Ensure authentication is required for all operations
|
// Recursively search in subdirectories
|
||||||
});`,
|
fixAllComponentImports(fullPath);
|
||||||
new_string: `// import { createClient } from '@base44/sdk';
|
} else if (entry.isFile() && (entry.name.endsWith('.jsx') || entry.name.endsWith('.js'))) {
|
||||||
|
let content = fs.readFileSync(fullPath, 'utf8');
|
||||||
|
const originalContent = content;
|
||||||
|
|
||||||
// --- MIGRATION MOCK ---
|
// Regex to find all relative imports to the components directory
|
||||||
// This mock completely disables the Base44 SDK to allow for local development.
|
// Handles: from "./components/", from "../components/", from "../../components/", etc.
|
||||||
export const base44 = {
|
const importRegex = /from\s+(['"])((\.\.\/)+|\.\/)components\//g;
|
||||||
auth: {
|
|
||||||
me: () => Promise.resolve(null),
|
|
||||||
logout: () => {},
|
|
||||||
},
|
|
||||||
entities: {
|
|
||||||
ActivityLog: {
|
|
||||||
filter: () => Promise.resolve([]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'src/main.jsx',
|
|
||||||
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'
|
|
||||||
import '@/index.css'
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
content = content.replace(importRegex, 'from $1@/components/');
|
||||||
|
|
||||||
|
if (content !== originalContent) {
|
||||||
|
console.log(`✅ Fixing component imports in ${fullPath}`);
|
||||||
|
fs.writeFileSync(fullPath, content, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Exécution ---
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
console.log('--- Applying patches for local environment ---');
|
||||||
|
|
||||||
|
// The specific patches are now less critical as the global fix is more robust,
|
||||||
|
// but we can keep them for specific, non-import related changes.
|
||||||
|
const patches = [
|
||||||
|
{
|
||||||
|
file: 'src/main.jsx',
|
||||||
|
old_string: `ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<App />
|
<App />
|
||||||
)`,
|
)`,
|
||||||
new_string: `import React from 'react'
|
new_string: `import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import App from '@/App.jsx'
|
|
||||||
import '@/index.css'
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -90,17 +85,15 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
|||||||
<App />
|
<App />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)`
|
||||||
`
|
},
|
||||||
},
|
{
|
||||||
{
|
file: 'src/pages/Layout.jsx',
|
||||||
file: 'src/pages/Layout.jsx',
|
old_string: `const { data: user } = useQuery({
|
||||||
search_string: `const { data: user } = useQuery`,
|
|
||||||
old_string: ` const { data: user } = useQuery({
|
|
||||||
queryKey: ['current-user-layout'],
|
queryKey: ['current-user-layout'],
|
||||||
queryFn: () => base44.auth.me(),
|
queryFn: () => base44.auth.me(),
|
||||||
});`,
|
});`,
|
||||||
new_string: ` // const { data: user } = useQuery({
|
new_string: ` // const { data: user } = useQuery({
|
||||||
// queryKey: ['current-user-layout'],
|
// queryKey: ['current-user-layout'],
|
||||||
// queryFn: () => base44.auth.me(),
|
// queryFn: () => base44.auth.me(),
|
||||||
// });
|
// });
|
||||||
@@ -113,90 +106,17 @@ ReactDOM.createRoot(document.getElementById('root')).render(
|
|||||||
profile_picture: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
profile_picture: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
|
||||||
};
|
};
|
||||||
`
|
`
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'src/pages/Layout.jsx',
|
|
||||||
search_string: `const { data: unreadCount = 0 } = useQuery`,
|
|
||||||
old_string: ` const { data: unreadCount = 0 } = useQuery({
|
|
||||||
queryKey: ['unread-notifications', user?.id],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!user?.id) return 0;
|
|
||||||
const notifications = await base44.entities.ActivityLog.filter({
|
|
||||||
user_id: user?.id,
|
|
||||||
is_read: false
|
|
||||||
});
|
|
||||||
return notifications.length;
|
|
||||||
},
|
|
||||||
enabled: !!user?.id,
|
|
||||||
initialData: 0,
|
|
||||||
refetchInterval: 10000,
|
|
||||||
});`,
|
|
||||||
new_string: ` // Get unread notification count
|
|
||||||
// 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 => {
|
patches.forEach(patchInfo => {
|
||||||
applyPatch(patchInfo.file, patchInfo);
|
applyPatch(patchInfo.file, patchInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('--- Correction globale des imports de composants ---');
|
console.log('--- Global component import fixes ---');
|
||||||
fixComponentImports(path.join(projectRoot, 'src'));
|
fixAllComponentImports(path.join(projectRoot, 'src'));
|
||||||
|
|
||||||
console.log('--- Fin de l\'application des patchs ---');
|
console.log('--- End of patching process ---');
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
Reference in New Issue
Block a user