985 lines
35 KiB
TypeScript
985 lines
35 KiB
TypeScript
import { json } from "@remix-run/node";
|
|
import { useLoaderData } from "@remix-run/react";
|
|
import { useState, useEffect } from "react";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle
|
|
} from "~/components/ui/card";
|
|
import { Badge } from "~/components/ui/badge";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Progress } from "~/components/ui/progress";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
|
import { Input } from "~/components/ui/input";
|
|
import { Label } from "~/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue
|
|
} from "~/components/ui/select";
|
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
|
import { Separator } from "~/components/ui/separator";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger
|
|
} from "~/components/ui/dialog";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger
|
|
} from "~/components/ui/alert-dialog";
|
|
import {
|
|
Truck,
|
|
MapPin,
|
|
Clock,
|
|
CheckCircle,
|
|
AlertTriangle,
|
|
XCircle,
|
|
RotateCcw,
|
|
Users,
|
|
Target,
|
|
TrendingUp,
|
|
Calendar,
|
|
RefreshCw,
|
|
Phone,
|
|
MessageSquare,
|
|
Plus,
|
|
Edit,
|
|
Trash2,
|
|
Navigation,
|
|
Package,
|
|
Timer,
|
|
Zap
|
|
} from "lucide-react";
|
|
|
|
// Interfaces
|
|
interface CollectionArea {
|
|
id: string;
|
|
name: string;
|
|
zone: string;
|
|
priority: "high" | "medium" | "low";
|
|
status: "pending" | "in-progress" | "completed" | "overdue" | "cancelled";
|
|
scheduledTime: string;
|
|
estimatedDuration: number; // minutes
|
|
actualStartTime?: string;
|
|
completedTime?: string;
|
|
assignedTruck: string;
|
|
assignedDriver: string;
|
|
driverContact: string;
|
|
estimatedVolume: number; // kg
|
|
actualVolume?: number; // kg
|
|
households: number;
|
|
lastCollection: string;
|
|
notes?: string;
|
|
coordinates: { lat: number; lng: number };
|
|
urgentIssues?: string[];
|
|
}
|
|
|
|
interface DailyStats {
|
|
totalAreas: number;
|
|
completedAreas: number;
|
|
inProgressAreas: number;
|
|
overdueAreas: number;
|
|
totalTargetVolume: number;
|
|
collectedVolume: number;
|
|
activeTrucks: number;
|
|
availableTrucks: number;
|
|
estimatedCompletion: string;
|
|
}
|
|
|
|
interface TruckStatus {
|
|
id: string;
|
|
driver: string;
|
|
contact: string;
|
|
status: "active" | "available" | "maintenance" | "break";
|
|
currentLocation?: string;
|
|
currentCapacity: number; // percentage
|
|
assignedAreas: string[];
|
|
lastUpdate: string;
|
|
}
|
|
|
|
interface CollectionData {
|
|
date: string;
|
|
stats: DailyStats;
|
|
areas: CollectionArea[];
|
|
trucks: TruckStatus[];
|
|
emergencyContacts: {
|
|
supervisor: string;
|
|
dispatcher: string;
|
|
maintenance: string;
|
|
};
|
|
}
|
|
|
|
export const loader = async (): Promise<Response> => {
|
|
// Mock data - dalam implementasi nyata, ambil dari database
|
|
const today = new Date().toISOString().split("T")[0];
|
|
|
|
const collectionData: CollectionData = {
|
|
date: today,
|
|
stats: {
|
|
totalAreas: 18,
|
|
completedAreas: 12,
|
|
inProgressAreas: 4,
|
|
overdueAreas: 2,
|
|
totalTargetVolume: 4500,
|
|
collectedVolume: 3200,
|
|
activeTrucks: 6,
|
|
availableTrucks: 2,
|
|
estimatedCompletion: "16:30"
|
|
},
|
|
areas: [
|
|
{
|
|
id: "area-001",
|
|
name: "Kelurahan Merdeka Blok A",
|
|
zone: "Zona Utara",
|
|
priority: "high",
|
|
status: "overdue",
|
|
scheduledTime: "07:00",
|
|
estimatedDuration: 90,
|
|
assignedTruck: "B-001",
|
|
assignedDriver: "Budi Santoso",
|
|
driverContact: "081234567890",
|
|
estimatedVolume: 280,
|
|
actualVolume: 245,
|
|
households: 120,
|
|
lastCollection: "2025-07-05",
|
|
coordinates: { lat: -6.2088, lng: 106.8456 },
|
|
urgentIssues: ["Jalan rusak", "Akses terbatas"],
|
|
actualStartTime: "07:15",
|
|
completedTime: "09:00"
|
|
},
|
|
{
|
|
id: "area-002",
|
|
name: "Komplek Permata Indah",
|
|
zone: "Zona Selatan",
|
|
priority: "medium",
|
|
status: "in-progress",
|
|
scheduledTime: "08:30",
|
|
estimatedDuration: 60,
|
|
assignedTruck: "B-003",
|
|
assignedDriver: "Sari Dewi",
|
|
driverContact: "081234567891",
|
|
estimatedVolume: 180,
|
|
households: 85,
|
|
lastCollection: "2025-07-05",
|
|
coordinates: { lat: -6.22, lng: 106.83 },
|
|
actualStartTime: "08:45"
|
|
},
|
|
{
|
|
id: "area-003",
|
|
name: "Jl. Sudirman Raya",
|
|
zone: "Zona Tengah",
|
|
priority: "high",
|
|
status: "overdue",
|
|
scheduledTime: "09:00",
|
|
estimatedDuration: 120,
|
|
assignedTruck: "B-004",
|
|
assignedDriver: "Dedi Kurniawan",
|
|
driverContact: "081234567892",
|
|
estimatedVolume: 350,
|
|
households: 200,
|
|
lastCollection: "2025-07-04",
|
|
coordinates: { lat: -6.215, lng: 106.84 },
|
|
urgentIssues: ["Volume sangat tinggi", "Kemacetan akses"],
|
|
notes: "Perlu 2 trip untuk mengangkut semua"
|
|
},
|
|
{
|
|
id: "area-004",
|
|
name: "Perumahan Indah Permai",
|
|
zone: "Zona Timur",
|
|
priority: "medium",
|
|
status: "completed",
|
|
scheduledTime: "10:00",
|
|
estimatedDuration: 75,
|
|
assignedTruck: "B-002",
|
|
assignedDriver: "Andi Wijaya",
|
|
driverContact: "081234567893",
|
|
estimatedVolume: 200,
|
|
actualVolume: 195,
|
|
households: 95,
|
|
lastCollection: "2025-07-05",
|
|
coordinates: { lat: -6.195, lng: 106.86 },
|
|
actualStartTime: "10:15",
|
|
completedTime: "11:30"
|
|
},
|
|
{
|
|
id: "area-005",
|
|
name: "Pasar Tradisional Sentral",
|
|
zone: "Zona Tengah",
|
|
priority: "high",
|
|
status: "in-progress",
|
|
scheduledTime: "11:30",
|
|
estimatedDuration: 45,
|
|
assignedTruck: "B-005",
|
|
assignedDriver: "Rini Astuti",
|
|
driverContact: "081234567894",
|
|
estimatedVolume: 420,
|
|
households: 50,
|
|
lastCollection: "2025-07-05",
|
|
coordinates: { lat: -6.21, lng: 106.835 },
|
|
actualStartTime: "11:45",
|
|
notes: "Sampah organik tinggi dari pasar"
|
|
},
|
|
{
|
|
id: "area-006",
|
|
name: "Cluster Villa Harmoni",
|
|
zone: "Zona Barat",
|
|
priority: "low",
|
|
status: "pending",
|
|
scheduledTime: "13:00",
|
|
estimatedDuration: 60,
|
|
assignedTruck: "B-006",
|
|
assignedDriver: "Toni Setiawan",
|
|
driverContact: "081234567895",
|
|
estimatedVolume: 150,
|
|
households: 75,
|
|
lastCollection: "2025-07-05",
|
|
coordinates: { lat: -6.205, lng: 106.82 }
|
|
}
|
|
],
|
|
trucks: [
|
|
{
|
|
id: "B-001",
|
|
driver: "Budi Santoso",
|
|
contact: "081234567890",
|
|
status: "active",
|
|
currentLocation: "Kelurahan Merdeka",
|
|
currentCapacity: 85,
|
|
assignedAreas: ["area-001"],
|
|
lastUpdate: "10 menit yang lalu"
|
|
},
|
|
{
|
|
id: "B-002",
|
|
driver: "Andi Wijaya",
|
|
contact: "081234567893",
|
|
status: "available",
|
|
currentLocation: "Pool Kendaraan",
|
|
currentCapacity: 0,
|
|
assignedAreas: ["area-004"],
|
|
lastUpdate: "5 menit yang lalu"
|
|
},
|
|
{
|
|
id: "B-003",
|
|
driver: "Sari Dewi",
|
|
contact: "081234567891",
|
|
status: "active",
|
|
currentLocation: "Komplek Permata",
|
|
currentCapacity: 60,
|
|
assignedAreas: ["area-002"],
|
|
lastUpdate: "2 menit yang lalu"
|
|
},
|
|
{
|
|
id: "B-004",
|
|
driver: "Dedi Kurniawan",
|
|
contact: "081234567892",
|
|
status: "active",
|
|
currentLocation: "Dalam perjalanan",
|
|
currentCapacity: 0,
|
|
assignedAreas: ["area-003"],
|
|
lastUpdate: "15 menit yang lalu"
|
|
},
|
|
{
|
|
id: "B-005",
|
|
driver: "Rini Astuti",
|
|
contact: "081234567894",
|
|
status: "active",
|
|
currentLocation: "Pasar Sentral",
|
|
currentCapacity: 40,
|
|
assignedAreas: ["area-005"],
|
|
lastUpdate: "1 menit yang lalu"
|
|
},
|
|
{
|
|
id: "B-006",
|
|
driver: "Toni Setiawan",
|
|
contact: "081234567895",
|
|
status: "available",
|
|
currentLocation: "Pool Kendaraan",
|
|
currentCapacity: 0,
|
|
assignedAreas: ["area-006"],
|
|
lastUpdate: "30 menit yang lalu"
|
|
}
|
|
],
|
|
emergencyContacts: {
|
|
supervisor: "081234560001",
|
|
dispatcher: "081234560002",
|
|
maintenance: "081234560003"
|
|
}
|
|
};
|
|
|
|
return json(collectionData);
|
|
};
|
|
|
|
export default function PengumpulanHarian() {
|
|
const data = useLoaderData<CollectionData>();
|
|
const [selectedArea, setSelectedArea] = useState<CollectionArea | null>(null);
|
|
const [viewMode, setViewMode] = useState<"list" | "map">("list");
|
|
const [filterStatus, setFilterStatus] = useState<string>("all");
|
|
const [filterPriority, setFilterPriority] = useState<string>("all");
|
|
|
|
// Filter areas
|
|
const filteredAreas = data.areas.filter((area) => {
|
|
const statusMatch = filterStatus === "all" || area.status === filterStatus;
|
|
const priorityMatch =
|
|
filterPriority === "all" || area.priority === filterPriority;
|
|
return statusMatch && priorityMatch;
|
|
});
|
|
|
|
// Calculate progress percentage
|
|
const progressPercentage = Math.round(
|
|
(data.stats.completedAreas / data.stats.totalAreas) * 100
|
|
);
|
|
const volumePercentage = Math.round(
|
|
(data.stats.collectedVolume / data.stats.totalTargetVolume) * 100
|
|
);
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case "completed":
|
|
return (
|
|
<Badge variant="default" className="bg-green-100 text-green-800">
|
|
Selesai
|
|
</Badge>
|
|
);
|
|
case "in-progress":
|
|
return (
|
|
<Badge variant="default" className="bg-blue-100 text-blue-800">
|
|
Berlangsung
|
|
</Badge>
|
|
);
|
|
case "pending":
|
|
return <Badge variant="secondary">Menunggu</Badge>;
|
|
case "overdue":
|
|
return <Badge variant="destructive">Terlambat</Badge>;
|
|
case "cancelled":
|
|
return (
|
|
<Badge variant="outline" className="text-red-600">
|
|
Dibatalkan
|
|
</Badge>
|
|
);
|
|
default:
|
|
return <Badge variant="outline">{status}</Badge>;
|
|
}
|
|
};
|
|
|
|
const getPriorityBadge = (priority: string) => {
|
|
switch (priority) {
|
|
case "high":
|
|
return <Badge variant="destructive">Tinggi</Badge>;
|
|
case "medium":
|
|
return (
|
|
<Badge variant="default" className="bg-yellow-100 text-yellow-800">
|
|
Sedang
|
|
</Badge>
|
|
);
|
|
case "low":
|
|
return <Badge variant="secondary">Rendah</Badge>;
|
|
default:
|
|
return <Badge variant="outline">{priority}</Badge>;
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case "completed":
|
|
return <CheckCircle className="h-4 w-4 text-green-600" />;
|
|
case "in-progress":
|
|
return <RefreshCw className="h-4 w-4 text-blue-600 animate-spin" />;
|
|
case "pending":
|
|
return <Clock className="h-4 w-4 text-gray-600" />;
|
|
case "overdue":
|
|
return <AlertTriangle className="h-4 w-4 text-red-600" />;
|
|
case "cancelled":
|
|
return <XCircle className="h-4 w-4 text-red-600" />;
|
|
default:
|
|
return <Package className="h-4 w-4 text-gray-600" />;
|
|
}
|
|
};
|
|
|
|
const getTruckStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case "active":
|
|
return (
|
|
<Badge variant="default" className="bg-green-100 text-green-800">
|
|
Aktif
|
|
</Badge>
|
|
);
|
|
case "available":
|
|
return <Badge variant="secondary">Tersedia</Badge>;
|
|
case "maintenance":
|
|
return (
|
|
<Badge variant="default" className="bg-yellow-100 text-yellow-800">
|
|
Maintenance
|
|
</Badge>
|
|
);
|
|
case "break":
|
|
return <Badge variant="outline">Istirahat</Badge>;
|
|
default:
|
|
return <Badge variant="outline">{status}</Badge>;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex-1 space-y-4 p-4 pt-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-3xl font-bold tracking-tight flex items-center gap-2">
|
|
Pengumpulan Harian
|
|
<Badge variant="destructive" className="animate-pulse">
|
|
<Zap className="h-3 w-3 mr-1" />
|
|
URGENT
|
|
</Badge>
|
|
</h2>
|
|
<p className="text-muted-foreground">
|
|
Monitoring dan koordinasi pengumpulan sampah -{" "}
|
|
{new Date(data.date).toLocaleDateString("id-ID", {
|
|
weekday: "long",
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric"
|
|
})}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button variant="outline" size="sm">
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
Refresh
|
|
</Button>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="destructive" size="sm">
|
|
<AlertTriangle className="mr-2 h-4 w-4" />
|
|
Emergency
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Emergency Response</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Hubungi kontak darurat untuk situasi mendesak
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between p-3 border rounded">
|
|
<div>
|
|
<p className="font-medium">Supervisor</p>
|
|
<p className="text-sm text-gray-500">
|
|
{data.emergencyContacts.supervisor}
|
|
</p>
|
|
</div>
|
|
<Button size="sm">
|
|
<Phone className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 border rounded">
|
|
<div>
|
|
<p className="font-medium">Dispatcher</p>
|
|
<p className="text-sm text-gray-500">
|
|
{data.emergencyContacts.dispatcher}
|
|
</p>
|
|
</div>
|
|
<Button size="sm">
|
|
<Phone className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center justify-between p-3 border rounded">
|
|
<div>
|
|
<p className="font-medium">Maintenance</p>
|
|
<p className="text-sm text-gray-500">
|
|
{data.emergencyContacts.maintenance}
|
|
</p>
|
|
</div>
|
|
<Button size="sm">
|
|
<Phone className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Tutup</AlertDialogCancel>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Tambah Area
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
Progress Harian
|
|
</CardTitle>
|
|
<Target className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{data.stats.completedAreas}/{data.stats.totalAreas}
|
|
</div>
|
|
<Progress value={progressPercentage} className="mt-2" />
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
{progressPercentage}% area selesai • Est. selesai{" "}
|
|
{data.stats.estimatedCompletion}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
Volume Terkumpul
|
|
</CardTitle>
|
|
<Package className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{data.stats.collectedVolume.toLocaleString()} kg
|
|
</div>
|
|
<Progress value={volumePercentage} className="mt-2" />
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
{volumePercentage}% dari target (
|
|
{data.stats.totalTargetVolume.toLocaleString()} kg)
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Status Truk</CardTitle>
|
|
<Truck className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{data.stats.activeTrucks}/
|
|
{data.stats.activeTrucks + data.stats.availableTrucks}
|
|
</div>
|
|
<div className="flex items-center space-x-2 mt-2">
|
|
<div className="flex items-center space-x-1">
|
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
<span className="text-xs">{data.stats.activeTrucks} Aktif</span>
|
|
</div>
|
|
<div className="flex items-center space-x-1">
|
|
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
|
|
<span className="text-xs">
|
|
{data.stats.availableTrucks} Tersedia
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
Area Terlambat
|
|
</CardTitle>
|
|
<AlertTriangle className="h-4 w-4 text-red-500" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-red-600">
|
|
{data.stats.overdueAreas}
|
|
</div>
|
|
<div className="text-xs text-red-600 mt-2 font-medium">
|
|
Perlu penanganan segera
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{data.stats.inProgressAreas} area sedang dikerjakan
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Filters and View Toggle */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Label htmlFor="status-filter">Status:</Label>
|
|
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
|
<SelectTrigger className="w-[120px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Semua</SelectItem>
|
|
<SelectItem value="pending">Menunggu</SelectItem>
|
|
<SelectItem value="in-progress">Berlangsung</SelectItem>
|
|
<SelectItem value="completed">Selesai</SelectItem>
|
|
<SelectItem value="overdue">Terlambat</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Label htmlFor="priority-filter">Prioritas:</Label>
|
|
<Select value={filterPriority} onValueChange={setFilterPriority}>
|
|
<SelectTrigger className="w-[120px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Semua</SelectItem>
|
|
<SelectItem value="high">Tinggi</SelectItem>
|
|
<SelectItem value="medium">Sedang</SelectItem>
|
|
<SelectItem value="low">Rendah</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Button
|
|
variant={viewMode === "list" ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setViewMode("list")}
|
|
>
|
|
List
|
|
</Button>
|
|
<Button
|
|
variant={viewMode === "map" ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setViewMode("map")}
|
|
>
|
|
Map
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<Tabs defaultValue="areas" className="space-y-4">
|
|
<TabsList>
|
|
<TabsTrigger value="areas">Area Pengumpulan</TabsTrigger>
|
|
<TabsTrigger value="trucks">Status Truk</TabsTrigger>
|
|
<TabsTrigger value="timeline">Timeline Hari Ini</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="areas" className="space-y-4">
|
|
<div className="grid gap-4">
|
|
{filteredAreas.map((area) => (
|
|
<Card
|
|
key={area.id}
|
|
className={`${
|
|
area.status === "overdue" ? "border-red-200 bg-red-50" : ""
|
|
}`}
|
|
>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
{getStatusIcon(area.status)}
|
|
<div>
|
|
<CardTitle className="text-lg">{area.name}</CardTitle>
|
|
<CardDescription className="flex items-center space-x-2">
|
|
<span>{area.zone}</span>
|
|
<span>•</span>
|
|
<span>{area.households} rumah</span>
|
|
<span>•</span>
|
|
<span>Truk {area.assignedTruck}</span>
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{getPriorityBadge(area.priority)}
|
|
{getStatusBadge(area.status)}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<div className="text-sm font-medium mb-2">
|
|
Jadwal & Progress
|
|
</div>
|
|
<div className="space-y-1 text-sm">
|
|
<div className="flex items-center space-x-2">
|
|
<Clock className="h-3 w-3" />
|
|
<span>Target: {area.scheduledTime}</span>
|
|
</div>
|
|
{area.actualStartTime && (
|
|
<div className="flex items-center space-x-2">
|
|
<Timer className="h-3 w-3" />
|
|
<span>Mulai: {area.actualStartTime}</span>
|
|
</div>
|
|
)}
|
|
{area.completedTime && (
|
|
<div className="flex items-center space-x-2">
|
|
<CheckCircle className="h-3 w-3 text-green-600" />
|
|
<span>Selesai: {area.completedTime}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center space-x-2">
|
|
<Package className="h-3 w-3" />
|
|
<span>
|
|
{area.actualVolume
|
|
? `${area.actualVolume} kg`
|
|
: `Est. ${area.estimatedVolume} kg`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-sm font-medium mb-2">
|
|
Driver & Kontak
|
|
</div>
|
|
<div className="space-y-1 text-sm">
|
|
<div className="flex items-center space-x-2">
|
|
<Users className="h-3 w-3" />
|
|
<span>{area.assignedDriver}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Phone className="h-3 w-3" />
|
|
<span>{area.driverContact}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Truck className="h-3 w-3" />
|
|
<span>Truk {area.assignedTruck}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="text-sm font-medium mb-2">Actions</div>
|
|
<div className="flex items-center space-x-2">
|
|
<Button size="sm" variant="outline">
|
|
<Phone className="h-3 w-3 mr-1" />
|
|
Call
|
|
</Button>
|
|
<Button size="sm" variant="outline">
|
|
<MessageSquare className="h-3 w-3 mr-1" />
|
|
Chat
|
|
</Button>
|
|
<Button size="sm" variant="outline">
|
|
<Navigation className="h-3 w-3 mr-1" />
|
|
Track
|
|
</Button>
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm" variant="outline">
|
|
<Edit className="h-3 w-3 mr-1" />
|
|
Edit
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
Update Status - {area.name}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Update status pengumpulan dan informasi terkait
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="status">Status</Label>
|
|
<Select defaultValue={area.status}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="pending">
|
|
Menunggu
|
|
</SelectItem>
|
|
<SelectItem value="in-progress">
|
|
Berlangsung
|
|
</SelectItem>
|
|
<SelectItem value="completed">
|
|
Selesai
|
|
</SelectItem>
|
|
<SelectItem value="overdue">
|
|
Terlambat
|
|
</SelectItem>
|
|
<SelectItem value="cancelled">
|
|
Dibatalkan
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="volume">
|
|
Volume Aktual (kg)
|
|
</Label>
|
|
<Input
|
|
id="volume"
|
|
type="number"
|
|
defaultValue={
|
|
area.actualVolume || area.estimatedVolume
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="notes">Catatan</Label>
|
|
<Input
|
|
id="notes"
|
|
defaultValue={area.notes || ""}
|
|
placeholder="Tambahkan catatan..."
|
|
/>
|
|
</div>
|
|
<Button className="w-full">Update Status</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{area.urgentIssues && area.urgentIssues.length > 0 && (
|
|
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
|
<div className="flex items-center space-x-2 mb-2">
|
|
<AlertTriangle className="h-4 w-4 text-red-600" />
|
|
<span className="text-sm font-medium text-red-800">
|
|
Isu Mendesak:
|
|
</span>
|
|
</div>
|
|
<ul className="text-sm text-red-700 space-y-1">
|
|
{area.urgentIssues.map((issue, index) => (
|
|
<li key={index}>• {issue}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{area.notes && (
|
|
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded">
|
|
<div className="text-sm text-blue-800">
|
|
<strong>Catatan:</strong> {area.notes}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="trucks" className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{data.trucks.map((truck) => (
|
|
<Card key={truck.id}>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
<Truck className="h-6 w-6" />
|
|
<div>
|
|
<CardTitle>Truk {truck.id}</CardTitle>
|
|
<CardDescription>{truck.driver}</CardDescription>
|
|
</div>
|
|
</div>
|
|
{getTruckStatusBadge(truck.status)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm">Kapasitas:</span>
|
|
<span className="text-sm font-medium">
|
|
{truck.currentCapacity}%
|
|
</span>
|
|
</div>
|
|
<Progress value={truck.currentCapacity} />
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<MapPin className="h-4 w-4 text-gray-400" />
|
|
<span className="text-sm">{truck.currentLocation}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Clock className="h-4 w-4 text-gray-400" />
|
|
<span className="text-sm">
|
|
Update: {truck.lastUpdate}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2 pt-2">
|
|
<Button size="sm" variant="outline">
|
|
<Phone className="h-3 w-3 mr-1" />
|
|
Call
|
|
</Button>
|
|
<Button size="sm" variant="outline">
|
|
<Navigation className="h-3 w-3 mr-1" />
|
|
Track
|
|
</Button>
|
|
<Button size="sm" variant="outline">
|
|
<MessageSquare className="h-3 w-3 mr-1" />
|
|
Chat
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="timeline" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Timeline Pengumpulan Hari Ini</CardTitle>
|
|
<CardDescription>
|
|
Jadwal dan progress pengumpulan sampah realtime
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-[600px]">
|
|
<div className="space-y-4">
|
|
{data.areas
|
|
.sort((a, b) =>
|
|
a.scheduledTime.localeCompare(b.scheduledTime)
|
|
)
|
|
.map((area, index) => (
|
|
<div
|
|
key={area.id}
|
|
className="flex items-start space-x-4 pb-4 border-b last:border-b-0"
|
|
>
|
|
<div className="flex-shrink-0 mt-1">
|
|
{getStatusIcon(area.status)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="text-sm font-medium">{area.name}</h4>
|
|
<div className="flex items-center space-x-2">
|
|
{getPriorityBadge(area.priority)}
|
|
{getStatusBadge(area.status)}
|
|
</div>
|
|
</div>
|
|
<div className="mt-1 text-sm text-gray-500">
|
|
{area.scheduledTime} • {area.assignedDriver} • Truk{" "}
|
|
{area.assignedTruck}
|
|
</div>
|
|
{area.actualStartTime && (
|
|
<div className="mt-1 text-xs text-blue-600">
|
|
Dimulai: {area.actualStartTime}
|
|
</div>
|
|
)}
|
|
{area.completedTime && (
|
|
<div className="mt-1 text-xs text-green-600">
|
|
Selesai: {area.completedTime} • Volume:{" "}
|
|
{area.actualVolume} kg
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|