diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..92b4e9c4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +[*] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/bugreport-sdk_gphone64_arm64-BE4B.251210.005-2026-03-06-09-42-33.zip b/bugreport-sdk_gphone64_arm64-BE4B.251210.005-2026-03-06-09-42-33.zip old mode 100644 new mode 100755 diff --git a/capacitor.config.json b/capacitor.config.json old mode 100644 new mode 100755 diff --git a/components.json b/components.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Comodity.json b/entities/probindo.com/Comodity.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Distribution.json b/entities/probindo.com/Distribution.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Farmer.json b/entities/probindo.com/Farmer.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Harvest.json b/entities/probindo.com/Harvest.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Land.json b/entities/probindo.com/Land.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/LandValidation.json b/entities/probindo.com/LandValidation.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Offtakers.json b/entities/probindo.com/Offtakers.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Plant.json b/entities/probindo.com/Plant.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/PlantInspection.json b/entities/probindo.com/PlantInspection.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Synclog.json b/entities/probindo.com/Synclog.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/User/me.json b/entities/probindo.com/User/me.json old mode 100644 new mode 100755 diff --git a/entities/probindo.com/Validator.json b/entities/probindo.com/Validator.json old mode 100644 new mode 100755 diff --git a/entities/public-settings/probindo.com.json b/entities/public-settings/probindo.com.json old mode 100644 new mode 100755 diff --git a/eslint.config.js b/eslint.config.js old mode 100644 new mode 100755 diff --git a/index.html b/index.html old mode 100644 new mode 100755 index 34a8e295..56a3dbe2 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + Agro Monev diff --git a/jsconfig.json b/jsconfig.json old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 index af6d3c31..c848ee23 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@capacitor/android": "^8.1.0", "@capacitor/cli": "^8.1.0", "@capacitor/core": "^8.1.0", + "@capacitor/geolocation": "^8.1.0", "@capacitor/ios": "^8.1.0", "@emotion/is-prop-valid": "^1.4.0", "@hello-pangea/dnd": "^17.0.0", @@ -52,6 +53,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "crypto-js": "^4.2.0", "date-fns": "^3.6.0", "dexie": "^4.3.0", "dexie-react-hooks": "^4.2.0", @@ -493,6 +495,18 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/geolocation": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.1.0.tgz", + "integrity": "sha512-dHeGuVlT3ELxxn8WaoRsGjfx8tmziaC0ZuaKhUBCoKFjOue4fAnmLczCMQsYW7OykgiyBh0pirNPwWZ0/LXB7Q==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.4" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor/ios": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.1.0.tgz", @@ -502,6 +516,12 @@ "@capacitor/core": "^8.1.0" } }, + "node_modules/@capacitor/synapse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz", + "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", + "license": "ISC" + }, "node_modules/@emotion/is-prop-valid": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", @@ -4607,6 +4627,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/css-box-model": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 15e303c5..88e4ccb5 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@capacitor/android": "^8.1.0", "@capacitor/cli": "^8.1.0", "@capacitor/core": "^8.1.0", + "@capacitor/geolocation": "^8.1.0", "@capacitor/ios": "^8.1.0", "@emotion/is-prop-valid": "^1.4.0", "@hello-pangea/dnd": "^17.0.0", diff --git a/pertanian-app.code-workspace b/pertanian-app.code-workspace old mode 100644 new mode 100755 index 68f93f75..e53c7909 --- a/pertanian-app.code-workspace +++ b/pertanian-app.code-workspace @@ -11,6 +11,15 @@ }, { "path": "../pertamina-point" + }, + { + "path": "../dev/wayfinder-project" + }, + { + "path": "../own-cloud" + }, + { + "path": "../skhp" } ], "settings": {} diff --git a/postcss.config.js b/postcss.config.js old mode 100644 new mode 100755 diff --git a/public/logo1.png b/public/logo1.png new file mode 100644 index 00000000..aded690f Binary files /dev/null and b/public/logo1.png differ diff --git a/public/logo_001.png b/public/logo_001.png new file mode 100755 index 00000000..778f20bf Binary files /dev/null and b/public/logo_001.png differ diff --git a/public/sql-wasm.wasm b/public/sql-wasm.wasm old mode 100644 new mode 100755 diff --git a/src/.gitignore b/src/.gitignore old mode 100644 new mode 100755 diff --git a/src/App.jsx b/src/App.jsx old mode 100644 new mode 100755 diff --git a/src/App_baru.jsx b/src/App_baru.jsx old mode 100644 new mode 100755 diff --git a/src/Layout copy.jsx b/src/Layout copy.jsx old mode 100644 new mode 100755 diff --git a/src/Layout.jsx b/src/Layout.jsx old mode 100644 new mode 100755 index e7ea036e..a3888fa1 --- a/src/Layout.jsx +++ b/src/Layout.jsx @@ -19,7 +19,7 @@ function SyncIndicator() { const updateStatus = async () => { const isConnected = await checkInternet(); - const count = await OfflineService.getPendingCount(); // Ambil dari SQLite + const count = await OfflineService.getPendingCount(); if (mounted) { setOnline(isConnected); @@ -94,7 +94,6 @@ async function checkInternet() { try { const controller = new AbortController(); setTimeout(() => controller.abort(), 3000); - // Gunakan favicon google sebagai probe yang ringan await fetch("https://www.google.com/favicon.ico", { method: "HEAD", mode: "no-cors", @@ -109,16 +108,16 @@ async function checkInternet() { const navigation = [ - { name: "Dashboard", page: "Dashboard", icon: LayoutDashboard , roles: ['super_admin']}, - { name: "Petani", page: "Farmers", icon: Users, roles: ['super_admin'] }, - { name: "Lahan", page: "Lands", icon: Map, roles: ['super_admin'] }, - { name: "Tanaman", page: "Plants", icon: TreePine, roles: ['super_admin'] }, - { name: "Produktivitas", page: "ProductivityMonitoring", icon: LayoutDashboard, roles: ['super_admin', 'petani'] }, - { name: "Validator", page: "Validators", icon: Shield, roles: ['super_admin'] }, - { name: "Offtaker", page: "Offtakers", icon: Users, roles: ['super_admin'] }, - { name: "Portal Petani", page: "FarmerPortal", icon: Users, roles: ['super_admin', 'petani'] }, - { name: "Portal Validator", page: "ValidatorPortal", icon: Shield, roles: ['super_admin'] }, - { name: "Portal Offtaker", page: "OfftakerPortal", icon: Users, roles: ['super_admin'] }, + { 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 Layout({ children, currentPageName }) { @@ -148,8 +147,9 @@ export default function Layout({ children, currentPageName }) {
-
- +
+ {/* */} +
AgroMonev
@@ -173,8 +173,9 @@ export default function Layout({ children, currentPageName }) {
-
- +
+ {/* */} +

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} + {farmer?.name ) : ( {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 && ( -
- - - -
- )} - - {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 && } + {/* SECTION 2: AREA INFORMASI LUAS & SIMPAN (Muncul jika titik >= 3) */} + {points.length >= 3 && ( +
+
+

Total Estimasi Luas

+
+ {area.toFixed(4)} + Ha +
+
+ + {!readOnly && ( + + )} +
+ )} + + {/* 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 && ( +
+ +
)}
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 }
+ + + +
+ ); + } 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, regency: e.target.value ? '1' : '0'}))} + value={locationData.regencies[0]?.nama} + readOnly placeholder="Nama kabupaten" />
-
-
- + 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

- { + console.log('Filter level changed to:', v); + console.log('Before setFilterLevel, current filterLevel:', filterLevel); + setFilterLevel(v); + setSelectedRegion("all"); + console.log('After setFilterLevel, new value should be:', v); + }}> @@ -261,7 +382,8 @@ export default function ProductivityMonitoring() {

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 +

+ +
+
+
+
+ ); + } + + if (!validator) { + return ( +
+ + + +
+ +
+

Akses Ditolak

+

+ Email {user.email} tidak terdaftar sebagai validator. +

+

+ Hubungi admin untuk mendaftarkan Anda sebagai validator wilayah. +

+ +
+
+
+
+ ); + } + + 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 +
+ +
+
+ ))} +
+ ) : ( + + )} +
+ + + {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 */} + setSelectedLand(null)}> + + + Validasi Lahan: {selectedLand?.name} + + + {selectedLand && ( +
+
+
+

Pemilik

+

{getFarmerName(selectedLand.farmer_id)}

+
+
+

Luas

+

{selectedLand.area_hectares?.toFixed(4)} Ha

+
+
+

Lokasi

+

{selectedLand.village}, {selectedLand.district}, {selectedLand.regency}

+
+
+ +
+ +
+ +
+ +