AgroMonev
diff --git a/src/Layout2.jsx b/src/Layout2.jsx
old mode 100644
new mode 100755
index 6d3017dc..0134da17
--- a/src/Layout2.jsx
+++ b/src/Layout2.jsx
@@ -60,7 +60,7 @@ const navigation = [
{ name: "Lahan", page: "Lands", icon: Map },
{ name: "Tanaman", page: "Plants", icon: TreePine },
{ name: "Produktivitas", page: "ProductivityMonitoring", icon: LayoutDashboard },
- { name: "Validator", page: "Validators", icon: Shield },
+ { name: "Facilitator", page: "Validators", icon: Shield },
{ name: "Offtaker", page: "Offtakers", icon: Users },
{ name: "Portal Petani", page: "FarmerPortal", icon: Users },
{ name: "Portal Validator", page: "ValidatorPortal", icon: Shield },
diff --git a/src/_mock/api_lama.js b/src/_mock/api_lama.js
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Comodity.json b/src/_mock/data/Comodity.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Distribution.json b/src/_mock/data/Distribution.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Farmer.json b/src/_mock/data/Farmer.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Harvest.json b/src/_mock/data/Harvest.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Inspection.json b/src/_mock/data/Inspection.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Land.json b/src/_mock/data/Land.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/LandValidation.json b/src/_mock/data/LandValidation.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Offtaker.json b/src/_mock/data/Offtaker.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Plant.json b/src/_mock/data/Plant.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/PlantInspection.json b/src/_mock/data/PlantInspection.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Province.json b/src/_mock/data/Province.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/Synclog.json b/src/_mock/data/Synclog.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/offlineData.js b/src/_mock/data/offlineData.js
old mode 100644
new mode 100755
diff --git a/src/_mock/data/propinsi.json b/src/_mock/data/propinsi.json
old mode 100644
new mode 100755
diff --git a/src/_mock/data/validator.json b/src/_mock/data/validator.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/Comodity.json b/src/_mock/schema/Comodity.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/Distribution.json b/src/_mock/schema/Distribution.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/Farmer.json b/src/_mock/schema/Farmer.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/Land.json b/src/_mock/schema/Land.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/LandValidation.json b/src/_mock/schema/LandValidation.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/Plant.json b/src/_mock/schema/Plant.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/PlantInspection.json b/src/_mock/schema/PlantInspection.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/Synclog.json b/src/_mock/schema/Synclog.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/Validator.json b/src/_mock/schema/Validator.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/farmers.json b/src/_mock/schema/farmers.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/lands.json b/src/_mock/schema/lands.json
old mode 100644
new mode 100755
diff --git a/src/_mock/schema/plants.json b/src/_mock/schema/plants.json
old mode 100644
new mode 100755
diff --git a/src/api/Client.js b/src/api/Client.js
old mode 100644
new mode 100755
diff --git a/src/api/entities.js b/src/api/entities.js
old mode 100644
new mode 100755
diff --git a/src/components/UserNotRegisteredError.jsx b/src/components/UserNotRegisteredError.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/EmptyState.jsx b/src/components/common/EmptyState.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/OfflineStorage_001.jsx b/src/components/common/OfflineStorage_001.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/OfflineStorage_002.jsx b/src/components/common/OfflineStorage_002.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/RoleBasedGuard.jsx b/src/components/common/RoleBasedGuard.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/SyncManager copy.jsx b/src/components/common/SyncManager copy.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/SyncManager.jsx b/src/components/common/SyncManager.jsx
old mode 100644
new mode 100755
index 77f81fc8..40cef171
--- a/src/components/common/SyncManager.jsx
+++ b/src/components/common/SyncManager.jsx
@@ -17,12 +17,11 @@ export function useSyncManager() {
const hasPending = await OfflineService.hasPendingData();
if (hasPending) {
- // console.log("📤 Memulai Push Sinkronisasi...");
await OfflineService.syncAllPending();
toast.success("Sinkronisasi data berhasil");
} else {
- // console.log("🔄 Data lokal bersih. Melakukan Pull dari server...");
- await OfflineService.downloadFromServer();
+ // await OfflineService.downloadFromServer();
+ await OfflineService.syncDataFromServer();
}
} catch (error) {
console.error('Sync process failed:', error);
diff --git a/src/components/common/SyncStatus.jsx b/src/components/common/SyncStatus.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/SyncStatusBar.jsx b/src/components/common/SyncStatusBar.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/offlineStorage.jsx b/src/components/common/offlineStorage.jsx
old mode 100644
new mode 100755
index ff55a4f0..e0c3f881
--- a/src/components/common/offlineStorage.jsx
+++ b/src/components/common/offlineStorage.jsx
@@ -14,7 +14,7 @@ const agroApi = axios.create({
export const OfflineService = {
hasPendingData: async () => {
- const tables = ['farmers', 'lands', 'plants', 'harvest', 'plant_inspections', 'offtakers', 'validators'];
+ const tables = ['farmers', 'lands', 'plants', 'harvests', 'plant_inspections', 'offtakers', 'validators', 'distributions','profiles'];
for (const table of tables) {
const count = await db[table].where('sync_status').equals('pending').count();
if (count > 0) return true;
@@ -44,7 +44,7 @@ export const OfflineService = {
});
return id;
} catch (err) {
- console.error("❌ Dexie Error:", err);
+ console.error("Error:", err);
throw err;
}
},
@@ -78,8 +78,6 @@ export const OfflineService = {
if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
return await dbTable.where(filter).toArray();
}
-
- console.log(`Fetching ...... all from ${table}`);
return await dbTable.reverse().toArray();
} catch (e) {
@@ -93,9 +91,11 @@ export const OfflineService = {
db.farmers.where('sync_status').equals('pending').count(),
db.lands.where('sync_status').equals('pending').count(),
db.plants.where('sync_status').equals('pending').count(),
- db.harvest.where('sync_status').equals('pending').count(),
+ db.harvests.where('sync_status').equals('pending').count(),
db.plant_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(),
// db.validators.where('sync_status').equals('pending').count(),
]);
@@ -126,6 +126,50 @@ export const OfflineService = {
throw err;
}
},
+ syncDataFromServer : async () => {
+ const token = localStorage.getItem('access_token');
+ const baseURL = import.meta.env.VITE_BASE44_API_URL.replace(/\/+$/, "");
+ const syncConfigs = [
+ { 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", endpoint: 'map/inspeksi'},
+ { 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" },
+ { 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 config of syncConfigs) {
+ try {
+ console.log(`Syncing table: ${config.from.name}...`);
+
+ const response = await axios.get(`${baseURL}/api/${config.endpoint}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ console.log(response, "Res")
+ if (response.status !== 200) throw new Error(`HTTP error! status: ${response.status}`);
+
+ const result = await response;
+ const dataToSave = config.to ? result[config.to] : (result.data || result.data.data || []);
+
+ if (Array.isArray(dataToSave)) {
+ await config.table.bulkPut(dataToSave);
+ console.log(`Berhasil sinkron ${dataToSave.length} data ke ${config.table.name}`);
+ } else if (typeof dataToSave === 'object' && dataToSave !== null) {
+ await config.table.put(dataToSave);
+ }
+
+ } catch (error) {
+ console.error(`Gagal sinkronisasi ${config.endpoint}:`, error);
+ // Lanjut ke config berikutnya meski satu error
+ continue;
+ }
+ }
+ },
syncAll: async () => {
const queue = await db.pending_sync.toArray();
for (const item of queue) {
@@ -147,18 +191,19 @@ export const OfflineService = {
const baseURL = import.meta.env.VITE_BASE44_API_URL.replace(/\/+$/, "");
console.log(user, "Userr")
const syncConfigs = [
- { endpoint: `auth/profile/1`, table: db.farmers },
+ { endpoint: `auth/profile/1`, table: db.farmers },
+ // { endpoint: `auth/me`, table: db.users },
{ endpoint: 'map/lahan', table: db.lands },
{ endpoint: 'map/tanaman', table: db.plants },
- { endpoint: 'map/panen', table: db.harvest},
+ { endpoint: 'map/panen', table: db.harvests},
{ endpoint: 'map/inspeksi', table: db.plant_inspections},
{ endpoint: 'map/offtaker', table: db.offtakers},
{ endpoint: 'distribusi/panen', table: db.distributions},
// { endpoint: 'map/validator', table: db.validators}
- { table: db.villages, to: "desa-kelurahan", endpoint: "master/desa-kelurahan" },
- { table: db.districts, to: "kecamatan", endpoint: "master/kecamatan"},
- { table: db.regencies, to: "kabupaten-kota", endpoint: "master/kabupaten-kota"},
- { table: db.provinces, to: "provinsi", endpoint: "master/provinsi"},
+ { endpoint: "master/desa-kelurahan", table: db.villages, to: "desa-kelurahan"},
+ { endpoint: "master/kecamatan", table: db.districts, to: "kecamatan"},
+ { endpoint: "master/kabupaten-kota", table: db.regencies, to: "kabupaten-kota"},
+ { endpoint: "master/provinsi", table: db.provinces, to: "provinsi"},
];
@@ -168,34 +213,55 @@ export const OfflineService = {
headers: { Authorization: `Bearer ${token}` }
});
- const respondData = Array.isArray(respond.data) ? respond.data : respond.data.data || [];
- if(respondData.length > 0){
- const localIds = await config.table.toCollection().primaryKeys();
- const newData = respondData.filter(item => !localIds.includes(item.id));
- if(newData.length > 0){
- await config.table.bulkAdd(newData.map(item => ({
- ...item,
- sync_status: 'synced'
- })));
- }else {
- // console.log(` Data ${config.endpoint} sudah mutakhir (tidak ada data baru).`);
+ const rawData = respond.data?.data || respond.data;
+ console.log(rawData, "Raw Data")
+ let respondData = [];
+
+ // const respondData = (config.endpoint.includes('auth/profile') || config.endpoint.includes('auth/me'))
+ // ? [rawData]
+ // : Array.isArray(rawData) ? rawData : [];
+
+ if (config.endpoint.includes('auth/profile') || config.endpoint.includes('auth/me')) {
+ if (rawData && typeof rawData === 'object') {
+ const { password, ...cleanData } = rawData;
+ respondData = [cleanData]
+ }
+ } else {
+ // Jika data memang seharusnya array (List)
+ respondData = Array.isArray(rawData) ? rawData : [];
+ }
+
+ console.log(respondData," || data " ,config.table)
+ if(respondData.length > 0 && respondData[0] !== null){
+ const preparedData = respondData.map(item => ({
+ ...item,
+ id: item.id,
+ sync_status: 'synced'
+ })).filter(item => item.id !== undefined);
+
+ if (preparedData.length > 0) {
+ await config.table.bulkPut(preparedData);
+ console.log(`✅ Berhasil sync ${preparedData.length} data ke ${config.table.name}`);
}
}
}
} catch (err) {
console.error("Gagal download data:", err);
+ if (err.failures) {
+ console.error("Detail kegagalan Dexie:", err.failures);
+ }
}
},
syncAllPending: async () => {
const tables = [
- { from: db.farmers, to:"profile", endpoint: 'auth/profile' },
+ { 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' },
- { endpoint: 'map/panen', to:"panen", from: db.harvest},
- { endpoint: 'map/inspeksi', to:"inspeksi", from: db.plant_inspections},
- { endpoint: 'map/offtaker', to:"offtaker", from: db.offtakers},
- { endpoint: 'distribusi/panen', to:"panen", from: db.distributions},
+ { from: db.harvests, to:"panen", endpoint: 'map/panen'},
+ { from: db.plant_inspections, to:"inspeksi", endpoint: 'map/inspeksi'},
+ { 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"},
diff --git a/src/components/common/useOfflineEntity copy.jsx b/src/components/common/useOfflineEntity copy.jsx
old mode 100644
new mode 100755
diff --git a/src/components/common/useOfflineEntity.jsx b/src/components/common/useOfflineEntity.jsx
old mode 100644
new mode 100755
diff --git a/src/components/dashboard/CommodityPieChart.jsx b/src/components/dashboard/CommodityPieChart.jsx
old mode 100644
new mode 100755
diff --git a/src/components/dashboard/RegionChart.jsx b/src/components/dashboard/RegionChart.jsx
old mode 100644
new mode 100755
diff --git a/src/components/dashboard/StatCard.jsx b/src/components/dashboard/StatCard.jsx
old mode 100644
new mode 100755
diff --git a/src/components/distribution/DistributionForm.jsx b/src/components/distribution/DistributionForm.jsx
old mode 100644
new mode 100755
diff --git a/src/components/distribution/DistributionForm_01.jsx b/src/components/distribution/DistributionForm_01.jsx
old mode 100644
new mode 100755
diff --git a/src/components/distribution/DistributionGrading.jsx b/src/components/distribution/DistributionGrading.jsx
old mode 100644
new mode 100755
diff --git a/src/components/farmers/FarmerCard.jsx b/src/components/farmers/FarmerCard.jsx
old mode 100644
new mode 100755
index 8571c631..fde3322d
--- a/src/components/farmers/FarmerCard.jsx
+++ b/src/components/farmers/FarmerCard.jsx
@@ -12,18 +12,19 @@ const statusColors = {
};
const statusLabels = {
- pending: "Menunggu Verifikasi",
- verified: "Terverifikasi",
- rejected: "Ditolak"
+ pending: "W",
+ verified: "V",
+ rejected: "X"
};
export default function FarmerCard({ farmer }) {
+ console.log("Petani : ", farmer);
return (
{farmer.photo_url ? (
-

+

) : (
{farmer.full_name?.charAt(0)?.toUpperCase() || "P"}
@@ -34,18 +35,18 @@ export default function FarmerCard({ farmer }) {
-
{farmer.full_name}
-
NIK: {farmer.nik}
+
{farmer?.full_name || farmer?.name || farmer?.nama }
+
NIK: {farmer?.kk}
-
- {statusLabels[farmer.verification_status]}
+
+ {statusLabels[farmer.user.email_verified_at !== null || farmer.user.email_verified_at !== undefined ? 'verified' : 'pending']}
- {farmer.village}, {farmer.district}
+ {farmer.alamat}
{farmer.farmer_group && (
@@ -53,10 +54,10 @@ export default function FarmerCard({ farmer }) {
{farmer.farmer_group}
)}
- {farmer.phone && (
+ {farmer.telepon && (
-
{farmer.phone}
+
{farmer?.telepon}
)}
diff --git a/src/components/farmers/FarmerForm.jsx b/src/components/farmers/FarmerForm.jsx
old mode 100644
new mode 100755
diff --git a/src/components/farmers/LinkUserAccount.jsx b/src/components/farmers/LinkUserAccount.jsx
old mode 100644
new mode 100755
diff --git a/src/components/harvest/HarvestForm.jsx b/src/components/harvest/HarvestForm.jsx
old mode 100644
new mode 100755
diff --git a/src/components/lands/GPSLandMapper copy 2.jsx b/src/components/lands/GPSLandMapper copy 2.jsx
new file mode 100755
index 00000000..b89f21de
--- /dev/null
+++ b/src/components/lands/GPSLandMapper copy 2.jsx
@@ -0,0 +1,242 @@
+import { useState, useEffect, useMemo } from "react";
+import { MapContainer, TileLayer, Marker, Polygon, useMap, Circle } from "react-leaflet";
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Locate, MapPin, Trash2, Save, Navigation, RotateCcw, AlertTriangle } from "lucide-react";
+import { toast } from "sonner";
+import "leaflet/dist/leaflet.css";
+import L from "leaflet";
+
+// Fix default marker icon
+delete L.Icon.Default.prototype._getIconUrl;
+L.Icon.Default.mergeOptions({
+ iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
+ iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
+ shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
+});
+
+const getColorByIndex = (index) => {
+ const colors = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#ec4899", "#14b8a6"];
+ return colors[index % colors.length];
+};
+
+/**
+ * Kalkulasi Luas Lahan (Hektar) yang lebih presisi
+ */
+function calculatePolygonArea(points) {
+ if (points.length < 3) return 0;
+
+ const radius = 6378137; // Radius Bumi (meters)
+ let area = 0;
+
+ for (let i = 0; i < points.length; i++) {
+ const p1 = points[i];
+ const p2 = points[(i + 1) % points.length];
+
+ // Konversi ke Radians
+ const lat1 = p1[0] * Math.PI / 180;
+ const lon1 = p1[1] * Math.PI / 180;
+ const lat2 = p2[0] * Math.PI / 180;
+ const lon2 = p2[1] * Math.PI / 180;
+
+ area += (lon2 - lon1) * (2 + Math.sin(lat1) + Math.sin(lat2));
+ }
+
+ area = Math.abs(area * radius * radius / 2.0);
+ return area / 10000; // Return in Hectares
+}
+
+function MapController({ center, zoom }) {
+ const map = useMap();
+ useEffect(() => {
+ if (center) {
+ // Zoom 20 untuk melihat detail batas lahan lebih dekat
+ map.flyTo(center, zoom || 20, { duration: 1.5 });
+ }
+ }, [center, map, zoom]);
+ return null;
+}
+
+export default function GPSLandMapper({ existingPolygon, onSave, readOnly = false }) {
+ const [points, setPoints] = useState([]);
+ const [currentLocation, setCurrentLocation] = useState(null);
+ const [isLocating, setIsLocating] = useState(false);
+ const [gpsAccuracy, setGpsAccuracy] = useState(null);
+ const [watchId, setWatchId] = useState(null);
+
+ const area = useMemo(() => calculatePolygonArea(points), [points]);
+
+ const getDistance = (p1, p2) => {
+ if (!p1 || !p2) return 0;
+ const R = 6371000;
+ const dLat = (p2[0] - p1[0]) * Math.PI / 180;
+ const dLon = (p2[1] - p1[1]) * Math.PI / 180;
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(p1[0] * Math.PI / 180) * Math.cos(p2[0] * Math.PI / 180) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ };
+
+ useEffect(() => {
+ if (existingPolygon && existingPolygon.length > 0) {
+ // Pastikan format dari database [lng, lat] dibalik ke [lat, lng] untuk Leaflet
+ const converted = Array.isArray(existingPolygon[0])
+ ? existingPolygon.map(p => [p[1], p[0]])
+ : [];
+ setPoints(converted);
+ }
+ }, [existingPolygon]);
+
+ const startGPSTracking = () => {
+ if (!navigator.geolocation) {
+ toast.error("GPS tidak didukung");
+ return;
+ }
+ setIsLocating(true);
+ const id = navigator.geolocation.watchPosition(
+ (pos) => {
+ setCurrentLocation([pos.coords.latitude, pos.coords.longitude]);
+ setGpsAccuracy(pos.coords.accuracy);
+ setIsLocating(false);
+ },
+ (err) => {
+ setIsLocating(false);
+ toast.error("Gagal mendapatkan lokasi GPS");
+ },
+ { enableHighAccuracy: true, maximumAge: 0 }
+ );
+ setWatchId(id);
+ };
+
+ const stopGPSTracking = () => {
+ if (watchId !== null) navigator.geolocation.clearWatch(watchId);
+ };
+
+ useEffect(() => {
+ startGPSTracking();
+ return () => stopGPSTracking();
+ }, []);
+
+ const addCornerPoint = () => {
+ if (!currentLocation) {
+ toast.error("Tunggu GPS mengunci lokasi");
+ return;
+ }
+
+ if (gpsAccuracy > 10) {
+ toast.error(`Akurasi buruk (±${gpsAccuracy.toFixed(0)}m). Pindah ke tempat terbuka.`);
+ return;
+ }
+
+ if (points.length > 0) {
+ const dist = getDistance(points[points.length - 1], currentLocation);
+ if (dist < 1) {
+ toast.error("Anda belum bergerak dari titik terakhir");
+ return;
+ }
+ }
+
+ setPoints([...points, currentLocation]);
+ toast.success(`Titik ${points.length + 1} ditandai`);
+ };
+
+ const removeLastPoint = () => setPoints(points.slice(0, -1));
+ const clearAllPoints = () => setPoints([]);
+
+ const handleSave = () => {
+ if (points.length < 3) {
+ toast.error("Minimal harus ada 3 titik");
+ return;
+ }
+ onSave({
+ polygon_coordinates: points.map(p => [p[1], p[0]]), // [lng, lat]
+ center_lat: points.reduce((s, p) => s + p[0], 0) / points.length,
+ center_lng: points.reduce((s, p) => s + p[1], 0) / points.length,
+ area_hectares: area
+ });
+ };
+
+ const createCornerIcon = (index) => L.divIcon({
+ className: "custom-corner-icon",
+ html: `
${index + 1}
`,
+ iconSize: [32, 32], iconAnchor: [16, 16]
+ });
+
+ return (
+
+
+
+
+
+
+
GPS {currentLocation ? "Aktif" : "Mencari..."}
+
10 ? 'text-red-500' : 'text-slate-500'}`}>
+ Akurasi: ±{gpsAccuracy?.toFixed(1)}m {gpsAccuracy > 10 && "(Lemah)"}
+
+
+
+ {currentLocation &&
{currentLocation[0].toFixed(5)}, {currentLocation[1].toFixed(5)}}
+
+
+ {!readOnly && (
+
+
+
+
+
+ )}
+
+ {points.length >= 3 && (
+
+
+
Estimasi Luas
+
{area.toFixed(4)} Ha
+
+ {!readOnly &&
}
+
+ )}
+
+
+
+
+ {/* LAYER SATELIT UNTUK PRESISI */}
+
+
+
+
+ {currentLocation && (
+ <>
+
+
`,
+ iconSize:[20,20], iconAnchor:[10,10]
+ })} />
+ >
+ )}
+
+ {points.map((p, i) => )}
+ {points.length >= 3 && }
+
+
+ {!readOnly && (
+
+
+ Tips: Berjalanlah ke setiap sudut lahan. Tunggu lingkaran biru mengecil sebelum menekan "Tandai Titik" untuk hasil yang lebih presisi.
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/lands/GPSLandMapper copy.jsx b/src/components/lands/GPSLandMapper copy.jsx
old mode 100644
new mode 100755
diff --git a/src/components/lands/GPSLandMapper.jsx b/src/components/lands/GPSLandMapper.jsx
old mode 100644
new mode 100755
index b89f21de..66ed99c4
--- a/src/components/lands/GPSLandMapper.jsx
+++ b/src/components/lands/GPSLandMapper.jsx
@@ -3,146 +3,52 @@ import { MapContainer, TileLayer, Marker, Polygon, useMap, Circle } from "react-
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
-import { Locate, MapPin, Trash2, Save, Navigation, RotateCcw, AlertTriangle } from "lucide-react";
+import { Trash2, Save, Navigation, RotateCcw, Zap, ZapOff, Map as MapIcon, Loader2 } from "lucide-react";
import { toast } from "sonner";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
+import { Geolocation } from '@capacitor/geolocation';
-// Fix default marker icon
-delete L.Icon.Default.prototype._getIconUrl;
-L.Icon.Default.mergeOptions({
- iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
+// Konfigurasi Icon Marker agar muncul di Android
+const customMarkerIcon = new L.Icon({
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
+ iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
});
const getColorByIndex = (index) => {
const colors = ["#ef4444", "#f59e0b", "#22c55e", "#3b82f6", "#8b5cf6", "#ec4899", "#14b8a6"];
return colors[index % colors.length];
};
-
-/**
- * Kalkulasi Luas Lahan (Hektar) yang lebih presisi
- */
-function calculatePolygonArea(points) {
- if (points.length < 3) return 0;
-
- const radius = 6378137; // Radius Bumi (meters)
- let area = 0;
-
- for (let i = 0; i < points.length; i++) {
- const p1 = points[i];
- const p2 = points[(i + 1) % points.length];
-
- // Konversi ke Radians
- const lat1 = p1[0] * Math.PI / 180;
- const lon1 = p1[1] * Math.PI / 180;
- const lat2 = p2[0] * Math.PI / 180;
- const lon2 = p2[1] * Math.PI / 180;
-
- area += (lon2 - lon1) * (2 + Math.sin(lat1) + Math.sin(lat2));
- }
-
- area = Math.abs(area * radius * radius / 2.0);
- return area / 10000; // Return in Hectares
-}
-
-function MapController({ center, zoom }) {
+// Komponen Pengatur Peta (PENTING: Memperbaiki masalah layar putih)
+function MapController({ center, isTracking }) {
const map = useMap();
+
useEffect(() => {
- if (center) {
- // Zoom 20 untuk melihat detail batas lahan lebih dekat
- map.flyTo(center, zoom || 20, { duration: 1.5 });
+ // Memaksa Leaflet menghitung ulang ukuran container setelah render pertama
+ setTimeout(() => {
+ map.invalidateSize();
+ }, 500);
+ }, [map]);
+
+ useEffect(() => {
+ if (center && isTracking) {
+ map.setView(center, 19, { animate: true });
}
- }, [center, map, zoom]);
+ }, [center, isTracking, map]);
+
return null;
}
-export default function GPSLandMapper({ existingPolygon, onSave, readOnly = false }) {
+export default function GPSLandMapper({ onSave, readOnly = false }) {
const [points, setPoints] = useState([]);
const [currentLocation, setCurrentLocation] = useState(null);
- const [isLocating, setIsLocating] = useState(false);
const [gpsAccuracy, setGpsAccuracy] = useState(null);
const [watchId, setWatchId] = useState(null);
-
- const area = useMemo(() => calculatePolygonArea(points), [points]);
-
- const getDistance = (p1, p2) => {
- if (!p1 || !p2) return 0;
- const R = 6371000;
- const dLat = (p2[0] - p1[0]) * Math.PI / 180;
- const dLon = (p2[1] - p1[1]) * Math.PI / 180;
- const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
- Math.cos(p1[0] * Math.PI / 180) * Math.cos(p2[0] * Math.PI / 180) *
- Math.sin(dLon / 2) * Math.sin(dLon / 2);
- return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
- };
-
- useEffect(() => {
- if (existingPolygon && existingPolygon.length > 0) {
- // Pastikan format dari database [lng, lat] dibalik ke [lat, lng] untuk Leaflet
- const converted = Array.isArray(existingPolygon[0])
- ? existingPolygon.map(p => [p[1], p[0]])
- : [];
- setPoints(converted);
- }
- }, [existingPolygon]);
-
- const startGPSTracking = () => {
- if (!navigator.geolocation) {
- toast.error("GPS tidak didukung");
- return;
- }
- setIsLocating(true);
- const id = navigator.geolocation.watchPosition(
- (pos) => {
- setCurrentLocation([pos.coords.latitude, pos.coords.longitude]);
- setGpsAccuracy(pos.coords.accuracy);
- setIsLocating(false);
- },
- (err) => {
- setIsLocating(false);
- toast.error("Gagal mendapatkan lokasi GPS");
- },
- { enableHighAccuracy: true, maximumAge: 0 }
- );
- setWatchId(id);
- };
-
- const stopGPSTracking = () => {
- if (watchId !== null) navigator.geolocation.clearWatch(watchId);
- };
-
- useEffect(() => {
- startGPSTracking();
- return () => stopGPSTracking();
- }, []);
-
- const addCornerPoint = () => {
- if (!currentLocation) {
- toast.error("Tunggu GPS mengunci lokasi");
- return;
- }
-
- if (gpsAccuracy > 10) {
- toast.error(`Akurasi buruk (±${gpsAccuracy.toFixed(0)}m). Pindah ke tempat terbuka.`);
- return;
- }
-
- if (points.length > 0) {
- const dist = getDistance(points[points.length - 1], currentLocation);
- if (dist < 1) {
- toast.error("Anda belum bergerak dari titik terakhir");
- return;
- }
- }
-
- setPoints([...points, currentLocation]);
- toast.success(`Titik ${points.length + 1} ditandai`);
- };
-
- const removeLastPoint = () => setPoints(points.slice(0, -1));
- const clearAllPoints = () => setPoints([]);
+ const [isTracking, setIsTracking] = useState(false);
+ const [positionBuffer, setPositionBuffer] = useState([]);
const handleSave = () => {
if (points.length < 3) {
@@ -157,83 +63,181 @@ export default function GPSLandMapper({ existingPolygon, onSave, readOnly = fals
});
};
- const createCornerIcon = (index) => L.divIcon({
- className: "custom-corner-icon",
- html: `${index + 1}
`,
- iconSize: [32, 32], iconAnchor: [16, 16]
- });
+ // Hitung Luas
+ const area = useMemo(() => {
+ if (points.length < 3) return 0;
+ const radius = 6378137;
+ let areaVal = 0;
+ for (let i = 0; i < points.length; i++) {
+ const p1 = points[i];
+ const p2 = points[(i + 1) % points.length];
+ const lat1 = p1[0] * Math.PI / 180;
+ const lon1 = p1[1] * Math.PI / 180;
+ const lat2 = p2[0] * Math.PI / 180;
+ const lon2 = p2[1] * Math.PI / 180;
+ areaVal += (lon2 - lon1) * (2 + Math.sin(lat1) + Math.sin(lat2));
+ }
+ return Math.abs(areaVal * radius * radius / 2.0) / 10000;
+ }, [points]);
+
+ const startGPSTracking = async () => {
+ try {
+ const permissions = await Geolocation.requestPermissions();
+ if (permissions.location !== 'granted') {
+ toast.error("Izin lokasi diperlukan");
+ return;
+ }
+
+ const id = await Geolocation.watchPosition(
+ { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 },
+ (position, err) => {
+ if (err) return;
+ if (position) {
+ const coords = [position.coords.latitude, position.coords.longitude];
+ setCurrentLocation(coords);
+ setGpsAccuracy(position.coords.accuracy);
+ setPositionBuffer(prev => [...prev, coords].slice(-5));
+ }
+ }
+ );
+ setWatchId(id);
+ } catch (e) {
+ toast.error("GPS Error: " + e.message);
+ }
+ };
+
+ const stopGPSTracking = async () => {
+ if (watchId) {
+ await Geolocation.clearWatch({ id: watchId });
+ setWatchId(null);
+ }
+ };
+
+ useEffect(() => {
+ if (isTracking) startGPSTracking();
+ else stopGPSTracking();
+ return () => stopGPSTracking();
+ }, [isTracking]);
+
+ const addPoint = () => {
+ if (!currentLocation) return;
+ if (gpsAccuracy > 15) {
+ toast.error("Akurasi terlalu rendah, tunggu sinyal stabil");
+ return;
+ }
+ // Averaging
+ const lat = positionBuffer.reduce((a, b) => a + b[0], 0) / positionBuffer.length;
+ const lng = positionBuffer.reduce((a, b) => a + b[1], 0) / positionBuffer.length;
+ setPoints([...points, [lat, lng]]);
+ toast.success("Titik sudut ditambahkan");
+ };
return (
-
-
-
-
-
-
-
GPS {currentLocation ? "Aktif" : "Mencari..."}
-
10 ? 'text-red-500' : 'text-slate-500'}`}>
- Akurasi: ±{gpsAccuracy?.toFixed(1)}m {gpsAccuracy > 10 && "(Lemah)"}
-
-
+
+
+ {/* SECTION 1: KONTROL UTAMA (Selalu Ada) */}
+
+
+
+
+
+ {isTracking ? `Akurasi: ±${gpsAccuracy?.toFixed(1)}m` : "GPS Nonaktif"}
+
- {currentLocation &&
{currentLocation[0].toFixed(5)}, {currentLocation[1].toFixed(5)}}
+ {currentLocation &&
Sinyal Terkunci}
- {!readOnly && (
-
-
-
-
-
- {/* LAYER SATELIT UNTUK PRESISI */}
-
-
-
-
- {currentLocation && (
+ ) : (
<>
-
-
`,
- iconSize:[20,20], iconAnchor:[10,10]
- })} />
+
+ Tandai Sudut ({points.length})
+
+
setIsTracking(false)} className="h-12 w-12 border-red-200 text-red-500 hover:bg-red-50">
+
+
>
)}
+
+
- {points.map((p, i) =>
)}
- {points.length >= 3 &&
}
+ {/* SECTION 2: AREA INFORMASI LUAS & SIMPAN (Muncul jika titik >= 3) */}
+ {points.length >= 3 && (
+
+
+
Total Estimasi Luas
+
+ {area.toFixed(4)}
+ Ha
+
+
+
+ {!readOnly && (
+
onSave({ points, area })}
+ className="bg-emerald-600 hover:bg-emerald-700 text-white shadow-lg px-6 h-11 rounded-xl font-bold"
+ >
+ Simpan
+
+ )}
+
+ )}
+
+ {/* SECTION 3: MAP CONTAINER */}
+
+
+
+
+
+ {currentLocation && isTracking && (
+
+ )}
+
+ {points.map((p, i) => (
+ ${i+1}
`
+ })}
+ />
+ ))}
+
+ {points.length >= 3 && (
+
+ )}
- {!readOnly && (
-
-
- Tips: Berjalanlah ke setiap sudut lahan. Tunggu lingkaran biru mengecil sebelum menekan "Tandai Titik" untuk hasil yang lebih presisi.
-
+ {/* Floating Reset Button (Hanya ini yang di dalam peta) */}
+ {points.length > 0 && (
+
+ setPoints(points.slice(0, -1))}
+ >
+ Batal
+
+ setPoints([])}
+ >
+ Reset
+
)}
diff --git a/src/components/lands/LandCard.jsx b/src/components/lands/LandCard.jsx
old mode 100644
new mode 100755
index b635f873..cef22de5
--- a/src/components/lands/LandCard.jsx
+++ b/src/components/lands/LandCard.jsx
@@ -4,6 +4,9 @@ import { Button } from "@/components/ui/button";
import { MapPin, Maximize2, TreePine, ChevronRight } from "lucide-react";
import { Link } from "react-router-dom";
import { createPageUrl } from "@/utils";
+import { entity } from "@/api/entities";
+import { OfflineService } from "../common/offlineStorage";
+import { useEffect, useState } from "react";
const validationColors = {
valid: "bg-emerald-100 text-emerald-700 border-emerald-200",
@@ -27,15 +30,26 @@ const statusColors = {
};
export default function LandCard({ land, farmerName, plantCount = 0 }) {
+ const [village, setVillage] = useState();
+
+ useEffect(() => {
+ const fetchVillages = async () => {
+ const villages = await OfflineService.getEntities('villages');
+ const findvillage = villages.find(data => data.id === land.desa_kelurahan_id);
+
+ setVillage(findvillage);
+ };
+ fetchVillages();
+ });
+
return (
-
{land.nama}
+
{land.nama || land?.name}
{farmerName &&
{farmerName}
}
-
- {validationLabels[land.validation_status]}
+ Lahan
@@ -61,7 +75,7 @@ export default function LandCard({ land, farmerName, plantCount = 0 }) {
- {land.village || land.desa_kelurahan_id }, {land.district || 'Unknown'}
+ {village?.nama || village?.name || land.desa_kelurahan_id }
diff --git a/src/components/lands/LandMap copy.jsx b/src/components/lands/LandMap copy.jsx
old mode 100644
new mode 100755
diff --git a/src/components/lands/LandMap.jsx b/src/components/lands/LandMap.jsx
old mode 100644
new mode 100755
diff --git a/src/components/plants/GPSPLantTagger copy.jsx b/src/components/plants/GPSPLantTagger copy.jsx
old mode 100644
new mode 100755
diff --git a/src/components/plants/GPSPLantTagger.jsx b/src/components/plants/GPSPLantTagger.jsx
old mode 100644
new mode 100755
diff --git a/src/components/plants/PlantForm.jsx b/src/components/plants/PlantForm.jsx
old mode 100644
new mode 100755
diff --git a/src/components/plants/PlantInspectionForm.jsx b/src/components/plants/PlantInspectionForm.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/accordion.jsx b/src/components/ui/accordion.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/alert-dialog.jsx b/src/components/ui/alert-dialog.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/alert.jsx b/src/components/ui/alert.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/aspect-ratio.jsx b/src/components/ui/aspect-ratio.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/avatar.jsx b/src/components/ui/avatar.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/badge.jsx b/src/components/ui/badge.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/breadcrumb.jsx b/src/components/ui/breadcrumb.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/calendar.jsx b/src/components/ui/calendar.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/card.jsx b/src/components/ui/card.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/carousel.jsx b/src/components/ui/carousel.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/chart.jsx b/src/components/ui/chart.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/checkbox.jsx b/src/components/ui/checkbox.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/collapsible.jsx b/src/components/ui/collapsible.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/command.jsx b/src/components/ui/command.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/context-menu.jsx b/src/components/ui/context-menu.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/dialog.jsx b/src/components/ui/dialog.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/drawer.jsx b/src/components/ui/drawer.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/dropdown-menu.jsx b/src/components/ui/dropdown-menu.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/form.jsx b/src/components/ui/form.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/hover-card.jsx b/src/components/ui/hover-card.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/input-otp.jsx b/src/components/ui/input-otp.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/input.jsx b/src/components/ui/input.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/label.jsx b/src/components/ui/label.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/menubar.jsx b/src/components/ui/menubar.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/navigation-menu.jsx b/src/components/ui/navigation-menu.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/pagination.jsx b/src/components/ui/pagination.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/popper.jsx b/src/components/ui/popper.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/progress.jsx b/src/components/ui/progress.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/radio-group.jsx b/src/components/ui/radio-group.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/resizable.jsx b/src/components/ui/resizable.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/scroll-area.jsx b/src/components/ui/scroll-area.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/select.jsx b/src/components/ui/select.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/sheet.jsx b/src/components/ui/sheet.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/sidebar.jsx b/src/components/ui/sidebar.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/skeleton.jsx b/src/components/ui/skeleton.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/slider.jsx b/src/components/ui/slider.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/sonner.jsx b/src/components/ui/sonner.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/sparator.jsx b/src/components/ui/sparator.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/switch.jsx b/src/components/ui/switch.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/table.jsx b/src/components/ui/table.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/tabs.jsx b/src/components/ui/tabs.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/textarea.jsx b/src/components/ui/textarea.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/toast.jsx b/src/components/ui/toast.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/toaster.jsx b/src/components/ui/toaster.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/toggle-group.jsx b/src/components/ui/toggle-group.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/toggle.jsx b/src/components/ui/toggle.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/tooltip.jsx b/src/components/ui/tooltip.jsx
old mode 100644
new mode 100755
diff --git a/src/components/ui/use-toast.jsx b/src/components/ui/use-toast.jsx
old mode 100644
new mode 100755
diff --git a/src/hooks/use-mobile.jsx b/src/hooks/use-mobile.jsx
old mode 100644
new mode 100755
diff --git a/src/index.css b/src/index.css
old mode 100644
new mode 100755
diff --git a/src/lib/AuthContext copy.jsx b/src/lib/AuthContext copy.jsx
old mode 100644
new mode 100755
diff --git a/src/lib/AuthContext.jsx b/src/lib/AuthContext.jsx
old mode 100644
new mode 100755
index 130d6015..6c90ae3f
--- a/src/lib/AuthContext.jsx
+++ b/src/lib/AuthContext.jsx
@@ -89,22 +89,30 @@ export const AuthProvider = ({ children }) => {
const userResponse = await axios.get(`${getBaseUrl()}/api/auth/me`, {
headers: { Authorization: `Bearer ${access_token}` }
});
- const userData = userResponse.data;
+ const userDataRaw = userResponse.data;
+ const userData = {
+ ...userDataRaw,
+ verification_status: 'verified'
+ }
localStorage.setItem('access_token', access_token);
setToken(access_token);
if (!db.isOpen()) await db.open();
const encryptedPW = encryptPassword(password);
-
+ try{
await db.users.put({
id: userData.id || userData.user_id,
...userData,
password:encryptedPW,
sync_status: 'synced',
- last_login: new Date().toISOString()
+ last_login: new Date().toISOString(),
+ access_token: access_token,
+ verification_status: 'verified'
});
-
+ }catch(err){
+ console.log(err)
+ }
setUser(userData);
setIsAuthenticated(true);
return { success: true };
@@ -113,13 +121,19 @@ export const AuthProvider = ({ children }) => {
try {
if (!db.isOpen()) await db.open();
const localUser = await db.users.where("email").equals(email).first();
-
+
if (localUser && localUser.password) {
const decryptedPW = decryptPassword(localUser.password);
+
if (decryptedPW === password) {
+ localStorage.setItem('access_token', localUser.access_token);
+ localStorage.setItem('user_data', JSON.stringify(localUser));
+
+ setToken(localUser.access_token);
setUser(localUser);
setIsAuthenticated(true);
- return { success: true, isOffline: true };
+
+ return { success: true};
}
}
} catch (dexieErr) {
diff --git a/src/lib/NavigationTracker.jsx b/src/lib/NavigationTracker.jsx
old mode 100644
new mode 100755
diff --git a/src/lib/PageNotFound.jsx b/src/lib/PageNotFound.jsx
old mode 100644
new mode 100755
diff --git a/src/lib/app-params.js b/src/lib/app-params.js
old mode 100644
new mode 100755
diff --git a/src/lib/query-client.js b/src/lib/query-client.js
old mode 100644
new mode 100755
diff --git a/src/lib/utils.js b/src/lib/utils.js
old mode 100644
new mode 100755
diff --git a/src/main.jsx b/src/main.jsx
old mode 100644
new mode 100755
diff --git a/src/pages.config.js b/src/pages.config.js
old mode 100644
new mode 100755
diff --git a/src/pages/Dashboard copy.jsx b/src/pages/Dashboard copy.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/FarmerDetail.jsx b/src/pages/FarmerDetail.jsx
old mode 100644
new mode 100755
index 3af1970a..021042a3
--- a/src/pages/FarmerDetail.jsx
+++ b/src/pages/FarmerDetail.jsx
@@ -43,9 +43,10 @@ export default function FarmerDetail() {
queryKey: ['farmer', id],
queryFn: async (data) => {
if(!id) return null;
-
+ console.log(id, paramsId);
try{
- const response = await entity('farmer').get(id);
+ const response = await entity('map','petani').get(id);
+ console.log(response);
if (typeof response === 'string' && response.includes('')) {
console.warn("API returned HTML instead of JSON. Falling back to local.");
} else if (response && !response.error) {
@@ -137,7 +138,7 @@ export default function FarmerDetail() {
const verifyMutation = useMutation({
mutationFn: async (status) => {
- console.log("Data dari Verifikais :", [status, id])
+
try {
const onlineUpdate = await base44.entities.Farmer.update(id, { verification_status: status })
if(typeof onlineUpdate == 'string' && onlineUpdate.includes('')){
diff --git a/src/pages/FarmerPortal copy 2.jsx b/src/pages/FarmerPortal copy 2.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/FarmerPortal copy.jsx b/src/pages/FarmerPortal copy.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/FarmerPortal.jsx b/src/pages/FarmerPortal.jsx
old mode 100644
new mode 100755
index 8a3c2c47..a16f8e11
--- a/src/pages/FarmerPortal.jsx
+++ b/src/pages/FarmerPortal.jsx
@@ -51,6 +51,48 @@ export default function FarmerPortal() {
const [isEditing, setIsEditing] = useState(false);
const [selectedKtp, setSelectedKtp] = useState(null);
const [ktpPreview, setKtpPreview] = useState(null);
+ const [locationData, setLocationData ] = useState({
+ provinces: [],
+ regencies: [],
+ districts: [],
+ villages: []
+ });
+
+ useEffect(() => {
+ const prepareDataLocation = async() => {
+ if(!navigator.onLine){
+ try{
+ const [villages, districts, regencies, provinces] = await Promise.all([
+ OfflineService.getEntities('villages'),
+ OfflineService.getEntities('districts'),
+ OfflineService.getEntities('regencies'),
+ OfflineService.getEntities('provinces'),
+ ]);
+
+ setLocationData({
+ villages: villages, districts:districts, regencies:regencies,provinces:provinces});
+ }catch(err){
+ console.error(err)
+ }
+ }else{
+ try{
+ const [villages, districts, regencies, provinces] = await Promise.all([
+ OfflineService.getEntities('villages'),
+ OfflineService.getEntities('districts'),
+ OfflineService.getEntities('regencies'),
+ OfflineService.getEntities('provinces'),
+ ]);
+
+ setLocationData({villages: villages, districts:districts, regencies:regencies,provinces:provinces});
+ console.log("Offline")
+ }catch(err){
+ console.error(err)
+ }
+ }
+ };
+
+ prepareDataLocation();
+ }, []);
const [farmerFormData, setFarmerFormData] = useState({
nama: farmer?.nama || farmer?.name || "",
@@ -117,8 +159,14 @@ export default function FarmerPortal() {
return data;
}catch(err){
- console.error("data gagal diabbil")
- return []
+ console.error("Local Data")
+ const localDist = await OfflineService.getEntities('distributions') || [];
+ const filteredDist = localDist && localDist.length > 0 ? localDist.filter((dis) => {
+ if(dis.farmer_id === farmer.id){
+ return dis;
+ }
+ }) : [];
+ return filteredDist;
}
},
enabled: !!farmer?.id
@@ -135,7 +183,15 @@ export default function FarmerPortal() {
}
return []
}catch(err){
- console.error("Error :",err)
+ console.warn("loda local data");
+ const localHarv = await OfflineService.getEntities('harvests') || [];
+ const filteredHarv = localHarv && localHarv.length > 0 ?
+ localHarv.filter((data) => {
+ if(data.farmer_id === farmer.id){
+ return data;
+ }
+ }) : [];
+ return filteredHarv;
}
},
@@ -156,6 +212,7 @@ export default function FarmerPortal() {
console.warn("Online data is un reachable")
}
try{
+ console.log(profile?.id || 0, "PRofile id")
const offlineData = await OfflineService.getEntities('lands');
const offlineLahan = safeArray(offlineData).filter(lh => lh.profile_id === profile.id);
const combined = new Map();
@@ -357,7 +414,11 @@ export default function FarmerPortal() {
entity('auth', 'profile/1').list()
]);
- const dataMe = rMe.data || [];
+ const dataMeRaw = rMe.data || [];
+ const dataMe = {
+ ...dataMeRaw,
+ 'verification_status':'verified',
+ }
const dataProfile = rFar.data || [];
currentUser = {
@@ -380,16 +441,27 @@ export default function FarmerPortal() {
}catch(err){
console.error("Error : ", err)
const localUser = localStorage.getItem('user_data');
+ const userBase = JSON.parse(localUser);
+ const localProfilesRaw = await OfflineService.getEntities('farmers') || [];
+
+ const localProfile = localProfilesRaw && localProfilesRaw.length > 0 ? localProfilesRaw.find(prof => {
+ return Number(prof.id) === Number(userBase.id)
+ }
+ ) : null;
+
currentUser = localUser ? JSON.parse(localUser) : null
+ currentProfile = localProfile
+ console.log(" Local Profile => ",localProfile, " Curr Profile => " ,currentProfile, " Curr User=> ",currentUser, "local profile")
}
if (currentUser) {
setUser(currentUser);
setFarmer(currentUser);
+ setProfile(currentUser);
setFarmerProf((prev) => ({
...prev,
- nama: currentUser.profile?.nama || currentUser.name || 'Tanpa Nama',
+ nama: currentUser.profile?.nama || currentUser.nama || currentUser.name || 'Tanpa Nama',
email: currentUser.email || '',
user_id: currentUser.id || 0
}));
@@ -425,6 +497,28 @@ export default function FarmerPortal() {
base44.auth.logout();
};
+ useEffect(() => {
+ const autoFillLocation = async () => {
+ if(!newLandData.desa_kelurahan_id) return;
+ const selectedVillage = locationData.villages.find(data => newLandData.desa_kelurahan_id === data.id);
+ if(!selectedVillage) return;
+
+ 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();
+ }, [newLandData.desa_kelurahan_id, locationData.villages]);
+
// profile control
const handleToggleEdit = async () => {
const formDataBaru = new FormData();
@@ -438,7 +532,7 @@ export default function FarmerPortal() {
formDataBaru.append('ktp', farmerFormData.ktp);
formDataBaru.append('email', farmerFormData.email);
formDataBaru.append('desa_kelurahan_id', '1');
- console.log("Isi FormData:", Object.fromEntries(formDataBaru.entries()));
+
try {
updateProfileMutation.mutate(formDataBaru);
toast.success("Profil berhasil diperbarui");
@@ -529,6 +623,7 @@ export default function FarmerPortal() {
}
// Logged in but no farmer profile linked
+
if (!farmer) {
return (
@@ -563,6 +658,28 @@ export default function FarmerPortal() {
const alivePlants = plants.length;
const totalArea = lands.reduce((sum, l) => sum + parseFloat(l.area_hectares || l.luas_lahan || 0), 0);
+ if (user.role !== 'petani') {
+ return (
+
+
+
+
+
+ Portal Petani
+
+ Masuk untuk melihat data panen sampai distribusi
+
+
+ Masuk
+
+
+
+
+
+ );
+ }
return (
@@ -1123,7 +1240,7 @@ export default function FarmerPortal() {
-
+
-
+
setNewLandData(prev => ({ ...prev, district: e.target.value ? '1' : '0' }))}
+ value={locationData.districts[0]?.nama || "-"}
+ readOnly
placeholder="Nama kecamatan"
/>
+
+
-
-
+
setNewLandData(prev => ({
- ...prev,
- village: e.target.value,
- }))}
+ value={locationData.provinces[0]?.nama}
+ readOnly
placeholder="Nama desa"
/>
diff --git a/src/pages/FarmerRegister copy.jsx b/src/pages/FarmerRegister copy.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/FarmerRegister.jsx b/src/pages/FarmerRegister.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/Farmers.jsx b/src/pages/Farmers.jsx
old mode 100644
new mode 100755
index b8e76ae6..fdde0aba
--- a/src/pages/Farmers.jsx
+++ b/src/pages/Farmers.jsx
@@ -23,7 +23,7 @@ export default function Farmers() {
queryFn: async () => {
let serverData;
try {
- serverData = await entity("map","petani").list();
+ serverData = await entity("auth","users").list();
} catch (err) {
console.warn("Server unreachable, loading local data only");
}
@@ -47,13 +47,16 @@ export default function Farmers() {
// Filter farmers
const filteredFarmers = farmersSafe.filter(farmer => {
const matchSearch = !search ||
- farmer.full_name?.toLowerCase().includes(search.toLowerCase()) ||
- farmer.nik?.includes(search) ||
+ farmer.full_name?.toLowerCase().includes(search.toLowerCase()) || farmer.nama?.toLowerCase().includes(search.toLowerCase()) ||
+ farmer.nik?.includes(search) || farmer.kk?.includes(search) || farmer.ktp?.includes(search) ||
farmer.farmer_group?.toLowerCase().includes(search.toLowerCase());
-
- const matchStatus = statusFilter === "all" || farmer.verification_status === statusFilter;
- const matchRegion = regionFilter === "all" || farmer.regency === regionFilter;
+
+ const currentStatus = farmer.user.email_verified_at !== null || farmer.user.email_verified_at !== undefined ? 'verified' : 'pending';
+ const matchStatus = statusFilter === "all" || currentStatus === statusFilter;
+
+ const matchRegion = regionFilter === "all" || farmer.regency === regionFilter;
+ console.log("status filter => ", matchStatus, farmer.user.email_verified_at, statusFilter);
return matchSearch && matchStatus && matchRegion;
});
diff --git a/src/pages/LandDetail.jsx b/src/pages/LandDetail.jsx
old mode 100644
new mode 100755
index e022cd6b..4852dee2
--- a/src/pages/LandDetail.jsx
+++ b/src/pages/LandDetail.jsx
@@ -277,7 +277,7 @@ export default function LandDetail() {
});
}
}, [land, isEditing]);
- console.log(land, "Data lahan");
+
if (isLoading) {
return (
diff --git a/src/pages/LandRegister.jsx b/src/pages/LandRegister.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/Lands.jsx b/src/pages/Lands.jsx
old mode 100644
new mode 100755
index 044febbd..e04cba80
--- a/src/pages/Lands.jsx
+++ b/src/pages/Lands.jsx
@@ -34,17 +34,17 @@ import { OfflineService } from "@/components/common/offlineStorage";
console.log(resp)
if (Array.isArray(resp)) serverData = resp;
} catch (e) {
- console.warn("Server lands offline/error");
+ console.warn("Server lands offline/error : ", e);
}
const localData = await OfflineService.getEntities("lands");
- console.log(localData)
+
const combined = new Map();
serverData.forEach(l => { if(l.id) combined.set(l.id, l); });
localData.forEach(l => { if(l.id) combined.set(l.id, l); });
const result = Array.from(combined.values());
- console.log("🔍 Total Lands Loaded (Server + Local):", result.length);
+ // console.log("🔍 Total Lands Loaded (Server + Local):", result.length);
return result;
}
});
@@ -59,8 +59,8 @@ import { OfflineService } from "@/components/common/offlineStorage";
queryFn: async () => {
let serverData = [];
try {
- const resp = await base44.entities.Farmer.list();
- serverData = Array.isArray(resp) ? resp : [];
+ const resp = await entity("map","profile").list();
+ serverData = Array.isArray(resp?.data) ? resp.data : Array.isArray(resp?.data?.data) ? resp?.data?.data : [];
} catch (e) { }
const localData = await OfflineService.getEntities("farmers");
const combined = new Map();
@@ -72,7 +72,12 @@ import { OfflineService } from "@/components/common/offlineStorage";
const { data: rawplants = [] } = useQuery({
queryKey: ["plants"],
- queryFn: () => base44.entities.Plant.list()
+ queryFn: async () => {
+ const rawplants = await entity('map','tanaman').list();
+ const plants = Array.isArray(rawplants?.data) ? rawplants?.data : Array.isArray(rawplants?.data?.data) ? rawplants?.data?.data : [];
+ console.log(plants, "Tanamana")
+ return plants
+ }
});
const lands = Array.isArray(rawlands) ? rawlands : [];
@@ -82,7 +87,7 @@ import { OfflineService } from "@/components/common/offlineStorage";
const farmerMap = useMemo(() => {
const map = {};
farmers.forEach(f => {
- if (f?.id) map[f.id] = f.full_name;
+ if (f?.id) map[f.id] = f?.nama || f?.full_name || `Petani ${f.id}`;
});
return map;
}, [farmers]);
@@ -99,13 +104,12 @@ import { OfflineService } from "@/components/common/offlineStorage";
// });
const filteredLands = useMemo(() => {
- // Pastikan lands adalah array sebelum filter
if (!Array.isArray(lands)) return [];
return lands.filter(land => {
if (!land) return false;
- const landName = land.name?.toLowerCase() || "";
+ const landName = land.nama?.toLowerCase() || "";
const village = land.village?.toLowerCase() || "";
const farmerName = farmerMap[land.farmer_id]?.toLowerCase() || "";
const searchTerm = search.toLowerCase();
@@ -134,7 +138,7 @@ import { OfflineService } from "@/components/common/offlineStorage";
});
return map;
}, [plants]);
-
+ console.log(farmerMap,farmers, "FArmer MAp", plants, lands);
return (
@@ -242,8 +246,8 @@ import { OfflineService } from "@/components/common/offlineStorage";
>
p.land_id === land.id).length}
+ farmerName={farmerMap[land.profile_id]}
+ plantCount={plants.filter(p => p.lahan.id === land.id).length}
/>
))}
diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/NotAccessible.jsx b/src/pages/NotAccessible.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/OfftakerPortal copy.jsx b/src/pages/OfftakerPortal copy.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/OfftakerPortal.jsx b/src/pages/OfftakerPortal.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/Offtakers.jsx b/src/pages/Offtakers.jsx
old mode 100644
new mode 100755
index 764e2feb..2ace09fd
--- a/src/pages/Offtakers.jsx
+++ b/src/pages/Offtakers.jsx
@@ -12,6 +12,7 @@ import EmptyState from "@/components/common/EmptyState";
import { Package, Plus, Phone, MapPin, Loader2, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { motion } from "framer-motion";
+import { entity } from "@/api/entities";
export default function Offtakers() {
const queryClient = useQueryClient();
@@ -27,10 +28,11 @@ export default function Offtakers() {
const { data: offtakers = [], isLoading } = useQuery({
queryKey: ["offtakers"],
- queryFn: () => {
+ queryFn: async() => {
try{
- const data = base44.entities.Offtaker.list();
- return Array.isArray(data) ? data : [];
+ const dataRaw = await entity('map', 'offtaker').list();
+ const data = Array.isArray(dataRaw) ? dataRaw : Array.isArray(dataRaw?.data) ? dataRaw?.data : Array.isArray(dataRaw?.data?.data) ? dataRaw?.data?.data : [];
+ return data;
}catch(e){
return [];
}
diff --git a/src/pages/Plants.jsx b/src/pages/Plants.jsx
old mode 100644
new mode 100755
index a880e80e..2fbe1166
--- a/src/pages/Plants.jsx
+++ b/src/pages/Plants.jsx
@@ -12,6 +12,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { TreePine, Search, Filter, MapPin, Calendar, ChevronRight } from "lucide-react";
import { motion } from "framer-motion";
import { format } from "date-fns";
+import { entity } from "@/api/entities";
const statusColors = {
alive: "bg-emerald-100 text-emerald-700 border-emerald-200",
@@ -34,17 +35,26 @@ export default function Plants() {
const { data: rawplants = [], isLoading } = useQuery({
queryKey: ["plants"],
- queryFn: () => base44.entities.Plant.list("-created_date")
+ queryFn: async() => {
+ const rawData = await entity('map', 'tanaman').list();
+ const data = Array.isArray(rawData?.data) ? rawData?.data : Array.isArray(rawData?.data?.data) ? rawData?.data?.data: [];
+
+ return data;
+ }
});
const { data: rawlands = [] } = useQuery({
queryKey: ["lands"],
- queryFn: () => base44.entities.Land.list()
+ queryFn: async() => {
+ const rawData = await entity('map', 'lahan').list();
+ const data = Array.isArray(rawData?.data) ? rawData?.data : Array.isArray(rawData?.data?.data) ? rawData?.data?.data : [];
+ return data;
+ }
});
const { data: rawfarmers = [] } = useQuery({
queryKey: ["farmers"],
- queryFn: () => base44.entities.Farmer.list()
+ queryFn: async() => await entity('auth', 'profile').list()
});
const lands = Array.isArray(rawlands) ? rawlands : [];
@@ -83,7 +93,7 @@ export default function Plants() {
// return matchSearch && matchStatus && matchCommodity;
// });
-
+
const filteredPlants = useMemo(() => {
const searchTerm = search.toLowerCase();
return plants.filter(plant => {
@@ -91,7 +101,7 @@ export default function Plants() {
const matchSearch = !search ||
plant.commodity_name?.toLowerCase().includes(searchTerm) ||
- landMap[plant.land_id]?.name?.toLowerCase().includes(searchTerm) ||
+ landMap[plant.land_id]?.nama?.toLowerCase().includes(searchTerm) ||
farmerMap[plant.farmer_id]?.toLowerCase().includes(searchTerm);
const matchStatus = statusFilter === "all" || plant.status === statusFilter;
@@ -112,7 +122,7 @@ export default function Plants() {
const groupedByCommodity = useMemo(() => {
const groups = {};
filteredPlants.forEach(plant => {
- const key = plant.commodity_name || "Lainnya";
+ const key = plant?.nama || plant?.commodity_name || "Lainnya";
if (!groups[key]) groups[key] = [];
groups[key].push(plant);
});
@@ -256,7 +266,7 @@ export default function Plants() {
{commodityPlants.map((plant, idx) => {
- const land = landMap[plant.land_id];
+ const land = landMap[plant.lahan.id];
return (
-
- {statusLabels[plant.status]}
+
+ {statusLabels[plant.status ?? 'alive']}
@@ -281,11 +291,11 @@ export default function Plants() {
- {land?.name || "Lahan tidak diketahui"}
+ {land?.nama || land?.name || "Lahan tidak diketahui"}
- {plant.plant_date ? format(new Date(plant.plant_date), "dd MMM yyyy") : "-"}
+ {plant.created_at ? format(new Date(plant.created_at), "dd MMM yyyy") : "-"}
diff --git a/src/pages/ProductivityMonitoring.jsx b/src/pages/ProductivityMonitoring.jsx
old mode 100644
new mode 100755
index 4624722a..70df058a
--- a/src/pages/ProductivityMonitoring.jsx
+++ b/src/pages/ProductivityMonitoring.jsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import base44 from "@/api/Client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -40,19 +40,37 @@ const issueLabels = {
export default function ProductivityMonitoring() {
const [filterLevel, setFilterLevel] = useState("regency");
const [selectedRegion, setSelectedRegion] = useState("all");
+
+ // Reset selected region when filter level changes
+ useEffect(() => {
+ setSelectedRegion("all");
+ }, [filterLevel]);
+
+ const getMe = async() => {
+ const resp = await entity('auth', 'profile/1').list();
+ const me = resp.data;
+ return me;
+ };
+
const {data: rawLands = []} = useQuery({
queryKey: ['lands'],
queryFn: async () => {
+ let me;
let dataServer = [];
try{
+ me = await getMe();
+
const res = await entity('map', 'lahan').list();
- dataServer = Array.isArray(res.data) ? res.data : [];
+ const data = res.data;
+ dataServer = Array.isArray(data) ? data.filter((land) => land.profile_id === me.id) : [];
}catch(err){
console.error(err)
}
- const localData = await OfflineService.getEntities('lands');
+ const localRes = await OfflineService.getEntities('lands');
+ const localData = localRes.filter((data) => data.profile_id === me.id);
+
const combined = new Map();
dataServer.forEach(d => {
if (d.id) combined.set(d.id, { ...d, storage: 'server' });
@@ -71,15 +89,28 @@ export default function ProductivityMonitoring() {
}
});
+ const { data: villages = []} = useQuery({
+ queryKey: ['villages'],
+ queryFn: async () => {
+ const resp = await OfflineService.getEntities('villages');
+
+ return resp||resp?.data || resp?.data?.data || [];
+ }
+ });
+
const {data: rawPlants = []} = useQuery({
queryKey: ['plants'],
queryFn: async () => {
let data = [];
+ let me;
try{
+ me = await getMe();
+ console.log(me, "Med")
const res = await entity('map', 'tanaman').list();
- data = Array.isArray(res.data.data) ? res.data.data :[];
+ if(me){data = Array.isArray(res.data.data) ? res.data.data.filter((data) => data.profile_id === me.id) :[];}
}catch(e){}
- const local = await OfflineService.getEntities('plants');
+ let local = await OfflineService.getEntities('plants');
+ local = Array.isArray(local) ? local.filter((l) => l.profile_id === me.id) : [];
const mapped = new Map();
data.forEach(d => {
if(d.id) mapped.set(d.id, {...d, storage:'server'})
@@ -93,18 +124,14 @@ export default function ProductivityMonitoring() {
const lands = Array.isArray(rawLands) ? rawLands : [];
const plants = Array.isArray(rawPlants) ? rawPlants : [];
-
- // const { data: harvests = [] } = useQuery({
- // queryKey: ["harvests"],
- // queryFn: () => base44.entities.Harvest.list()
- // });
const { data: rawHarvests = [] } = useQuery({
queryKey: ["harvests"],
queryFn: async () => {
try {
const res = await entity('map', 'panen').list();
- return Array.isArray(res.data.data) ? res.data.data : [];
+ let harvests = res?.data || res?.data?.data || [];
+ return Array.isArray(harvests) ? harvests.filter((hr) => hr.profile_id === getMe.id) : [];
} catch (e) {
return [];
}
@@ -114,30 +141,60 @@ export default function ProductivityMonitoring() {
const harvests = Array.isArray(rawHarvests) ? rawHarvests : [];
// Get unique regions
- const regions = {
+ const regions = useMemo(() => ({
regency: [...new Set(lands.map(l => l.regency).filter(Boolean))],
district: [...new Set(lands.map(l => l.district).filter(Boolean))],
- village: [...new Set(lands.map(l => l.village).filter(Boolean))]
- };
+ village: filterLevel === "village"
+ ? [...new Set([
+ ...villages.map(v => v.name || v.desa || v.village_name).filter(Boolean),
+ ...lands.map(l => l.village || l.village_name || l.desa || l.village_id).filter(Boolean)
+ ])]
+ : [...new Set(lands.map(l => l.village || l.village_name || l.desa || l.village_id).filter(Boolean))]
+ }), [lands, villages, filterLevel]);
+
+ // Create village mapping for proper filtering
+ const villageMapping = lands.reduce((acc, land) => {
+ const villageName = land.village || land.village_name || land.desa || land.village_id;
+ const villageId = land.village_id || land.village;
+ if (villageName && villageId) {
+ acc[villageName] = villageId;
+ }
+ return acc;
+ }, {});
// Filter data by region
const filteredLands = selectedRegion === "all"
? lands
- : lands.filter(l => l[filterLevel] === selectedRegion);
+ : filterLevel === "village"
+ ? lands.filter(l => {
+ const villageName = l.village || l.village_name || l.desa || l.village_id;
+ const villageId = l.village_id || l.village;
+ return villageName === selectedRegion || villageId === selectedRegion;
+ })
+ : lands.filter(l => l[filterLevel] === selectedRegion);
const filteredLandIds = filteredLands.map(l => l.id);
const filteredPlants = plants.filter(p => filteredLandIds.includes(p.land_id || p.lahan_id));
-
- // Calculate productivity stats
+
const calculateProductivityStats = () => {
const totalLands = filteredLands.length;
- if (totalLands === 0) return { avg: 0, distribution: [] };
-
+ if (totalLands === 0) return { avg: 0, distribution: [], harvestBasedAvg: 0, combinedAvg: 0 };
+
const landsWithProductivity = filteredLands.filter(l => l.productivity_percentage !== undefined);
- const avgProductivity = landsWithProductivity.length > 0
+ const landBasedAvg = landsWithProductivity.length > 0
? landsWithProductivity.reduce((sum, l) => sum + (l.productivity_percentage || 0), 0) / landsWithProductivity.length
: 0;
+ // Calculate harvest-based productivity
+ const harvestProductivity = calculateHarvestBasedProductivity();
+
+ // Combined average (weighted)
+ const combinedAvg = landsWithProductivity.length > 0 && harvestProductivity.totalHarvests > 0
+ ? (landBasedAvg + harvestProductivity.avg) / 2
+ : landsWithProductivity.length > 0
+ ? landBasedAvg
+ : harvestProductivity.avg;
+
const distribution = [
{ name: "Sangat Produktif (>80%)", value: filteredLands.filter(l => (l.productivity_percentage || 0) > 80).length, color: "#10b981" },
{ name: "Produktif (60-80%)", value: filteredLands.filter(l => (l.productivity_percentage || 0) >= 60 && (l.productivity_percentage || 0) <= 80).length, color: "#22c55e" },
@@ -145,7 +202,65 @@ export default function ProductivityMonitoring() {
{ name: "Tidak Produktif (<40%)", value: filteredLands.filter(l => (l.productivity_percentage || 0) < 40).length, color: "#ef4444" }
];
- return { avg: avgProductivity, distribution };
+ return {
+ avg: landBasedAvg,
+ distribution,
+ harvestBasedAvg: harvestProductivity.avg,
+ combinedAvg,
+ harvestDetails: harvestProductivity
+ };
+ };
+
+ // Calculate harvest-based productivity
+ const calculateHarvestBasedProductivity = () => {
+ const filteredHarvests = harvests.filter(h => filteredLandIds.includes(h.land_id));
+
+ if (filteredHarvests.length === 0) {
+ return { avg: 0, totalHarvests: 0, totalWeight: 0, landProductivity: [] };
+ }
+
+ // Group harvests by land
+ const harvestByLand = filteredHarvests.reduce((acc, harvest) => {
+ if (!acc[harvest.land_id]) {
+ acc[harvest.land_id] = { totalWeight: 0, harvestCount: 0 };
+ }
+ acc[harvest.land_id].totalWeight += harvest.weight_kg || 0;
+ acc[harvest.land_id].harvestCount++;
+ return acc;
+ }, {});
+
+ // Calculate productivity per land based on harvest weight vs land area
+ const landProductivity = Object.entries(harvestByLand).map(([landId, data]) => {
+ const land = filteredLands.find(l => l.id === landId);
+ const area = land?.area_hectares || 1; // Default to 1 hectare if not specified
+ const yieldPerHa = data.totalWeight / area;
+
+ // Normalize to percentage (assuming 1000kg/ha as optimal yield)
+ const productivityPercentage = Math.min((yieldPerHa / 1000) * 100, 100);
+
+ return {
+ landId,
+ landName: land?.name || `Lahan ${landId.slice(0, 8)}`,
+ area,
+ totalWeight: data.totalWeight,
+ harvestCount: data.harvestCount,
+ yieldPerHa,
+ productivityPercentage
+ };
+ });
+
+ const totalWeight = filteredHarvests.reduce((sum, h) => sum + (h.weight_kg || 0), 0);
+ const totalArea = landProductivity.reduce((sum, lp) => sum + lp.area, 0);
+ const overallYieldPerHa = totalArea > 0 ? totalWeight / totalArea : 0;
+ const avgProductivity = Math.min((overallYieldPerHa / 1000) * 100, 100);
+
+ return {
+ avg: avgProductivity,
+ totalHarvests: filteredHarvests.length,
+ totalWeight,
+ landProductivity,
+ overallYieldPerHa
+ };
};
// Calculate plant health stats
@@ -227,7 +342,13 @@ export default function ProductivityMonitoring() {
Pantau produktivitas lahan dan tanaman
-
Avg Produktivitas
-
{productivityStats.avg.toFixed(0)}%
+
{productivityStats.combinedAvg.toFixed(0)}%
+
Lahan: {productivityStats.avg.toFixed(0)}% | Panen: {productivityStats.harvestBasedAvg.toFixed(0)}%
@@ -301,7 +423,8 @@ export default function ProductivityMonitoring() {
Bermasalah
- {filteredPlants.filter(p => p.issue_type && p.issue_type !== "none").length}
+
+ {filteredPlants.filter(p => p?.kondisi_tanaman === 'sakit').length}
@@ -309,6 +432,52 @@ export default function ProductivityMonitoring() {
+ {/* Productivity Details */}
+
+
+
+
+
+
+
+
+
Produktivitas Lahan
+
{productivityStats.avg.toFixed(1)}%
+
Berdasarkan data lahan
+
+
+
+
+
+
+
+
+
+
+
+
Produktivitas Panen
+
{productivityStats.harvestBasedAvg.toFixed(1)}%
+
{productivityStats.harvestDetails?.totalWeight || 0}kg dari {productivityStats.harvestDetails?.totalHarvests || 0} panen
+
+
+
+
+
+
+
+
+
+
+
+
Rata-rata Hasil/Ha
+
{productivityStats.harvestDetails?.overallYieldPerHa?.toFixed(0) || 0} kg
+
Total produktivitas per hektar
+
+
+
+
+
+
{/* Charts */}
{/* Regional Productivity Chart */}
diff --git a/src/pages/Register.jsx b/src/pages/Register.jsx
old mode 100644
new mode 100755
diff --git a/src/pages/ValidatorPortal copy.jsx b/src/pages/ValidatorPortal copy.jsx
new file mode 100755
index 00000000..168b07bd
--- /dev/null
+++ b/src/pages/ValidatorPortal copy.jsx
@@ -0,0 +1,398 @@
+import { useState, useEffect } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import base44 from "@/api/Client";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Textarea } from "@/components/ui/textarea";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import LandMap from "@/components/lands/LandMap";
+import EmptyState from "@/components/common/EmptyState";
+import {
+ Shield, Map, CheckCircle, XCircle, Clock, LogOut,
+ User, MapPin, Loader2, FileCheck, AlertTriangle
+} from "lucide-react";
+import { toast } from "sonner";
+import { motion } from "framer-motion";
+import { format } from "date-fns";
+import { id } from "date-fns/locale";
+import { entity } from "@/api/entities";
+
+const roleLabels = {
+ kepala_desa: "Kepala Desa",
+ kepala_dusun: "Kepala Dusun",
+ rt: "Ketua RT",
+ rw: "Ketua RW",
+ fasilitator: "Fasilitator"
+};
+
+const statusStyles = {
+ pending: { bg: "bg-amber-100", text: "text-amber-700", label: "Menunggu Validasi" },
+ valid: { bg: "bg-emerald-100", text: "text-emerald-700", label: "Valid" },
+ invalid: { bg: "bg-red-100", text: "text-red-700", label: "Tidak Valid" },
+ need_review: { bg: "bg-blue-100", text: "text-blue-700", label: "Perlu Review" }
+};
+
+export default function ValidatorPortal() {
+ const queryClient = useQueryClient();
+ const [user, setUser] = useState(null);
+ const [validator, setValidator] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [selectedLand, setSelectedLand] = useState(null);
+ const [validationNotes, setValidationNotes] = useState("");
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const currentUser = await entity('auth', 'me').list();
+ console.log((currentUser).data.role)
+ // if(currentUser.data.role !== 'validator'){
+ // setUser(null)
+ // setValidator(null)
+ // } else{
+ setUser(currentUser);
+ setValidator(currentUser)
+ // }
+
+ // const validators = await base44.entities.Validator.filter({ user_email: currentUser.email });
+ // if (validators.length > 0 && validators[0].is_active) {
+ // setValidator(validators[0]);
+ // }
+ } catch (error) {
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ loadData();
+ }, []);
+
+ // Load lands in validator's area that need validation
+ const { data: lands = [] } = useQuery({
+ queryKey: ["validator-lands", validator?.village],
+ queryFn: () => base44.entities.Land.filter({ village: validator.village }),
+ enabled: !!validator?.village
+ });
+
+ // Load farmers for reference
+ const { data: farmers = [] } = useQuery({
+ queryKey: ["farmers"],
+ queryFn: () => base44.entities.Farmer.list(),
+ enabled: !!validator
+ });
+
+ // Load validation history
+ const { data: validations = [] } = useQuery({
+ queryKey: ["validations", validator?.id],
+ queryFn: () => base44.entities.LandValidation.filter({ validator_email: validator.user_email }),
+ enabled: !!validator
+ });
+
+ // Validate land mutation
+ const validateMutation = useMutation({
+ mutationFn: async ({ landId, status }) => {
+ const validationStatus = status === "approved" ? "valid" : "invalid";
+
+ // Update land status
+ await base44.entities.Land.update(landId, {
+ validation_status: validationStatus,
+ validation_notes: validationNotes
+ });
+
+ // Create validation record
+ await base44.entities.LandValidation.create({
+ land_id: landId,
+ validator_role: validator.role,
+ validator_name: validator.full_name,
+ validator_email: validator.user_email,
+ status: status,
+ notes: validationNotes,
+ validated_at: new Date().toISOString()
+ });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["validator-lands"] });
+ queryClient.invalidateQueries({ queryKey: ["validations"] });
+ setSelectedLand(null);
+ setValidationNotes("");
+ toast.success("Validasi berhasil disimpan!");
+ }
+ });
+
+ const handleLogin = () => {
+ base44.auth.redirectToLogin(window.location.href);
+ };
+
+ const handleLogout = () => {
+ base44.auth.logout();
+ };
+
+ const getFarmerName = (farmerId) => {
+ const farmer = farmers.find(f => f.id === farmerId);
+ return farmer?.full_name || "Tidak diketahui";
+ };
+
+ const pendingLands = lands.filter(l => l.validation_status === "pending" || l.validation_status === "need_review");
+ const validatedLands = lands.filter(l => l.validation_status === "valid" || l.validation_status === "invalid");
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!user) {
+ return (
+
+
+
+
+
+
+
+ Portal Validator
+
+ Masuk untuk memvalidasi lahan di wilayah Anda
+
+
+ Masuk
+
+
+
+
+
+ );
+ }
+
+ if (!validator) {
+ return (
+
+
+
+
+
+ Akses Ditolak
+
+ Email {user.email} tidak terdaftar sebagai validator.
+
+
+ Hubungi admin untuk mendaftarkan Anda sebagai validator wilayah.
+
+
+
+ Keluar
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+
{validator.full_name}
+
+ {roleLabels[validator.role]} - {validator.village}
+
+
+
+
+
+
+
+
+ {/* Stats */}
+
+
+
+ {pendingLands.length}
+ Perlu Validasi
+
+
+
+ {validations.filter(v => v.status === "approved").length}
+ Disetujui
+
+
+
+ {validations.filter(v => v.status === "rejected").length}
+ Ditolak
+
+
+
+ {/* Tabs */}
+
+
+
+
+
+ Perlu Validasi ({pendingLands.length})
+
+
+
+ Riwayat
+
+
+
+
+ {pendingLands.length > 0 ? (
+
+ {pendingLands.map(land => (
+
+
+
+
+
{land.name}
+
Pemilik: {getFarmerName(land.farmer_id)}
+
+
+ {statusStyles[land.validation_status]?.label}
+
+
+
+
+
+ {land.village}, {land.district}
+
+ {land.area_hectares?.toFixed(2)} Ha
+
+ setSelectedLand(land)}
+ >
+ Validasi Lahan
+
+
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+
+ {validations.length > 0 ? (
+
+ {validations.map(v => {
+ const land = lands.find(l => l.id === v.land_id);
+ return (
+
+
+
+
{land?.name || "Lahan"}
+
+ {v.validated_at ? format(new Date(v.validated_at), "dd MMM yyyy, HH:mm", { locale: id }) : "-"}
+
+
+
+ {v.status === "approved" ? "Disetujui" : "Ditolak"}
+
+
+ {v.notes && {v.notes}
}
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Validation Dialog */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/ValidatorPortal.jsx b/src/pages/ValidatorPortal.jsx
old mode 100644
new mode 100755
index 994905e1..1cb02276
--- a/src/pages/ValidatorPortal.jsx
+++ b/src/pages/ValidatorPortal.jsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import base44 from "@/api/Client";
+import base44 from "@/api/Client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -11,18 +11,20 @@ import LandMap from "@/components/lands/LandMap";
import EmptyState from "@/components/common/EmptyState";
import {
Shield, Map, CheckCircle, XCircle, Clock, LogOut,
- User, MapPin, Loader2, FileCheck, AlertTriangle
+ User, MapPin, Loader2, FileCheck, AlertTriangle, Users
} from "lucide-react";
import { toast } from "sonner";
import { motion } from "framer-motion";
import { format } from "date-fns";
import { id } from "date-fns/locale";
+import { entity } from "@/api/entities";
const roleLabels = {
kepala_desa: "Kepala Desa",
kepala_dusun: "Kepala Dusun",
rt: "Ketua RT",
- rw: "Ketua RW"
+ rw: "Ketua RW",
+ fasilitator: "Fasilitator"
};
const statusStyles = {
@@ -37,21 +39,36 @@ export default function ValidatorPortal() {
const [user, setUser] = useState(null);
const [validator, setValidator] = useState(null);
const [isLoading, setIsLoading] = useState(true);
+
+ // States for Land Validation
const [selectedLand, setSelectedLand] = useState(null);
+
+ // States for Farmer Validation
+ const [selectedFarmer, setSelectedFarmer] = useState(null);
+
const [validationNotes, setValidationNotes] = useState("");
useEffect(() => {
const loadData = async () => {
+ const allowedRoles = ['validator', 'super_admin', 'fasilitator'];
try {
- const currentUser = await base44.auth.me();
- setUser(currentUser);
+ const currentUser = await entity('auth', 'me').list();
+ const CurrentUserRole = currentUser.data.role;
- const validators = await base44.entities.Validator.filter({ user_email: currentUser.email });
- if (validators.length > 0 && validators[0].is_active) {
- setValidator(validators[0]);
+ if(allowedRoles.includes(CurrentUserRole)){
+ const dataUser = currentUser.data;
+ setUser(dataUser);
+ setValidator(dataUser)
+ setIsLoading(false)
+
+ } else{
+ console.log("Not Admin and not Validator")
+ setUser(null)
+ setValidator(null)
}
+
} catch (error) {
- // Not logged in
+ console.error("Gagal hubungkan ke server => " , error);
} finally {
setIsLoading(false);
}
@@ -59,39 +76,44 @@ export default function ValidatorPortal() {
loadData();
}, []);
- // Load lands in validator's area that need validation
+ // --- QUERIES ---
+
const { data: lands = [] } = useQuery({
- queryKey: ["validator-lands", validator?.village],
- queryFn: () => base44.entities.Land.filter({ village: validator.village }),
- enabled: !!validator?.village
+ queryKey: ["validator-lands"],
+ queryFn: async() => {
+ const response = await entity('map', 'lahan').list();
+ return Array.isArray(response.data) ? response.data : [];
+ },
+ // enabled: !!validator?.village
});
- // Load farmers for reference
const { data: farmers = [] } = useQuery({
- queryKey: ["farmers"],
- queryFn: () => base44.entities.Farmer.list(),
- enabled: !!validator
+ queryKey: ["validator-farmers"],
+ queryFn: async () => {
+ const farmerRaw = await entity('auth', 'profile').list();
+ const farmersData = Array.isArray(farmerRaw?.data) ? farmerRaw?.data : Array.isArray(farmerRaw?.data?.data) ? farmerRaw?.data?.data : [];
+ const farmers = farmersData.filter((farmer) => farmer.user.role === 'petani');
+ return farmers;
+ },
+ // enabled: !!validator?.village
});
- // Load validation history
const { data: validations = [] } = useQuery({
queryKey: ["validations", validator?.id],
queryFn: () => base44.entities.LandValidation.filter({ validator_email: validator.user_email }),
enabled: !!validator
});
- // Validate land mutation
- const validateMutation = useMutation({
+ // --- MUTATIONS ---
+
+ // Land Validation Mutation
+ const validateLandMutation = useMutation({
mutationFn: async ({ landId, status }) => {
const validationStatus = status === "approved" ? "valid" : "invalid";
-
- // Update land status
await base44.entities.Land.update(landId, {
validation_status: validationStatus,
validation_notes: validationNotes
});
-
- // Create validation record
await base44.entities.LandValidation.create({
land_id: landId,
validator_role: validator.role,
@@ -107,282 +129,199 @@ export default function ValidatorPortal() {
queryClient.invalidateQueries({ queryKey: ["validations"] });
setSelectedLand(null);
setValidationNotes("");
- toast.success("Validasi berhasil disimpan!");
+ toast.success("Validasi lahan disimpan!");
}
});
- const handleLogin = () => {
- base44.auth.redirectToLogin(window.location.href);
- };
+ // Farmer Validation Mutation
+ const validateFarmerMutation = useMutation({
+ mutationFn: async ({ farmerId, status }) => {
+ const validationStatus = status === "approved" ? "valid" : "invalid";
+ await base44.put(`auth/profile/${farmerId}/verify`,{
+ // status: validationStatus,
+ // notes: validationNotes,
+ // validated_by: validator.full_name,
+ // validated_at: new Date().toISOString(),
+ email_verified_at : new Date().toISOString(),
+ });
- const handleLogout = () => {
- base44.auth.logout();
- };
+ // await base44.put('',{
+ // email_verified_at : new Date().toISOString(),
+ // })
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["validator-farmers"] });
+ setSelectedFarmer(null);
+ setValidationNotes("");
+ toast.success("Validasi petani berhasil!");
+ }
+ });
- const getFarmerName = (farmerId) => {
- const farmer = farmers.find(f => f.id === farmerId);
- return farmer?.full_name || "Tidak diketahui";
- };
+ // --- HELPERS ---
+
+ const handleLogout = () => base44.auth.logout();
+ const handleLogin = () => base44.auth.redirectToLogin(window.location.href);
const pendingLands = lands.filter(l => l.validation_status === "pending" || l.validation_status === "need_review");
- const validatedLands = lands.filter(l => l.validation_status === "valid" || l.validation_status === "invalid");
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (!user) {
- return (
-
-
-
-
-
-
-
- Portal Validator
-
- Masuk untuk memvalidasi lahan di wilayah Anda
-
-
- Masuk
-
-
-
-
-
- );
- }
-
- if (!validator) {
- return (
-
-
-
-
-
- Akses Ditolak
-
- Email {user.email} tidak terdaftar sebagai validator.
-
-
- Hubungi admin untuk mendaftarkan Anda sebagai validator wilayah.
-
-
-
- Keluar
-
-
-
-
-
- );
- }
-
+
+ const pendingFarmers = farmers.filter(f => f.user.email_verified_at === "pending" || !f.user.email_verified_at);
+
+ if (isLoading) return
;
+ if (!user) return
Login Member
;
+
return (
-
-
- {/* Header */}
-
-
-
-
+
+
+ {/* Header Section */}
+
+
+
+
+
{validator.full_name}
+
{roleLabels[validator.role]} - {validator.village}
+
-
-
{validator.full_name}
-
- {roleLabels[validator.role]} - {validator.village}
-
+
+
+
+ {/* Stats Row */}
+
+
+
+ {pendingLands.length}
+ Lahan Tertunda
+
+
+
+ {pendingFarmers.length}
+ Petani Tertunda
+
+
+
+ {validations.length}
+ Total Validasi
+
+
+
+ Aktif
+ Sesi Kerja
+
+
+
+
+
+ Validasi Lahan
+ Validasi Petani
+ Riwayat
+
+
+ {/* TAB: VALIDASI LAHAN */}
+
+ {pendingLands.length > 0 ? (
+ pendingLands.map(land => (
+
+
+
+
{land.name}
+
{land.area_hectares} Ha • {land.village}
+
+ setSelectedLand(land)}>Periksa Lahan
+
+
+ ))
+ ) : }
+
+
+ {/* TAB: VALIDASI PETANI */}
+
+ {pendingFarmers.length > 0 ? (
+ pendingFarmers.map(farmer => (
+
+
+
+
+
+
+
+
{farmer.full_name || farmer.nama || ''}
+
NIK: {farmer.nik || farmer.ktp || 'N/A'}
+
+
+ setSelectedFarmer(farmer)}>Verifikasi Profil
+
+
+ ))
+ ) : }
+
+
+ {/* TAB: RIWAYAT */}
+
+ {validations.map(v => (
+
+
+ Validasi Lahan ID: {v.land_id}
+ {v.status}
+
+ {format(new Date(v.validated_at), "PPP", { locale: id })}
+
+ ))}
+
+
+
+
+ {/* MODAL: VALIDASI LAHAN */}
+
- {/* Stats */}
-
-
-
- {pendingLands.length}
- Perlu Validasi
-
-
-
- {validations.filter(v => v.status === "approved").length}
- Disetujui
-
-
-
- {validations.filter(v => v.status === "rejected").length}
- Ditolak
-
-
-
- {/* Tabs */}
-
-
-
-
-
- Perlu Validasi ({pendingLands.length})
-
-
-
- Riwayat
-
-
-
-
- {pendingLands.length > 0 ? (
-
- {pendingLands.map(land => (
-
-
-
-
-
{land.name}
-
Pemilik: {getFarmerName(land.farmer_id)}
-
-
- {statusStyles[land.validation_status]?.label}
-
-
-
-
-
- {land.village}, {land.district}
-
- {land.area_hectares?.toFixed(2)} Ha
-
- setSelectedLand(land)}
- >
- Validasi Lahan
-
-
-
- ))}
+ {/* MODAL: VALIDASI PETANI */}
+