TKK_E32231767/src/screens/ControlScreen.tsx

401 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useCallback } from "react";
import {
View,
Text,
ScrollView,
StyleSheet,
TouchableOpacity,
TextInput,
Alert,
ActivityIndicator,
Keyboard,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import Slider from "@react-native-community/slider";
import CylinderSlider from "../components/CylinderSlinder";
import { setConveyorSpeed, getConveyorSpeed } from "../services/api";
// ─── Konstanta ────────────────────────────────────────────────────────────────
const SPEED_MODES = [
{ key: "pelan", label: "Pelan", value: 50, desc: "Cocok untuk telur rapuh" },
{ key: "sedang", label: "Sedang", value: 128, desc: "Mode normal" },
{ key: "cepat", label: "Cepat", value: 220, desc: "Throughput tinggi" },
];
function getSpeedLabel(v: number) {
if (v < 80) return "Pelan";
if (v < 170) return "Sedang";
return "Cepat";
}
function getSpeedColor(v: number) {
if (v < 80) return "#16a34a";
if (v < 170) return "#d97706";
return "#dc2626";
}
// ─── Komponen utama ───────────────────────────────────────────────────────────
export default function ControlScreen() {
const [speed, setSpeed] = useState(128);
const [speedMode, setSpeedMode] = useState("sedang");
const [speedInput, setSpeedInput] = useState("128");
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [loadingCurrent, setLoadingCurrent] = useState(true);
const [refreshing, setRefreshing] = useState(false);
// ── Fetch speed dari server ───────────────────────────────────────────────
const fetchSpeed = useCallback(async (isManual = false) => {
if (isManual) setRefreshing(true);
else setLoadingCurrent(true);
try {
const currentSpeed = await getConveyorSpeed();
setSpeed(currentSpeed);
setSpeedInput(String(currentSpeed));
const match = SPEED_MODES.find((m) => m.value === currentSpeed);
setSpeedMode(match ? match.key : "");
} catch {
if (isManual) {
Alert.alert("Gagal", "Tidak dapat mengambil data dari server.");
}
} finally {
setLoadingCurrent(false);
setRefreshing(false);
}
}, []);
// Fetch pertama kali saat app buka
useEffect(() => { fetchSpeed(); }, []);
// ✅ Fetch ulang SETIAP KALI tab Control difokuskan (balik dari tab lain)
// Ini memastikan data selalu sinkron dengan server, bukan dari state lama
useFocusEffect(
useCallback(() => {
fetchSpeed();
}, [fetchSpeed])
);
// ── Handler ───────────────────────────────────────────────────────────────
const applySpeedMode = (key: string, value: number) => {
setSpeedMode(key);
setSpeed(value);
setSpeedInput(String(value));
};
// Dipanggil dari cylinder atau slider — TIDAK dari input teks
const handleSpeedChange = (v: number) => {
const rounded = Math.round(v);
setSpeed(rounded);
setSpeedInput(String(rounded));
const match = SPEED_MODES.find((m) => m.value === rounded);
setSpeedMode(match ? match.key : "");
};
/**
* Terapkan nilai dari input teks.
* HANYA dipanggil saat tombol "Terapkan" diklik — BUKAN saat blur/dismiss keyboard.
*/
const handleApplyInput = () => {
Keyboard.dismiss();
const val = Math.max(0, Math.min(255, parseInt(speedInput, 10) || 0));
setSpeed(val);
setSpeedInput(String(val));
const match = SPEED_MODES.find((m) => m.value === val);
setSpeedMode(match ? match.key : "");
};
const handleSave = async () => {
try {
setSaving(true);
await setConveyorSpeed(speed);
setSaved(true);
setTimeout(() => setSaved(false), 2500);
} catch {
Alert.alert("Error", "Gagal mengirim data ke perangkat. Cek koneksi server.");
} finally {
setSaving(false);
}
};
const activeColor = getSpeedColor(speed);
// ── Loading state ─────────────────────────────────────────────────────────
if (loadingCurrent) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator color="#0284c7" size="large" />
<Text style={styles.loadingText}>Memuat konfigurasi...</Text>
</View>
);
}
// ── Render ────────────────────────────────────────────────────────────────
return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled" // tap di luar input tidak dismiss & terapkan
>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.title}>Kontrol Conveyor</Text>
<Text style={styles.subtitle}>Atur kecepatan laju conveyor</Text>
</View>
<TouchableOpacity
onPress={() => fetchSpeed(true)}
disabled={refreshing}
style={[styles.refreshBtn, refreshing && { opacity: 0.5 }]}
>
{refreshing
? <ActivityIndicator color="#0284c7" size="small" />
: <Text style={styles.refreshIcon}></Text>
}
</TouchableOpacity>
</View>
{/* Cylinder */}
<View style={styles.cylinderCard}>
<View style={styles.cylinderWrapper}>
<CylinderSlider
value={speed}
max={255}
onChange={handleSpeedChange}
color={activeColor}
label="CONVEYOR SPEED"
/>
<View style={styles.cylinderInfo}>
<Text style={styles.cylinderPct}>
{Math.round((speed / 255) * 100)}%
</Text>
<Text style={styles.cylinderHint}>Geser ke atas / bawah untuk adjust</Text>
</View>
</View>
</View>
{/* Control Card */}
<View style={[styles.controlCard, { borderColor: activeColor + "44" }]}>
{/* Header nilai */}
<View style={styles.controlHeaderRow}>
<View>
<Text style={[styles.controlLabel, { color: activeColor }]}>
Kecepatan Conveyor
</Text>
<View style={styles.valueRow}>
<Text style={[styles.controlValueText, { color: "#1e293b" }]}>{speed}</Text>
<Text style={styles.controlValueUnit}> / 255</Text>
</View>
</View>
<View style={[
styles.modeBadge,
{ backgroundColor: activeColor + "15", borderColor: activeColor + "44" }
]}>
<Text style={[styles.modeBadgeText, { color: activeColor }]}>
{getSpeedLabel(speed)}
</Text>
</View>
</View>
{/* Mode preset */}
<Text style={styles.sectionLabel}>Mode Cepat</Text>
<View style={styles.modeRow}>
{SPEED_MODES.map((m) => {
const active = speedMode === m.key;
const mColor = getSpeedColor(m.value);
return (
<TouchableOpacity
key={m.key}
onPress={() => applySpeedMode(m.key, m.value)}
style={[
styles.modeButton,
{
borderColor: active ? mColor : "#e2e8f0",
backgroundColor: active ? mColor + "15" : "#f8fafc",
},
]}
>
<Text style={[styles.modeButtonLabel, { color: active ? mColor : "#64748b" }]}>
{m.label}
</Text>
<Text style={styles.modeButtonDesc}>{m.desc}</Text>
{active && (
<View style={[styles.activeIndicator, { backgroundColor: mColor }]} />
)}
</TouchableOpacity>
);
})}
</View>
{/* Slider */}
<Text style={styles.sectionLabel}>Slider Manual</Text>
<View style={styles.sliderRow}>
<Text style={styles.sliderMin}>0</Text>
<Slider
style={styles.slider}
minimumValue={0}
maximumValue={255}
step={1}
value={speed}
onValueChange={handleSpeedChange}
minimumTrackTintColor={activeColor}
maximumTrackTintColor="#e2e8f0"
thumbTintColor={activeColor}
/>
<Text style={styles.sliderMax}>255</Text>
</View>
{/* Input angka — onBlur DIHAPUS, hanya terapkan via tombol */}
<Text style={styles.sectionLabel}>Input Angka (0255)</Text>
<View style={styles.inputRow}>
<TextInput
value={speedInput}
onChangeText={setSpeedInput}
// onBlur TIDAK ADA supaya dismiss keyboard tidak langsung terapkan
// onSubmitEditing TIDAK ADA supaya tekan Enter tidak langsung terapkan
keyboardType="numeric"
maxLength={3}
returnKeyType="done"
style={[styles.numberInput, { borderColor: activeColor + "66" }]}
/>
{/* Tombol Terapkan — SATU-SATUNYA cara nilai input diterapkan */}
<TouchableOpacity
onPress={handleApplyInput}
style={[
styles.applyButton,
{ backgroundColor: activeColor + "15", borderColor: activeColor + "44" },
]}
>
<Text style={[styles.applyButtonText, { color: activeColor }]}>Terapkan</Text>
</TouchableOpacity>
</View>
</View>
{/* Speed bar indikator */}
<View style={styles.speedBar}>
<View style={[
styles.speedBarFill,
{ width: `${(speed / 255) * 100}%` as any, backgroundColor: activeColor },
]} />
<Text style={styles.speedBarLabel}>
{speed < 80
? "⚠️ Kecepatan rendah — pastikan conveyor bergerak"
: speed > 200
? "⚠️ Kecepatan tinggi — awasi kondisi telur"
: "✓ Kecepatan normal"}
</Text>
</View>
{/* Tombol kirim */}
<TouchableOpacity
onPress={handleSave}
disabled={saving}
style={[
styles.saveButton,
{ backgroundColor: saved ? "#16a34a" : "#0284c7", opacity: saving ? 0.7 : 1 },
]}
>
<Text style={styles.saveButtonText}>
{saving ? "Mengirim..." : saved ? "✓ Kecepatan Tersimpan!" : "Kirim ke Perangkat"}
</Text>
</TouchableOpacity>
</ScrollView>
);
}
// ─── Styles ───────────────────────────────────────────────────────────────────
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#f8fafc" },
content: { padding: 16, paddingBottom: 32 },
loadingContainer: { flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "#f8fafc", gap: 12 },
loadingText: { fontSize: 13, color: "#94a3b8" },
header: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 20 },
title: { fontSize: 24, fontWeight: "800", color: "#0f172a", letterSpacing: -0.5 },
subtitle: { fontSize: 12, color: "#94a3b8", marginTop: 2 },
refreshBtn: {
width: 40, height: 40, borderRadius: 20,
backgroundColor: "#fff", borderWidth: 1, borderColor: "#e2e8f0",
alignItems: "center", justifyContent: "center",
shadowColor: "#000", shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05, shadowRadius: 3, elevation: 2,
},
refreshIcon: { fontSize: 22, color: "#0284c7", fontWeight: "700", lineHeight: 26 },
// Cylinder card
cylinderCard: {
backgroundColor: "#fff", borderWidth: 1, borderColor: "#e2e8f0",
borderRadius: 16, paddingVertical: 28, paddingHorizontal: 16,
marginBottom: 16, alignItems: "center",
shadowColor: "#000", shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05, shadowRadius: 4, elevation: 2,
},
cylinderWrapper: { alignItems: "center", gap: 16 },
cylinderInfo: { alignItems: "center" },
cylinderPct: { fontSize: 32, fontWeight: "800", color: "#0f172a", fontFamily: "monospace" },
cylinderHint: { fontSize: 11, color: "#94a3b8", marginTop: 2 },
// Control card
controlCard: {
backgroundColor: "#fff", borderWidth: 1, borderRadius: 16,
padding: 16, marginBottom: 12,
shadowColor: "#000", shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.04, shadowRadius: 3, elevation: 1,
},
controlHeaderRow: {
flexDirection: "row", justifyContent: "space-between",
alignItems: "flex-start", marginBottom: 16,
},
controlLabel: { fontSize: 12, fontWeight: "700", marginBottom: 4 },
valueRow: { flexDirection: "row", alignItems: "baseline" },
controlValueText: { fontSize: 32, fontWeight: "800", fontFamily: "monospace" },
controlValueUnit: { fontSize: 13, color: "#94a3b8" },
modeBadge: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 20, borderWidth: 1 },
modeBadgeText: { fontSize: 11, fontWeight: "600" },
sectionLabel: { fontSize: 10, color: "#94a3b8", fontWeight: "600", marginBottom: 8, marginTop: 4 },
// Mode buttons
modeRow: { flexDirection: "row", gap: 8, marginBottom: 16 },
modeButton: { flex: 1, paddingVertical: 10, paddingHorizontal: 6, borderRadius: 12, borderWidth: 1, alignItems: "center", gap: 3, position: "relative" },
modeButtonLabel:{ fontSize: 11, fontWeight: "700" },
modeButtonDesc: { fontSize: 9, color: "#94a3b8", textAlign: "center" },
activeIndicator:{ position: "absolute", top: 6, right: 6, width: 6, height: 6, borderRadius: 3 },
// Slider
sliderRow: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
sliderMin: { fontSize: 10, color: "#94a3b8", width: 16, textAlign: "center" },
sliderMax: { fontSize: 10, color: "#94a3b8", width: 24, textAlign: "center" },
slider: { flex: 1, height: 40 },
// Input angka
inputRow: { flexDirection: "row", gap: 10, alignItems: "center" },
numberInput: {
width: 80, backgroundColor: "#f8fafc", borderWidth: 1.5,
borderRadius: 10, paddingHorizontal: 10, paddingVertical: 9,
color: "#0f172a", fontSize: 18, fontFamily: "monospace",
textAlign: "center", fontWeight: "700",
},
applyButton: { flex: 1, paddingVertical: 12, borderRadius: 10, borderWidth: 1, alignItems: "center" },
applyButtonText: { fontSize: 13, fontWeight: "700" },
inputHint: { fontSize: 10, color: "#cbd5e1", marginTop: 6, fontStyle: "italic" },
// Speed bar
speedBar: {
backgroundColor: "#f1f5f9", borderRadius: 12, overflow: "hidden",
marginBottom: 16, height: 40, justifyContent: "center",
borderWidth: 1, borderColor: "#e2e8f0",
},
speedBarFill: { position: "absolute", top: 0, left: 0, bottom: 0, opacity: 0.15, borderRadius: 12 },
speedBarLabel: { fontSize: 11, color: "#64748b", paddingHorizontal: 12 },
// Save button
saveButton: { borderRadius: 14, paddingVertical: 16, alignItems: "center" },
saveButtonText: { color: "#fff", fontSize: 15, fontWeight: "800", letterSpacing: 0.3 },
});