181 lines
6.5 KiB
TypeScript
181 lines
6.5 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { FeedSackLogEntry, FeedSackColumnConfig } from '../../types/counting.ts';
|
|
import { formatNumber } from '../../utils/formatters.ts';
|
|
import { getEffectiveDate } from '../../utils/dateFilter.ts';
|
|
|
|
interface FeedSaldoCardProps {
|
|
iotData: FeedSackLogEntry[];
|
|
columnConfig: FeedSackColumnConfig[];
|
|
startDate: Date;
|
|
saldoAwal?: number;
|
|
effectiveDate?: Date;
|
|
}
|
|
|
|
const FeedSaldoCard: React.FC<FeedSaldoCardProps> = ({
|
|
iotData,
|
|
columnConfig,
|
|
startDate,
|
|
saldoAwal = 0,
|
|
effectiveDate,
|
|
}) => {
|
|
const displayDate = effectiveDate ?? getEffectiveDate();
|
|
|
|
const saldoData = useMemo(() => {
|
|
const inboundCameras = columnConfig
|
|
.filter((c) => c.type === 'inbound')
|
|
.map((c) => c.cameraName);
|
|
const usageCameras = columnConfig.filter((c) => c.type === 'usage').map((c) => c.cameraName);
|
|
const outboundCameras = columnConfig
|
|
.filter((c) => c.type === 'outbound')
|
|
.map((c) => c.cameraName);
|
|
|
|
// Date filtering: from startDate to today
|
|
const filterStartDate = new Date(startDate);
|
|
filterStartDate.setHours(0, 0, 0, 0);
|
|
|
|
const today = new Date(displayDate);
|
|
today.setHours(23, 59, 59, 999);
|
|
|
|
// Calculate IOT saldo (cumulative)
|
|
const inbound = iotData
|
|
.filter(
|
|
(log) =>
|
|
inboundCameras.includes(log.cameraName) &&
|
|
log.timestamp >= filterStartDate &&
|
|
log.timestamp <= today
|
|
)
|
|
.reduce((sum, log) => sum + log.count, 0);
|
|
|
|
const usage = iotData
|
|
.filter(
|
|
(log) =>
|
|
usageCameras.includes(log.cameraName) &&
|
|
log.timestamp >= filterStartDate &&
|
|
log.timestamp <= today
|
|
)
|
|
.reduce((sum, log) => sum + log.count, 0);
|
|
|
|
const outbound = iotData
|
|
.filter(
|
|
(log) =>
|
|
outboundCameras.includes(log.cameraName) &&
|
|
log.timestamp >= filterStartDate &&
|
|
log.timestamp <= today
|
|
)
|
|
.reduce((sum, log) => sum + log.count, 0);
|
|
|
|
const saldo = saldoAwal + inbound - usage - outbound;
|
|
|
|
return { inbound, usage, outbound, saldo, saldoAwal };
|
|
}, [iotData, columnConfig, startDate, saldoAwal, displayDate]);
|
|
|
|
const formatTodayLabel = () =>
|
|
new Intl.DateTimeFormat('id-ID', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
timeZone: 'Asia/Jakarta',
|
|
}).format(displayDate);
|
|
|
|
const formatDateRange = () => {
|
|
const start = startDate.toLocaleDateString('id-ID', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
});
|
|
return `${start} - ${formatTodayLabel()}`;
|
|
};
|
|
|
|
const getSaldoStatusColor = (saldo: number) => {
|
|
if (saldo > 0) return 'text-green-600';
|
|
if (saldo < 0) return 'text-red-600';
|
|
return 'text-gray-600';
|
|
};
|
|
|
|
return (
|
|
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-xl shadow-sm border-2 border-blue-200">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-blue-500 text-white p-3 rounded-lg">
|
|
<i className="fa-solid fa-scale-balanced text-2xl"></i>
|
|
</div>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-800">Saldo Karung Pakan</h2>
|
|
<p className="text-sm text-gray-600">{formatDateRange()}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Saldo Display */}
|
|
<div className="bg-white rounded-lg p-6 border border-blue-200">
|
|
<div className="grid grid-cols-5 gap-6">
|
|
{/* Saldo Awal */}
|
|
<div className="text-center">
|
|
<div className="flex items-center justify-center gap-2 mb-2">
|
|
<i className="fa-solid fa-box-archive text-blue-600"></i>
|
|
<p className="text-sm font-semibold text-gray-700">Saldo Awal</p>
|
|
</div>
|
|
<p className="text-3xl font-bold text-blue-600">{formatNumber(saldoData.saldoAwal)}</p>
|
|
<p className="text-xs text-gray-400 mt-1">karung</p>
|
|
</div>
|
|
|
|
{/* Karung Masuk */}
|
|
<div className="text-center">
|
|
<div className="flex items-center justify-center gap-2 mb-2">
|
|
<i className="fa-solid fa-arrow-down text-green-600"></i>
|
|
<p className="text-sm font-semibold text-gray-700">Karung Masuk</p>
|
|
</div>
|
|
<p className="text-3xl font-bold text-green-600">{formatNumber(saldoData.inbound)}</p>
|
|
<p className="text-xs text-gray-400 mt-1">karung</p>
|
|
</div>
|
|
|
|
{/* Karung Dituang */}
|
|
<div className="text-center">
|
|
<div className="flex items-center justify-center gap-2 mb-2">
|
|
<i className="fa-solid fa-arrow-up text-orange-600"></i>
|
|
<p className="text-sm font-semibold text-gray-700">Karung Dituang</p>
|
|
</div>
|
|
<p className="text-3xl font-bold text-orange-600">{formatNumber(saldoData.usage)}</p>
|
|
<p className="text-xs text-gray-400 mt-1">karung</p>
|
|
</div>
|
|
|
|
{/* Karung Keluar */}
|
|
<div className="text-center">
|
|
<div className="flex items-center justify-center gap-2 mb-2">
|
|
<i className="fa-solid fa-arrow-right text-red-600"></i>
|
|
<p className="text-sm font-semibold text-gray-700">Karung Keluar</p>
|
|
</div>
|
|
<p className="text-3xl font-bold text-red-600">{formatNumber(saldoData.outbound)}</p>
|
|
<p className="text-xs text-gray-400 mt-1">karung</p>
|
|
</div>
|
|
|
|
{/* Saldo */}
|
|
<div className="text-center border-l-2 border-blue-200 pl-6">
|
|
<div className="flex items-center justify-center gap-2 mb-2">
|
|
<i className="fa-solid fa-scale-balanced text-blue-600"></i>
|
|
<p className="text-sm font-semibold text-gray-700">Saldo</p>
|
|
</div>
|
|
<p className={`text-4xl font-bold ${getSaldoStatusColor(saldoData.saldo)}`}>
|
|
{formatNumber(saldoData.saldo)}
|
|
</p>
|
|
<p className="text-xs text-gray-400 mt-1">karung</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Warning for negative saldo */}
|
|
{saldoData.saldo < 0 && (
|
|
<div className="mt-4 bg-yellow-50 border border-yellow-200 rounded-lg p-3 flex items-start gap-2">
|
|
<i className="fa-solid fa-triangle-exclamation text-yellow-600 mt-0.5"></i>
|
|
<p className="text-xs text-yellow-800">
|
|
<strong>Peringatan:</strong> Saldo negatif menunjukkan karung dituang dan keluar lebih
|
|
banyak dari karung masuk. Periksa kembali data atau konfigurasi kolom.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FeedSaldoCard;
|