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 ( +
+ {!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; + } +} + + +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'] }, +]; + +export default function LayoutCopy({ children, currentPageName }) { + const [sidebarOpen, setSidebarOpen] = useState(false); + const location = useLocation(); + const { user, logout } = useAuth(); + + // if (!user) { + // 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 */} - {/* Main Content */} -
+
{children}
+
); -} +} \ No newline at end of file diff --git a/src/components/common/SyncManager.jsx b/src/components/common/SyncManager.jsx index 40cef171..6ff3033a 100755 --- a/src/components/common/SyncManager.jsx +++ b/src/components/common/SyncManager.jsx @@ -15,13 +15,16 @@ export function useSyncManager() { setSyncing(true); try { const hasPending = await OfflineService.hasPendingData(); - + // checking if has pending data if (hasPending) { await OfflineService.syncAllPending(); toast.success("Sinkronisasi data berhasil"); - } else { + } + // othewise resync all data from local to server and server to local + else { // await OfflineService.downloadFromServer(); - await OfflineService.syncDataFromServer(); + // await OfflineService.syncDataFromServer(); + await OfflineService.reconcileAndPush(); } } catch (error) { console.error('Sync process failed:', error); diff --git a/src/components/common/ThemeButton.jsx b/src/components/common/ThemeButton.jsx new file mode 100644 index 00000000..3a18c7c7 --- /dev/null +++ b/src/components/common/ThemeButton.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { useTheme } from './ThemeContext'; + +const ThemeButton = ({ children, onClick }) => { + const { theme } = useTheme(); + + const buttonStyle = { + backgroundColor: theme === 'light' ? '#fff' : '#333', + color: theme === 'light' ? '#000' : '#fff', + border: '1px solid #ccc', + padding: '10px 20px', + cursor: 'pointer', + }; + + return ( + + ); +}; + +export default ThemeButton; \ No newline at end of file diff --git a/src/components/common/ThemeContext.jsx b/src/components/common/ThemeContext.jsx new file mode 100644 index 00000000..b7a630bc --- /dev/null +++ b/src/components/common/ThemeContext.jsx @@ -0,0 +1,19 @@ +import React, { createContext, useContext, useState } from 'react'; + +const ThemeContext = createContext(); + +export const ThemeProvider = ({ children }) => { + const [theme, setTheme] = useState('light'); // 'light' atau 'dark' + + const toggleTheme = () => { + setTheme((prev) => (prev === 'light' ? 'dark' : 'light')); + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => useContext(ThemeContext); diff --git a/src/components/common/offlineStorage.jsx b/src/components/common/offlineStorage.jsx index e0c3f881..82bf4604 100755 --- a/src/components/common/offlineStorage.jsx +++ b/src/components/common/offlineStorage.jsx @@ -93,6 +93,7 @@ export const OfflineService = { db.plants.where('sync_status').equals('pending').count(), db.harvests.where('sync_status').equals('pending').count(), db.plant_inspections.where('sync_status').equals('pending').count(), + db.land_inspections.where('sync_status').equals('pending').count(), db.offtakers.where('sync_status').equals('pending').count(), db.profiles.where('sync_status').equals('pending').count(), db.distributions.where('sync_status').equals('pending').count(), @@ -134,7 +135,8 @@ export const OfflineService = { { from: db.lands, to:"lahan", endpoint: 'map/lahan' }, { from: db.plants, to:"tanaman", endpoint: 'map/tanaman' }, { from: db.harvests, to:"panen", endpoint: 'map/panen'}, - { from: db.plant_inspections, to:"inspeksi", endpoint: 'map/inspeksi'}, + { from: db.plant_inspections, to:"inspeksi-tanaman", endpoint: 'map/inspeksi/tanaman'}, + { from: db.land_inspections, to:"inspeksi-lahan", endpoint: 'map/inspeksi/lahan'}, { from: db.offtakers, to:"offtaker", endpoint: 'map/offtaker'}, { from: db.distributions, to:"panen", endpoint: 'distribusi/panen'}, { from: db.villages, to: "desa-kelurahan", endpoint: "master/desa-kelurahan" }, @@ -146,13 +148,13 @@ export const OfflineService = { for (const config of syncConfigs) { try { console.log(`Syncing table: ${config.from.name}...`); - + // getting data from server const response = await axios.get(`${baseURL}/api/${config.endpoint}`, { headers: { Authorization: `Bearer ${token}` } }); - console.log(response, "Res") + // check respond status if (response.status !== 200) throw new Error(`HTTP error! status: ${response.status}`); - + // take respond data const result = await response; const dataToSave = config.to ? result[config.to] : (result.data || result.data.data || []); @@ -196,7 +198,8 @@ export const OfflineService = { { endpoint: 'map/lahan', table: db.lands }, { endpoint: 'map/tanaman', table: db.plants }, { endpoint: 'map/panen', table: db.harvests}, - { endpoint: 'map/inspeksi', table: db.plant_inspections}, + { endpoint: 'map/inspeksi/tanaman', table: db.plant_inspections}, + { endpoint: 'map/inspeksi/lahan', table: db.land_inspections}, { endpoint: 'map/offtaker', table: db.offtakers}, { endpoint: 'distribusi/panen', table: db.distributions}, // { endpoint: 'map/validator', table: db.validators} @@ -255,11 +258,12 @@ export const OfflineService = { }, syncAllPending: async () => { const tables = [ - { from: db.farmers, to:"profile", endpoint: 'auth/profile/1' }, + { from: db.farmers, to:"profile", endpoint: 'auth/profile' }, { from: db.lands, to:"lahan", endpoint: 'map/lahan' }, { from: db.plants, to:"tanaman", endpoint: 'map/tanaman' }, { from: db.harvests, to:"panen", endpoint: 'map/panen'}, - { from: db.plant_inspections, to:"inspeksi", endpoint: 'map/inspeksi'}, + { from: db.plant_inspections, to:"inspeksi-tanaman", endpoint: 'map/inspeksi/tanaman'}, + { from: db.land_inspections, to:"inspeksi-lahan", endpoint: 'map/inspeksi/lahan'}, { from: db.offtakers, to:"offtaker", endpoint: 'map/offtaker'}, { from: db.distributions, to:"panen", endpoint: 'distribusi/panen'}, // { endpoint: 'map/validator', to:"validator", from: db.validators} @@ -271,25 +275,40 @@ export const OfflineService = { const token = localStorage.getItem('access_token'); const baseURL = import.meta.env.VITE_BASE44_API_URL.replace(/\/+$/, ""); - let hasChanged = false; for (const item of tables) { + //get pending data const items = await item.from.where('sync_status').equals('pending').toArray(); + for (const record of items) { try { + // data preparation const { sync_status, ...dataToPush } = record; - + //convert the data + if(item.to === 'lahan' && typeof dataToPush.path === 'string' ){ + const pathToArray = JSON.parse(dataToPush.path); + dataToPush.path = pathToArray + console.log("Data lahan => ", dataToPush); + } + if (dataToPush.luas_lahan) { dataToPush.luas_lahan = parseFloat(parseFloat(dataToPush.luas_lahan).toFixed(2)); } - + //try to push const response = await axios.post(`${baseURL}/api/${item.endpoint}`, dataToPush, { headers: { Authorization: `Bearer ${token}` } }); - + //check respond from server if (response.status === 200 || response.status === 201) { - await item.table.delete(record.id); + // server id + const newId = response.data?.data?.id || record.id; + + await item.from.update(record.id, { + sync_status : 'synced', + id : newId + }) + // await item.table.delete(record.id); hasChanged = true; } } catch (e) { @@ -305,6 +324,58 @@ export const OfflineService = { await OfflineService.downloadFromServer(); } }, + reconcileAndPush: async () => { + const token = localStorage.getItem('access_token'); + const baseURL = import.meta.env.VITE_BASE44_API_URL.replace(/\/+$/, ""); + + const tables = [ + { from: db.farmers, to:"profile", endpoint: 'auth/profile/1' }, + { from: db.lands, to:"lahan", endpoint: 'map/lahan' }, + { from: db.plants, to:"tanaman", endpoint: 'map/tanaman' }, + { from: db.harvests, to:"panen", endpoint: 'map/panen'}, + { from: db.plant_inspections, to:"inspeksi-tanaman", endpoint: 'map/inspeksi/tanaman'}, + { from: db.land_inspections, to:"inspeksi-lahan", endpoint: 'map/inspeksi/lahan'}, + { from: db.offtakers, to:"offtaker", endpoint: 'map/offtaker'}, + { from: db.distributions, to:"panen", endpoint: 'distribusi/panen'}, + // { endpoint: 'map/validator', to:"validator", from: db.validators} + { from: db.villages, to: "desa-kelurahan", endpoint: "master/desa-kelurahan" }, + { from: db.districts, to: "kecamatan", endpoint: "master/kecamatan"}, + { from: db.regencies, to: "kabupaten-kota", endpoint: "master/kabupaten-kota"}, + { from: db.provinces, to: "provinsi", endpoint: "master/provinsi"}, + ]; + + for (const item of tables) { + try { + // get all data from server + const response = await axios.get(`${baseURL}/api/${item.endpoint}`, { + headers: { Authorization: `Bearer ${token}` } + }); + const serverData = response.data?.data || []; + const serverIds = new Set(serverData.map(d => d.id)); + + // check data doesnt exis on server but exist locally by id + const localSynced = await item.from.where('sync_status').equals('synced').toArray(); + if(item.to == 'tanaman'){ + console.log(localSynced, "Tanaman "); + } + const missingInServer = localSynced.filter(local => !serverIds.has(local.id)); + + if (missingInServer.length > 0) { + console.log(`Menemukan ${missingInServer.length} data hantu di ${item.from.name}, mengembalikan ke pending...`); + + // returning data to pending + const idsToFix = missingInServer.map(d => d.id); + await item.from.where('id').anyOf(idsToFix).modify({ + sync_status: 'pending' + }); + } + } catch (err) { + console.error(`Gagal rekonsiliasi tabel ${item.from.name}:`, err); + } + } + // sync all pending when state change + await OfflineService.syncAllPending(); + }, cacheMasterData: async (villages) => { if (villages.length > 0) { await db.master_villages.clear(); diff --git a/src/components/farmers/FarmerCard.jsx b/src/components/farmers/FarmerCard.jsx index fde3322d..921a3d19 100755 --- a/src/components/farmers/FarmerCard.jsx +++ b/src/components/farmers/FarmerCard.jsx @@ -4,6 +4,9 @@ import { Button } from "@/components/ui/button"; import { MapPin, Phone, Users, ChevronRight } from "lucide-react"; import { Link } from "react-router-dom"; import { createPageUrl } from "@/utils"; +import { Switch } from "@/components/ui/switch"; +import { entity } from "@/api/entities"; +import { useMutation } from "@tanstack/react-query"; const statusColors = { pending: "bg-amber-100 text-amber-700 border-amber-200", @@ -18,7 +21,13 @@ const statusLabels = { }; export default function FarmerCard({ farmer }) { - console.log("Petani : ", farmer); + const toggleActiveMutation = useMutation({ + mutationFn: async ({ id, email_verified_at, farmer }) => await entity('auth','profile').update(id, { email_verified_at, ...farmer}), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["validators"] }); + toast.success("Status validator diperbarui"); + } + }); return (
@@ -27,7 +36,7 @@ export default function FarmerCard({ farmer }) { {farmer?.name ) : ( - {farmer.full_name?.charAt(0)?.toUpperCase() || "P"} + {farmer.full_name?.charAt(0)?.toUpperCase() || "N/A"} )}
@@ -38,9 +47,16 @@ export default function FarmerCard({ farmer }) {

{farmer?.full_name || farmer?.name || farmer?.nama }

NIK: {farmer?.kk}

- - {statusLabels[farmer.user.email_verified_at !== null || farmer.user.email_verified_at !== undefined ? 'verified' : 'pending']} - + { + const timestamp = checked ? new Date().toISOString() : null; + toggleActiveMutation.mutate({ id: farmer.id, email_verified_at: timestamp, farmer: farmer }) + }} + /> + {/* + {statusLabels[farmer?.user?.email_verified_at !== null || farmer?.user?.email_verified_at !== undefined ? 'verified' : 'pending']} + */}
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 (
@@ -81,13 +167,66 @@ export default function FarmerForm({ initialData, onSubmit, isLoading }) { + {/*

Data Pengguna

*/} +
+
+ + handleChange("email", e.target.value)} + placeholder="E-mail" + required + className="h-11" + /> +
+
+ + handleChange("nama", e.target.value)} + placeholder="Nama sesuai KTP" + required + className="h-11" + /> +
+
+
+
+ + handleChange("password", e.target.value)} + required + /> +
+
+ + handleChange("password_confirmation", e.target.value)} + required + /> +
+
+
+ {/*

Data Petani

*/}
- + handleChange("nik", e.target.value)} + onChange={(e) => handleChange("ktp", e.target.value)} placeholder="16 digit NIK" maxLength={16} required @@ -95,6 +234,18 @@ export default function FarmerForm({ initialData, onSubmit, isLoading }) { />
+ + handleChange("kk", e.target.value)} + placeholder="nomor induk kartu keluarga" + maxLength={16} + required + className="h-11" + /> +
+ {/*
+
*/} +
+
+
+ +