251 lines
7.4 KiB
JavaScript
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;
|