/* History page script */
document.addEventListener('DOMContentLoaded', () => {
const loadBtn = document.getElementById('loadHistory');
const cowSelect = document.getElementById('cowSelect');
const dateStart = document.getElementById('dateStart');
const dateEnd = document.getElementById('dateEnd');
const historyBody = document.getElementById('historyBody');
const searchInput = document.getElementById('searchInput');
const paginationControls = document.getElementById('paginationControls');
const paginationInfo = document.getElementById('paginationInfo');
const exportCsvBtn = document.getElementById('exportCsvBtn');
const exportPdfBtn = document.getElementById('exportPdfBtn');
let allData = [];
let filteredData = [];
let currentPage = 1;
const perPage = 10;
let tempChart = null;
let cowDetails = null; // Store cow details
/*
|--------------------------------------------------------------------------
| Quick Date Presets
|--------------------------------------------------------------------------
*/
document.querySelectorAll('.btn-preset').forEach(btn => {
btn.addEventListener('click', function () {
document.querySelectorAll('.btn-preset').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const today = new Date();
const range = this.dataset.range;
if (range === 'today') {
dateStart.value = formatDate(today);
dateEnd.value = formatDate(today);
} else {
const days = parseInt(range, 10);
const past = new Date();
past.setDate(today.getDate() - (days - 1));
dateStart.value = formatDate(past);
dateEnd.value = formatDate(today);
}
loadHistory();
});
});
function formatDate(d) {
return d.toISOString().split('T')[0];
}
/*
|--------------------------------------------------------------------------
| Bikin daftar tanggal antara start - end (inklusif)
|--------------------------------------------------------------------------
*/
function getDateRange(start, end) {
const dates = [];
let current = new Date(start);
const last = new Date(end);
while (current <= last) {
dates.push(formatDate(current));
current.setDate(current.getDate() + 1);
}
return dates;
}
/*
|--------------------------------------------------------------------------
| Ambil satu hari data dari API asli (GET ?cowId=..&date=..)
|--------------------------------------------------------------------------
*/
async function fetchCowDetails(cowId) {
try {
const formData = new FormData();
formData.append("action", "get");
formData.append("id", cowId);
const resp = await fetch("../ajax/cow.php", {
method: "POST",
body: formData
});
const result = await resp.json();
if (result.success && result.data) {
return result.data;
}
return null;
} catch (err) {
console.error('Gagal memuat detail sapi:', err);
return null;
}
}
async function fetchOneDay(cowId, date) {
try {
const resp = await fetch(`${BASE_URL_HISTORY}?cowId=${encodeURIComponent(cowId)}&date=${encodeURIComponent(date)}`);
const result = await resp.json();
if (!result.success || !Array.isArray(result.data)) return [];
return result.data;
} catch (err) {
console.error(`Gagal memuat data tanggal ${date}:`, err);
return [];
}
}
/*
|--------------------------------------------------------------------------
| Load Data (loop tiap tanggal dalam rentang, lalu digabung)
|--------------------------------------------------------------------------
*/
async function loadHistory() {
const cowId = cowSelect.value;
if (!cowId) {
alert('Pilih sapi terlebih dahulu');
return;
}
if (!dateStart.value || !dateEnd.value) {
alert('Pilih rentang tanggal terlebih dahulu');
return;
}
if (dateStart.value > dateEnd.value) {
alert('Tanggal awal tidak boleh lebih besar dari tanggal akhir');
return;
}
renderLoadingState();
const dates = getDateRange(dateStart.value, dateEnd.value);
// Ambil detail sapi terlebih dahulu
cowDetails = await fetchCowDetails(cowId);
// Ambil data tiap tanggal secara paralel
const results = await Promise.all(dates.map(date => fetchOneDay(cowId, date)));
allData = results.flat();
// Urutkan berdasarkan waktu terbaru dulu
allData.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
currentPage = 1;
applySearch();
updateSummary(allData);
renderChart(allData);
}
/*
|--------------------------------------------------------------------------
| Helper: ambil label movement dari struktur asli (object/string)
|--------------------------------------------------------------------------
*/
function getMovementLabel(movement) {
// Simplified logic: only show "Diam" or "Aktif"
if (!movement || !movement.status) return 'Diam';
return movement.status === 0 ? 'Diam' : 'Gerak';
}
function isMovementActive(movement) {
if (!movement || !movement.status) return false;
return movement.status !== 0;
}
/*
|--------------------------------------------------------------------------
| Search / Filter Client-side
|--------------------------------------------------------------------------
*/
function calculateAge(birthDate) {
if (!birthDate) return '-';
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age + ' tahun';
}
function applySearch() {
const keyword = (searchInput.value || '').toLowerCase().trim();
if (!keyword) {
filteredData = [...allData];
} else {
filteredData = allData.filter(item => {
const movementLabel = getMovementLabel(item.movement);
const movementText = isMovementActive(item.movement) ? 'Gerak' : 'Diam';
const cowInfo = cowDetails ? [
cowDetails.name ?? '',
cowDetails.code ?? '',
cowDetails.breed ?? '',
cowDetails.gender ?? '',
cowDetails.weight ?? '',
'normal',
movementText
].join(' ') : '';
const haystack = [
item.timestamp ?? '',
item.temperature ?? '',
cowInfo,
item.gps?.latitude ?? '',
item.gps?.longitude ?? ''
].join(' ').toLowerCase();
return haystack.includes(keyword);
});
}
renderTable();
}
searchInput.addEventListener('input', () => {
currentPage = 1;
applySearch();
});
/*
|--------------------------------------------------------------------------
| Render Table + Pagination
|--------------------------------------------------------------------------
*/
function renderTable() {
if (filteredData.length === 0) {
historyBody.innerHTML = '
| Tidak ada data |
';
paginationControls.innerHTML = '';
paginationInfo.textContent = '';
return;
}
const totalPages = Math.ceil(filteredData.length / perPage);
if (currentPage > totalPages) currentPage = totalPages;
const startIdx = (currentPage - 1) * perPage;
const pageData = filteredData.slice(startIdx, startIdx + perPage);
const rows = pageData.map(item => {
const t = new Date(item.timestamp);
const time = isNaN(t.getTime()) ? '-' : t.toLocaleString();
const temp = (item.temperature !== undefined && item.temperature !== null) ? item.temperature + ' °C' : '-';
const movementLabel = getMovementLabel(item.movement);
const movementClass = isMovementActive(item.movement) ? 'badge-movement-active' : 'badge-movement-idle';
const gps = item.gps ? `${item.gps.latitude || '-'}, ${item.gps.longitude || '-'}` : '-';
// Cow details
const cowName = cowDetails ? cowDetails.name : '-';
const cowCode = cowDetails ? cowDetails.code : '-';
const cowBreed = cowDetails ? cowDetails.breed : '-';
const cowGender = cowDetails ? cowDetails.gender : '-';
const cowWeight = cowDetails ? cowDetails.weight + ' kg' : '-';
const cowAge = cowDetails ? calculateAge(cowDetails.birthDate) : '-';
const cowCondition = 'Normal';
return `
| ${time} |
${temp} |
${cowCode} |
${cowName} |
${cowBreed} |
${cowGender} |
${cowAge} |
${cowWeight} |
${cowCondition} |
${movementLabel} |
${gps} |
`;
}).join('');
historyBody.innerHTML = rows;
paginationInfo.textContent = `Menampilkan ${startIdx + 1}-${Math.min(startIdx + perPage, filteredData.length)} dari ${filteredData.length} data`;
renderPagination(totalPages);
}
function renderPagination(totalPages) {
paginationControls.innerHTML = '';
if (totalPages <= 1) return;
const createPageItem = (label, page, disabled = false, active = false) => {
const li = document.createElement('li');
li.className = `page-item ${disabled ? 'disabled' : ''} ${active ? 'active' : ''}`;
const a = document.createElement('a');
a.className = 'page-link';
a.href = '#';
a.textContent = label;
a.addEventListener('click', (e) => {
e.preventDefault();
if (disabled) return;
currentPage = page;
renderTable();
});
li.appendChild(a);
return li;
};
paginationControls.appendChild(createPageItem('«', currentPage - 1, currentPage === 1));
for (let i = 1; i <= totalPages; i++) {
paginationControls.appendChild(createPageItem(i, i, false, i === currentPage));
}
paginationControls.appendChild(createPageItem('»', currentPage + 1, currentPage === totalPages));
}
function renderLoadingState() {
historyBody.innerHTML = `
|
Memuat data...
|
`;
paginationControls.innerHTML = '';
paginationInfo.textContent = '';
}
/*
|--------------------------------------------------------------------------
| Summary Cards
|--------------------------------------------------------------------------
*/
function updateSummary(data) {
const temps = data.map(d => parseFloat(d.temperature)).filter(t => !isNaN(t));
document.getElementById('totalRecords').textContent = data.length;
if (temps.length === 0) {
document.getElementById('avgTemp').textContent = '-';
document.getElementById('maxTemp').textContent = '-';
document.getElementById('minTemp').textContent = '-';
return;
}
const avg = (temps.reduce((a, b) => a + b, 0) / temps.length).toFixed(1);
const max = Math.max(...temps).toFixed(1);
const min = Math.min(...temps).toFixed(1);
document.getElementById('avgTemp').textContent = avg + '°C';
document.getElementById('maxTemp').textContent = max + '°C';
document.getElementById('minTemp').textContent = min + '°C';
}
/*
|--------------------------------------------------------------------------
| Chart Tren Suhu (Chart.js)
|--------------------------------------------------------------------------
*/
function renderChart(data) {
const ctx = document.getElementById('tempChart');
if (!ctx) return;
// Urutkan lama -> baru khusus untuk chart biar tren terbaca alami
const sorted = [...data].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
const labels = sorted.map(d => {
const t = new Date(d.timestamp);
return isNaN(t.getTime()) ? '' : t.toLocaleString(undefined, { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' });
});
const temps = sorted.map(d => parseFloat(d.temperature) || null);
if (tempChart) tempChart.destroy();
tempChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Suhu (°C)',
data: temps,
borderColor: '#60a5fa',
backgroundColor: 'rgba(96, 165, 250, 0.15)',
fill: true,
tension: 0.35,
pointRadius: 2,
pointBackgroundColor: '#38bdf8'
}]
},
options: {
responsive: true,
plugins: {
legend: { labels: { color: '#e2e8f0' } }
},
scales: {
x: {
ticks: { color: '#94a3b8', maxTicksLimit: 8 },
grid: { color: 'rgba(255,255,255,0.05)' }
},
y: {
ticks: { color: '#94a3b8' },
grid: { color: 'rgba(255,255,255,0.05)' }
}
}
}
});
}
/*
|--------------------------------------------------------------------------
| Helper: statistik ringkas untuk kop laporan (CSV & PDF)
|--------------------------------------------------------------------------
*/
function calculateStats(data) {
const temps = data.map(d => parseFloat(d.temperature)).filter(t => !isNaN(t));
const activeCount = data.filter(d => isMovementActive(d.movement)).length;
return {
total: data.length,
avg: temps.length ? (temps.reduce((a, b) => a + b, 0) / temps.length).toFixed(1) : '-',
max: temps.length ? Math.max(...temps).toFixed(1) : '-',
min: temps.length ? Math.min(...temps).toFixed(1) : '-',
activeCount,
idleCount: data.length - activeCount
};
}
function generateReportNumber() {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
const rand = String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0');
return `SC/RPT/${y}${m}${d}/${rand}`;
}
function formatLongDate(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr);
if (isNaN(d.getTime())) return dateStr;
return d.toLocaleDateString('id-ID', { day: '2-digit', month: 'long', year: 'numeric' });
}
/*
|--------------------------------------------------------------------------
| Export CSV — format surat laporan terstruktur
|--------------------------------------------------------------------------
*/
exportCsvBtn.addEventListener('click', () => {
if (filteredData.length === 0) {
alert('Tidak ada data untuk diexport.');
return;
}
const stats = calculateStats(filteredData);
const reportNo = generateReportNumber();
const now = new Date();
const rows = [];
const addRow = (...cols) => rows.push(cols);
const csvEscape = (val) => `"${String(val ?? '').replace(/"/g, '""')}"`;
const blankRow = () => rows.push(['']);
// ---------- KOP LAPORAN ----------
addRow('SMART COLLAR - LAPORAN RIWAYAT MONITORING TERNAK SAPI');
addRow('Sistem Monitoring Kesehatan Ternak Sapi');
blankRow();
addRow('Nomor Laporan', ':', reportNo);
addRow('Tanggal Dicetak', ':', now.toLocaleDateString('id-ID', { day: '2-digit', month: 'long', year: 'numeric' }) + ' ' + now.toLocaleTimeString('id-ID'));
addRow('Periode Monitoring', ':', `${formatLongDate(dateStart.value)} s/d ${formatLongDate(dateEnd.value)}`);
blankRow();
// ---------- INFORMASI SAPI ----------
addRow('DATA IDENTITAS SAPI');
if (cowDetails) {
addRow('Kode Sapi', ':', cowDetails.code ?? '-');
addRow('Nama Sapi', ':', cowDetails.name ?? '-');
addRow('Breed', ':', cowDetails.breed ?? '-');
addRow('Gender', ':', cowDetails.gender ?? '-');
addRow('Umur', ':', calculateAge(cowDetails.birthDate));
addRow('Berat', ':', cowDetails.weight ? cowDetails.weight + ' kg' : '-');
} else {
addRow('Data sapi tidak tersedia', '', '');
}
blankRow();
// ---------- RINGKASAN STATISTIK ----------
addRow('RINGKASAN HASIL MONITORING');
addRow('Total Catatan Data', ':', stats.total);
addRow('Suhu Rata-rata', ':', stats.avg !== '-' ? stats.avg + ' °C' : '-');
addRow('Suhu Tertinggi', ':', stats.max !== '-' ? stats.max + ' °C' : '-');
addRow('Suhu Terendah', ':', stats.min !== '-' ? stats.min + ' °C' : '-');
addRow('Jumlah Catatan Aktif Bergerak', ':', stats.activeCount);
addRow('Jumlah Catatan Diam', ':', stats.idleCount);
blankRow();
// ---------- TABEL DATA ----------
// Kolom identitas sapi tidak diulang di sini karena sudah tercantum sekali
// pada bagian "Data Identitas Sapi" di atas (satu laporan = satu sapi).
addRow('DETAIL DATA MONITORING');
addRow('Waktu', 'Suhu', 'Kondisi', 'Aktivitas', 'Latitude', 'Longitude');
filteredData.forEach(item => {
const t = new Date(item.timestamp);
const time = isNaN(t.getTime()) ? '-' : t.toLocaleString('id-ID');
const movementText = isMovementActive(item.movement) ? 'Gerak' : 'Diam';
addRow(
time,
item.temperature ?? '',
'Normal',
movementText,
item.gps?.latitude ?? '',
item.gps?.longitude ?? ''
);
});
blankRow();
addRow(`Laporan ini dibuat secara otomatis oleh Smart Collar System pada ${now.toLocaleString('id-ID')}.`);
let csvContent = rows.map(row => row.map(csvEscape).join(',')).join('\n');
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `laporan-riwayat-monitoring-${dateStart.value}_${dateEnd.value}.csv`;
link.click();
URL.revokeObjectURL(url);
});
exportPdfBtn.addEventListener('click', () => {
if (filteredData.length === 0) {
alert('Tidak ada data untuk diexport.');
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 18;
const contentWidth = pageWidth - (2 * margin);
const stats = calculateStats(filteredData);
const reportNo = generateReportNumber();
const now = new Date();
const COLOR_PRIMARY = [37, 99, 153]; // biru kop/aksen
const COLOR_DARK = [30, 30, 30];
const COLOR_MUTED = [110, 110, 110];
const COLOR_LINE = [190, 190, 190];
const COLOR_ROW_ALT = [244, 247, 250];
/* ---------------------------------------------------------------- *
* KOP SURAT / LETTERHEAD (dicetak di setiap halaman)
* ---------------------------------------------------------------- */
function drawLetterhead() {
doc.setFillColor(...COLOR_PRIMARY);
doc.rect(0, 0, pageWidth, 24, 'F');
doc.setTextColor(255, 255, 255);
doc.setFont(undefined, 'bold');
doc.setFontSize(14);
doc.text('SMART COLLAR MONITORING SYSTEM', margin, 11);
doc.setFont(undefined, 'normal');
doc.setFontSize(9);
doc.text('Sistem Monitoring Kesehatan & Aktivitas Ternak Sapi', margin, 17);
doc.setFontSize(8);
doc.text(`No. Laporan: ${reportNo}`, pageWidth - margin, 11, { align: 'right' });
doc.text(now.toLocaleDateString('id-ID', { day: '2-digit', month: 'long', year: 'numeric' }), pageWidth - margin, 17, { align: 'right' });
doc.setDrawColor(...COLOR_PRIMARY);
doc.setLineWidth(0.8);
doc.line(0, 24, pageWidth, 24);
}
function drawFooter(pageNum, totalPages) {
const footerY = pageHeight - 12;
doc.setDrawColor(...COLOR_LINE);
doc.setLineWidth(0.2);
doc.line(margin, footerY - 5, pageWidth - margin, footerY - 5);
doc.setFontSize(7.5);
doc.setFont(undefined, 'normal');
doc.setTextColor(...COLOR_MUTED);
doc.text('Dokumen ini dihasilkan otomatis oleh Smart Collar System dan sah tanpa tanda tangan basah.', margin, footerY);
doc.text(`Halaman ${pageNum} dari ${totalPages}`, pageWidth - margin, footerY, { align: 'right' });
}
drawLetterhead();
let y = 34;
/* ---------------------------------------------------------------- *
* JUDUL LAPORAN & PERIHAL (gaya surat resmi)
* ---------------------------------------------------------------- */
doc.setTextColor(...COLOR_DARK);
doc.setFontSize(13);
doc.setFont(undefined, 'bold');
doc.text('LAPORAN RIWAYAT MONITORING TERNAK SAPI', pageWidth / 2, y, { align: 'center' });
y += 7;
doc.setFontSize(9);
doc.setFont(undefined, 'normal');
doc.text(`Perihal : Rekapitulasi data suhu tubuh dan aktivitas gerak ternak`, margin, y);
y += 5;
doc.text(`Periode : ${formatLongDate(dateStart.value)} s/d ${formatLongDate(dateEnd.value)}`, margin, y);
y += 8;
/* ---------------------------------------------------------------- *
* KOTAK INFORMASI IDENTITAS SAPI
* ---------------------------------------------------------------- */
const infoBoxHeight = 28;
doc.setDrawColor(...COLOR_LINE);
doc.setFillColor(250, 250, 251);
doc.roundedRect(margin, y, contentWidth, infoBoxHeight, 1.5, 1.5, 'FD');
doc.setFont(undefined, 'bold');
doc.setFontSize(9.5);
doc.setTextColor(...COLOR_PRIMARY);
doc.text('DATA IDENTITAS SAPI', margin + 4, y + 6);
doc.setFont(undefined, 'normal');
doc.setFontSize(9);
doc.setTextColor(...COLOR_DARK);
const colA = margin + 4;
const colB = margin + contentWidth / 2 + 2;
const labelValue = (label, value, x, yy) => {
doc.setFont(undefined, 'bold');
doc.text(label, x, yy);
doc.setFont(undefined, 'normal');
doc.text(String(value ?? '-'), x + 24, yy);
};
if (cowDetails) {
labelValue('Kode Sapi', cowDetails.code, colA, y + 13);
labelValue('Nama Sapi', cowDetails.name, colB, y + 13);
labelValue('Breed', cowDetails.breed, colA, y + 19);
labelValue('Gender', cowDetails.gender, colB, y + 19);
labelValue('Umur', calculateAge(cowDetails.birthDate), colA, y + 25);
labelValue('Berat', (cowDetails.weight ? cowDetails.weight + ' kg' : '-'), colB, y + 25);
} else {
doc.text('Data identitas sapi tidak tersedia.', colA, y + 15);
}
y += infoBoxHeight + 6;
/* ---------------------------------------------------------------- *
* KOTAK RINGKASAN STATISTIK
* ---------------------------------------------------------------- */
const summaryBoxHeight = 22;
doc.setDrawColor(...COLOR_LINE);
doc.setFillColor(250, 250, 251);
doc.roundedRect(margin, y, contentWidth, summaryBoxHeight, 1.5, 1.5, 'FD');
doc.setFont(undefined, 'bold');
doc.setFontSize(9.5);
doc.setTextColor(...COLOR_PRIMARY);
doc.text('RINGKASAN HASIL MONITORING', margin + 4, y + 6);
const summaryItems = [
['Total Data', stats.total],
['Suhu Rata-rata', stats.avg !== '-' ? stats.avg + ' °C' : '-'],
['Suhu Tertinggi', stats.max !== '-' ? stats.max + ' °C' : '-'],
['Suhu Terendah', stats.min !== '-' ? stats.min + ' °C' : '-'],
['Aktif Bergerak', stats.activeCount],
['Diam', stats.idleCount]
];
const cellW = contentWidth / summaryItems.length;
summaryItems.forEach((sitem, i) => {
const cx = margin + cellW * i + cellW / 2;
doc.setFont(undefined, 'normal');
doc.setFontSize(7.5);
doc.setTextColor(...COLOR_MUTED);
doc.text(sitem[0], cx, y + 13, { align: 'center' });
doc.setFont(undefined, 'bold');
doc.setFontSize(10.5);
doc.setTextColor(...COLOR_DARK);
doc.text(String(sitem[1]), cx, y + 19, { align: 'center' });
});
y += summaryBoxHeight + 8;
/* ---------------------------------------------------------------- *
* TABEL DETAIL DATA
* ---------------------------------------------------------------- */
doc.setFont(undefined, 'bold');
doc.setFontSize(9.5);
doc.setTextColor(...COLOR_PRIMARY);
doc.text('DETAIL DATA MONITORING', margin, y);
y += 4;
// Kolom identitas sapi (kode/nama/breed/gender/berat) tidak diulang di sini
// karena sudah ditampilkan sekali di kotak "Data Identitas Sapi" di atas
// (satu laporan = satu sapi, nilainya sama untuk semua baris).
// Lebar kolom dijumlah persis = contentWidth agar tidak meluber ke samping.
const colWidths = [40, 24, 30, 30, 50]; // total = 174 = contentWidth
const headers = ['Waktu', 'Suhu', 'Kondisi', 'Aktivitas', 'Lokasi GPS'];
const xPos = colWidths.map((w, i) => margin + colWidths.slice(0, i).reduce((a, b) => a + b, 0));
const rowHeight = 6.5;
function drawTableHeader(yy) {
doc.setFillColor(...COLOR_PRIMARY);
doc.rect(margin, yy, contentWidth, rowHeight, 'F');
doc.setFont(undefined, 'bold');
doc.setFontSize(7.8);
doc.setTextColor(255, 255, 255);
headers.forEach((h, i) => doc.text(h, xPos[i] + 1.5, yy + rowHeight - 2));
// garis vertikal pemisah kolom pada header
doc.setDrawColor(255, 255, 255);
doc.setLineWidth(0.2);
xPos.forEach(x => doc.line(x, yy, x, yy + rowHeight));
return yy + rowHeight;
}
let tableTop = y;
let cursorY = drawTableHeader(tableTop);
const bottomLimit = pageHeight - 22;
function newPage() {
doc.addPage();
drawLetterhead();
cursorY = drawTableHeader(34);
}
filteredData.forEach((item) => {
if (cursorY + rowHeight > bottomLimit) {
newPage();
}
const t = new Date(item.timestamp);
const time = isNaN(t.getTime()) ? '-' : t.toLocaleString('id-ID', { dateStyle: 'short', timeStyle: 'short' });
const gpsText = item.gps ? `${item.gps.latitude ?? '-'}, ${item.gps.longitude ?? '-'}` : '-';
const rowData = [
time,
item.temperature ? item.temperature + '°C' : '-',
'Normal',
getMovementLabel(item.movement),
gpsText
];
const isEven = Math.round((cursorY - tableTop) / rowHeight) % 2 === 0;
if (isEven) {
doc.setFillColor(...COLOR_ROW_ALT);
doc.rect(margin, cursorY, contentWidth, rowHeight, 'F');
}
doc.setFont(undefined, 'normal');
doc.setFontSize(7.5);
doc.setTextColor(...COLOR_DARK);
rowData.forEach((val, i) => {
doc.text(String(val), xPos[i] + 1.5, cursorY + rowHeight - 2, { maxWidth: colWidths[i] - 2 });
});
// garis horizontal & vertikal (grid tabel penuh)
doc.setDrawColor(...COLOR_LINE);
doc.setLineWidth(0.15);
doc.line(margin, cursorY, margin + contentWidth, cursorY);
xPos.forEach(x => doc.line(x, cursorY, x, cursorY + rowHeight));
doc.line(margin + contentWidth, cursorY, margin + contentWidth, cursorY + rowHeight);
cursorY += rowHeight;
});
// garis penutup bawah tabel
doc.setDrawColor(...COLOR_LINE);
doc.line(margin, cursorY, margin + contentWidth, cursorY);
/* ---------------------------------------------------------------- *
* BLOK PENUTUP & TANDA TANGAN
* ---------------------------------------------------------------- */
const signatureBlockHeight = 38;
if (cursorY + signatureBlockHeight > bottomLimit) {
newPage();
}
cursorY += 10;
doc.setFont(undefined, 'normal');
doc.setFontSize(8.5);
doc.setTextColor(...COLOR_DARK);
doc.text(
'Demikian laporan riwayat monitoring ini dibuat berdasarkan data yang tercatat pada sistem Smart Collar',
margin, cursorY
);
doc.text('untuk digunakan sebagaimana mestinya.', margin, cursorY + 5);
cursorY += 16;
const signCol = pageWidth - margin - 60;
doc.text(`Padalarang, ${formatLongDate(now.toISOString().split('T')[0])}`, signCol, cursorY);
doc.text('Petugas Monitoring,', signCol, cursorY + 5);
cursorY += 28;
doc.setFont(undefined, 'bold');
doc.text('( ......................................... )', signCol, cursorY);
// Cetak footer + nomor halaman pada setiap halaman setelah semua konten selesai
const totalPageCount = doc.internal.getNumberOfPages();
for (let p = 1; p <= totalPageCount; p++) {
doc.setPage(p);
drawFooter(p, totalPageCount);
}
const fileName = `laporan-riwayat-monitoring-${dateStart.value}_${dateEnd.value}.pdf`;
doc.save(fileName);
});
/*
|--------------------------------------------------------------------------
| Event Listener Utama
|--------------------------------------------------------------------------
*/
loadBtn.addEventListener('click', loadHistory);
});