401 lines
16 KiB
TypeScript
401 lines
16 KiB
TypeScript
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 (0–255)</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 },
|
||
}); |