diff --git a/package.json b/package.json
index 88e4ccb5..3f1f10b6 100755
--- a/package.json
+++ b/package.json
@@ -107,6 +107,7 @@
"globals": "^15.14.0",
"postcss": "^8.5.3",
"tailwindcss": "^3.4.17",
+ "terser": "^5.46.2",
"typescript": "^5.8.2",
"vite": "^6.1.0"
}
diff --git a/pertanian-app.code-workspace b/pertanian-app.code-workspace
index e53c7909..6b2bf820 100755
--- a/pertanian-app.code-workspace
+++ b/pertanian-app.code-workspace
@@ -20,6 +20,12 @@
},
{
"path": "../skhp"
+ },
+ {
+ "path": "../dev/clipflow"
+ },
+ {
+ "path": "../dev/front-clipflow"
}
],
"settings": {}
diff --git a/src/Layout copy 2.jsx b/src/Layout copy 2.jsx
new file mode 100755
index 00000000..8c4dc9c4
--- /dev/null
+++ b/src/Layout copy 2.jsx
@@ -0,0 +1,252 @@
+import { Link, useLocation } from "react-router-dom";
+import { createPageUrl } from "@/utils";
+import {
+ LayoutDashboard, Users, Map, TreePine,
+ CloudOff, Menu, X, ChevronRight, Shield, Wifi, WifiOff, RefreshCw, LogOut
+} from "lucide-react";
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { OfflineService, isOnline as checkIsOnline } from "@/components/common/offlineStorage";
+import {useAuth} from '@/lib/AuthContext';
+
+function SyncIndicator() {
+ const [online, setOnline] = useState(checkIsOnline());
+ const [pendingCount, setPendingCount] = useState(0);
+
+ useEffect(() => {
+ let mounted = true;
+
+ const updateStatus = async () => {
+ const isConnected = await checkInternet();
+ const count = await OfflineService.getPendingCount();
+
+ if (mounted) {
+ setOnline(isConnected);
+ setPendingCount(count);
+ }
+ };
+
+ updateStatus();
+
+ // Event listener untuk perubahan koneksi
+ const handleStatusChange = () => updateStatus();
+ window.addEventListener("online", handleStatusChange);
+ window.addEventListener("offline", handleStatusChange);
+
+ const interval = setInterval(updateStatus, 3000); // Polling setiap 3 detik
+
+ return () => {
+ mounted = false;
+ window.removeEventListener("online", handleStatusChange);
+ window.removeEventListener("offline", handleStatusChange);
+ clearInterval(interval);
+ };
+ }, []);
+
+ const isSyncPending = online && pendingCount > 0;
+
+ return (
+
Memuat Sesi...
;
+ // }
+
+ const filteredNavigation = navigation.filter((item) => {
+ if (!user?.role) return false;
+ return item.roles.includes(user.role);
+ });
+
+ const isActivePage = (pageName) => {
+ return currentPageName === pageName || location.pathname.includes(pageName.toLowerCase());
+ };
+
+ return (
+
+ {/* Mobile Header */}
+
+
+
+
+
+ {/*
*/}
+

+
+
AgroMonev
+
+
+
+
+ {/* Mobile Sidebar Overlay */}
+ {sidebarOpen && (
+
setSidebarOpen(false)}
+ />
+ )}
+
+ {/* Sidebar */}
+
+
+ {/* Main Content */}
+
+ {children}
+
+
+ );
+}
diff --git a/src/Layout.jsx b/src/Layout.jsx
index a3888fa1..9edb8415 100755
--- a/src/Layout.jsx
+++ b/src/Layout.jsx
@@ -1,147 +1,172 @@
+import { moodConfig } from "@/utils/moodColor";
import { Link, useLocation } from "react-router-dom";
import { createPageUrl } from "@/utils";
import {
LayoutDashboard, Users, Map, TreePine,
- CloudOff, Menu, X, ChevronRight, Shield, Wifi, WifiOff, RefreshCw, LogOut
+ CloudOff, Menu, X, ChevronRight, Shield, Wifi, WifiOff, RefreshCw, LogOut, MessageSquare, Globe, BookOpenText
} from "lucide-react";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { OfflineService, isOnline as checkIsOnline } from "@/components/common/offlineStorage";
import {useAuth} from '@/lib/AuthContext';
+import SmartPopup from '@/components/ui/smartpopup';
-function SyncIndicator() {
- const [online, setOnline] = useState(checkIsOnline());
- const [pendingCount, setPendingCount] = useState(0);
-
- useEffect(() => {
- let mounted = true;
-
- const updateStatus = async () => {
- const isConnected = await checkInternet();
- const count = await OfflineService.getPendingCount();
-
- if (mounted) {
- setOnline(isConnected);
- setPendingCount(count);
- }
- };
-
- updateStatus();
-
- // Event listener untuk perubahan koneksi
- const handleStatusChange = () => updateStatus();
- window.addEventListener("online", handleStatusChange);
- window.addEventListener("offline", handleStatusChange);
-
- const interval = setInterval(updateStatus, 3000); // Polling setiap 3 detik
-
- return () => {
- mounted = false;
- window.removeEventListener("online", handleStatusChange);
- window.removeEventListener("offline", handleStatusChange);
- clearInterval(interval);
- };
- }, []);
-
- const isSyncPending = online && pendingCount > 0;
-
- return (
-
- {!online &&
}
- {online && isSyncPending &&
}
- {online && !isSyncPending &&
}
-
-
-
- {!online ? "Offline" : isSyncPending ? "Online (Sinkronisasi...)" : "Online"}
-
-
- {!online ? "Data disimpan lokal" : isSyncPending ? `${pendingCount} data diproses` : "Data sinkron"}
-
-
-
- );
-}
-
-// async function checkInternet() {
-// if (!navigator.onLine) return false;
-
-// try {
-// const controller = new AbortController();
-// setTimeout(() => controller.abort(), 3000);
-
-// const res = await fetch("/ping", {
-// method: "HEAD",
-// cache: "no-store",
-// signal: controller.signal,
-// });
-
-// return res.ok;
-// } catch {
-// return false;
-// }
-// }
-async function checkInternet() {
- if (!navigator.onLine) return false;
- try {
- const controller = new AbortController();
- setTimeout(() => controller.abort(), 3000);
- await fetch("https://www.google.com/favicon.ico", {
- method: "HEAD",
- mode: "no-cors",
- cache: "no-store",
- signal: controller.signal,
- });
- return true;
- } catch {
- return false;
+ async function checkInternet() {
+ if (!navigator.onLine) return false;
+ try {
+ const controller = new AbortController();
+ setTimeout(() => controller.abort(), 3000);
+ await fetch("https://www.google.com/favicon.ico", {
+ method: "HEAD",
+ mode: "no-cors",
+ cache: "no-store",
+ signal: controller.signal,
+ });
+ return true;
+ } catch {
+ return false;
+ }
}
-}
+ const wikiConfig = JSON.parse(import.meta.env.VITE_MEDIA_WIKI_URL || "[]");
-const navigation = [
- { name: "Dashboard", page: "Dashboard", icon: LayoutDashboard , roles: ['super_admin','fasilitator']},
- { name: "Petani", page: "Farmers", icon: Users, roles: ['super_admin','fasilitator'] },
- { name: "Lahan", page: "Lands", icon: Map, roles: ['super_admin','fasilitator'] },
- { name: "Tanaman", page: "Plants", icon: TreePine, roles: ['super_admin','fasilitator'] },
- { name: "Produktivitas", page: "ProductivityMonitoring", icon: LayoutDashboard, roles: ['super_admin', 'petani','fasilitator'] },
- { name: "Facilitator", page: "Validators", icon: Shield, roles: ['super_admin','fasilitator'] },
- { name: "Offtaker", page: "Offtakers", icon: Users, roles: ['super_admin','fasilitator'] },
- { name: "Portal Petani", page: "FarmerPortal", icon: Users, roles: ['super_admin', 'petani','fasilitator'] },
- { name: "Portal Validator", page: "ValidatorPortal", icon: Shield, roles: ['super_admin','fasilitator'] },
- { name: "Portal Offtaker", page: "OfftakerPortal", icon: Users, roles: ['super_admin','fasilitator'] },
-];
+ const RawNavigation = [
+ { name: "Dashboard", page: "Dashboard", icon: LayoutDashboard , roles: ['super_admin','fasilitator']},
+ { name: "Petani", page: "Farmers", icon: Users, roles: ['super_admin','fasilitator'] },
+ { name: "Lahan", page: "Lands", icon: Map, roles: ['super_admin','fasilitator'] },
+ { name: "Tanaman", page: "Plants", icon: TreePine, roles: ['super_admin','fasilitator'] },
+ { name: "Produktivitas", page: "ProductivityMonitoring", icon: LayoutDashboard, roles: ['super_admin', 'petani','fasilitator'] },
+ { name: "Facilitator", page: "Validators", icon: Shield, roles: ['super_admin','fasilitator'] },
+ { name: "Offtaker", page: "Offtakers", icon: Users, roles: ['super_admin','fasilitator'] },
+ { name: "Portal Petani", page: "FarmerPortal", icon: Users, roles: ['super_admin', 'petani','fasilitator'] },
+ { name: "Portal Validator", page: "ValidatorPortal", icon: Shield, roles: ['super_admin','fasilitator'] },
+ { name: "Portal Offtaker", page: "OfftakerPortal", icon: Users, roles: ['super_admin','fasilitator', 'offtaker'] },
+ // { name: "Wiki", url: "https://wiki.probindo.com",icon: BookOpenText, roles: ['super_admin','fasilitator', 'petani', 'offtaker'] },
+ ];
-export default function Layout({ children, currentPageName }) {
+ const navigation = [
+ ...RawNavigation,
+ ...wikiConfig.filter(item => item.active).map(item => ({
+ name: item.name,
+ url: item.url,
+ icon: BookOpenText,
+ roles: ['super_admin', 'fasilitator', 'petani', 'offtaker']
+ }))
+ ]
+ console.log("Final Navigation:", navigation, "ENV Config:", wikiConfig);
+ function SyncIndicator(colorMood = "rose") {
+ const [online, setOnline] = useState(checkIsOnline());
+ const [pendingCount, setPendingCount] = useState(0);
+
+ const currentTheme = moodConfig[colorMood] || moodConfig.emerald;
+
+ useEffect(() => {
+ let mounted = true;
+
+ const updateStatus = async () => {
+ const isConnected = await checkInternet();
+ const count = await OfflineService.getPendingCount();
+
+ if (mounted) {
+ setOnline(isConnected);
+ setPendingCount(count);
+ }
+ };
+
+ updateStatus();
+
+ // Event listener untuk perubahan koneksi
+ const handleStatusChange = () => updateStatus();
+ window.addEventListener("online", handleStatusChange);
+ window.addEventListener("offline", handleStatusChange);
+
+ const interval = setInterval(updateStatus, 3000); // Polling setiap 3 detik
+
+ return () => {
+ mounted = false;
+ window.removeEventListener("online", handleStatusChange);
+ window.removeEventListener("offline", handleStatusChange);
+ clearInterval(interval);
+ };
+ }, []);
+
+ const isSyncPending = online && pendingCount > 0;
+
+ return (
+
+ {!online &&
}
+ {online && isSyncPending &&
}
+ {online && !isSyncPending &&
}
+
+
+
+ {!online ? "Offline" : isSyncPending ? "Online (Sinkronisasi...)" : "Online"}
+
+
+ {!online ? "Data disimpan lokal" : isSyncPending ? `${pendingCount} data diproses` : "Data sinkron"}
+
+
+
+ );
+ }
+
+export default function Layout({ children, currentPageName, colorMood = "emerald" }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const location = useLocation();
const { user, logout } = useAuth();
- // if (!user) {
- // return
Memuat Sesi...
;
- // }
-
+ // Ambil konfigurasi warna berdasarkan mood yang dipilih
+ const currentTheme = moodConfig[colorMood] || moodConfig.emerald;
+
+ // MEssage
+ const [showMessages, setShowMessages] = useState(false);
+
+ const [messages] = useState([
+ { id: 1, title: "Pelatihan", description: "Pelatihan Bibit Kemiri pada tanggal: 24 Apr 2026, agenda simulasi penanaman yang baik, mengidentifikasikan mondisi tanah", date: "24 Apr 2026", read: false },
+ ]);
+
+ const unreadCount = messages.filter(m => !m.read).length;
+
+
const filteredNavigation = navigation.filter((item) => {
if (!user?.role) return false;
return item.roles.includes(user.role);
});
const isActivePage = (pageName) => {
+ if(!currentPageName || !pageName) return false;
return currentPageName === pageName || location.pathname.includes(pageName.toLowerCase());
};
+ const themeHex = {
+ purple: "#7c3aed",
+ rose: "#f2859d",
+ emerald: "#10b981",
+ blue: "#3b82f6"
+ };
+
+ // const themeRGB = {
+ // purple: "124, 58, 237",
+ // rose: "242, 133, 157",
+ // emerald: "16, 185, 129",
+ // blue: "59, 130, 246"
+ // };
+
return (
-
- {/* Mobile Header */}
-
+
+ {/* ... bagian header mobile ... */}
+
+ { user.role === 'petani' &&(
+
+
+ {showMessages && (
+ <>
+
setShowMessages(false)} />
+
+
+
+
Pesan Terakhir
+ Tandai dibaca
+
+
+
+ {messages.length > 0 ? (
+ messages.map((msg) => (
+
+
+
{msg.title}
+ {msg.date}
+
+
+ {msg.description}
+
+
+ ))
+ ) : (
+
+ Tidak ada pesan baru
+
+ )}
+
+
+
+
+ >
+ )}
+
+ )}
{/* Mobile Sidebar Overlay */}
@@ -166,63 +248,76 @@ export default function Layout({ children, currentPageName }) {
{/* Sidebar */}
diff --git a/src/components/farmers/FarmerForm.jsx b/src/components/farmers/FarmerForm.jsx
index b2dd8a72..86bc7598 100755
--- a/src/components/farmers/FarmerForm.jsx
+++ b/src/components/farmers/FarmerForm.jsx
@@ -15,17 +15,32 @@ import { useQuery } from "@tanstack/react-query";
// ];
export default function FarmerForm({ initialData, onSubmit, isLoading }) {
+ const [locationData, setLocationData ] = useState({
+ provinces: [],
+ regencies: [],
+ districts: [],
+ villages: []
+ });
const [formData, setFormData] = useState(initialData || {
- nik: "",
- full_name: "",
- phone: "",
+ ktp: "",
+ kk:"",
+ nama: "",
+ email:"",
+ desa_kelurahan_id:'',
+ telepon: "",
+ alamat: "",
farmer_group: "",
village: "",
district: "",
regency: "",
province: "",
verification_status: "pending",
- sync_status: "pending"
+ sync_status: "pending",
+ kecamatan_id:"",
+ kabkota: "",
+ provinsi:"",
+ password:"",
+ password_confirmation: ""
});
const { data: rawProvinces } = useQuery({
@@ -71,6 +86,77 @@ export default function FarmerForm({ initialData, onSubmit, isLoading }) {
}
}, [provinces]);
+ useEffect(() => {
+ const autoFillLocation = async () => {
+ if(!formData.desa_kelurahan_id) return;
+ const selectedVillage = locationData.villages.find(data => formData.desa_kelurahan_id === data.id);
+ if(!selectedVillage) return;
+ console.log(selectedVillage, "Selected Village");
+ if (selectedVillage) {
+ const selectedDistrict = selectedVillage.kecamatan;
+ const selectedRegency = selectedDistrict?.kabupatenKota;
+ const selectedProvince = selectedRegency?.provinsi;
+ setLocationData((prev) => ({
+ ...prev,
+ districts: selectedDistrict ? [selectedDistrict] : [],
+ regencies: selectedRegency ? [selectedRegency] : [],
+ provinces: selectedProvince ? [selectedProvince] : [],
+ }));
+ }
+ };
+
+ autoFillLocation();
+ }, [formData.desa_kelurahan_id, locationData.villages]);
+
+ useEffect(() => {
+ const prepareDataLocation = async () => {
+ // 1. Cek status koneksi
+ const isOnline = navigator.onLine;
+
+ try {
+ let data;
+ if (isOnline) {
+ console.log("Fetching from Server...");
+ const [v, d, r, p] = await Promise.all([
+ entity('master', 'desa-kelurahan').list(),
+ entity('master', 'kecamatan').list(),
+ entity('master', 'kabupaten-kota').list(),
+ entity('master', 'provinsi').list(),
+ ]);
+
+ data = {
+ villages: v?.data?.data || v?.data || [],
+ districts: d?.data?.data || d?.data || [],
+ regencies: r?.data?.data || r?.data || [],
+ provinces: p?.data?.data || p?.data || []
+ };
+ } else {
+ // JIKA OFFLINE: Ambil dari Dexie / SQLite
+ console.log("Loading from Offline Storage...");
+ const [v, d, r, p] = await Promise.all([
+ OfflineService.getEntities('villages'),
+ OfflineService.getEntities('districts'),
+ OfflineService.getEntities('regencies'),
+ OfflineService.getEntities('provinces'),
+ ]);
+
+ data = {
+ villages: v || [],
+ districts: d || [],
+ regencies: r || [],
+ provinces: p || []
+ };
+ }
+
+ setLocationData(data);
+ } catch (err) {
+ console.error("Gagal memuat data lokasi:", err);
+ }
+ };
+
+ prepareDataLocation();
+ }, []);
+ console.log(locationData);
return (