first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { api, ApiError, AUTH_UNAUTHORIZED_EVENT } from '../services/apiClient.ts';
|
||||
import type { User } from '../types/api.ts';
|
||||
|
||||
type AuthContextValue = {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
login: (userName: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
refreshUser: () => Promise<User | null>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const onUnauthorized = () => setUser(null);
|
||||
window.addEventListener(AUTH_UNAUTHORIZED_EVENT, onUnauthorized);
|
||||
return () => window.removeEventListener(AUTH_UNAUTHORIZED_EVENT, onUnauthorized);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const me = await api.auth.me();
|
||||
if (!cancelled) setUser(me);
|
||||
} catch (err) {
|
||||
if (
|
||||
!cancelled &&
|
||||
!(err instanceof ApiError && (err.status === 401 || err.status === 403))
|
||||
) {
|
||||
console.error(err);
|
||||
}
|
||||
if (!cancelled) setUser(null);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (userName: string, password: string) => {
|
||||
await api.auth.csrf();
|
||||
const next = await api.auth.login(userName, password);
|
||||
setUser(next);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
try {
|
||||
await api.auth.logout();
|
||||
} finally {
|
||||
setUser(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshUser = useCallback(async () => {
|
||||
try {
|
||||
const me = await api.auth.me();
|
||||
setUser(me);
|
||||
return me;
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
||||
setUser(null);
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ user, loading, login, logout, refreshUser }),
|
||||
[user, loading, login, logout, refreshUser]
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { api, errorMessage } from '../services/apiClient.ts';
|
||||
import type { Cycle, Flock, Kandang, Site } from '../types/api.ts';
|
||||
import { useAuth } from './AuthContext.tsx';
|
||||
|
||||
type FarmContextValue = {
|
||||
sites: Site[];
|
||||
kandangs: Kandang[];
|
||||
flocks: Flock[];
|
||||
cycles: Cycle[];
|
||||
selectedSiteId: number | null;
|
||||
selectedKandangId: number | null;
|
||||
selectedCycleId: number | null;
|
||||
selectedSite: Site | null;
|
||||
selectedKandang: Kandang | null;
|
||||
selectedCycle: Cycle | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
setSelectedSiteId: (id: number | null) => void;
|
||||
setSelectedKandangId: (id: number | null) => void;
|
||||
setSelectedCycleId: (id: number | null) => void;
|
||||
reload: () => Promise<void>;
|
||||
updateCycle: (cycleId: number, patch: Partial<Cycle>) => void;
|
||||
};
|
||||
|
||||
const FarmContext = createContext<FarmContextValue | null>(null);
|
||||
|
||||
export const FarmProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { user } = useAuth();
|
||||
const [sites, setSites] = useState<Site[]>([]);
|
||||
const [kandangs, setKandangs] = useState<Kandang[]>([]);
|
||||
const [flocks, setFlocks] = useState<Flock[]>([]);
|
||||
const [cycles, setCycles] = useState<Cycle[]>([]);
|
||||
const [selectedSiteId, setSelectedSiteId] = useState<number | null>(null);
|
||||
const [selectedKandangId, setSelectedKandangId] = useState<number | null>(null);
|
||||
const [selectedCycleId, setSelectedCycleId] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setReloadToken((token) => token + 1);
|
||||
}, []);
|
||||
|
||||
const updateCycle = useCallback((cycleId: number, patch: Partial<Cycle>) => {
|
||||
setCycles((prev) => prev.map((c) => (c.id === cycleId ? { ...c, ...patch } : c)));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
setSites([]);
|
||||
setKandangs([]);
|
||||
setFlocks([]);
|
||||
setCycles([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const nextSites = await api.sites.list();
|
||||
if (cancelled) return;
|
||||
setSites(nextSites);
|
||||
setSelectedSiteId((prev) => {
|
||||
if (prev && nextSites.some((s) => s.id === prev)) return prev;
|
||||
return nextSites[0]?.id ?? null;
|
||||
});
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(errorMessage(err));
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [user, reloadToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || selectedSiteId == null) {
|
||||
setKandangs([]);
|
||||
setSelectedKandangId(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const next = await api.kandangs.list({ site_id: selectedSiteId });
|
||||
if (cancelled) return;
|
||||
setKandangs(next);
|
||||
setSelectedKandangId((prev) => {
|
||||
if (prev && next.some((k) => k.id === prev)) return prev;
|
||||
return next[0]?.id ?? null;
|
||||
});
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(errorMessage(err));
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [user, selectedSiteId, reloadToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || selectedKandangId == null) {
|
||||
setFlocks([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const next = await api.flocks.list({ kandang_id: selectedKandangId });
|
||||
if (cancelled) return;
|
||||
setFlocks(next);
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(errorMessage(err));
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [user, selectedKandangId, reloadToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || selectedKandangId == null) {
|
||||
setCycles([]);
|
||||
setSelectedCycleId(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const next = await api.cycles.list({ kandang_id: selectedKandangId });
|
||||
if (cancelled) return;
|
||||
setCycles(next);
|
||||
setSelectedCycleId((prev) => {
|
||||
if (prev && next.some((c) => c.id === prev)) return prev;
|
||||
const active = next.find((c) => c.status === 'active');
|
||||
if (active) return active.id;
|
||||
return next[0]?.id ?? null;
|
||||
});
|
||||
} catch (err) {
|
||||
if (!cancelled) setError(errorMessage(err));
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [user, selectedKandangId, reloadToken]);
|
||||
|
||||
const selectedSite = useMemo(
|
||||
() => sites.find((s) => s.id === selectedSiteId) ?? null,
|
||||
[sites, selectedSiteId]
|
||||
);
|
||||
const selectedKandang = useMemo(
|
||||
() => kandangs.find((k) => k.id === selectedKandangId) ?? null,
|
||||
[kandangs, selectedKandangId]
|
||||
);
|
||||
const selectedCycle = useMemo(
|
||||
() => cycles.find((c) => c.id === selectedCycleId) ?? null,
|
||||
[cycles, selectedCycleId]
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
sites,
|
||||
kandangs,
|
||||
flocks,
|
||||
cycles,
|
||||
selectedSiteId,
|
||||
selectedKandangId,
|
||||
selectedCycleId,
|
||||
selectedSite,
|
||||
selectedKandang,
|
||||
selectedCycle,
|
||||
loading,
|
||||
error,
|
||||
setSelectedSiteId,
|
||||
setSelectedKandangId,
|
||||
setSelectedCycleId,
|
||||
reload,
|
||||
updateCycle,
|
||||
}),
|
||||
[
|
||||
sites,
|
||||
kandangs,
|
||||
flocks,
|
||||
cycles,
|
||||
selectedSiteId,
|
||||
selectedKandangId,
|
||||
selectedCycleId,
|
||||
selectedSite,
|
||||
selectedKandang,
|
||||
selectedCycle,
|
||||
loading,
|
||||
error,
|
||||
reload,
|
||||
updateCycle,
|
||||
]
|
||||
);
|
||||
|
||||
return <FarmContext.Provider value={value}>{children}</FarmContext.Provider>;
|
||||
};
|
||||
|
||||
export function useFarm(): FarmContextValue {
|
||||
const ctx = useContext(FarmContext);
|
||||
if (!ctx) throw new Error('useFarm must be used within FarmProvider');
|
||||
return ctx;
|
||||
}
|
||||
Reference in new issue
Block a user