mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 09:35:44 +00:00
Updated the passenger web portal and added more endpoints to the api
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { ReactNode, useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ChevronUp, ChevronDown, MoreHorizontal } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import ActionButton from './ActionButton';
|
||||
@@ -42,6 +43,22 @@ export default function DataTable<T extends Record<string, any>>({
|
||||
}: DataTableProps<T>) {
|
||||
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' } | null>(null);
|
||||
const [expandedActions, setExpandedActions] = useState<string | null>(null);
|
||||
const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
const buttonRefs = useRef<Record<string, HTMLButtonElement | null>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
const isButtonClick = Object.values(buttonRefs.current).some(ref => ref?.contains(target));
|
||||
const isDropdownClick = document.querySelector('[data-dropdown-menu]')?.contains(target);
|
||||
|
||||
if (expandedActions && !isButtonClick && !isDropdownClick) {
|
||||
setExpandedActions(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [expandedActions]);
|
||||
|
||||
// Ensure data is always an array
|
||||
const safeData = Array.isArray(data) ? data : [];
|
||||
@@ -81,7 +98,7 @@ export default function DataTable<T extends Record<string, any>>({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('card p-0 overflow-visible', className)}>
|
||||
<div className={cn('card p-0', className)}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
@@ -145,66 +162,45 @@ export default function DataTable<T extends Record<string, any>>({
|
||||
))}
|
||||
{actions && actions.length > 0 && (
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="relative">
|
||||
{(() => {
|
||||
const visibleActions = actions.filter(action => !action.show || action.show(item));
|
||||
|
||||
if (visibleActions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (visibleActions.length === 1) {
|
||||
const action = visibleActions[0];
|
||||
return (
|
||||
<ActionButton
|
||||
onClick={() => action.onClick(item)}
|
||||
variant={action.variant || 'secondary'}
|
||||
size="sm"
|
||||
icon={action.icon}
|
||||
>
|
||||
{action.label}
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
|
||||
{(() => {
|
||||
const visibleActions = actions.filter(action => !action.show || action.show(item));
|
||||
|
||||
if (visibleActions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (visibleActions.length === 1) {
|
||||
const action = visibleActions[0];
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedActions(expandedActions === item.id ? null : item.id);
|
||||
}}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
{expandedActions === item.id && (
|
||||
<div className="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
|
||||
<div className="py-1">
|
||||
{visibleActions.map((action, actionIndex) => (
|
||||
<button
|
||||
key={actionIndex}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
action.onClick(item);
|
||||
setExpandedActions(null);
|
||||
}}
|
||||
className={cn(
|
||||
'w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
|
||||
action.variant === 'danger' && 'text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20'
|
||||
)}
|
||||
>
|
||||
{action.icon && <action.icon className="h-4 w-4" />}
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<ActionButton
|
||||
onClick={() => action.onClick(item)}
|
||||
variant={action.variant || 'secondary'}
|
||||
size="sm"
|
||||
icon={action.icon}
|
||||
>
|
||||
{action.label}
|
||||
</ActionButton>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={(el) => { buttonRefs.current[item.id] = el; }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
setDropdownPosition({
|
||||
top: rect.bottom + window.scrollY,
|
||||
left: rect.right + window.scrollX - 192, // 192px = w-48
|
||||
});
|
||||
setExpandedActions(expandedActions === item.id ? null : item.id);
|
||||
}}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
@@ -218,6 +214,47 @@ export default function DataTable<T extends Record<string, any>>({
|
||||
{emptyMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{expandedActions && dropdownPosition && typeof window !== 'undefined' && createPortal(
|
||||
<div
|
||||
data-dropdown-menu
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${dropdownPosition.top}px`,
|
||||
left: `${dropdownPosition.left}px`,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
className="w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="py-1">
|
||||
{actions
|
||||
?.filter(action => !action.show || action.show(sortedData.find(item => item.id === expandedActions)!))
|
||||
.map((action, actionIndex) => {
|
||||
const item = sortedData.find(item => item.id === expandedActions);
|
||||
if (!item) return null;
|
||||
return (
|
||||
<button
|
||||
key={actionIndex}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setExpandedActions(null);
|
||||
action.onClick(item);
|
||||
}}
|
||||
className={cn(
|
||||
'w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
|
||||
action.variant === 'danger' && 'text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20'
|
||||
)}
|
||||
>
|
||||
{action.icon && <action.icon className="h-4 w-4" />}
|
||||
{action.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user