125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { FeedSackColumnConfig } from '../../types/counting.ts';
|
|
|
|
/**
|
|
* The classification the insight context now uses: camera -> type comes from
|
|
* the saved column configuration, and a camera absent from it is not feed-sack
|
|
* data at all. Mirrors `cameraTypeByName` / `sumFeedRows` in
|
|
* FeedSackCountingPage.
|
|
*/
|
|
const classify = (
|
|
logs: Array<{ cameraName: string; count: number }>,
|
|
config: FeedSackColumnConfig[]
|
|
) => {
|
|
const byName = new Map(config.map((c) => [c.cameraName, c.type]));
|
|
const totals = { masuk: 0, dituang: 0, keluar: 0, diabaikan: 0 };
|
|
for (const log of logs) {
|
|
const type = byName.get(log.cameraName);
|
|
if (type === 'inbound') totals.masuk += log.count;
|
|
else if (type === 'usage') totals.dituang += log.count;
|
|
else if (type === 'outbound') totals.keluar += log.count;
|
|
else totals.diabaikan += log.count;
|
|
}
|
|
return totals;
|
|
};
|
|
|
|
// Exactly what feed_sack_column_config holds in the database.
|
|
const CONFIG: FeedSackColumnConfig[] = [
|
|
{
|
|
id: 'a',
|
|
label: 'Karung Masuk',
|
|
cameraName: 'kandang_1_karung_masuk',
|
|
color: '#22c55e',
|
|
type: 'inbound',
|
|
},
|
|
{
|
|
id: 'b',
|
|
label: 'Karung Dituang Kandang Atas',
|
|
cameraName: 'karung_tuang_feeder_kiri_atas',
|
|
color: '#f97316',
|
|
type: 'usage',
|
|
},
|
|
{
|
|
id: 'c',
|
|
label: 'Karung Dituang Kandang Bawah',
|
|
cameraName: 'karung_tuang_feeder_kanan_bawah',
|
|
color: '#a855f7',
|
|
type: 'usage',
|
|
},
|
|
{
|
|
id: 'd',
|
|
label: 'Karung Keluar',
|
|
cameraName: 'kandang_1_karung_masuk_out',
|
|
color: '#3b82f6',
|
|
type: 'outbound',
|
|
},
|
|
];
|
|
|
|
// Real totals from the live API for cycle days 43-49 (3-9 Jul 2026).
|
|
const WEEK_43_49 = [
|
|
{ cameraName: 'karung_tuang_feeder_kanan_bawah', count: 11 },
|
|
{ cameraName: 'karung_tuang_feeder_kiri_atas', count: 10 },
|
|
{ cameraName: 'kandang_1_karung_masuk_out', count: 0 },
|
|
{ cameraName: 'kandang_1_karung_masuk', count: 14 },
|
|
{ cameraName: 'kandang_atas_feeder_kiri', count: 1057 },
|
|
{ cameraName: 'kandang_bawah_feeder_kanan', count: 1127 },
|
|
];
|
|
|
|
describe('feed sack classification', () => {
|
|
it('counts only configured cameras', () => {
|
|
const t = classify(WEEK_43_49, CONFIG);
|
|
expect(t.dituang).toBe(21); // was 2205 when unconfigured cameras leaked in
|
|
expect(t.masuk).toBe(14);
|
|
expect(t.keluar).toBe(0);
|
|
expect(t.diabaikan).toBe(2184);
|
|
});
|
|
|
|
it('gives a believable daily average', () => {
|
|
const t = classify(WEEK_43_49, CONFIG);
|
|
expect(Math.round(t.dituang / 7)).toBe(3); // was 315 karung/hari
|
|
});
|
|
|
|
it('files kandang_1_karung_masuk_out as outbound, not inbound', () => {
|
|
// The old substring rule matched "karung_masuk" inside "..._out".
|
|
const t = classify([{ cameraName: 'kandang_1_karung_masuk_out', count: 387 }], CONFIG);
|
|
expect(t.keluar).toBe(387);
|
|
expect(t.masuk).toBe(0);
|
|
});
|
|
|
|
it('ignores a camera that is not feed-sack data', () => {
|
|
const t = classify([{ cameraName: 'kandang_atas_feeder_kiri', count: 7695 }], CONFIG);
|
|
expect(t.dituang).toBe(0);
|
|
expect(t.diabaikan).toBe(7695);
|
|
});
|
|
});
|
|
|
|
/** Manual entries for the same cameras, days 43-49 (3-9 Jul 2026). */
|
|
const MANUAL_43_49 = [
|
|
{ cameraName: 'kandang_1_karung_masuk', count: 8 }, // 3 Jul
|
|
{ cameraName: 'kandang_1_karung_masuk', count: 6 }, // 6 Jul
|
|
{ cameraName: 'kandang_1_karung_masuk', count: 2 }, // 7 Jul
|
|
];
|
|
|
|
describe('manual figures shown beside the camera figures', () => {
|
|
it('totals manual separately from IOT', () => {
|
|
const iot = classify(WEEK_43_49, CONFIG);
|
|
const manual = classify(MANUAL_43_49, CONFIG);
|
|
expect(iot.masuk).toBe(14);
|
|
expect(manual.masuk).toBe(16);
|
|
});
|
|
|
|
it('never merges the two into one number', () => {
|
|
// Both count the same sacks; 30 would be double-counting.
|
|
const iot = classify(WEEK_43_49, CONFIG);
|
|
const manual = classify(MANUAL_43_49, CONFIG);
|
|
expect(iot.masuk + manual.masuk).toBe(30);
|
|
expect(iot.masuk).not.toBe(30);
|
|
expect(manual.masuk).not.toBe(30);
|
|
});
|
|
|
|
it('reports zero manual for a camera with no entries in the window', () => {
|
|
const manual = classify(MANUAL_43_49, CONFIG);
|
|
expect(manual.dituang).toBe(0); // manual "dituang" entries stop at 2 Jul
|
|
});
|
|
});
|