refact: tufy up layout

This commit is contained in:
pahmiudahgede 2025-07-10 23:34:12 +07:00
parent b6dec0baac
commit 782e472aa9
4 changed files with 1271 additions and 240 deletions

View File

@ -129,54 +129,15 @@ const operationalMenuItems: MenuItem[] = [
title: "Transaksi & Pembayaran", title: "Transaksi & Pembayaran",
icon: <CreditCard className="w-5 h-5" />, icon: <CreditCard className="w-5 h-5" />,
children: [ children: [
{ title: "Transaksi Hari Ini", href: "/pengelola/transactions/today" },
{ {
title: "Pembayaran Tertunda", title: "Transaksi Hari Ini",
href: "/pengelola/transactions/pending", href: "/pengelola/dashboard/transactions"
badge: "urgent" }
},
{
title: "Verifikasi Pembayaran",
href: "/pengelola/transactions/verification"
},
{ title: "Riwayat Transaksi", href: "/pengelola/transactions/history" },
{ title: "Rekap Harian", href: "/pengelola/transactions/daily-recap" }
] ]
} }
]; ];
const fieldMenuItems: MenuItem[] = [ const fieldMenuItems: MenuItem[] = [
{
title: "Area Coverage",
icon: <MapPin className="w-5 h-5" />,
children: [
{ title: "Peta Operasional", href: "/pengelola/coverage/map" },
{ title: "Rute Pengumpulan", href: "/pengelola/coverage/routes" },
{
title: "Titik Pengumpulan",
href: "/pengelola/coverage/collection-points"
},
{
title: "Area Prioritas",
href: "/pengelola/coverage/priority-areas",
badge: "new"
}
]
},
{
title: "Penjadwalan",
icon: <Calendar className="w-5 h-5" />,
children: [
{ title: "Jadwal Mingguan", href: "/pengelola/schedule/weekly" },
{ title: "Pengepul On-duty", href: "/pengelola/schedule/on-duty" },
{ title: "Shift Management", href: "/pengelola/schedule/shifts" },
{
title: "Emergency Pickup",
href: "/pengelola/schedule/emergency",
badge: "urgent"
}
]
},
{ {
title: "Komunikasi", title: "Komunikasi",
icon: <MessageCircle className="w-5 h-5" />, icon: <MessageCircle className="w-5 h-5" />,
@ -185,76 +146,20 @@ const fieldMenuItems: MenuItem[] = [
{ {
title: "Broadcast Message", title: "Broadcast Message",
href: "/pengelola/dashboard/broadcast" href: "/pengelola/dashboard/broadcast"
},
{
title: "Notifikasi Operasional",
href: "/pengelola/dashboard/notifications"
},
{
title: "Support Ticket",
href: "/pengelola/dashboard/support",
badge: "urgent"
} }
] ]
},
{
title: "Task Management",
icon: <ClipboardList className="w-5 h-5" />,
children: [
{ title: "Task Harian", href: "/pengelola/tasks/daily", badge: "urgent" },
{ title: "Checklist Operasi", href: "/pengelola/tasks/checklist" },
{ title: "Follow-up Required", href: "/pengelola/tasks/followup" },
{ title: "Completed Tasks", href: "/pengelola/tasks/completed" }
]
} }
]; ];
const reportingMenuItems: MenuItem[] = [ const reportingMenuItems: MenuItem[] = [
{
title: "Laporan Operasional",
icon: <BarChart3 className="w-5 h-5" />,
children: [
{ title: "Laporan Harian", href: "/pengelola/reports/daily" },
{ title: "Laporan Mingguan", href: "/pengelola/reports/weekly" },
{ title: "Performa Tim", href: "/pengelola/reports/team-performance" },
{ title: "Analisis Trend", href: "/pengelola/reports/trends" }
]
},
{
title: "Monitoring Kinerja",
icon: <TrendingUp className="w-5 h-5" />,
children: [
{ title: "KPI Dashboard", href: "/pengelola/kpi/dashboard" },
{ title: "Target Achievement", href: "/pengelola/kpi/targets" },
{ title: "Efficiency Metrics", href: "/pengelola/kpi/efficiency" },
{ title: "Quality Metrics", href: "/pengelola/kpi/quality" }
]
},
{
title: "Alerts & Issues",
icon: <AlertCircle className="w-5 h-5" />,
children: [
{
title: "Alert Aktif",
href: "/pengelola/alerts/active",
badge: "urgent"
},
{ title: "Issue Tracking", href: "/pengelola/alerts/issues" },
{ title: "Maintenance Schedule", href: "/pengelola/alerts/maintenance" },
{ title: "Escalation Log", href: "/pengelola/alerts/escalation" }
]
},
{ {
title: "Pengaturan", title: "Pengaturan",
icon: <Settings className="w-5 h-5" />, icon: <Settings className="w-5 h-5" />,
children: [ children: [
{ title: "Profil Pengelola", href: "/pengelola/settings/profile" },
{ {
title: "Notifikasi Setting", title: "Profil Pengelola",
href: "/pengelola/settings/notifications" href: "/pengelola/dashboard/pengaturan"
}, }
{ title: "Area Tanggung Jawab", href: "/pengelola/settings/area" },
{ title: "Tim & Koordinator", href: "/pengelola/settings/team" }
] ]
} }
]; ];

View File

@ -0,0 +1,394 @@
// app/routes/dashboard.settings.tsx
import {
json,
type LoaderFunctionArgs,
type ActionFunctionArgs
} from "@remix-run/node";
import { Form, useLoaderData, useNavigation } from "@remix-run/react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
import { Separator } from "~/components/ui/separator";
import { Badge } from "~/components/ui/badge";
import { Building2, MapPin, Save, User } from "lucide-react";
// Loader untuk mengambil data profil perusahaan
export async function loader({ request }: LoaderFunctionArgs) {
// Simulasi data - ganti dengan query database Anda
const companyProfile = {
id: "1",
name: "PT. Kelola Sampah Indonesia",
email: "admin@kelolasampah.co.id",
phone: "+62 21 1234 5678",
website: "https://kelolasampah.co.id",
description:
"Perusahaan pengelolaan sampah terpadu dengan fokus pada daur ulang dan pengelolaan limbah yang ramah lingkungan.",
address: {
street: "Jl. Lingkungan Hijau No. 123",
city: "Jakarta",
province: "DKI Jakarta",
postalCode: "12345",
country: "Indonesia"
},
establishedYear: "2020",
licenseNumber: "LIC-2020-001"
};
return json({ companyProfile });
}
// Action untuk handle form submission
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "updateProfile") {
// Handle update profil perusahaan
const profileData = {
name: formData.get("name"),
email: formData.get("email"),
phone: formData.get("phone"),
website: formData.get("website"),
description: formData.get("description"),
establishedYear: formData.get("establishedYear"),
licenseNumber: formData.get("licenseNumber")
};
// Simulasi update - ganti dengan update database Anda
console.log("Updating profile:", profileData);
return json({
success: true,
message: "Profil perusahaan berhasil diperbarui"
});
}
if (intent === "updateAddress") {
// Handle update alamat
const addressData = {
street: formData.get("street"),
city: formData.get("city"),
province: formData.get("province"),
postalCode: formData.get("postalCode"),
country: formData.get("country")
};
// Simulasi update - ganti dengan update database Anda
console.log("Updating address:", addressData);
return json({
success: true,
message: "Alamat perusahaan berhasil diperbarui"
});
}
return json({ success: false, message: "Invalid action" });
}
export default function SettingsPage() {
const { companyProfile } = useLoaderData<typeof loader>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<div className="p-6 space-y-6">
<div className="mb-6">
<h1 className="text-3xl font-bold tracking-tight">Pengaturan</h1>
<p className="text-muted-foreground mt-2">
Kelola profil perusahaan dan informasi alamat Anda
</p>
</div>
{/* Main Grid Layout */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Left Column - Profil Perusahaan */}
<div className="xl:col-span-2 space-y-6">
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-primary" />
<CardTitle>Profil Perusahaan</CardTitle>
</div>
<CardDescription>
Informasi dasar tentang perusahaan pengelola sampah
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="updateProfile" />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Nama Perusahaan</Label>
<Input
id="name"
name="name"
defaultValue={companyProfile.name}
placeholder="Masukkan nama perusahaan"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
defaultValue={companyProfile.email}
placeholder="admin@perusahaan.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone">No. Telepon</Label>
<Input
id="phone"
name="phone"
defaultValue={companyProfile.phone}
placeholder="+62 21 1234 5678"
/>
</div>
<div className="space-y-2">
<Label htmlFor="website">Website</Label>
<Input
id="website"
name="website"
defaultValue={companyProfile.website}
placeholder="https://perusahaan.com"
/>
</div>
<div className="space-y-2">
<Label htmlFor="establishedYear">Tahun Berdiri</Label>
<Input
id="establishedYear"
name="establishedYear"
defaultValue={companyProfile.establishedYear}
placeholder="2020"
/>
</div>
<div className="space-y-2">
<Label htmlFor="licenseNumber">No. Izin Usaha</Label>
<Input
id="licenseNumber"
name="licenseNumber"
defaultValue={companyProfile.licenseNumber}
placeholder="LIC-2020-001"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">Deskripsi Perusahaan</Label>
<Textarea
id="description"
name="description"
defaultValue={companyProfile.description}
placeholder="Deskripsikan perusahaan Anda..."
rows={3}
/>
</div>
<div className="flex justify-end">
<Button type="submit" disabled={isSubmitting}>
<Save className="h-4 w-4 mr-2" />
{isSubmitting ? "Menyimpan..." : "Simpan Profil"}
</Button>
</div>
</Form>
</CardContent>
</Card>
{/* Alamat Perusahaan Card */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<MapPin className="h-5 w-5 text-primary" />
<CardTitle>Alamat Perusahaan</CardTitle>
</div>
<CardDescription>
Informasi lokasi dan alamat lengkap perusahaan
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="updateAddress" />
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="street">Alamat Lengkap</Label>
<Input
id="street"
name="street"
defaultValue={companyProfile.address.street}
placeholder="Jl. Nama Jalan No. 123"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="city">Kota</Label>
<Input
id="city"
name="city"
defaultValue={companyProfile.address.city}
placeholder="Jakarta"
/>
</div>
<div className="space-y-2">
<Label htmlFor="province">Provinsi</Label>
<Input
id="province"
name="province"
defaultValue={companyProfile.address.province}
placeholder="DKI Jakarta"
/>
</div>
<div className="space-y-2">
<Label htmlFor="postalCode">Kode Pos</Label>
<Input
id="postalCode"
name="postalCode"
defaultValue={companyProfile.address.postalCode}
placeholder="12345"
/>
</div>
<div className="space-y-2">
<Label htmlFor="country">Negara</Label>
<Input
id="country"
name="country"
defaultValue={companyProfile.address.country}
placeholder="Indonesia"
/>
</div>
</div>
</div>
<div className="flex justify-end">
<Button type="submit" disabled={isSubmitting}>
<Save className="h-4 w-4 mr-2" />
{isSubmitting ? "Menyimpan..." : "Simpan Alamat"}
</Button>
</div>
</Form>
</CardContent>
</Card>
</div>
{/* Right Column - Info & Status */}
<div className="space-y-6">
{/* Status Card */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Status Perusahaan</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Status</span>
<Badge variant="default" className="bg-green-600">
Aktif
</Badge>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
Verifikasi
</span>
<Badge variant="secondary">Terverifikasi</Badge>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
Tahun Berdiri
</span>
<span className="text-sm font-medium">
{companyProfile.establishedYear}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">
No. Izin
</span>
<span className="text-sm font-medium">
{companyProfile.licenseNumber}
</span>
</div>
</div>
<Separator />
<div className="space-y-2">
<p className="text-sm font-medium">Aktivitas Terakhir</p>
<p className="text-xs text-muted-foreground">
Profil diperbarui 2 hari yang lalu
</p>
</div>
</CardContent>
</Card>
{/* Quick Actions */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Aksi Cepat</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Button
variant="outline"
className="w-full justify-start"
size="sm"
>
<User className="h-4 w-4 mr-2" />
Edit Profil
</Button>
<Button
variant="outline"
className="w-full justify-start"
size="sm"
>
<Building2 className="h-4 w-4 mr-2" />
Lihat Preview
</Button>
<Button
variant="outline"
className="w-full justify-start"
size="sm"
>
<MapPin className="h-4 w-4 mr-2" />
Lokasi di Peta
</Button>
</CardContent>
</Card>
{/* Info Card */}
<Card className="bg-muted/30">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<User className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<div className="space-y-1">
<p className="text-sm font-medium">Informasi Penting</p>
<p className="text-xs text-muted-foreground leading-relaxed">
Pastikan informasi yang Anda masukkan akurat dan terkini.
Data ini akan digunakan untuk laporan dan komunikasi dengan
pihak terkait.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,442 @@
// app/routes/dashboard.pencatatan.tsx
import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useLoaderData, useNavigation, useFetcher } from "@remix-run/react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
import { Badge } from "~/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
import { Separator } from "~/components/ui/separator";
import {
Plus,
Search,
Filter,
Download,
Eye,
Edit,
Trash2,
Receipt,
Users,
Scale,
Calendar,
MapPin
} from "lucide-react";
import { useState } from "react";
// Loader untuk mengambil data
export async function loader({ request }: LoaderFunctionArgs) {
// Simulasi data - ganti dengan query database Anda
const wasteRecords = [
{
id: "WR-001",
date: "2024-07-10",
collectorName: "Budi Santoso",
collectorPhone: "+62 812 3456 7890",
wasteType: "Plastik PET",
weight: 150.5,
pricePerKg: 3500,
totalPrice: 526750,
location: "Kelurahan Kebayoran",
status: "completed",
notes: "Kondisi baik, sudah dipilah"
},
{
id: "WR-002",
date: "2024-07-09",
collectorName: "Siti Rahayu",
collectorPhone: "+62 813 9876 5432",
wasteType: "Kardus",
weight: 200.0,
pricePerKg: 2800,
totalPrice: 560000,
location: "Kelurahan Menteng",
status: "pending",
notes: "Perlu pengecekan kualitas"
},
{
id: "WR-003",
date: "2024-07-08",
collectorName: "Ahmad Wijaya",
collectorPhone: "+62 814 1122 3344",
wasteType: "Kaleng Aluminium",
weight: 75.2,
pricePerKg: 8500,
totalPrice: 639200,
location: "Kelurahan Cempaka Putih",
status: "completed",
notes: "Kualitas premium"
}
];
const wasteTypes = [
"Plastik PET",
"Kardus",
"Kaleng Aluminium",
"Kertas",
"Plastik HDPE",
"Besi/Logam",
"Kaca"
];
const collectors = [
{ name: "Budi Santoso", phone: "+62 812 3456 7890" },
{ name: "Siti Rahayu", phone: "+62 813 9876 5432" },
{ name: "Ahmad Wijaya", phone: "+62 814 1122 3344" }
];
return json({ wasteRecords, wasteTypes, collectors });
}
// Action untuk handle form submission
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "createRecord") {
const recordData = {
collectorName: formData.get("collectorName"),
collectorPhone: formData.get("collectorPhone"),
wasteType: formData.get("wasteType"),
weight: parseFloat(formData.get("weight") as string),
pricePerKg: parseFloat(formData.get("pricePerKg") as string),
location: formData.get("location"),
notes: formData.get("notes")
};
// Simulasi create - ganti dengan insert database Anda
console.log("Creating waste record:", recordData);
return json({ success: true, message: "Pencatatan sampah berhasil disimpan" });
}
return json({ success: false, message: "Invalid action" });
}
export default function WasteRecordingPage() {
const { wasteRecords, wasteTypes, collectors } = useLoaderData<typeof loader>();
const navigation = useNavigation();
const [searchTerm, setSearchTerm] = useState("");
const [selectedType, setSelectedType] = useState("all");
const [selectedStatus, setSelectedStatus] = useState("all");
const isSubmitting = navigation.state === "submitting";
// Filter records
const filteredRecords = wasteRecords.filter(record => {
const matchesSearch = record.collectorName.toLowerCase().includes(searchTerm.toLowerCase()) ||
record.wasteType.toLowerCase().includes(searchTerm.toLowerCase()) ||
record.id.toLowerCase().includes(searchTerm.toLowerCase());
const matchesType = selectedType === "all" || record.wasteType === selectedType;
const matchesStatus = selectedStatus === "all" || record.status === selectedStatus;
return matchesSearch && matchesType && matchesStatus;
});
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(amount);
};
const getStatusBadge = (status: string) => {
switch (status) {
case "completed":
return <Badge variant="default" className="bg-green-600">Selesai</Badge>;
case "pending":
return <Badge variant="secondary">Pending</Badge>;
default:
return <Badge variant="outline">{status}</Badge>;
}
};
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">Pencatatan Sampah</h1>
<p className="text-muted-foreground mt-2">
Catat pembelian sampah dari pengepul dan kelola transaksi
</p>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-4 gap-6">
{/* Left Column - Form Input */}
<div className="xl:col-span-1 space-y-6">
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Plus className="h-5 w-5 text-primary" />
<CardTitle>Tambah Pencatatan</CardTitle>
</div>
<CardDescription>
Input data pembelian sampah baru
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-4">
<input type="hidden" name="intent" value="createRecord" />
<div className="space-y-2">
<Label htmlFor="collectorName">Nama Pengepul</Label>
<Select name="collectorName">
<SelectTrigger>
<SelectValue placeholder="Pilih pengepul" />
</SelectTrigger>
<SelectContent>
{collectors.map((collector) => (
<SelectItem key={collector.name} value={collector.name}>
{collector.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="collectorPhone">No. Telepon</Label>
<Input
id="collectorPhone"
name="collectorPhone"
placeholder="+62 812 xxxx xxxx"
/>
</div>
<div className="space-y-2">
<Label htmlFor="wasteType">Jenis Sampah</Label>
<Select name="wasteType">
<SelectTrigger>
<SelectValue placeholder="Pilih jenis sampah" />
</SelectTrigger>
<SelectContent>
{wasteTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="weight">Berat (kg)</Label>
<Input
id="weight"
name="weight"
type="number"
step="0.1"
placeholder="0.0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="pricePerKg">Harga per Kg (Rp)</Label>
<Input
id="pricePerKg"
name="pricePerKg"
type="number"
placeholder="3500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="location">Lokasi</Label>
<Input
id="location"
name="location"
placeholder="Kelurahan/Daerah"
/>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Catatan</Label>
<Textarea
id="notes"
name="notes"
placeholder="Catatan kondisi atau kualitas sampah..."
rows={3}
/>
</div>
<Button type="submit" disabled={isSubmitting} className="w-full">
<Receipt className="h-4 w-4 mr-2" />
{isSubmitting ? "Menyimpan..." : "Simpan Pencatatan"}
</Button>
</Form>
</CardContent>
</Card>
{/* Summary Card */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Ringkasan Hari Ini</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Transaksi</span>
<span className="font-semibold">3</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Berat</span>
<span className="font-semibold">425.7 kg</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Nilai</span>
<span className="font-semibold text-green-600">Rp 1.725.950</span>
</div>
<Separator />
<div className="text-xs text-muted-foreground">
Data per {new Date().toLocaleDateString('id-ID')}
</div>
</CardContent>
</Card>
</div>
{/* Right Column - Records Table */}
<div className="xl:col-span-3 space-y-6">
{/* Filters */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Cari berdasarkan pengepul, jenis sampah, atau ID..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Jenis Sampah" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Semua Jenis</SelectItem>
{wasteTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Semua Status</SelectItem>
<SelectItem value="completed">Selesai</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="icon">
<Download className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Records Table */}
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<div>
<CardTitle>Riwayat Pencatatan</CardTitle>
<CardDescription>
Daftar transaksi pembelian sampah dari pengepul
</CardDescription>
</div>
<Badge variant="outline">
{filteredRecords.length} dari {wasteRecords.length} records
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Tanggal</TableHead>
<TableHead>Pengepul</TableHead>
<TableHead>Jenis Sampah</TableHead>
<TableHead>Berat (kg)</TableHead>
<TableHead>Harga/kg</TableHead>
<TableHead>Total</TableHead>
<TableHead>Status</TableHead>
<TableHead>Aksi</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRecords.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="text-center py-6 text-muted-foreground">
Tidak ada data yang ditemukan
</TableCell>
</TableRow>
) : (
filteredRecords.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-medium">{record.id}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
{new Date(record.date).toLocaleDateString('id-ID')}
</div>
</TableCell>
<TableCell>
<div>
<div className="font-medium">{record.collectorName}</div>
<div className="text-sm text-muted-foreground">{record.collectorPhone}</div>
</div>
</TableCell>
<TableCell>{record.wasteType}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Scale className="h-4 w-4 text-muted-foreground" />
{record.weight}
</div>
</TableCell>
<TableCell>{formatCurrency(record.pricePerKg)}</TableCell>
<TableCell className="font-semibold text-green-600">
{formatCurrency(record.totalPrice)}
</TableCell>
<TableCell>{getStatusBadge(record.status)}</TableCell>
<TableCell>
<div className="flex gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-600">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,17 @@
import { json } from "@remix-run/node"; import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react"; import { useLoaderData } from "@remix-run/react";
import { useState } from "react";
// Interface untuk data waste
interface WasteData {
id: string;
trash_name: string;
trash_icon: string;
estimated_price: number;
variety: string;
created_at: string;
updated_at: string;
}
import { import {
Recycle, Recycle,
Plus, Plus,
@ -9,7 +21,15 @@ import {
Edit, Edit,
Trash2, Trash2,
TrendingUp, TrendingUp,
TrendingDown TrendingDown,
Package,
FileText,
Coffee,
Cpu,
Archive,
Glasses,
X,
Save
} from "lucide-react"; } from "lucide-react";
import { import {
Card, Card,
@ -35,74 +55,195 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from "~/components/ui/dropdown-menu"; } from "~/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from "~/components/ui/dialog";
import { Label } from "~/components/ui/label";
import { Textarea } from "~/components/ui/textarea";
// Icon mapping untuk setiap jenis sampah
const getWasteIcon = (trashName: string) => {
const name = trashName.toLowerCase();
if (name.includes("plastik")) return Package;
if (name.includes("kertas")) return FileText;
if (name.includes("kaleng")) return Coffee;
if (name.includes("besi") || name.includes("tembaga")) return Cpu;
if (name.includes("kardus")) return Archive;
if (name.includes("kaca") || name.includes("beling")) return Glasses;
return Recycle; // default icon
};
export const loader = async () => { export const loader = async () => {
// Simulasi data API sesuai format yang diminta
const wasteData = { const wasteData = {
summary: { meta: {
totalTypes: 24, status: 200,
totalVolume: 5678, message: "Trash categories retrieved successfully"
avgPrice: 2500,
trending: "up"
}, },
wasteTypes: [ data: [
{ {
id: 1, id: "9520dfd4-3bc8-4173-ac3d-4b17d466bc90",
name: "Plastik PET", trash_name: "Plastik",
category: "Plastik", trash_icon:
currentPrice: 3000, "/uploads/icontrash/a4e99d8c-8380-470f-87f1-01dc62fbe114_icontrash.png",
priceChange: "+5%", estimated_price: 1500,
volume: 1500, variety:
trend: "up", "Jerigen plastik, tempat makanan thin wall, ember, galon air mineral, botol sabun, botol, sampo dan plastik keras sejenisnya",
status: "active" created_at: "2025-06-12T05:08:43+07:00",
updated_at: "2025-06-12T05:08:43+07:00"
}, },
{ {
id: 2, id: "8636ceee-6c13-41ab-abc6-5b0c603ba360",
name: "Kertas HVS", trash_name: "Kertas",
category: "Kertas", trash_icon:
currentPrice: 2000, "/uploads/icontrash/a6414ed3-0675-4b38-a2c7-c6d3d24810cf_icontrash.png",
priceChange: "-2%", estimated_price: 1250,
volume: 2100, variety:
trend: "down", "Kertas HVS, koran, majalah, buku, kertas, karton dan sejenisnya",
status: "active" created_at: "2025-06-12T05:10:21+07:00",
updated_at: "2025-06-12T05:10:21+07:00"
}, },
{ {
id: 3, id: "bec932a7-da0a-4e7b-b33c-a5e225e56cef",
name: "Aluminium", trash_name: "Kaleng",
category: "Logam", trash_icon:
currentPrice: 8500, "/uploads/icontrash/49b2ca06-cbfe-4650-bfb9-7d14aff2e09b_icontrash.png",
priceChange: "+12%", estimated_price: 1000,
volume: 450, variety: "Kaleng sarden, kaleng aerosol, kaleng makanan, dll",
trend: "up", created_at: "2025-06-12T05:14:38+07:00",
status: "active" updated_at: "2025-06-12T05:14:38+07:00"
}, },
{ {
id: 4, id: "9af0a2f2-4c9c-49b0-8f0b-ea8c38d9edd3",
name: "Plastik HDPE", trash_name: "Besi/Tembaga",
category: "Plastik", trash_icon:
currentPrice: 2800, "/uploads/icontrash/2a80005a-3038-4192-b70c-b22a54f11ae6_icontrash.png",
priceChange: "+3%", estimated_price: 3500,
volume: 900, variety: "Besi, tembaga, aluminium",
trend: "up", created_at: "2025-06-12T05:16:44+07:00",
status: "active" updated_at: "2025-06-12T05:16:44+07:00"
}, },
{ {
id: 5, id: "c5319782-b658-4639-83aa-8b88feb1b2a8",
name: "Kertas Karton", trash_name: "Kardus",
category: "Kertas", trash_icon:
currentPrice: 1500, "/uploads/icontrash/1d900090-4b24-4d42-9c0e-e486839b9f63_icontrash.png",
priceChange: "0%", estimated_price: 1500,
volume: 1200, variety: "Kardus paket, kardus kemasan produk, dll",
trend: "stable", created_at: "2025-06-12T05:19:15+07:00",
status: "active" updated_at: "2025-06-12T05:19:15+07:00"
},
{
id: "131c7ca9-6f2d-4e98-a016-916c23ec45e9",
trash_name: "Kaca/Beling",
trash_icon:
"/uploads/icontrash/3be4f3ab-99a2-4b3c-930b-b2e0055cd705_icontrash.png",
estimated_price: 500,
variety:
"Botol kaca minuman, botol kaca kosmetik, botol sirup, botol saus, botol kecap, gelas kaca, piring kaca dan sejenisnya",
created_at: "2025-06-12T05:21:55+07:00",
updated_at: "2025-06-12T05:21:55+07:00"
} }
] ]
}; };
return json({ wasteData }); // Hitung summary dari data
const summary = {
totalTypes: wasteData.data.length,
totalVolume: 0, // Tidak ada data volume di API baru
avgPrice: Math.round(
wasteData.data.reduce((sum, item) => sum + item.estimated_price, 0) /
wasteData.data.length
),
trending: "up"
};
return json({
wasteData: wasteData.data,
summary
});
}; };
export default function WasteManagement() { export default function WasteManagement() {
const { wasteData } = useLoaderData<typeof loader>(); const { wasteData, summary } = useLoaderData<typeof loader>();
// State untuk modal
const [showAddModal, setShowAddModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedWaste, setSelectedWaste] = useState<WasteData | null>(null);
// State untuk form
const [formData, setFormData] = useState({
trash_name: "",
estimated_price: "",
variety: ""
});
// Handle form change
const handleFormChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value
}));
};
// Handle add
const handleAdd = () => {
setFormData({
trash_name: "",
estimated_price: "",
variety: ""
});
setShowAddModal(true);
};
// Handle edit
const handleEdit = (waste: WasteData) => {
setSelectedWaste(waste);
setFormData({
trash_name: waste.trash_name,
estimated_price: waste.estimated_price.toString(),
variety: waste.variety
});
setShowEditModal(true);
};
// Handle delete
const handleDelete = (waste: WasteData) => {
setSelectedWaste(waste);
setShowDeleteModal(true);
};
// Handle save (add/edit)
const handleSave = () => {
// Di sini Anda bisa menambahkan logika untuk menyimpan data
console.log("Saving data:", formData);
setShowAddModal(false);
setShowEditModal(false);
// Reset form
setFormData({
trash_name: "",
estimated_price: "",
variety: ""
});
};
// Handle confirm delete
const handleConfirmDelete = () => {
// Di sini Anda bisa menambahkan logika untuk menghapus data
if (selectedWaste) {
console.log("Deleting:", selectedWaste);
}
setShowDeleteModal(false);
setSelectedWaste(null);
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@ -113,45 +254,33 @@ export default function WasteManagement() {
Manajemen Data Sampah Manajemen Data Sampah
</h1> </h1>
<p className="text-gray-600 dark:text-gray-400 mt-1"> <p className="text-gray-600 dark:text-gray-400 mt-1">
Kelola jenis sampah, harga, dan volume transaksi Kelola jenis sampah, harga, dan variety transaksi
</p> </p>
</div> </div>
<Button className="gap-2 bg-green-600 hover:bg-green-700"> <Button
onClick={handleAdd}
className="gap-2 bg-green-600 hover:bg-green-700"
>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
Tambah Jenis Sampah Tambah Jenis Sampah
</Button> </Button>
</div> </div>
{/* Summary Cards */} {/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Total Jenis</CardTitle> <CardTitle className="text-sm font-medium">Total Jenis</CardTitle>
<Recycle className="h-4 w-4 text-muted-foreground" /> <Recycle className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">{summary.totalTypes}</div>
{wasteData.summary.totalTypes}
</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
jenis sampah terdaftar jenis sampah terdaftar
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Volume Total</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{wasteData.summary.totalVolume.toLocaleString()}
</div>
<p className="text-xs text-muted-foreground">kg bulan ini</p>
</CardContent>
</Card>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"> <CardTitle className="text-sm font-medium">
@ -161,7 +290,7 @@ export default function WasteManagement() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
Rp {wasteData.summary.avgPrice.toLocaleString()} Rp {summary.avgPrice.toLocaleString()}
</div> </div>
<p className="text-xs text-muted-foreground">per kilogram</p> <p className="text-xs text-muted-foreground">per kilogram</p>
</CardContent> </CardContent>
@ -170,7 +299,7 @@ export default function WasteManagement() {
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Trend Harga</CardTitle> <CardTitle className="text-sm font-medium">Trend Harga</CardTitle>
{wasteData.summary.trending === "up" ? ( {summary.trending === "up" ? (
<TrendingUp className="h-4 w-4 text-green-600" /> <TrendingUp className="h-4 w-4 text-green-600" />
) : ( ) : (
<TrendingDown className="h-4 w-4 text-red-600" /> <TrendingDown className="h-4 w-4 text-red-600" />
@ -207,6 +336,13 @@ export default function WasteManagement() {
<Filter className="h-4 w-4" /> <Filter className="h-4 w-4" />
Filter Filter
</Button> </Button>
<Button
onClick={handleAdd}
className="gap-2 bg-green-600 hover:bg-green-700"
>
<Plus className="h-4 w-4" />
Tambah
</Button>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@ -214,84 +350,238 @@ export default function WasteManagement() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Icon</TableHead>
<TableHead>Nama Sampah</TableHead> <TableHead>Nama Sampah</TableHead>
<TableHead>Kategori</TableHead> <TableHead>Harga Estimasi</TableHead>
<TableHead>Harga Saat Ini</TableHead> <TableHead>Variety</TableHead>
<TableHead>Perubahan</TableHead>
<TableHead>Volume (kg)</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Aksi</TableHead> <TableHead className="text-right">Aksi</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{wasteData.wasteTypes.map((waste) => ( {wasteData.map((waste) => {
<TableRow key={waste.id}> const IconComponent = getWasteIcon(waste.trash_name);
<TableCell className="font-medium">{waste.name}</TableCell> return (
<TableCell> <TableRow key={waste.id}>
<Badge variant="outline">{waste.category}</Badge> <TableCell>
</TableCell> <div className="flex items-center justify-center w-10 h-10 bg-green-100 rounded-lg">
<TableCell className="font-mono"> <IconComponent className="w-6 h-6 text-green-600" />
Rp {waste.currentPrice.toLocaleString()} </div>
</TableCell> </TableCell>
<TableCell> <TableCell className="font-medium">
<div {waste.trash_name}
className={`flex items-center gap-1 ${ </TableCell>
waste.trend === "up" <TableCell className="font-mono">
? "text-green-600" Rp {waste.estimated_price.toLocaleString()}
: waste.trend === "down" </TableCell>
? "text-red-600" <TableCell className="max-w-xs">
: "text-gray-600" <div className="truncate" title={waste.variety}>
}`} {waste.variety}
> </div>
{waste.trend === "up" && ( </TableCell>
<TrendingUp className="h-3 w-3" /> <TableCell className="text-right">
)} <DropdownMenu>
{waste.trend === "down" && ( <DropdownMenuTrigger asChild>
<TrendingDown className="h-3 w-3" /> <Button variant="ghost" className="h-8 w-8 p-0">
)} <MoreHorizontal className="h-4 w-4" />
<span className="text-sm">{waste.priceChange}</span> </Button>
</div> </DropdownMenuTrigger>
</TableCell> <DropdownMenuContent align="end">
<TableCell>{waste.volume.toLocaleString()}</TableCell> <DropdownMenuItem
<TableCell> className="gap-2"
<Badge onClick={() => handleEdit(waste)}
variant={ >
waste.status === "active" ? "default" : "secondary" <Edit className="h-4 w-4" />
} Edit
className={ </DropdownMenuItem>
waste.status === "active" <DropdownMenuItem
? "bg-green-100 text-green-800" className="gap-2 text-red-600"
: "" onClick={() => handleDelete(waste)}
} >
> <Trash2 className="h-4 w-4" />
{waste.status === "active" ? "Aktif" : "Nonaktif"} Hapus
</Badge> </DropdownMenuItem>
</TableCell> </DropdownMenuContent>
<TableCell className="text-right"> </DropdownMenu>
<DropdownMenu> </TableCell>
<DropdownMenuTrigger asChild> </TableRow>
<Button variant="ghost" className="h-8 w-8 p-0"> );
<MoreHorizontal className="h-4 w-4" /> })}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="gap-2">
<Edit className="h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem className="gap-2 text-red-600">
<Trash2 className="h-4 w-4" />
Hapus
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody> </TableBody>
</Table> </Table>
</CardContent> </CardContent>
</Card> </Card>
{/* Add Modal */}
<Dialog open={showAddModal} onOpenChange={setShowAddModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Plus className="h-5 w-5 text-green-600" />
Tambah Jenis Sampah
</DialogTitle>
<DialogDescription>
Tambahkan jenis sampah baru dengan detail lengkap
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="add-name">Nama Sampah</Label>
<Input
id="add-name"
value={formData.trash_name}
onChange={(e) => handleFormChange("trash_name", e.target.value)}
placeholder="Masukkan nama sampah"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="add-price">Harga Estimasi (Rp)</Label>
<Input
id="add-price"
type="number"
value={formData.estimated_price}
onChange={(e) =>
handleFormChange("estimated_price", e.target.value)
}
placeholder="Masukkan harga per kg"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="add-variety">Variety</Label>
<Textarea
id="add-variety"
value={formData.variety}
onChange={(e) => handleFormChange("variety", e.target.value)}
placeholder="Deskripsi jenis sampah yang termasuk dalam kategori ini"
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddModal(false)}>
Batal
</Button>
<Button
onClick={handleSave}
className="bg-green-600 hover:bg-green-700"
>
<Save className="h-4 w-4 mr-2" />
Simpan
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit Modal */}
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Edit className="h-5 w-5 text-blue-600" />
Edit Jenis Sampah
</DialogTitle>
<DialogDescription>
Perbarui informasi jenis sampah
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="edit-name">Nama Sampah</Label>
<Input
id="edit-name"
value={formData.trash_name}
onChange={(e) => handleFormChange("trash_name", e.target.value)}
placeholder="Masukkan nama sampah"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-price">Harga Estimasi (Rp)</Label>
<Input
id="edit-price"
type="number"
value={formData.estimated_price}
onChange={(e) =>
handleFormChange("estimated_price", e.target.value)
}
placeholder="Masukkan harga per kg"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="edit-variety">Variety</Label>
<Textarea
id="edit-variety"
value={formData.variety}
onChange={(e) => handleFormChange("variety", e.target.value)}
placeholder="Deskripsi jenis sampah yang termasuk dalam kategori ini"
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowEditModal(false)}>
Batal
</Button>
<Button
onClick={handleSave}
className="bg-blue-600 hover:bg-blue-700"
>
<Save className="h-4 w-4 mr-2" />
Update
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Modal */}
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-red-600" />
Hapus Jenis Sampah
</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin menghapus jenis sampah ini? Tindakan ini
tidak dapat dibatalkan.
</DialogDescription>
</DialogHeader>
{selectedWaste && (
<div className="py-4">
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="flex items-center gap-3">
{(() => {
const IconComponent = getWasteIcon(
selectedWaste.trash_name
);
return (
<div className="flex items-center justify-center w-10 h-10 bg-red-100 rounded-lg">
<IconComponent className="w-6 h-6 text-red-600" />
</div>
);
})()}
<div>
<h4 className="font-medium">{selectedWaste.trash_name}</h4>
<p className="text-sm text-gray-600">
Rp {selectedWaste.estimated_price.toLocaleString()}
</p>
</div>
</div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowDeleteModal(false)}>
Batal
</Button>
<Button
onClick={handleConfirmDelete}
className="bg-red-600 hover:bg-red-700"
>
<Trash2 className="h-4 w-4 mr-2" />
Hapus
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
); );
} }