Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function App() {

useEffect(() => {
if (BYPASS_LOGIN) {
setIsLoading(true);
setIsLoading(false);
return;
}

Expand Down
103 changes: 103 additions & 0 deletions Frontend/src/components/AlertCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';

interface AlertCardProps {
criticalCount: number;
warningCount: number;
}

const AlertCard: React.FC<AlertCardProps> = ({ criticalCount, warningCount }) => {
const hasCritical = criticalCount > 0;
const hasWarning = warningCount > 0;
const hasAlert = hasCritical || hasWarning;

// ปกติ
if (!hasAlert) {
return (
<div style={{
flex: 1,
minHeight: 110,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 8,
padding: '20px 24px',
background: 'var(--color-bg-surface)',
borderRadius: 10,
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
border: '1px solid var(--color-border-line)',
borderTop: '4px solid var(--color-status-normal)',
boxSizing: 'border-box' as const,
}}>
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text-primary)' }}>
สถานะระบบ
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<i className="bi bi-check-circle-fill" style={{ fontSize: 22, color: 'var(--color-status-normal)' }} />
<span style={{
fontFamily: 'var(--font-data)',
fontSize: 18,
fontWeight: 700,
color: 'var(--color-status-normal)',
}}>ปกติทุกสถานี</span>
</div>
</div>
);
}

// มีแจ้งเตือน
const accentColor = hasCritical ? 'var(--color-status-critical)' : 'var(--color-status-warning)';
const bgColor = hasCritical ? 'rgba(239,68,68,0.09)' : 'rgba(255,174,0,0.08)';

return (
<div style={{
flex: 1,
minHeight: 110,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: 10,
padding: '18px 24px',
background: bgColor,
borderRadius: 10,
boxShadow: hasCritical ? '0 2px 12px rgba(239,68,68,0.2)' : '0 2px 12px rgba(255,174,0,0.15)',
border: `1px solid ${hasCritical ? 'rgba(239,68,68,0.25)' : 'rgba(255,174,0,0.25)'}`,
borderTop: `4px solid ${accentColor}`,
boxSizing: 'border-box' as const,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<i
className="bi bi-exclamation-triangle-fill"
style={{
fontSize: 18,
color: accentColor,
animation: hasCritical ? 'pulse 1.5s ease-in-out infinite' : 'none',
}}
/>
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text-primary)' }}>
แจ้งเตือน
</span>
</div>

<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' as const }}>
{hasCritical && (
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<span style={{ fontFamily: 'var(--font-data)', fontSize: 28, fontWeight: 700, color: 'var(--color-status-critical)', lineHeight: 1 }}>
{criticalCount}
</span>
<span style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}>สถานีวิกฤต</span>
</div>
)}
{hasWarning && (
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<span style={{ fontFamily: 'var(--font-data)', fontSize: 28, fontWeight: 700, color: 'var(--color-status-warning)', lineHeight: 1 }}>
{warningCount}
</span>
<span style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}>สถานีเฝ้าระวัง</span>
</div>
)}
</div>
</div>
);
};

export default AlertCard;
148 changes: 93 additions & 55 deletions Frontend/src/components/Dashboard-StationTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,56 @@ interface TableRowData {
rainfall: string;
status: "normal" | "warning" | "critical";
rawTimestamp: string;
signal: "online" | "offline";
}

const ROW_LIMIT = 20;

// --- StatusBadge sub-component ---
const StatusBadge: React.FC<{ status: "normal" | "warning" | "critical" }> = ({ status }) => {
const map = {
normal: { label: "ปกติ", cls: styles.badgeNormal },
warning: { label: "เฝ้าระวัง", cls: styles.badgeWarning },
critical: { label: "วิกฤต", cls: styles.badgeCritical },
};
const { label, cls } = map[status];
return (
<span className={`${styles.statusBadge} ${cls}`}>
<span className={styles.badgeDot} />
{label}
</span>
);
};

// --- SignalIcon sub-component ---
const SignalIcon: React.FC<{ signal: "online" | "offline" }> = ({ signal }) => {
const isOnline = signal === "online";
return (
<div className={styles.iconCell}>
<i
className={`bi ${isOnline ? "bi-reception-4" : "bi-reception-1"} ${
isOnline ? styles.iconOnline : styles.iconOffline
}`}
/>
</div>
);
};

// --- BatteryIcon sub-component ---
const BatteryIcon: React.FC<{ signal: "online" | "offline" }> = ({ signal }) => {
const isOnline = signal === "online";
return (
<div className={styles.iconCell}>
<i
className={`bi ${isOnline ? "bi-battery-full" : "bi-battery-empty"} ${
isOnline ? styles.iconOnline : styles.iconOffline
}`}
/>
</div>
);
};

// --- Main Component ---
const StationTable: React.FC<StationTableProps> = React.memo(({
waterData,
rainData,
Expand All @@ -35,38 +81,36 @@ const StationTable: React.FC<StationTableProps> = React.memo(({
const date = new Date(isoString);
const today = new Date();
const isToday = date.toDateString() === today.toDateString();

const timeStr = date
.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })
.replace(":", ".");
const dateStr = date.toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
});

return `${isToday ? "Today" : dateStr}, ${timeStr}`;
} catch {
return isoString;
}
};

const calculateStatus = (
water: string,
): "normal" | "warning" | "critical" => {
const calculateStatus = (water: string): "normal" | "warning" | "critical" => {
const val = parseFloat(water);
if (isNaN(val)) return "normal";
if (val >= 5.0) return "critical";
if (val >= 4.5) return "warning";
return "normal";
};

// สมมติว่าข้อมูลจริงๆ จะมี signal field; ตอนนี้ใช้ค่า default = online
for (const item of waterData) {
dataMap.set(item.monitorTime, {
rawTimestamp: item.monitorTime,
timestamp: formatDisplayTime(item.monitorTime),
waterLevel: parseFloat(item.monitorValue).toFixed(3),
rainfall: "-",
name: stationName,
signal: "online",
});
}

Expand All @@ -76,8 +120,8 @@ const StationTable: React.FC<StationTableProps> = React.memo(({
timestamp: formatDisplayTime(item.monitorTime),
waterLevel: "-",
name: stationName,
signal: "online",
};

existing.rainfall = parseFloat(item.monitorValue).toFixed(3);
dataMap.set(item.monitorTime, existing);
}
Expand All @@ -100,14 +144,11 @@ const StationTable: React.FC<StationTableProps> = React.memo(({
}, [waterData, rainData, stationName]);

const handleExportCSV = useCallback(() => {
const headers = [
"Station Name,Timestamp,Water Level (m),Rainfall (mm/h),Status",
];
const headers = ["Station Name,Timestamp,Water Level (m),Rainfall (mm/h),Status"];
const rows = tableData.map(
(row) =>
`${row.name},${row.rawTimestamp},${row.waterLevel},${row.rainfall},${row.status}`,
);

const csvContent =
"data:text/csv;charset=utf-8," + [headers, ...rows].join("\n");
const encodedUri = encodeURI(csvContent);
Expand All @@ -119,74 +160,71 @@ const StationTable: React.FC<StationTableProps> = React.memo(({
document.body.removeChild(link);
}, [tableData]);

// helper: CSS class สำหรับแถว
const rowClass = (status: "normal" | "warning" | "critical") => {
if (status === "critical") return `${styles.dataRow} ${styles.rowCritical}`;
if (status === "warning") return `${styles.dataRow} ${styles.rowWarning}`;
return `${styles.dataRow} ${styles.rowNormal}`;
};

// helper: CSS class สำหรับตัวเลข
const valueClass = (status: "normal" | "warning" | "critical") => {
if (status === "critical") return styles.valueCritical;
if (status === "warning") return styles.valueWarning;
return styles.valueNormal;
};

if (isLoading) {
return <div className={styles.loadingText}>Loading Data...</div>;
return <div className={styles.loadingText}>กำลังโหลดข้อมูล...</div>;
}

return (
<div className={styles.container}>
<div
style={{
display: "flex",
justifyContent: "flex-end",
padding: "12px 24px 0",
}}
>
<button
onClick={handleExportCSV}
style={{
padding: "4px 14px",
backgroundColor: "#FFFFFF",
border: "none",
borderRadius: "40px",
cursor: "pointer",
fontFamily: "'Inter', system-ui, sans-serif",
fontSize: "12px",
fontWeight: "600",
color: "#111827",
letterSpacing: "0.3px",
}}
>
<div className={styles.exportContainer}>
<button onClick={handleExportCSV} className={styles.exportButton}>
Export CSV
</button>
</div>

<div className={styles.tableHeader}>
<div>ชื่อสถานี</div>
<div>เวลา</div>
<div className={styles.iconCell}>สัญญาณ</div>
<div className={styles.iconCell}>แบตเตอรี่</div>
<div className={styles.centerAlign}>ระดับน้ำ(เมตร)</div>
<div className={styles.centerAlign}>ปริมาณน้ำฝน(มิลลิเมตร/ชั่วโมง)</div>
<div className={styles.centerAlign}>สัญญาณ</div>
<div className={styles.centerAlign}>แบตเตอรี่</div>
<div className={styles.centerAlign}>ระดับน้ำ (ม.)</div>
<div className={styles.centerAlign}>ปริมาณน้ำฝน (มม./ชม.)</div>
</div>

<div className={styles.tableBody}>
{tableData.length === 0 ? (
<div className={styles.emptyText}>ไม่มีข้อมูลสถานี</div>
) : (
tableData.map((row) => (
<div key={row.id} className={styles.dataRow}>
<div>
{row.name}
{row.status !== "normal" && (
<span
className={`${styles.statusText} ${
row.status === "critical"
? styles.statusCritical
: styles.statusWarning
}`}
>
({row.status})
</span>
)}
<div key={row.id} className={rowClass(row.status)}>
{/* ชื่อสถานี + Badge */}
<div className={styles.stationNameCell}>
<span className={styles.stationName}>{row.name}</span>
<StatusBadge status={row.status} />
</div>

{/* เวลา */}
<div>{row.timestamp}</div>

<div className={styles.iconCell}>สัญญาณ</div>
<div className={styles.iconCell}>แบตเตอรี่</div>
{/* สัญญาณ */}
<SignalIcon signal={row.signal} />

{/* แบตเตอรี่ */}
<BatteryIcon signal={row.signal} />

<div className={styles.centerAlign}>{row.waterLevel}</div>
<div className={styles.centerAlign}>{row.rainfall}</div>
{/* ระดับน้ำ */}
<div className={`${styles.centerAlign} ${valueClass(row.status)}`}>
{row.waterLevel}
</div>

{/* ปริมาณน้ำฝน */}
<div className={`${styles.centerAlign} ${styles.valueNormal}`}>
{row.rainfall}
</div>
</div>
))
)}
Expand Down
Loading