refact: tufy up layout
This commit is contained in:
parent
b6dec0baac
commit
782e472aa9
|
@ -129,54 +129,15 @@ const operationalMenuItems: MenuItem[] = [
|
|||
title: "Transaksi & Pembayaran",
|
||||
icon: <CreditCard className="w-5 h-5" />,
|
||||
children: [
|
||||
{ title: "Transaksi Hari Ini", href: "/pengelola/transactions/today" },
|
||||
{
|
||||
title: "Pembayaran Tertunda",
|
||||
href: "/pengelola/transactions/pending",
|
||||
badge: "urgent"
|
||||
},
|
||||
{
|
||||
title: "Verifikasi Pembayaran",
|
||||
href: "/pengelola/transactions/verification"
|
||||
},
|
||||
{ title: "Riwayat Transaksi", href: "/pengelola/transactions/history" },
|
||||
{ title: "Rekap Harian", href: "/pengelola/transactions/daily-recap" }
|
||||
title: "Transaksi Hari Ini",
|
||||
href: "/pengelola/dashboard/transactions"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
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",
|
||||
icon: <MessageCircle className="w-5 h-5" />,
|
||||
|
@ -185,76 +146,20 @@ const fieldMenuItems: MenuItem[] = [
|
|||
{
|
||||
title: "Broadcast Message",
|
||||
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[] = [
|
||||
{
|
||||
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",
|
||||
icon: <Settings className="w-5 h-5" />,
|
||||
children: [
|
||||
{ title: "Profil Pengelola", href: "/pengelola/settings/profile" },
|
||||
{
|
||||
title: "Notifikasi Setting",
|
||||
href: "/pengelola/settings/notifications"
|
||||
},
|
||||
{ title: "Area Tanggung Jawab", href: "/pengelola/settings/area" },
|
||||
{ title: "Tim & Koordinator", href: "/pengelola/settings/team" }
|
||||
title: "Profil Pengelola",
|
||||
href: "/pengelola/dashboard/pengaturan"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,17 @@
|
|||
import { json } from "@remix-run/node";
|
||||
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 {
|
||||
Recycle,
|
||||
Plus,
|
||||
|
@ -9,7 +21,15 @@ import {
|
|||
Edit,
|
||||
Trash2,
|
||||
TrendingUp,
|
||||
TrendingDown
|
||||
TrendingDown,
|
||||
Package,
|
||||
FileText,
|
||||
Coffee,
|
||||
Cpu,
|
||||
Archive,
|
||||
Glasses,
|
||||
X,
|
||||
Save
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
|
@ -35,74 +55,195 @@ import {
|
|||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} 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 () => {
|
||||
// Simulasi data API sesuai format yang diminta
|
||||
const wasteData = {
|
||||
summary: {
|
||||
totalTypes: 24,
|
||||
totalVolume: 5678,
|
||||
avgPrice: 2500,
|
||||
trending: "up"
|
||||
meta: {
|
||||
status: 200,
|
||||
message: "Trash categories retrieved successfully"
|
||||
},
|
||||
wasteTypes: [
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Plastik PET",
|
||||
category: "Plastik",
|
||||
currentPrice: 3000,
|
||||
priceChange: "+5%",
|
||||
volume: 1500,
|
||||
trend: "up",
|
||||
status: "active"
|
||||
id: "9520dfd4-3bc8-4173-ac3d-4b17d466bc90",
|
||||
trash_name: "Plastik",
|
||||
trash_icon:
|
||||
"/uploads/icontrash/a4e99d8c-8380-470f-87f1-01dc62fbe114_icontrash.png",
|
||||
estimated_price: 1500,
|
||||
variety:
|
||||
"Jerigen plastik, tempat makanan thin wall, ember, galon air mineral, botol sabun, botol, sampo dan plastik keras sejenisnya",
|
||||
created_at: "2025-06-12T05:08:43+07:00",
|
||||
updated_at: "2025-06-12T05:08:43+07:00"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Kertas HVS",
|
||||
category: "Kertas",
|
||||
currentPrice: 2000,
|
||||
priceChange: "-2%",
|
||||
volume: 2100,
|
||||
trend: "down",
|
||||
status: "active"
|
||||
id: "8636ceee-6c13-41ab-abc6-5b0c603ba360",
|
||||
trash_name: "Kertas",
|
||||
trash_icon:
|
||||
"/uploads/icontrash/a6414ed3-0675-4b38-a2c7-c6d3d24810cf_icontrash.png",
|
||||
estimated_price: 1250,
|
||||
variety:
|
||||
"Kertas HVS, koran, majalah, buku, kertas, karton dan sejenisnya",
|
||||
created_at: "2025-06-12T05:10:21+07:00",
|
||||
updated_at: "2025-06-12T05:10:21+07:00"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Aluminium",
|
||||
category: "Logam",
|
||||
currentPrice: 8500,
|
||||
priceChange: "+12%",
|
||||
volume: 450,
|
||||
trend: "up",
|
||||
status: "active"
|
||||
id: "bec932a7-da0a-4e7b-b33c-a5e225e56cef",
|
||||
trash_name: "Kaleng",
|
||||
trash_icon:
|
||||
"/uploads/icontrash/49b2ca06-cbfe-4650-bfb9-7d14aff2e09b_icontrash.png",
|
||||
estimated_price: 1000,
|
||||
variety: "Kaleng sarden, kaleng aerosol, kaleng makanan, dll",
|
||||
created_at: "2025-06-12T05:14:38+07:00",
|
||||
updated_at: "2025-06-12T05:14:38+07:00"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Plastik HDPE",
|
||||
category: "Plastik",
|
||||
currentPrice: 2800,
|
||||
priceChange: "+3%",
|
||||
volume: 900,
|
||||
trend: "up",
|
||||
status: "active"
|
||||
id: "9af0a2f2-4c9c-49b0-8f0b-ea8c38d9edd3",
|
||||
trash_name: "Besi/Tembaga",
|
||||
trash_icon:
|
||||
"/uploads/icontrash/2a80005a-3038-4192-b70c-b22a54f11ae6_icontrash.png",
|
||||
estimated_price: 3500,
|
||||
variety: "Besi, tembaga, aluminium",
|
||||
created_at: "2025-06-12T05:16:44+07:00",
|
||||
updated_at: "2025-06-12T05:16:44+07:00"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Kertas Karton",
|
||||
category: "Kertas",
|
||||
currentPrice: 1500,
|
||||
priceChange: "0%",
|
||||
volume: 1200,
|
||||
trend: "stable",
|
||||
status: "active"
|
||||
id: "c5319782-b658-4639-83aa-8b88feb1b2a8",
|
||||
trash_name: "Kardus",
|
||||
trash_icon:
|
||||
"/uploads/icontrash/1d900090-4b24-4d42-9c0e-e486839b9f63_icontrash.png",
|
||||
estimated_price: 1500,
|
||||
variety: "Kardus paket, kardus kemasan produk, dll",
|
||||
created_at: "2025-06-12T05:19:15+07:00",
|
||||
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() {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
|
@ -113,45 +254,33 @@ export default function WasteManagement() {
|
|||
Manajemen Data Sampah
|
||||
</h1>
|
||||
<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>
|
||||
</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" />
|
||||
Tambah Jenis Sampah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Jenis</CardTitle>
|
||||
<Recycle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{wasteData.summary.totalTypes}
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{summary.totalTypes}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
jenis sampah terdaftar
|
||||
</p>
|
||||
</CardContent>
|
||||
</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>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
|
@ -161,7 +290,7 @@ export default function WasteManagement() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
Rp {wasteData.summary.avgPrice.toLocaleString()}
|
||||
Rp {summary.avgPrice.toLocaleString()}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">per kilogram</p>
|
||||
</CardContent>
|
||||
|
@ -170,7 +299,7 @@ export default function WasteManagement() {
|
|||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<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" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4 text-red-600" />
|
||||
|
@ -207,6 +336,13 @@ export default function WasteManagement() {
|
|||
<Filter className="h-4 w-4" />
|
||||
Filter
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
className="gap-2 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
@ -214,58 +350,33 @@ export default function WasteManagement() {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Icon</TableHead>
|
||||
<TableHead>Nama Sampah</TableHead>
|
||||
<TableHead>Kategori</TableHead>
|
||||
<TableHead>Harga Saat Ini</TableHead>
|
||||
<TableHead>Perubahan</TableHead>
|
||||
<TableHead>Volume (kg)</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Harga Estimasi</TableHead>
|
||||
<TableHead>Variety</TableHead>
|
||||
<TableHead className="text-right">Aksi</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{wasteData.wasteTypes.map((waste) => (
|
||||
{wasteData.map((waste) => {
|
||||
const IconComponent = getWasteIcon(waste.trash_name);
|
||||
return (
|
||||
<TableRow key={waste.id}>
|
||||
<TableCell className="font-medium">{waste.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{waste.category}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono">
|
||||
Rp {waste.currentPrice.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div
|
||||
className={`flex items-center gap-1 ${
|
||||
waste.trend === "up"
|
||||
? "text-green-600"
|
||||
: waste.trend === "down"
|
||||
? "text-red-600"
|
||||
: "text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{waste.trend === "up" && (
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
)}
|
||||
{waste.trend === "down" && (
|
||||
<TrendingDown className="h-3 w-3" />
|
||||
)}
|
||||
<span className="text-sm">{waste.priceChange}</span>
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-green-100 rounded-lg">
|
||||
<IconComponent className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{waste.volume.toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
waste.status === "active" ? "default" : "secondary"
|
||||
}
|
||||
className={
|
||||
waste.status === "active"
|
||||
? "bg-green-100 text-green-800"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{waste.status === "active" ? "Aktif" : "Nonaktif"}
|
||||
</Badge>
|
||||
<TableCell className="font-medium">
|
||||
{waste.trash_name}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono">
|
||||
Rp {waste.estimated_price.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs">
|
||||
<div className="truncate" title={waste.variety}>
|
||||
{waste.variety}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
|
@ -275,11 +386,17 @@ export default function WasteManagement() {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem className="gap-2">
|
||||
<DropdownMenuItem
|
||||
className="gap-2"
|
||||
onClick={() => handleEdit(waste)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="gap-2 text-red-600">
|
||||
<DropdownMenuItem
|
||||
className="gap-2 text-red-600"
|
||||
onClick={() => handleDelete(waste)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Hapus
|
||||
</DropdownMenuItem>
|
||||
|
@ -287,11 +404,184 @@ export default function WasteManagement() {
|
|||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue