Login
This commit is contained in:
parent
132e9e3901
commit
8ca1307c21
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,3 +32,6 @@ pnpm-debug.log*
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
package-lock.json
|
||||
.env
|
||||
package-lock.json
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../siks"
|
||||
},
|
||||
{
|
||||
"path": "../pertamina-point"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
|
||||
@ -66,7 +66,6 @@ const AuthenticatedApp = () => {
|
||||
|
||||
function ProtectedLayout() {
|
||||
const { token } = useAuth();
|
||||
console.log("token on app", token)
|
||||
if (!token) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 => ({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user