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
.DS_Store
Thumbs.db
package-lock.json
.env
package-lock.json

View File

@ -57,6 +57,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",

View File

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

View File

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

View File

@ -126,9 +126,9 @@ export default function Layout({ children, currentPageName }) {
const location = useLocation();
const { user, logout } = useAuth();
if (!user) {
return <div className="flex h-screen items-center justify-center">Memuat Sesi...</div>;
}
// if (!user) {
// return <div className="flex h-screen items-center justify-center">Memuat Sesi...</div>;
// }
const filteredNavigation = navigation.filter((item) => {
if (!user?.role) return false;
@ -142,7 +142,7 @@ export default function Layout({ children, currentPageName }) {
return (
<div className="min-h-screen bg-slate-50">
{/* 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">
<Button variant="ghost" size="icon" onClick={() => setSidebarOpen(true)}>
<Menu className="w-6 h-6" />

View File

@ -15,12 +15,11 @@ export function useSyncManager() {
const hasPending = await OfflineService.hasPendingData();
if (hasPending) {
console.log("📤 Memulai Push Sinkronisasi...");
// console.log("📤 Memulai Push Sinkronisasi...");
await OfflineService.syncAllPending();
toast.success("Sinkronisasi data berhasil");
} 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();
}
} catch (error) {
@ -35,7 +34,6 @@ export function useSyncManager() {
const handleOnline = () => {
setOnline(true);
toast.success("Koneksi internet tersambung");
console.log("Check Online Status")
syncPendingData();
};
const handleOffline = () => {
@ -81,7 +79,7 @@ export function useSyncManager() {
setSyncing(true);
try {
// await OfflineService.syncAll(base44);
const count = await OfflineService.getPendingCount();
setPendingCount(count);
} catch (error) {

View File

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

View File

@ -34,10 +34,7 @@ export const OfflineService = {
};
try {
// update first
await db[table].put(record);
// gonna insert
await db.pending_sync.add({
id: `q_${Date.now()}`,
entity_type: entityType,
@ -46,7 +43,7 @@ export const OfflineService = {
created_at: new Date().toISOString()
});
console.log(`✅ Dexie: Data ${entityType} tersimpan aman!`);
// console.log(` Dexie: Data ${entityType} tersimpan aman!`);
return id;
} catch (err) {
console.error("❌ Dexie Error:", err);
@ -56,7 +53,7 @@ export const OfflineService = {
getEntities: async (entityType, filter = {}) => {
const user = localStorage.getItem('user_data')
console.log("Data user :", user)
// console.log("Data user :", user)
try {
if (!entityType) return [];
const table = entityType.toLowerCase().endsWith('s')
@ -79,19 +76,19 @@ export const OfflineService = {
if(invalidData.length > 0){
const idsToDelete = invalidData.map(d => d.id);
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) {
console.log(`🔍 Fetching ${table} with filter:`, filter);
// console.log(`Fetching ${table} with filter:`, filter);
return await dbTable.where(filter).toArray();
}
console.log(`🔍 Fetching all from ${table}`);
// console.log(`Fetching ...... all from ${table}`);
return await dbTable.reverse().toArray();
} catch (e) {
console.error("❌ Gagal ambil data dari Dexie:", e);
console.error("Errof fetching data from Dexie:", e);
return [];
}
},
@ -128,10 +125,10 @@ export const OfflineService = {
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;
} catch (err) {
console.error(" Dexie Delete Error:", err);
console.error(" Dexie Delete Error:", err);
throw err;
}
},
@ -164,11 +161,15 @@ export const OfflineService = {
{ 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"},
];
try {
console.log("📥 Mendownload data terbaru dari server...");
// console.log("📥 Mendownload data terbaru dari server...");
for(const config of syncConfigs){
const respond = await axios.get(`${baseURL}/api/${config.endpoint}`, {
headers: { Authorization: `Bearer ${token}` }
@ -184,12 +185,12 @@ export const OfflineService = {
sync_status: 'synced'
})));
}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) {
console.error("Gagal download data:", err);
}
@ -204,6 +205,10 @@ export const OfflineService = {
{ endpoint: 'map/offtaker', to:"offtaker", from: db.offtakers},
{ endpoint: 'distribusi/panen', to:"panen", from: db.distributions},
// { 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');
@ -227,7 +232,7 @@ export const OfflineService = {
if (response.status === 200 || response.status === 201) {
await item.table.delete(record.id);
console.log(`Synced & Deleted Local ID: ${record.id}`);
// console.log(`Synced & Deleted Local ID: ${record.id}`);
hasChanged = true;
}
} catch (e) {
@ -240,7 +245,7 @@ export const OfflineService = {
}
}
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();
}
},

View File

@ -1,7 +1,10 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';
import { db } from "@/utils/db";
import CryptoJS from 'crypto-js';
const AuthContext = createContext();
const SECRET_KEY = import.meta.env.VITE_CRYPTO_KEY || "Pr08ind0";
export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(
@ -17,6 +20,15 @@ export const AuthProvider = ({ children }) => {
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(() => {
checkInitialState();
}, []);
@ -25,12 +37,20 @@ export const AuthProvider = ({ children }) => {
try {
setIsLoadingAuth(true);
const localToken = localStorage.getItem('access_token');
const localUser = localStorage.getItem('user_data');
if (localToken && localUser) {
setUser(JSON.parse(localUser));
if (!db.isOpen()) await db.open();
const localUsers = await db.users.toArray();
const storedUser = localUsers.length > 0 ? localUsers[0] : null;
if (localToken && storedUser) {
setUser(storedUser);
setIsAuthenticated(true);
}
// if (localToken && localUser) {
// setUser(JSON.parse(localUser));
// setIsAuthenticated(true);
// }
if (navigator.onLine && localToken) {
try {
@ -40,6 +60,7 @@ export const AuthProvider = ({ children }) => {
const freshUser = response.data;
setUser(freshUser);
await db.users.put({ ...freshUser, sync_status: 'synced' });
localStorage.setItem('user_data', JSON.stringify(freshUser));
} catch (e) {
if (e.response?.status === 401) {
@ -55,25 +76,59 @@ export const AuthProvider = ({ children }) => {
};
const login = async (email, password) => {
try {
setIsLoadingAuth(true);
setIsLoadingAuth(true);
try{
const response = await axios.post(`${getBaseUrl()}/api/auth/login`, {
email,
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('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);
setIsAuthenticated(true);
return { success: true };
} 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 {
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 {
setIsLoadingAuth(false);

View File

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

View File

@ -128,8 +128,15 @@ export default function LandDetail() {
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) {
localData.map((data) => {
const isOwned = data.id === paramsId;
if(isOwned){
return isOwned;
}
})
return { ...localData[0], isOffline: true };
}
throw new Error("Lahan tidak ditemukan");
@ -270,7 +277,7 @@ export default function LandDetail() {
});
}
}, [land, isEditing]);
console.log(land, "Data lahan");
if (isLoading) {
return (
<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) => {
// Pastikan data adalah angka murni, bukan objek LatLng Leaflet
const cleanCoordinates = JSON.parse(JSON.stringify(polygonData.polygon_coordinates));
console.log(cleanCoordinates)
setFormData(prev => ({

View File

@ -4,47 +4,28 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
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 { useAuth } from "@/lib/AuthContext";
import { useNavigate } from "react-router-dom";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [user, setUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const dataToken = localStorage.getItem('access_token')
console.log(dataToken);
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
const response = await base44.post("/auth/login", {
email,
password,
});
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 {
const result = await login(email, password);
if (result.success) {
toast.success("Login berhasil!");
navigate("/ProductivityMonitoring");
} else {
toast.error(result.message);
setIsSubmitting(false);
}
};
@ -105,7 +86,10 @@ export default function Login() {
</Button>
</form>
<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>
</CardContent>
</Card>

View File

@ -15,6 +15,9 @@ db.version(1).stores({
profile: 'id,user_id, kk,ktp,email, sync_status, name, nama',
harvest: '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'
});