This commit is contained in:
fauzgabriel@gmail.com 2026-03-17 09:36:10 +07:00
parent 132e9e3901
commit 8ca1307c21
14 changed files with 132 additions and 75 deletions

3
.gitignore vendored
View File

@ -32,3 +32,6 @@ pnpm-debug.log*
# OS files # OS files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
package-lock.json
.env
package-lock.json

View File

@ -57,6 +57,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dexie": "^4.3.0", "dexie": "^4.3.0",
"dexie-react-hooks": "^4.2.0", "dexie-react-hooks": "^4.2.0",

View File

@ -8,6 +8,9 @@
}, },
{ {
"path": "../siks" "path": "../siks"
},
{
"path": "../pertamina-point"
} }
], ],
"settings": {} "settings": {}

View File

@ -66,7 +66,6 @@ const AuthenticatedApp = () => {
function ProtectedLayout() { function ProtectedLayout() {
const { token } = useAuth(); const { token } = useAuth();
console.log("token on app", token)
if (!token) { if (!token) {
return <Navigate to="/login" replace />; return <Navigate to="/login" replace />;
} }

View File

@ -126,9 +126,9 @@ export default function Layout({ children, currentPageName }) {
const location = useLocation(); const location = useLocation();
const { user, logout } = useAuth(); const { user, logout } = useAuth();
if (!user) { // if (!user) {
return <div className="flex h-screen items-center justify-center">Memuat Sesi...</div>; // return <div className="flex h-screen items-center justify-center">Memuat Sesi...</div>;
} // }
const filteredNavigation = navigation.filter((item) => { const filteredNavigation = navigation.filter((item) => {
if (!user?.role) return false; if (!user?.role) return false;
@ -142,7 +142,7 @@ export default function Layout({ children, currentPageName }) {
return ( return (
<div className="min-h-screen bg-slate-50"> <div className="min-h-screen bg-slate-50">
{/* Mobile Header */} {/* Mobile Header */}
<div className="lg:hidden fixed top-0 left-0 right-0 pt-safe h-16 bg-white border-b border-slate-200 z-50 flex items-center justify-between px-4"> <div className="lg:hidden fixed top-0 left-0 right-0 pt-safe h-[6rem] bg-white border-b border-slate-200 z-50 flex items-center justify-between px-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={() => setSidebarOpen(true)}> <Button variant="ghost" size="icon" onClick={() => setSidebarOpen(true)}>
<Menu className="w-6 h-6" /> <Menu className="w-6 h-6" />

View File

@ -15,12 +15,11 @@ export function useSyncManager() {
const hasPending = await OfflineService.hasPendingData(); const hasPending = await OfflineService.hasPendingData();
if (hasPending) { if (hasPending) {
console.log("📤 Memulai Push Sinkronisasi..."); // console.log("📤 Memulai Push Sinkronisasi...");
await OfflineService.syncAllPending(); await OfflineService.syncAllPending();
toast.success("Sinkronisasi data berhasil"); toast.success("Sinkronisasi data berhasil");
} else { } else {
// 2. Jika tidak ada pending, ambil data terbaru (PULL) // console.log("🔄 Data lokal bersih. Melakukan Pull dari server...");
console.log("🔄 Data lokal bersih. Melakukan Pull dari server...");
await OfflineService.downloadFromServer(); await OfflineService.downloadFromServer();
} }
} catch (error) { } catch (error) {
@ -35,7 +34,6 @@ export function useSyncManager() {
const handleOnline = () => { const handleOnline = () => {
setOnline(true); setOnline(true);
toast.success("Koneksi internet tersambung"); toast.success("Koneksi internet tersambung");
console.log("Check Online Status")
syncPendingData(); syncPendingData();
}; };
const handleOffline = () => { const handleOffline = () => {

View File

@ -12,7 +12,6 @@ export default function SyncStatusBar({ compact = false }) {
const { syncing, lastSync, forceSync } = useSyncManager(); const { syncing, lastSync, forceSync } = useSyncManager();
const [online, setOnline] = useState(false); const [online, setOnline] = useState(false);
const [pendingCount, setPendingCount] = useState() const [pendingCount, setPendingCount] = useState()
// const pendingCount = OfflineService.getPendingCount();
useEffect(() => { useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();

View File

@ -34,10 +34,7 @@ export const OfflineService = {
}; };
try { try {
// update first
await db[table].put(record); await db[table].put(record);
// gonna insert
await db.pending_sync.add({ await db.pending_sync.add({
id: `q_${Date.now()}`, id: `q_${Date.now()}`,
entity_type: entityType, entity_type: entityType,
@ -46,7 +43,7 @@ export const OfflineService = {
created_at: new Date().toISOString() created_at: new Date().toISOString()
}); });
console.log(`✅ Dexie: Data ${entityType} tersimpan aman!`); // console.log(` Dexie: Data ${entityType} tersimpan aman!`);
return id; return id;
} catch (err) { } catch (err) {
console.error("❌ Dexie Error:", err); console.error("❌ Dexie Error:", err);
@ -56,7 +53,7 @@ export const OfflineService = {
getEntities: async (entityType, filter = {}) => { getEntities: async (entityType, filter = {}) => {
const user = localStorage.getItem('user_data') const user = localStorage.getItem('user_data')
console.log("Data user :", user) // console.log("Data user :", user)
try { try {
if (!entityType) return []; if (!entityType) return [];
const table = entityType.toLowerCase().endsWith('s') const table = entityType.toLowerCase().endsWith('s')
@ -79,19 +76,19 @@ export const OfflineService = {
if(invalidData.length > 0){ if(invalidData.length > 0){
const idsToDelete = invalidData.map(d => d.id); const idsToDelete = invalidData.map(d => d.id);
await dbTable.bulkDelete(idsToDelete); await dbTable.bulkDelete(idsToDelete);
console.log(`🧹 Cleanup: Menghapus ${invalidData.length} data tanpa NIK dari ${table}`); // console.log(`Cleanup: Menghapus ${invalidData.length} data tanpa NIK dari ${table}`);
} }
if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) { if (filter && typeof filter === 'object' && Object.keys(filter).length > 0) {
console.log(`🔍 Fetching ${table} with filter:`, filter); // console.log(`Fetching ${table} with filter:`, filter);
return await dbTable.where(filter).toArray(); return await dbTable.where(filter).toArray();
} }
console.log(`🔍 Fetching all from ${table}`); // console.log(`Fetching ...... all from ${table}`);
return await dbTable.reverse().toArray(); return await dbTable.reverse().toArray();
} catch (e) { } catch (e) {
console.error("❌ Gagal ambil data dari Dexie:", e); console.error("Errof fetching data from Dexie:", e);
return []; return [];
} }
}, },
@ -128,10 +125,10 @@ export const OfflineService = {
created_at: new Date().toISOString() created_at: new Date().toISOString()
}); });
console.log(`🗑️ Dexie: Data ${entityType} dengan ID ${id} berhasil dihapus lokal!`); // console.log(`Dexie: Data ${entityType} dengan ID ${id} berhasil dihapus lokal!`);
return true; return true;
} catch (err) { } catch (err) {
console.error(" Dexie Delete Error:", err); console.error(" Dexie Delete Error:", err);
throw err; throw err;
} }
}, },
@ -164,11 +161,15 @@ export const OfflineService = {
{ endpoint: 'map/offtaker', table: db.offtakers}, { endpoint: 'map/offtaker', table: db.offtakers},
{ endpoint: 'distribusi/panen', table: db.distributions}, { endpoint: 'distribusi/panen', table: db.distributions},
// { endpoint: 'map/validator', table: db.validators} // { 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"},
]; ];
try { try {
console.log("📥 Mendownload data terbaru dari server..."); // console.log("📥 Mendownload data terbaru dari server...");
for(const config of syncConfigs){ for(const config of syncConfigs){
const respond = await axios.get(`${baseURL}/api/${config.endpoint}`, { const respond = await axios.get(`${baseURL}/api/${config.endpoint}`, {
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
@ -184,12 +185,12 @@ export const OfflineService = {
sync_status: 'synced' sync_status: 'synced'
}))); })));
}else { }else {
console.log(`✅ Data ${config.endpoint} sudah mutakhir (tidak ada data baru).`); // console.log(` Data ${config.endpoint} sudah mutakhir (tidak ada data baru).`);
} }
} }
} }
console.log("Data lokal berhasil diperbarui."); // console.log("Data lokal berhasil diperbarui.");
} catch (err) { } catch (err) {
console.error("Gagal download data:", err); console.error("Gagal download data:", err);
} }
@ -204,6 +205,10 @@ export const OfflineService = {
{ endpoint: 'map/offtaker', to:"offtaker", from: db.offtakers}, { endpoint: 'map/offtaker', to:"offtaker", from: db.offtakers},
{ endpoint: 'distribusi/panen', to:"panen", from: db.distributions}, { endpoint: 'distribusi/panen', to:"panen", from: db.distributions},
// { endpoint: 'map/validator', to:"validator", from: db.validators} // { endpoint: 'map/validator', to:"validator", from: db.validators}
{ from: db.villages, to: "desa-kelurahan", endpoint: "master/desa-kelurahan" },
{ from: db.districts, to: "kecamatan", endpoint: "master/kecamatan"},
{ from: db.regencies, to: "kabupaten-kota", endpoint: "master/kabupaten-kota"},
{ from: db.provinces, to: "provinsi", endpoint: "master/provinsi"},
]; ];
const token = localStorage.getItem('access_token'); const token = localStorage.getItem('access_token');
@ -227,7 +232,7 @@ export const OfflineService = {
if (response.status === 200 || response.status === 201) { if (response.status === 200 || response.status === 201) {
await item.table.delete(record.id); await item.table.delete(record.id);
console.log(`Synced & Deleted Local ID: ${record.id}`); // console.log(`Synced & Deleted Local ID: ${record.id}`);
hasChanged = true; hasChanged = true;
} }
} catch (e) { } catch (e) {
@ -240,7 +245,7 @@ export const OfflineService = {
} }
} }
if (hasChanged) { if (hasChanged) {
console.log("🔄 Memicu download data terbaru agar Dexie sinkron dengan Server..."); // console.log(" Memicu download data terbaru agar Dexie sinkron dengan Server...");
await OfflineService.downloadFromServer(); await OfflineService.downloadFromServer();
} }
}, },

View File

@ -1,7 +1,10 @@
import React, { createContext, useState, useContext, useEffect } from 'react'; import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import { db } from "@/utils/db";
import CryptoJS from 'crypto-js';
const AuthContext = createContext(); const AuthContext = createContext();
const SECRET_KEY = import.meta.env.VITE_CRYPTO_KEY || "Pr08ind0";
export const AuthProvider = ({ children }) => { export const AuthProvider = ({ children }) => {
const [token, setToken] = useState( const [token, setToken] = useState(
@ -17,6 +20,15 @@ export const AuthProvider = ({ children }) => {
return url.replace(/\/+$/, "").replace(/\/api$/, ""); return url.replace(/\/+$/, "").replace(/\/api$/, "");
}; };
const encryptPassword = (password) => {
return CryptoJS.AES.encrypt(password, SECRET_KEY).toString();
};
const decryptPassword = (hashedPassword) => {
const bytes = CryptoJS.AES.decrypt(hashedPassword, SECRET_KEY);
return bytes.toString(CryptoJS.enc.Utf8);
};
useEffect(() => { useEffect(() => {
checkInitialState(); checkInitialState();
}, []); }, []);
@ -25,13 +37,21 @@ export const AuthProvider = ({ children }) => {
try { try {
setIsLoadingAuth(true); setIsLoadingAuth(true);
const localToken = localStorage.getItem('access_token'); const localToken = localStorage.getItem('access_token');
const localUser = localStorage.getItem('user_data');
if (localToken && localUser) { if (!db.isOpen()) await db.open();
setUser(JSON.parse(localUser)); const localUsers = await db.users.toArray();
const storedUser = localUsers.length > 0 ? localUsers[0] : null;
if (localToken && storedUser) {
setUser(storedUser);
setIsAuthenticated(true); setIsAuthenticated(true);
} }
// if (localToken && localUser) {
// setUser(JSON.parse(localUser));
// setIsAuthenticated(true);
// }
if (navigator.onLine && localToken) { if (navigator.onLine && localToken) {
try { try {
const response = await axios.get(`${getBaseUrl()}/api/auth/me`, { const response = await axios.get(`${getBaseUrl()}/api/auth/me`, {
@ -40,6 +60,7 @@ export const AuthProvider = ({ children }) => {
const freshUser = response.data; const freshUser = response.data;
setUser(freshUser); setUser(freshUser);
await db.users.put({ ...freshUser, sync_status: 'synced' });
localStorage.setItem('user_data', JSON.stringify(freshUser)); localStorage.setItem('user_data', JSON.stringify(freshUser));
} catch (e) { } catch (e) {
if (e.response?.status === 401) { if (e.response?.status === 401) {
@ -55,25 +76,59 @@ export const AuthProvider = ({ children }) => {
}; };
const login = async (email, password) => { const login = async (email, password) => {
try { setIsLoadingAuth(true);
setIsLoadingAuth(true);
try{
const response = await axios.post(`${getBaseUrl()}/api/auth/login`, { const response = await axios.post(`${getBaseUrl()}/api/auth/login`, {
email, email,
password password
}); });
const { access_token, user: userData } = response.data; const { access_token} = response.data;
const userResponse = await axios.get(`${getBaseUrl()}/api/auth/me`, {
headers: { Authorization: `Bearer ${access_token}` }
});
const userData = userResponse.data;
localStorage.setItem('access_token', access_token); localStorage.setItem('access_token', access_token);
localStorage.setItem('user_data', JSON.stringify(userData)); setToken(access_token);
if (!db.isOpen()) await db.open();
const encryptedPW = encryptPassword(password);
await db.users.put({
id: userData.id || userData.user_id,
...userData,
password:encryptedPW,
sync_status: 'synced',
last_login: new Date().toISOString()
});
setUser(userData); setUser(userData);
setIsAuthenticated(true); setIsAuthenticated(true);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.log(error, "Warning error")
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) {
setUser(localUser);
setIsAuthenticated(true);
return { success: true, isOffline: true };
}
}
} catch (dexieErr) {
console.error("Gagal membaca database lokal", dexieErr);
}
return { return {
success: false, success: false,
message: error.response?.data?.message || 'Login gagal. Cek koneksi Anda.' message: error.response?.data?.message || 'Login gagal. Periksa koneksi atau data lokal Anda.'
}; };
} finally { } finally {
setIsLoadingAuth(false); setIsLoadingAuth(false);

View File

@ -20,14 +20,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { import {
User, Map as MapIcon, TreePine, Plus, LogOut, CheckCircle, Clock, Truck, User, Map as MapIcon, TreePine, Plus, LogOut, CheckCircle, Clock, Truck,
Edit, Loader2, MapPin, Phone, Users as UsersIcon, ClipboardCheck, AlertTriangle, Bug, Send, Package, Edit, Loader2, MapPin, Phone, Users as UsersIcon, ClipboardCheck, AlertTriangle, Bug, Send, Package,
Save, Edit2, CreditCard, FileText Save, CreditCard, FileText
} from "lucide-react"; } from "lucide-react";
import PlantInspectionForm from "@/components/plants/PlantInspectionForm"; import PlantInspectionForm from "@/components/plants/PlantInspectionForm";
import DistributionForm from "@/components/distribution/DistributionForm"; import DistributionForm from "@/components/distribution/DistributionForm";
import HarvestForm from "@/components/harvest/HarvestForm"; import HarvestForm from "@/components/harvest/HarvestForm";
import { toast } from "sonner"; import { toast } from "sonner";
import { motion, sync } from "framer-motion"; import { motion, sync } from "framer-motion";
import { useLocation } from 'react-router-dom';
import { OfflineService } from "@/components/common/offlineStorage"; import { OfflineService } from "@/components/common/offlineStorage";
@ -403,6 +402,8 @@ export default function FarmerPortal() {
// profile control // profile control
const handleToggleEdit = async () => { const handleToggleEdit = async () => {
const [file, setFile] = useState();
if (isEditing) { if (isEditing) {
try { try {
if(selectedKtp){ if(selectedKtp){
@ -898,7 +899,7 @@ export default function FarmerPortal() {
size="sm" size="sm"
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
> >
<Edit2 className="w-4 h-4 mr-2" /> Edit Profil <Edit className="w-4 h-4 mr-2" /> Edit Profil
</Button> </Button>
)} )}
</div> </div>

View File

@ -128,8 +128,15 @@ export default function LandDetail() {
console.warn("Server unreachable, searching locally..."); console.warn("Server unreachable, searching locally...");
} }
const localData = await OfflineService.getEntities("lands", { id: id }); const localData = await OfflineService.getEntities("lands");
if (localData && localData.length > 0) { if (localData && localData.length > 0) {
localData.map((data) => {
const isOwned = data.id === paramsId;
if(isOwned){
return isOwned;
}
})
return { ...localData[0], isOffline: true }; return { ...localData[0], isOffline: true };
} }
throw new Error("Lahan tidak ditemukan"); throw new Error("Lahan tidak ditemukan");
@ -270,7 +277,7 @@ export default function LandDetail() {
}); });
} }
}, [land, isEditing]); }, [land, isEditing]);
console.log(land, "Data lahan");
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center"> <div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center">

View File

@ -166,7 +166,6 @@ export default function LandRegister() {
// }; // };
const handlePolygonSave = (polygonData) => { const handlePolygonSave = (polygonData) => {
// Pastikan data adalah angka murni, bukan objek LatLng Leaflet
const cleanCoordinates = JSON.parse(JSON.stringify(polygonData.polygon_coordinates)); const cleanCoordinates = JSON.parse(JSON.stringify(polygonData.polygon_coordinates));
console.log(cleanCoordinates) console.log(cleanCoordinates)
setFormData(prev => ({ setFormData(prev => ({

View File

@ -4,47 +4,28 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { TreePine, Lock, Mail, Loader2 } from "lucide-react"; import { TreePine, Lock, Mail, Loader2, ArrowRight } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { useAuth } from "@/lib/AuthContext";
import { useNavigate } from "react-router-dom";
export default function Login() { export default function Login() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [user, setUser] = useState(null); const { login } = useAuth();
const [isAuthenticated, setIsAuthenticated] = useState(false); const navigate = useNavigate();
const dataToken = localStorage.getItem('access_token')
console.log(dataToken);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
try { const result = await login(email, password);
const response = await base44.post("/auth/login", { if (result.success) {
email, toast.success("Login berhasil!");
password, navigate("/ProductivityMonitoring");
}); } else {
toast.error(result.message);
const data = response.data;
console.log(response,data)
if (data.access_token) {
// setUser(userData);
setIsAuthenticated(true);
localStorage.setItem('access_token', data.access_token);
// localStorage.setItem('user_data', JSON.stringify(userData));
toast.success("Login berhasil!");
window.location.href = "/";
} else {
toast.error("Email atau password salah");
}
} catch (error) {
toast.error(error.response?.data?.message || "Terjadi kesalahan saat login");
} finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
@ -105,7 +86,10 @@ export default function Login() {
</Button> </Button>
</form> </form>
<div className="mt-6 text-center text-sm text-slate-500"> <div className="mt-6 text-center text-sm text-slate-500">
Lupa password? Silakan hubungi admin koperasi. Belum punya akun ? {" "}
<a href="/register" className="text-emerald-600 font-semibold hover:underline inline-flex items-center">
Daftar di sini <ArrowRight className="ml-1 h-3 w-3" />
</a>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -15,6 +15,9 @@ db.version(1).stores({
profile: 'id,user_id, kk,ktp,email, sync_status, name, nama', profile: 'id,user_id, kk,ktp,email, sync_status, name, nama',
harvest: 'id, plant_id, land_id,farmer_id, sync_status', harvest: 'id, plant_id, land_id,farmer_id, sync_status',
plant_inspections: 'id, plant_id, land_id, farmer_id, sync_status', plant_inspections: 'id, plant_id, land_id, farmer_id, sync_status',
master_villages: 'id, district_id, name', distributions: 'id, farmer_id,offtaker_id, sync_status',
distributions: 'id, farmer_id,offtaker_id, sync_status' villages: 'id, name',
districts: 'id, name',
regencies: 'id, name',
provinces: 'id, name'
}); });