first commit

This commit is contained in:
Alberto-Audrix committed 2026-09-08 15:21:23 +07:00
commit b54624be96
226 files changed
+108840

No files matched your search

+168
View File
@@ -0,0 +1,168 @@
import { api } from '../services/apiClient.ts';
import type { Cycle, KPI, Site } from '../types/api.ts';
export type KandangRankingRow = {
rank: number;
siteId: number;
siteName: string;
kandangId: number;
kandangName: string;
cycleId: number;
startDate: string;
endDate: string;
totalDays: number;
eef: number | null;
fcr: number | null;
};
export type LoadKandangRankingArgs = {
sites: Site[];
months: number;
/** Defaults to today (local). */
now?: Date;
};
function toIsoDate(d: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** Inclusive lower bound: end_date >= (today − months). */
export function rankingWindowStart(months: number, now = new Date()): string {
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate());
start.setMonth(start.getMonth() - months);
return toIsoDate(start);
}
export function isEndDateInWindow(endDate: string | null | undefined, windowStart: string): boolean {
if (!endDate) return false;
return endDate >= windowStart;
}
/**
* Latest valid FCR/EEF for a cycle.
* Matches EefDashboardPage: sort by age/date ascending, then walk from the end.
* (API KPI list is ordered `-date` / newest-first — do not assume array order.)
*/
export function latestValidRatios(kpis: KPI[]): { eef: number | null; fcr: number | null } {
const sorted = [...kpis].sort((a, b) => a.age - b.age || a.date.localeCompare(b.date));
let eef: number | null = null;
let fcr: number | null = null;
for (let i = sorted.length - 1; i >= 0; i--) {
const row = sorted[i];
if (!row) continue;
if (eef == null && row.eef > 0) eef = row.eef;
if (fcr == null && row.fcr > 0) fcr = row.fcr;
if (eef != null && fcr != null) break;
}
return { eef, fcr };
}
function assignRanks(
rows: Omit<KandangRankingRow, 'rank'>[]
): KandangRankingRow[] {
const withEef = rows
.filter((r) => r.eef != null)
.sort((a, b) => (b.eef as number) - (a.eef as number));
const withoutEef = rows.filter((r) => r.eef == null);
const ranked: KandangRankingRow[] = withEef.map((row, index) => ({
...row,
rank: index + 1,
}));
const unranked: KandangRankingRow[] = withoutEef.map((row) => ({
...row,
rank: 0,
}));
return [...ranked, ...unranked];
}
/**
* Load closed-cycle EEF rankings for the given sites within a months window.
* Uses one closed-cycles list + per-site kandangs + per-cycle KPI fetches.
*/
export async function loadKandangRanking({
sites,
months,
now = new Date(),
}: LoadKandangRankingArgs): Promise<KandangRankingRow[]> {
if (sites.length === 0 || months < 1) return [];
const windowStart = rankingWindowStart(months, now);
const siteById = new Map(sites.map((s) => [s.id, s]));
const kandangLists = await Promise.all(
sites.map((site) => api.kandangs.list({ site_id: site.id }))
);
type KandangMeta = { siteId: number; siteName: string; kandangName: string };
const kandangMeta = new Map<number, KandangMeta>();
kandangLists.forEach((list, i) => {
const site = sites[i];
if (!site) return;
for (const k of list) {
kandangMeta.set(k.id, {
siteId: site.id,
siteName: site.site_name,
kandangName: k.kandang_name,
});
}
});
if (kandangMeta.size === 0) return [];
let closedCycles: Cycle[] = [];
try {
closedCycles = await api.cycles.list({ status: 'closed' });
} catch {
// Fallback: per-kandang if global closed list fails
const perKandang = await Promise.all(
[...kandangMeta.keys()].map(async (kandangId) => {
try {
return await api.cycles.list({ kandang_id: kandangId, status: 'closed' });
} catch {
return [] as Cycle[];
}
})
);
closedCycles = perKandang.flat();
}
const inScope = closedCycles.filter((cycle) => {
if (!kandangMeta.has(cycle.kandang)) return false;
return isEndDateInWindow(cycle.end_date, windowStart);
});
const metricRows = await Promise.all(
inScope.map(async (cycle) => {
const meta = kandangMeta.get(cycle.kandang);
if (!meta || !cycle.end_date) return null;
// Ensure site still in requested set (defensive)
if (!siteById.has(meta.siteId)) return null;
let eef: number | null = null;
let fcr: number | null = null;
try {
const kpis = await api.kpis.list({ cycle_id: cycle.id });
({ eef, fcr } = latestValidRatios(kpis));
} catch {
// leave nulls
}
return {
siteId: meta.siteId,
siteName: meta.siteName,
kandangId: cycle.kandang,
kandangName: meta.kandangName || cycle.kandang_name,
cycleId: cycle.id,
startDate: cycle.start_date,
endDate: cycle.end_date,
totalDays: cycle.total_days || cycle.current_day || 0,
eef,
fcr,
} satisfies Omit<KandangRankingRow, 'rank'>;
})
);
return assignRanks(metricRows.filter((r): r is Omit<KandangRankingRow, 'rank'> => r != null));
}