440 lines
11 KiB
TypeScript
440 lines
11 KiB
TypeScript
import {
|
|
queryConfirmAlarm,
|
|
queryrUnconfirmAlarm,
|
|
} from "@/controller/AlarmController";
|
|
import { useThemeContext } from "@/hooks/use-theme-context";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import dayjs from "dayjs";
|
|
import React, { useMemo, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
Alert,
|
|
Modal,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
|
|
interface AlarmCardProps {
|
|
alarm: Model.Alarm;
|
|
onReload?: (onReload: boolean) => void;
|
|
}
|
|
|
|
export const AlarmCard: React.FC<AlarmCardProps> = ({ alarm, onReload }) => {
|
|
const { colors } = useThemeContext();
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [note, setNote] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
const canSubmit = useMemo(
|
|
() => note.trim().length > 0 || alarm.confirmed,
|
|
[note, alarm.confirmed]
|
|
);
|
|
|
|
// Determine level and colors based on alarm level
|
|
const getAlarmConfig = (level?: number) => {
|
|
if (level === 3) {
|
|
// Danger - Red
|
|
return {
|
|
level: 3,
|
|
icon: "warning" as const,
|
|
bgColor: "#fee2e2",
|
|
borderColor: "#DC0E0E",
|
|
iconColor: "#dc2626",
|
|
statusBg: "#dcfce7",
|
|
statusText: "#166534",
|
|
};
|
|
} else if (level === 2) {
|
|
// Caution - Yellow/Orange
|
|
return {
|
|
level: 2,
|
|
icon: "alert-circle" as const,
|
|
bgColor: "#fef3c7",
|
|
borderColor: "#FF6C0C",
|
|
iconColor: "#d97706",
|
|
statusBg: "#fef08a",
|
|
statusText: "#713f12",
|
|
};
|
|
} else {
|
|
// Info - Green
|
|
return {
|
|
level: 1,
|
|
icon: "information-circle" as const,
|
|
bgColor: "#fffefe",
|
|
borderColor: "#FF937E",
|
|
iconColor: "#FF937E",
|
|
statusBg: "#dcfce7",
|
|
statusText: "#166534",
|
|
};
|
|
}
|
|
};
|
|
|
|
const config = getAlarmConfig(alarm.level);
|
|
|
|
const formatDate = (timestamp?: number) => {
|
|
if (!timestamp) return "N/A";
|
|
return dayjs.unix(timestamp).format("YYYY-MM-DD HH:mm");
|
|
};
|
|
|
|
const ensurePayload = () => {
|
|
if (!alarm.id || !alarm.thing_id || !alarm.time) {
|
|
Alert.alert("Thiếu dữ liệu", "Không đủ thông tin để xác nhận cảnh báo");
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const submitConfirm = async (action: "confirm" | "unconfirm") => {
|
|
if (!ensurePayload()) return;
|
|
if (action === "confirm" && note.trim().length === 0) {
|
|
Alert.alert("Thông báo", "Vui lòng nhập ghi chú để xác nhận");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSubmitting(true);
|
|
if (action === "confirm") {
|
|
await queryConfirmAlarm({
|
|
id: alarm.id!,
|
|
thing_id: alarm.thing_id!,
|
|
time: alarm.time!,
|
|
description: note.trim(),
|
|
});
|
|
} else {
|
|
await queryrUnconfirmAlarm({
|
|
id: alarm.id!,
|
|
thing_id: alarm.thing_id!,
|
|
time: alarm.time!,
|
|
});
|
|
}
|
|
onReload?.(true);
|
|
} catch (error: any) {
|
|
console.error("Cannot confirm/unconfirm alarm: ", error);
|
|
const status = error?.response?.status ?? error?.status;
|
|
// If server returns 404, ignore silently
|
|
if (status !== 404) {
|
|
Alert.alert("Lỗi", "Không thể xử lý yêu cầu. Vui lòng thử lại.");
|
|
}
|
|
} finally {
|
|
setSubmitting(false);
|
|
setShowModal(false);
|
|
setNote("");
|
|
}
|
|
};
|
|
|
|
const handlePress = (alarm: Model.Alarm) => {
|
|
if (alarm.confirmed) {
|
|
Alert.alert(
|
|
"Thông báo",
|
|
"Bạn có chắc muốn ngừng xác nhận cảnh báo này?",
|
|
[
|
|
{ text: "Hủy", style: "cancel" },
|
|
{
|
|
text: "Ngừng xác nhận",
|
|
style: "destructive",
|
|
onPress: () => submitConfirm("unconfirm"),
|
|
},
|
|
]
|
|
);
|
|
} else {
|
|
setShowModal(true);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
styles.card,
|
|
{
|
|
backgroundColor: config.bgColor,
|
|
borderLeftColor: config.borderColor,
|
|
borderLeftWidth: 5,
|
|
boxShadow: "0px 1px 3px rgba(0, 0, 0, 0.2)",
|
|
},
|
|
]}
|
|
>
|
|
<View style={styles.container}>
|
|
{/* Left Side - Icon and Content */}
|
|
<View style={styles.content}>
|
|
{/* Icon */}
|
|
<View
|
|
style={[styles.iconContainer, { backgroundColor: config.bgColor }]}
|
|
>
|
|
<Ionicons name={config.icon} size={24} color={config.iconColor} />
|
|
</View>
|
|
|
|
{/* Title and Info */}
|
|
<View style={styles.textContainer}>
|
|
{/* Name */}
|
|
<View style={styles.titleRow}>
|
|
<Text
|
|
style={[styles.title, { color: colors.text }]}
|
|
numberOfLines={2}
|
|
>
|
|
{alarm.name || alarm.thing_name || "Unknown"}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Location (thing_name) and Time */}
|
|
<View style={styles.infoRow}>
|
|
<View style={styles.infoItem}>
|
|
<Text
|
|
style={[styles.infoLabel, { color: colors.textSecondary }]}
|
|
>
|
|
Trạm
|
|
</Text>
|
|
<Text
|
|
style={[styles.infoValue, { color: colors.text }]}
|
|
numberOfLines={1}
|
|
>
|
|
{alarm.thing_name || "Unknown"}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.infoItem}>
|
|
<Text
|
|
style={[styles.infoLabel, { color: colors.textSecondary }]}
|
|
>
|
|
Thời gian
|
|
</Text>
|
|
<Text
|
|
style={[styles.infoValue, { color: colors.text }]}
|
|
numberOfLines={1}
|
|
>
|
|
{formatDate(alarm.time)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Status Badge */}
|
|
<TouchableOpacity
|
|
style={styles.statusContainer}
|
|
onPress={() => handlePress(alarm)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.statusBadge,
|
|
{
|
|
backgroundColor: alarm.confirmed ? "#8FD14F" : "#EEEEEE",
|
|
},
|
|
]}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.statusText,
|
|
{ color: alarm.confirmed ? "#166534" : "black" },
|
|
]}
|
|
>
|
|
{alarm.confirmed ? "Đã xác nhận" : "Chờ xác nhận"}
|
|
</Text>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{alarm.confirmed && (
|
|
<View style={styles.rightIcon}>
|
|
<Ionicons
|
|
name="checkmark-done"
|
|
size={20}
|
|
color={alarm.confirmed ? "#78C841" : config.iconColor}
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
<Modal
|
|
visible={showModal}
|
|
transparent
|
|
animationType="fade"
|
|
onRequestClose={() => setShowModal(false)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View
|
|
style={[
|
|
styles.modalContent,
|
|
{ backgroundColor: colors.background },
|
|
]}
|
|
>
|
|
<Text style={[styles.modalTitle, { color: colors.text }]}>
|
|
Nhập ghi chú xác nhận
|
|
</Text>
|
|
<TextInput
|
|
style={[styles.input, { color: colors.text }]}
|
|
placeholder="Nhập ghi chú"
|
|
placeholderTextColor={colors.textSecondary}
|
|
multiline
|
|
value={note}
|
|
onChangeText={setNote}
|
|
editable={!submitting}
|
|
/>
|
|
<View style={styles.modalActions}>
|
|
<TouchableOpacity
|
|
style={[styles.modalButton, styles.cancelButton]}
|
|
onPress={() => {
|
|
setShowModal(false);
|
|
setNote("");
|
|
}}
|
|
disabled={submitting}
|
|
>
|
|
<Text style={styles.cancelText}>Hủy</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.modalButton,
|
|
styles.confirmButton,
|
|
!canSubmit && styles.disabledButton,
|
|
]}
|
|
onPress={() => submitConfirm("confirm")}
|
|
disabled={submitting || !canSubmit}
|
|
>
|
|
{submitting ? (
|
|
<ActivityIndicator color="#fff" size="small" />
|
|
) : (
|
|
<Text style={styles.confirmText}>Xác nhận</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
card: {
|
|
borderRadius: 12,
|
|
// borderWidth: 1,
|
|
paddingVertical: 16,
|
|
paddingHorizontal: 12,
|
|
marginBottom: 12,
|
|
},
|
|
container: {
|
|
flexDirection: "row",
|
|
alignItems: "flex-start",
|
|
justifyContent: "space-between",
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
flexDirection: "row",
|
|
alignItems: "flex-start",
|
|
},
|
|
iconContainer: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 12,
|
|
alignItems: "flex-start",
|
|
justifyContent: "flex-start",
|
|
// marginRight: 5,
|
|
},
|
|
textContainer: {
|
|
flex: 1,
|
|
},
|
|
titleRow: {
|
|
marginBottom: 8,
|
|
},
|
|
code: {
|
|
fontSize: 12,
|
|
fontWeight: "600",
|
|
marginBottom: 4,
|
|
},
|
|
title: {
|
|
fontSize: 16,
|
|
fontWeight: "600",
|
|
marginBottom: 8,
|
|
},
|
|
infoRow: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
marginBottom: 12,
|
|
gap: 16,
|
|
},
|
|
infoItem: {
|
|
flex: 1,
|
|
},
|
|
infoLabel: {
|
|
fontSize: 12,
|
|
marginBottom: 4,
|
|
},
|
|
infoValue: {
|
|
fontSize: 14,
|
|
fontWeight: "500",
|
|
},
|
|
statusContainer: {
|
|
marginTop: 8,
|
|
},
|
|
statusBadge: {
|
|
alignSelf: "flex-start",
|
|
paddingVertical: 6,
|
|
paddingHorizontal: 12,
|
|
borderRadius: 20,
|
|
borderWidth: 0.2,
|
|
},
|
|
statusText: {
|
|
fontSize: 12,
|
|
fontWeight: "600",
|
|
},
|
|
rightIcon: {
|
|
width: 24,
|
|
height: 24,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
marginLeft: 12,
|
|
},
|
|
modalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(0,0,0,0.3)",
|
|
justifyContent: "center",
|
|
paddingHorizontal: 16,
|
|
},
|
|
modalContent: {
|
|
borderRadius: 12,
|
|
padding: 16,
|
|
gap: 12,
|
|
},
|
|
modalTitle: {
|
|
fontSize: 16,
|
|
fontWeight: "700",
|
|
},
|
|
input: {
|
|
minHeight: 80,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: "#e5e7eb",
|
|
padding: 12,
|
|
textAlignVertical: "top",
|
|
},
|
|
modalActions: {
|
|
flexDirection: "row",
|
|
justifyContent: "flex-end",
|
|
gap: 12,
|
|
},
|
|
modalButton: {
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 10,
|
|
borderRadius: 8,
|
|
},
|
|
cancelButton: {
|
|
backgroundColor: "#e5e7eb",
|
|
},
|
|
confirmButton: {
|
|
backgroundColor: "#dc2626",
|
|
},
|
|
disabledButton: {
|
|
opacity: 0.6,
|
|
},
|
|
cancelText: {
|
|
color: "#111827",
|
|
fontWeight: "600",
|
|
},
|
|
confirmText: {
|
|
color: "#fff",
|
|
fontWeight: "700",
|
|
},
|
|
});
|
|
|
|
export default AlarmCard;
|