439 lines
16 KiB
TypeScript
439 lines
16 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { AuthenticatedLayout } from '../layout/AuthenticatedLayout';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { Badge } from '../ui/badge';
|
|
import { Input } from '../ui/input';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '../ui/table';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '../ui/select';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '../ui/dropdown-menu';
|
|
import { Checkbox } from '../ui/checkbox';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from '../ui/alert-dialog';
|
|
import { toast } from "sonner";
|
|
import {
|
|
Plus,
|
|
Download,
|
|
MoreHorizontal,
|
|
Eye,
|
|
ExternalLink,
|
|
Trash2,
|
|
Upload,
|
|
Camera,
|
|
MapPin,
|
|
Calendar,
|
|
Activity,
|
|
Filter
|
|
} from 'lucide-react';
|
|
import { Route } from '../../types/routes';
|
|
|
|
interface Facilities360Props {
|
|
onNavigate: (route: Route) => void;
|
|
onLogout: () => void;
|
|
user: any;
|
|
}
|
|
|
|
// Mock data for 360 tours
|
|
const mock360Tours = [
|
|
{
|
|
id: 'tour-1',
|
|
title: 'Main Campus Building',
|
|
photoCount: 24,
|
|
sequenceCount: 2,
|
|
lastUpdated: '2024-01-15',
|
|
publishStatus: 'Published',
|
|
views: 1247,
|
|
place: 'Kautilya Leadership Centre, New Delhi',
|
|
description: 'Complete tour of the main academic building including classrooms, library, and common areas'
|
|
},
|
|
{
|
|
id: 'tour-2',
|
|
title: 'Executive Training Center',
|
|
photoCount: 18,
|
|
sequenceCount: 1,
|
|
lastUpdated: '2024-01-12',
|
|
publishStatus: 'Pending',
|
|
views: 0,
|
|
place: 'KLC Executive Center, Gurugram',
|
|
description: 'Executive training facilities and boardrooms'
|
|
},
|
|
{
|
|
id: 'tour-3',
|
|
title: 'Library & Study Areas',
|
|
photoCount: 15,
|
|
sequenceCount: 0,
|
|
lastUpdated: '2024-01-10',
|
|
publishStatus: 'Rejected',
|
|
views: 45,
|
|
place: 'Kautilya Leadership Centre, New Delhi',
|
|
description: 'Digital library, reading rooms, and collaborative study spaces'
|
|
},
|
|
{
|
|
id: 'tour-4',
|
|
title: 'Innovation Lab',
|
|
photoCount: 12,
|
|
sequenceCount: 1,
|
|
lastUpdated: '2024-01-08',
|
|
publishStatus: 'Published',
|
|
views: 892,
|
|
place: 'KLC Innovation Hub, Bangalore',
|
|
description: 'Technology lab with latest equipment and collaboration spaces'
|
|
}
|
|
];
|
|
|
|
export function Facilities360({ onNavigate, onLogout, user }: Facilities360Props) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
const [selectedTours, setSelectedTours] = useState<string[]>([]);
|
|
const [showBulkActions, setShowBulkActions] = useState(false);
|
|
|
|
const breadcrumbs = [
|
|
{ label: 'Facilities 360 Tour' }
|
|
];
|
|
|
|
const filteredTours = mock360Tours.filter(tour => {
|
|
const matchesSearch = tour.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
tour.place.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesStatus = statusFilter === 'all' || tour.publishStatus.toLowerCase() === statusFilter;
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
|
|
const getStatusVariant = (status: string) => {
|
|
switch (status.toLowerCase()) {
|
|
case 'published': return 'default';
|
|
case 'pending': return 'secondary';
|
|
case 'rejected': return 'destructive';
|
|
default: return 'outline';
|
|
}
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status.toLowerCase()) {
|
|
case 'published': return 'text-green-600';
|
|
case 'pending': return 'text-yellow-600';
|
|
case 'rejected': return 'text-red-600';
|
|
default: return 'text-gray-600';
|
|
}
|
|
};
|
|
|
|
const handleSelectTour = (tourId: string, checked: boolean) => {
|
|
if (checked) {
|
|
setSelectedTours(prev => [...prev, tourId]);
|
|
} else {
|
|
setSelectedTours(prev => prev.filter(id => id !== tourId));
|
|
}
|
|
};
|
|
|
|
const handleSelectAll = (checked: boolean) => {
|
|
if (checked) {
|
|
setSelectedTours(filteredTours.map(tour => tour.id));
|
|
} else {
|
|
setSelectedTours([]);
|
|
}
|
|
};
|
|
|
|
const handleBulkPublish = () => {
|
|
toast.success(`Publishing ${selectedTours.length} tours to Google Maps`, {
|
|
duration: 3000
|
|
});
|
|
setSelectedTours([]);
|
|
setShowBulkActions(false);
|
|
};
|
|
|
|
const handleBulkUnpublish = () => {
|
|
toast.success(`Unpublishing ${selectedTours.length} tours from Google Maps`, {
|
|
duration: 3000
|
|
});
|
|
setSelectedTours([]);
|
|
setShowBulkActions(false);
|
|
};
|
|
|
|
const handleBulkDelete = () => {
|
|
toast.success(`Deleted ${selectedTours.length} tours`, {
|
|
duration: 3000
|
|
});
|
|
setSelectedTours([]);
|
|
setShowBulkActions(false);
|
|
};
|
|
|
|
const handleImportFromStreetView = () => {
|
|
toast.info('Import from Street View feature coming soon', {
|
|
duration: 2000
|
|
});
|
|
};
|
|
|
|
const renderEmptyState = () => (
|
|
<div className="text-center py-12">
|
|
<div className="mx-auto w-24 h-24 bg-muted rounded-full flex items-center justify-center mb-4">
|
|
<Camera className="h-12 w-12 text-muted-foreground" />
|
|
</div>
|
|
<h3 className="text-lg font-medium mb-2">No tours yet</h3>
|
|
<p className="text-muted-foreground mb-6">Create your first 360° facility tour to get started</p>
|
|
<Button
|
|
onClick={() => onNavigate('/facilities-360/new')}
|
|
className="min-h-[44px]"
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
New Tour
|
|
</Button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<AuthenticatedLayout
|
|
currentRoute="/facilities-360"
|
|
onNavigate={onNavigate}
|
|
onLogout={onLogout}
|
|
user={user}
|
|
breadcrumbs={breadcrumbs}
|
|
>
|
|
<div className="p-6 space-y-6 max-w-[1440px] mx-auto">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1>Facilities 360 Tour</h1>
|
|
<p className="text-muted-foreground mt-1">
|
|
Create, manage, and publish immersive 360° facility tours
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleImportFromStreetView}
|
|
className="min-h-[44px]"
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Import from Street View
|
|
</Button>
|
|
<Button
|
|
onClick={() => onNavigate('/facilities-360/new')}
|
|
className="min-h-[44px]"
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
New Tour
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{filteredTours.length === 0 && searchTerm === '' && statusFilter === 'all' ? (
|
|
renderEmptyState()
|
|
) : (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Tours Library</CardTitle>
|
|
<div className="flex items-center gap-4 pt-4">
|
|
<div className="flex-1">
|
|
<Input
|
|
placeholder="Search tours or places..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="max-w-sm min-h-[44px]"
|
|
/>
|
|
</div>
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-[140px] min-h-[44px]">
|
|
<SelectValue placeholder="Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Status</SelectItem>
|
|
<SelectItem value="published">Published</SelectItem>
|
|
<SelectItem value="pending">Pending</SelectItem>
|
|
<SelectItem value="rejected">Rejected</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{selectedTours.length > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleBulkPublish}
|
|
className="min-h-[44px]"
|
|
>
|
|
Publish ({selectedTours.length})
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleBulkUnpublish}
|
|
className="min-h-[44px]"
|
|
>
|
|
Unpublish
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
className="min-h-[44px]"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-1" />
|
|
Delete
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete Selected Tours</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to delete {selectedTours.length} selected tours?
|
|
This action cannot be undone and will remove the tours from Google Maps.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleBulkDelete}>
|
|
Delete Tours
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
{filteredTours.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No tours found matching your criteria.
|
|
</div>
|
|
) : (
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader className="bg-muted/50">
|
|
<TableRow>
|
|
<TableHead className="w-12">
|
|
<Checkbox
|
|
checked={selectedTours.length === filteredTours.length}
|
|
onCheckedChange={handleSelectAll}
|
|
aria-label="Select all tours"
|
|
/>
|
|
</TableHead>
|
|
<TableHead className="min-w-[250px]">Tour Title</TableHead>
|
|
<TableHead>Photos • Sequences</TableHead>
|
|
<TableHead>Last Updated</TableHead>
|
|
<TableHead>Publish Status</TableHead>
|
|
<TableHead>Views</TableHead>
|
|
<TableHead className="w-12">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredTours.map((tour) => (
|
|
<TableRow key={tour.id}>
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedTours.includes(tour.id)}
|
|
onCheckedChange={(checked) => handleSelectTour(tour.id, checked as boolean)}
|
|
aria-label={`Select ${tour.title}`}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div>
|
|
<div className="font-medium">{tour.title}</div>
|
|
<div className="text-sm text-muted-foreground flex items-center gap-1 mt-1">
|
|
<MapPin className="h-3 w-3" />
|
|
{tour.place}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground mt-1">
|
|
{tour.description}
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-4 text-sm">
|
|
<div className="flex items-center gap-1">
|
|
<Camera className="h-4 w-4 text-muted-foreground" />
|
|
{tour.photoCount}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
|
{tour.sequenceCount}
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-1 text-sm">
|
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
{tour.lastUpdated}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={getStatusVariant(tour.publishStatus)}>
|
|
{tour.publishStatus}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="text-sm font-medium">
|
|
{tour.views.toLocaleString()}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem
|
|
onClick={() => onNavigate(`/facilities-360/${tour.id}`)}
|
|
>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
Open
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<ExternalLink className="h-4 w-4 mr-2" />
|
|
Preview
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<Upload className="h-4 w-4 mr-2" />
|
|
{tour.publishStatus === 'Published' ? 'Unpublish' : 'Publish'}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive">
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
} |