Enhance AI Insight with new state management and PDF export functionality. Added selectedArea state to App for better context in reports. Introduced new markdown files for documentation on AI Insight improvements and setup instructions. Updated package dependencies and fixed TypeScript errors for improved stability.
CI / lint-and-test (push) Canceled after 0s
CI / lint-and-test (push) Canceled after 0s
This commit is contained in:
1 parent
a8b49374f1
commit
2a627a2c3f
84 files changed
+8415
-2692
No files matched your search
@@ -0,0 +1,124 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user