Files
dashboard/backend/routes/chickenCounts.js
T
Alberto-Audrix 32a36cceff
CI / lint-and-test (push) Canceled after 0s
first commit
2026-07-28 08:55:05 +07:00

251 lines
7.4 KiB
JavaScript

const express = require('express');
const router = express.Router();
const ChickenCount = require('../models/ChickenCount');
const Kandang = require('../models/Kandang');
const { validateApiKey } = require('../middleware/auth');
const {
isBeforeCutoffTime,
getPreviousDate,
createResponseMetadata,
} = require('../utils/dateUtils');
/**
* Normalize a kandang name to a slug: "Kandang Atas" -> "kandang-atas"
*/
function nameToSlug(name) {
return name.toLowerCase().replace(/\s+/g, '-');
}
// POST /api/chicken-counting (requires API key)
router.post('/', validateApiKey, async (req, res) => {
const timestamp = new Date().toISOString();
try {
// Check if body is empty or not JSON
if (!req.body || Object.keys(req.body).length === 0) {
return res.status(400).json({
success: false,
error:
'Request body is empty or not valid JSON. Make sure to set Content-Type: application/json',
});
}
const { date, kandang, filename, total_count } = req.body;
// Check missing fields individually for clear error messages
const missingFields = [];
if (!date) missingFields.push('date');
if (!kandang) missingFields.push('kandang');
if (total_count === undefined || total_count === null) missingFields.push('total_count');
if (missingFields.length > 0) {
return res.status(400).json({
success: false,
error: `Missing required fields: ${missingFields.join(', ')}`,
details: {
missing: missingFields,
received: {
date: date || null,
kandang: kandang || null,
total_count: total_count !== undefined ? total_count : null,
filename: filename || null,
},
},
});
}
// Validate date format
if (typeof date !== 'string') {
return res.status(400).json({
success: false,
error: `Invalid date type. Expected string, got ${typeof date}`,
details: { received: date, expected: 'YYYY-MM-DD' },
});
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
return res.status(400).json({
success: false,
error: `Invalid date format: "${date}". Use YYYY-MM-DD (e.g. "2026-03-01")`,
details: { received: date, expected: 'YYYY-MM-DD' },
});
}
// Validate date is a real date
const parsedDate = new Date(date + 'T00:00:00Z');
if (isNaN(parsedDate.getTime())) {
return res.status(400).json({
success: false,
error: `Invalid date value: "${date}". The date does not exist.`,
details: { received: date },
});
}
// Validate kandang type
if (typeof kandang !== 'string') {
return res.status(400).json({
success: false,
error: `Invalid kandang type. Expected string, got ${typeof kandang}`,
details: { received: kandang, expected: 'string (e.g. "kandang-atas")' },
});
}
// Validate total_count type and value
if (typeof total_count !== 'number') {
return res.status(400).json({
success: false,
error: `Invalid total_count type. Expected number, got ${typeof total_count}`,
details: { received: total_count, expected: 'number (e.g. 5612)' },
});
}
if (!Number.isInteger(total_count)) {
return res.status(400).json({
success: false,
error: `total_count must be an integer, got ${total_count}`,
details: { received: total_count },
});
}
if (total_count < 0) {
return res.status(400).json({
success: false,
error: `total_count cannot be negative`,
details: { received: total_count },
});
}
// Validate filename type if provided
if (filename !== undefined && filename !== null && typeof filename !== 'string') {
return res.status(400).json({
success: false,
error: `Invalid filename type. Expected string, got ${typeof filename}`,
details: { received: filename },
});
}
let allKandangs;
try {
allKandangs = await Kandang.getAll();
} catch (dbError) {
console.error(`[${timestamp}] ERROR: Failed to query kandangs table:`, dbError.message);
return res.status(500).json({
success: false,
error: 'Database error: Failed to query kandangs table',
details: { message: dbError.message },
});
}
if (allKandangs.length === 0) {
return res.status(500).json({
success: false,
error:
'No kandangs configured in the system. Please create kandangs first via /api/kandangs',
});
}
const matched = allKandangs.find((k) => nameToSlug(k.name) === kandang);
const validSlugs = allKandangs.map((k) => ({
slug: nameToSlug(k.name),
name: k.name,
id: k.id,
}));
if (!matched) {
return res.status(400).json({
success: false,
error: `Kandang '${kandang}' not found`,
details: {
received: kandang,
available: validSlugs,
},
});
}
// Check if this is a manual update from UI (source=manual query param)
const isManualUpdate = req.query.source === 'manual';
const operation = isManualUpdate ? 'Upserting (manual)' : 'Inserting (API, skip if exists)';
// Use upsert for manual updates, insertIfNotExists for API calls
let record;
try {
if (isManualUpdate) {
record = await ChickenCount.upsert(date, matched.id, filename || null, total_count);
} else {
record = await ChickenCount.insertIfNotExists(
date,
matched.id,
filename || null,
total_count
);
}
} catch (dbError) {
console.error(`[${timestamp}] ERROR: Failed to save chicken count:`, dbError.message);
console.error(`[${timestamp}] Stack:`, dbError.stack);
return res.status(500).json({
success: false,
error: 'Database error: Failed to save chicken count',
details: { message: dbError.message },
});
}
if (record.skipped) {
} else {
}
res.status(200).json({
success: true,
data: record,
});
} catch (error) {
console.error(`[${timestamp}] UNEXPECTED ERROR:`, error.message);
console.error(`[${timestamp}] Stack:`, error.stack);
res.status(500).json({
success: false,
error: 'Internal server error',
details: { message: error.message },
});
}
});
// GET /api/chicken-counting?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD&kandangId=1
router.get('/', async (req, res) => {
try {
let { startDate, endDate, kandangId } = req.query;
if (!startDate || !endDate) {
return res.status(400).json({
success: false,
error: 'startDate and endDate query parameters are required (YYYY-MM-DD)',
});
}
// Apply D-1 filter if querying single day and before cutoff (17:00 WIB)
let isD1Applied = false;
if (startDate === endDate && isBeforeCutoffTime()) {
startDate = getPreviousDate(startDate);
endDate = startDate;
isD1Applied = true;
}
const records = await ChickenCount.getByDateRange(
startDate,
endDate,
kandangId ? parseInt(kandangId) : null
);
res.json({
success: true,
data: records,
meta: createResponseMetadata(isD1Applied),
});
} catch (error) {
console.error('Error querying chicken counting:', error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
module.exports = router;