commit
32a36cceff
444 files changed
+67186
No files matched your search
@@ -0,0 +1,250 @@
|
||||
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;
|
||||
Reference in new issue
Block a user