feat: revamp detail screen soil temp

This commit is contained in:
Syaroful 2024-10-15 14:53:40 +07:00
parent e5b9cf0c4c
commit 426445a865
25 changed files with 1235 additions and 1154 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea/

View File

@ -2,7 +2,7 @@ import 'package:agrilink_vocpro/features/auth/view/login_screen.dart';
import 'package:agrilink_vocpro/features/dashboard/view/dashboard_screen.dart'; import 'package:agrilink_vocpro/features/dashboard/view/dashboard_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/conductivity/view/conductivity_screen.dart'; import 'package:agrilink_vocpro/features/home/pages/conductivity/view/conductivity_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/humidity/view/humidity_screen.dart'; import 'package:agrilink_vocpro/features/home/pages/humidity/view/humidity_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/light/view/light_screen.dart'; import 'package:agrilink_vocpro/features/home/pages/luminosity/view/light_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/nitrogen/view/nitrogen_screen.dart'; import 'package:agrilink_vocpro/features/home/pages/nitrogen/view/nitrogen_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/ph/view/ph_screen.dart'; import 'package:agrilink_vocpro/features/home/pages/ph/view/ph_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/phosphorus/view/phosphorus_screen.dart'; import 'package:agrilink_vocpro/features/home/pages/phosphorus/view/phosphorus_screen.dart';
@ -136,11 +136,14 @@ class AppRoute {
static GoRoute buildSoilTempRoute() { static GoRoute buildSoilTempRoute() {
return GoRoute( return GoRoute(
path: 'soil_temperature/:value', path: 'soil_temperature/:value1/:value2',
builder: (context, state) { builder: (context, state) {
final double value = final double value1 =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0; double.tryParse(state.pathParameters['value1'] ?? '') ?? 0.0;
return SoilTemperatureScreen(temperature: value); final double value2 =
double.tryParse(state.pathParameters['value2'] ?? '') ?? 0.0;
return SoilTemperatureScreen(
npk1Temperature: value1, npk2Temperature: value2);
}, },
); );
} }

View File

@ -1,8 +1,10 @@
import 'package:agrilink_vocpro/core/constant/app_constant.dart'; import 'package:agrilink_vocpro/core/constant/app_constant.dart';
import 'package:agrilink_vocpro/core/extension/extention.dart'; import 'package:agrilink_vocpro/core/extension/extention.dart';
import 'package:agrilink_vocpro/data/model/dht_graphic_response.dart';
import 'package:agrilink_vocpro/data/model/npk1_graphic_response.dart';
import 'package:agrilink_vocpro/data/model/npk2_graphic_response.dart';
import 'package:agrilink_vocpro/data/model/relay_response.dart'; import 'package:agrilink_vocpro/data/model/relay_response.dart';
import 'package:agrilink_vocpro/features/home/model/latest_data_response.dart'; import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/features/home/model/npk1_soil_temp_grafik.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -31,32 +33,74 @@ class AppService {
} }
} }
Future<Npk1SoilTempGrafik> getNpk1SoilTempGrafik({ //get grafik data dht
Future<DhtGraphicResponse> getGrafikDataDht({
required String metric, required String metric,
required String sensor,
}) async { }) async {
String dateNow = DateTime.now().toString(); String date = DateTime.now().toString();
String dateYesterday = final formatedDate = dateFormatterShort(date);
DateTime.now().subtract(Duration(days: 1)).toString();
final formatedDateNow = dateFormatterShort(dateNow);
final formatedDateYesterday = dateFormatterShort(dateYesterday);
try { try {
final result = await _dioWithoutInterceptor.get( final result = await _dioWithoutInterceptor.get(
'/sensor/getData?metric=$metric&range[start]=$formatedDateYesterday&range[end]=$formatedDateNow&range[time_range]=HOURLY&sensor=$sensor', '/sensor/getData?metric=$metric&range[start]=$formatedDate&range[end]=$formatedDate&range[time_range]=HOURLY&sensor=dht',
); );
if (result.statusCode == 200) { if (result.statusCode == 200) {
print(result.data.toString()); final data = DhtGraphicResponse.fromJson(result.data);
final data = Npk1SoilTempGrafik.fromJson(result.data);
return data; return data;
} else { } else {
throw Exception('Failed to load data'); throw Exception('Failed to load data');
} }
} on DioException catch (e) { } on DioException catch (e) {
print(e); final errorMessage = e.response?.data['message'];
rethrow; throw (errorMessage);
} }
} }
// get grafik data npk1
Future<Npk1GraphicResponse> getGraphicDataNpk1(
{required String metric}) async {
String date = DateTime.now().toString();
final formatedDate = dateFormatterShort(date);
try {
final result = await _dioWithoutInterceptor.get(
'/sensor/getData?metric=$metric&range[start]=$formatedDate&range[end]=$formatedDate&range[time_range]=HOURLY&sensor=npk1',
);
if (result.statusCode == 200) {
final data = Npk1GraphicResponse.fromJson(result.data);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final errorMessage = e.response?.data['message'];
throw (errorMessage);
}
}
// get grafik data npk2
Future<Npk2GraphicResponse> getGraphicDataNpk2(
{required String metric}) async {
String date = DateTime.now().toString();
final formatedDate = dateFormatterShort(date);
try {
final result = await _dioWithoutInterceptor.get(
'/sensor/getData?metric=$metric&range[start]=$formatedDate&range[end]=$formatedDate&range[time_range]=HOURLY&sensor=npk2',
);
if (result.statusCode == 200) {
final data = Npk2GraphicResponse.fromJson(result.data);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final errorMessage = e.response?.data['message'];
throw (errorMessage);
}
}
// get latest data
Future<LatestDataResponse> getLatestData() async { Future<LatestDataResponse> getLatestData() async {
try { try {
final result = await _dioWithoutInterceptor.get( final result = await _dioWithoutInterceptor.get(
@ -69,8 +113,8 @@ class AppService {
throw Exception('Failed to load data'); throw Exception('Failed to load data');
} }
} on DioException catch (e) { } on DioException catch (e) {
print(e); final errorMessage = e.response?.data['message'];
rethrow; throw (errorMessage);
} }
} }
} }

View File

@ -1,170 +1,170 @@
import 'dart:convert'; // import 'dart:convert';
import 'package:agrilink_vocpro/core/constant/app_constant.dart'; // import 'package:agrilink_vocpro/core/constant/app_constant.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart'; // import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:mqtt_client/mqtt_client.dart'; // import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart'; // import 'package:mqtt_client/mqtt_server_client.dart';
class MQTTService { // class MQTTService {
MqttServerClient? client; // MqttServerClient? client;
Future<ResultState> setupMqtt() async { // Future<ResultState> setupMqtt() async {
client = MqttServerClient(AppConstant.mqttServer, ''); // client = MqttServerClient(AppConstant.mqttServer, '');
client!.port = 1883; // client!.port = 1883;
client!.connectionMessage = MqttConnectMessage() // client!.connectionMessage = MqttConnectMessage()
.authenticateAs(AppConstant.mqttUsername, AppConstant.mqttPassword) // .authenticateAs(AppConstant.mqttUsername, AppConstant.mqttPassword)
.withClientIdentifier('mobile_client_controller') // .withClientIdentifier('mobile_client_controller')
.startClean() // reset session // .startClean() // reset session
.withWillQos(MqttQos.atLeastOnce); // .withWillQos(MqttQos.atLeastOnce);
try { // try {
print('MQTT: Connecting....'); // print('MQTT: Connecting....');
await client!.connect(); // await client!.connect();
print('MQTT: Connected'); // print('MQTT: Connected');
return ResultState.hasData; // return ResultState.hasData;
} catch (e) { // } catch (e) {
print('MQTT: Error: $e'); // print('MQTT: Error: $e');
return ResultState.error; // return ResultState.error;
} // }
} // }
Future<ResultState> publishMessage(String topic, String message) async { // Future<ResultState> publishMessage(String topic, String message) async {
final builder = MqttClientPayloadBuilder(); // final builder = MqttClientPayloadBuilder();
try { // try {
final bool isConnected = await isMqttConnected(); // Cek apakah terhubung // final bool isConnected = await isMqttConnected(); // Cek apakah terhubung
if (!isConnected) { // if (!isConnected) {
print('MQTT: Tidak terhubung ke broker. Tidak bisa publish message.'); // print('MQTT: Tidak terhubung ke broker. Tidak bisa publish message.');
return ResultState.error; // return ResultState.error;
} // }
print('MQTT: Published message to $topic: $message'); // print('MQTT: Published message to $topic: $message');
builder.addString(message); // builder.addString(message);
client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!); // client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
print('MQTT: Message published'); // print('MQTT: Message published');
return ResultState.hasData; // return ResultState.hasData;
} catch (e) { // } catch (e) {
print('MQTT: Error: $e'); // print('MQTT: Error: $e');
return ResultState.error; // return ResultState.error;
} // }
} // }
Future<ResultState> disconnectMqtt() async { // Future<ResultState> disconnectMqtt() async {
final bool isConnected = await isMqttConnected(); // final bool isConnected = await isMqttConnected();
if (isConnected) { // if (isConnected) {
print('Memutus koneksi dari broker...'); // print('Memutus koneksi dari broker...');
client!.disconnect(); // client!.disconnect();
await Future.delayed(const Duration(seconds: 1)); // await Future.delayed(const Duration(seconds: 1));
print('Koneksi telah terputus.'); // print('Koneksi telah terputus.');
return ResultState.hasData; // return ResultState.hasData;
} else { // } else {
print('Tidak ada koneksi yang sedang aktif.'); // print('Tidak ada koneksi yang sedang aktif.');
return ResultState.error; // return ResultState.error;
} // }
} // }
Future<bool> isMqttConnected() async { // Future<bool> isMqttConnected() async {
if (client != null && // if (client != null &&
client!.connectionStatus!.state == MqttConnectionState.connected) { // client!.connectionStatus!.state == MqttConnectionState.connected) {
return true; //connected // return true; //connected
} else { // } else {
return false; //not connected // return false; //not connected
} // }
} // }
Future<bool> subscribeToTopic(String topic) async { // Future<bool> subscribeToTopic(String topic) async {
bool isActive = false; // bool isActive = false;
if (client != null && // if (client != null &&
client!.connectionStatus!.state == MqttConnectionState.connected) { // client!.connectionStatus!.state == MqttConnectionState.connected) {
try { // try {
print('MQTT: Subscribing to $topic'); // print('MQTT: Subscribing to $topic');
client!.subscribe(topic, MqttQos.atMostOnce); // client!.subscribe(topic, MqttQos.atMostOnce);
print('MQTT: Subscribed to $topic'); // print('MQTT: Subscribed to $topic');
// Tambahkan log ini untuk memastikan bahwa listener dijalankan // // Tambahkan log ini untuk memastikan bahwa listener dijalankan
if (client!.updates != null) { // if (client!.updates != null) {
print('MQTT: Listening for updates...'); // print('MQTT: Listening for updates...');
} else { // } else {
print('MQTT: No updates stream available'); // print('MQTT: No updates stream available');
} // }
client!.updates!.listen( // client!.updates!.listen(
(List<MqttReceivedMessage<MqttMessage?>>? messages) { // (List<MqttReceivedMessage<MqttMessage?>>? messages) {
print('MQTT: Subscribe Message received!'); // print('MQTT: Subscribe Message received!');
if (messages != null && messages.isNotEmpty) { // if (messages != null && messages.isNotEmpty) {
final MqttPublishMessage recMessage = // final MqttPublishMessage recMessage =
messages[0].payload as MqttPublishMessage; // messages[0].payload as MqttPublishMessage;
final String payload = MqttPublishPayload.bytesToStringAsString( // final String payload = MqttPublishPayload.bytesToStringAsString(
recMessage.payload.message); // recMessage.payload.message);
print( // print(
'MQTT: Subscribe Message received on topic ${messages[0].topic}: $payload'); // 'MQTT: Subscribe Message received on topic ${messages[0].topic}: $payload');
if (payload == 'ON') { // if (payload == 'ON') {
isActive = true; // isActive = true;
// Update UI atau provider untuk menandakan relay ON // // Update UI atau provider untuk menandakan relay ON
} else if (payload == 'OFF') { // } else if (payload == 'OFF') {
isActive = false; // isActive = false;
// Update UI atau provider untuk menandakan relay OFF // // Update UI atau provider untuk menandakan relay OFF
} else { // } else {
print('MQTT: Invalid Subscribe message received'); // print('MQTT: Invalid Subscribe message received');
} // }
} else { // } else {
print('MQTT: No Subscribe messages received'); // print('MQTT: No Subscribe messages received');
} // }
}, // },
); // );
return isActive; // return isActive;
} catch (e) { // } catch (e) {
print('MQTT: Error subscribing to $topic: $e'); // print('MQTT: Error subscribing to $topic: $e');
return isActive; // return isActive;
} // }
} else { // } else {
print('MQTT: Not connected, cannot subscribe.'); // print('MQTT: Not connected, cannot subscribe.');
return false; // return false;
} // }
} // }
Future<ResultState> subscribeToRelayStatus() async { // Future<ResultState> subscribeToRelayStatus() async {
if (client != null && // if (client != null &&
client!.connectionStatus!.state == MqttConnectionState.connected) { // client!.connectionStatus!.state == MqttConnectionState.connected) {
try { // try {
print('MQTT: Subscribing to /smartfarming/getRelayStatus'); // print('MQTT: Subscribing to /smartfarming/getRelayStatus');
client!.subscribe('smartfarming/getRelayStatus', MqttQos.atMostOnce); // client!.subscribe('smartfarming/getRelayStatus', MqttQos.atMostOnce);
print('MQTT: Subscribed to /smartfarming/getRelayStatus'); // print('MQTT: Subscribed to /smartfarming/getRelayStatus');
client!.updates! // client!.updates!
.listen((List<MqttReceivedMessage<MqttMessage?>>? messages) { // .listen((List<MqttReceivedMessage<MqttMessage?>>? messages) {
if (messages != null && messages.isNotEmpty) { // if (messages != null && messages.isNotEmpty) {
final MqttPublishMessage recMessage = // final MqttPublishMessage recMessage =
messages[0].payload as MqttPublishMessage; // messages[0].payload as MqttPublishMessage;
final String payload = MqttPublishPayload.bytesToStringAsString( // final String payload = MqttPublishPayload.bytesToStringAsString(
recMessage.payload.message); // recMessage.payload.message);
print( // print(
'MQTT: Message received on topic ${messages[0].topic}: $payload'); // 'MQTT: Message received on topic ${messages[0].topic}: $payload');
// Parse the received JSON payload // // Parse the received JSON payload
final Map<String, dynamic> relayStatus = jsonDecode(payload); // final Map<String, dynamic> relayStatus = jsonDecode(payload);
print('MQTT: Relay status: $relayStatus'); // print('MQTT: Relay status: $relayStatus');
// Assuming you are using provider, notify it with new relay status // // Assuming you are using provider, notify it with new relay status
// _updateRelayStatus(relayStatus); // // _updateRelayStatus(relayStatus);
} else { // } else {
print('MQTT: No messages received'); // print('MQTT: No messages received');
} // }
}); // });
return ResultState.hasData; // return ResultState.hasData;
} catch (e) { // } catch (e) {
print('MQTT: Error subscribing: $e'); // print('MQTT: Error subscribing: $e');
return ResultState.error; // return ResultState.error;
} // }
} else { // } else {
print('MQTT: Not connected, cannot subscribe.'); // print('MQTT: Not connected, cannot subscribe.');
return ResultState.error; // return ResultState.error;
} // }
} // }
} // }

View File

@ -1,4 +1,3 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart'; import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart'; import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/core/widgets/app_button.dart'; import 'package:agrilink_vocpro/core/widgets/app_button.dart';
@ -47,17 +46,6 @@ class LoginScreen extends StatelessWidget {
hintText: 'Masukkan password', hintText: 'Masukkan password',
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
GestureDetector(
onTap: () {
print('Forgot password?');
},
child: Text(
'Forgot password?',
textAlign: TextAlign.end,
style: AppTheme.labelMedium
.copyWith(color: AppColor.secondary),
),
),
const SizedBox(height: 24), const SizedBox(height: 24),
AppButton( AppButton(
onPressed: () { onPressed: () {

View File

@ -1,6 +1,6 @@
import 'package:agrilink_vocpro/core/state/result_state.dart'; import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart'; import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart';
class ControlProvider extends ChangeNotifier { class ControlProvider extends ChangeNotifier {
final AppService _appService = AppService(); final AppService _appService = AppService();
@ -52,7 +52,9 @@ class ControlProvider extends ChangeNotifier {
relayState = ResultState.loading; relayState = ResultState.loading;
notifyListeners(); notifyListeners();
try { try {
print('try to get relay status...'); if (kDebugMode) {
print('try to get relay status...');
}
final result = await _appService.getRelayStatus(); final result = await _appService.getRelayStatus();
if (result.success == true) { if (result.success == true) {
for (var element in result.data!) { for (var element in result.data!) {
@ -72,27 +74,13 @@ class ControlProvider extends ChangeNotifier {
} catch (e) { } catch (e) {
relayState = ResultState.error; relayState = ResultState.error;
notifyListeners(); notifyListeners();
print(e); if (kDebugMode) {
print(e);
}
rethrow; rethrow;
} }
} }
// Future<void> disconnectMqtt() async {
// try {
// await _mqttService.disconnectMqtt();
// } catch (e) {
// print(e);
// rethrow;
// }
// notifyListeners();
// }
// @override
// void dispose() {
// disconnectMqtt();
// super.dispose();
// }
void switchControl1(bool value) { void switchControl1(bool value) {
_control_1 = value; _control_1 = value;
notifyListeners(); notifyListeners();

View File

@ -1,65 +0,0 @@
class Npk1SoilTempGrafik {
Data? data;
int? statusCode;
String? message;
Npk1SoilTempGrafik({this.data, this.statusCode, this.message});
Npk1SoilTempGrafik.fromJson(Map<String, dynamic> json) {
data = json['data'] != null ? Data.fromJson(json['data']) : null;
statusCode = json['statusCode'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (this.data != null) {
data['data'] = this.data!.toJson();
}
data['statusCode'] = statusCode;
data['message'] = message;
return data;
}
}
class Data {
List<Npk1>? npk1;
Data({this.npk1});
Data.fromJson(Map<String, dynamic> json) {
if (json['npk1'] != null) {
npk1 = <Npk1>[];
json['npk1'].forEach((v) {
npk1!.add(Npk1.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (npk1 != null) {
data['npk1'] = npk1!.map((v) => v.toJson()).toList();
}
return data;
}
}
class Npk1 {
num? hour;
num? soiltemperatureAvg;
Npk1({this.hour, this.soiltemperatureAvg});
Npk1.fromJson(Map<String, dynamic> json) {
hour = json['hour'];
soiltemperatureAvg = json['soiltemperature_avg'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['hour'] = hour;
data['soiltemperature_avg'] = soiltemperatureAvg;
return data;
}
}

View File

@ -0,0 +1,34 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/dht_graphic_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class HumidityProvider extends ChangeNotifier {
HumidityProvider() {
getHumidityData();
}
ResultState dataState = ResultState.initial;
List<Dht> dataFetched = [];
Future<void> getHumidityData() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGrafikDataDht(metric: 'viciHumidity');
if (result.data == null || result.data!.dht!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetched = result.data!.dht ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Humidity Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,4 +1,6 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart'; import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/humidity/provider/humidity_provider.dart';
import 'package:agrilink_vocpro/features/home/provider/home_provider.dart'; import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart'; import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:bootstrap_icons/bootstrap_icons.dart';
@ -16,153 +18,185 @@ class HumidityScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return ChangeNotifierProvider(
appBar: AppBar( create: (context) => HumidityProvider(),
title: Text('Humidity', style: AppTheme.labelMedium), child: Scaffold(
centerTitle: true, appBar: AppBar(
scrolledUnderElevation: 0, title: Text('Humidity', style: AppTheme.labelMedium),
leading: IconButton( centerTitle: true,
icon: const Icon(CupertinoIcons.back), scrolledUnderElevation: 0,
onPressed: () => Navigator.pop(context), leading: IconButton(
), icon: const Icon(CupertinoIcons.back),
backgroundColor: Colors.white, onPressed: () => Navigator.pop(context),
actions: const [ ),
Padding( backgroundColor: Colors.white,
padding: EdgeInsets.only(right: 16), actions: const [
child: Icon( Padding(
BootstrapIcons.droplet_half, padding: EdgeInsets.only(right: 16),
color: Colors.blue, child: Icon(
), BootstrapIcons.droplet_half,
) color: Colors.blue,
],
),
body: SafeArea(
child: Consumer<HomeProvider>(builder: (context, provider, child) {
return ListView(
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
), ),
SizedBox( )
height: 280.h, ],
child: Stack( ),
fit: StackFit.expand, body: SafeArea(
children: [ child: Consumer<HomeProvider>(builder: (context, provider, child) {
Center( return ListView(
child: Column( children: [
mainAxisSize: MainAxisSize.min, SizedBox(
children: [ height: MediaQuery.of(context).size.height * 0.05,
const Icon(BootstrapIcons.droplet_half, ),
size: 32, color: Colors.blue), SizedBox(
Text('$humidity %', style: AppTheme.headline1), height: 280.h,
], child: Stack(
fit: StackFit.expand,
children: [
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(BootstrapIcons.droplet_half,
size: 32, color: Colors.blue),
Text('$humidity %', style: AppTheme.headline1),
],
),
), ),
), RotatedBox(
RotatedBox( quarterTurns: 2,
quarterTurns: 2, child: AnimatedRadialGauge(
child: AnimatedRadialGauge( duration: const Duration(seconds: 3),
duration: const Duration(seconds: 3), curve: Curves.easeOut,
curve: Curves.easeOut, value: humidity,
value: humidity, axis: GaugeAxis(
axis: GaugeAxis( degrees: 360,
degrees: 360, min: 0,
min: 0, max: 100,
max: 100, pointer: null,
pointer: null, style: GaugeAxisStyle(
style: GaugeAxisStyle( background: Colors.grey.shade100,
background: Colors.grey.shade100, thickness: 50,
thickness: 50, ),
), progressBar: GaugeBasicProgressBar(
progressBar: GaugeBasicProgressBar( gradient: GaugeAxisGradient(colors: [
gradient: GaugeAxisGradient(colors: [ Colors.blue.shade200,
Colors.blue.shade200, Colors.blue,
Colors.blue, ]),
]), ),
), ),
), ),
), ),
),
],
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Humidity',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
),
const SizedBox(height: 16),
Padding(
padding: EdgeInsets.only(left: 16.w),
child: const Text('Grafik dalam 7 hari terakhir'),
),
const SizedBox(height: 16),
AspectRatio(
aspectRatio: 2.h,
child: Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
border:
Border.all(color: Colors.grey.shade300, width: 1.w)),
child: GarphicWidget(
gradientColors: [
Colors.blue.shade200,
Colors.blue,
], ],
), ),
), ),
), const SizedBox(height: 16),
SizedBox(height: 16.h), Row(
Padding( mainAxisAlignment: MainAxisAlignment.center,
padding: EdgeInsets.only(left: 16.w), children: [
child: const Text('Deskripsi'), Text(
), 'Humidity',
// ListView.builder( style: AppTheme.labelMedium,
// shrinkWrap: true, textAlign: TextAlign.center,
// physics: const NeverScrollableScrollPhysics(), ),
// itemCount: provider.humidtyRules.length, IconButton(
// itemBuilder: (context, index) { iconSize: 20.r,
// final item = provider.humidtyRules[index]; color: Colors.blue,
// return Theme( onPressed: () {},
// data: Theme.of(context) icon: const Icon(BootstrapIcons.info_circle))
// .copyWith(dividerColor: Colors.transparent), ],
// child: ExpansionTile( ),
// trailing: Text( const SizedBox(height: 16),
// item.censorText, Padding(
// style: TextStyle(color: item.color), padding: EdgeInsets.only(left: 16.w),
// ), child: const Text('Grafik dalam 7 hari terakhir'),
// expandedCrossAxisAlignment: CrossAxisAlignment.start, ),
// childrenPadding: EdgeInsets.all(16.r), const SizedBox(height: 16),
// title: Text( AspectRatio(
// 'Kelembaban ${item.minPercentage}% - ${item.maxPercentage}%'), aspectRatio: 2.h,
// children: [ child: Container(
// Text( margin: EdgeInsets.symmetric(horizontal: 16.w),
// item.description, decoration: BoxDecoration(
// style: AppTheme.labelMedium, color: Colors.white,
// ), borderRadius: BorderRadius.circular(16.w),
// SizedBox(height: 8.h), border: Border.all(
// Text('Tindakan', style: AppTheme.labelSmall), color: Colors.grey.shade300, width: 1.w)),
// SizedBox(height: 8.h), child: Consumer<HumidityProvider>(
// Text(item.action), builder: (context, provider, child) {
// ], switch (provider.dataState) {
// ), case ResultState.loading:
// ); return const Center(
// }) child: CupertinoActivityIndicator(),
], );
); case ResultState.hasData:
}), return GarphicWidget(
gradientColors: const [
Colors.cyan,
Colors.amber,
],
hour: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].hour ?? 0),
data: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].vicihumidityAvg
?.toDouble() ??
0),
);
case ResultState.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.exclamation_circle,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Terjadi Kesalahan',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.noData:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.database_fill_x,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Tidak Ada Data',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.initial:
return const SizedBox.shrink();
default:
return const Center(
child: Text('Default Error'),
);
}
}),
),
),
SizedBox(height: 16.h),
Padding(
padding: EdgeInsets.only(left: 16.w),
child: const Text('Deskripsi'),
),
],
);
}),
),
), ),
); );
} }

View File

@ -1,224 +0,0 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
class LightScreen extends StatelessWidget {
const LightScreen({super.key, this.lightIntensity = 0});
final double lightIntensity;
double get value => lightIntensity;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Light', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
BootstrapIcons.sun,
color: Colors.yellow.shade600,
),
)
],
),
body: SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
SizedBox(
height: 240.h,
child: Stack(
fit: StackFit.expand,
children: [
const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(BootstrapIcons.sun,
size: 32, color: Colors.orange),
],
),
),
AnimatedRadialGauge(
duration: const Duration(seconds: 3),
curve: Curves.easeOut,
value: value,
axis: GaugeAxis(
degrees: 360,
min: 0,
max: 1000,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 100,
),
progressBar: GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.yellow.shade100,
Colors.orange.shade200,
]),
),
),
),
],
),
),
const SizedBox(height: 16),
Text(
'${value.toStringAsFixed(0)} lux',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Intensitas Cahaya',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
aspectRatio: 1.8.h,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300, width: 1.w),
borderRadius: BorderRadius.circular(16.w),
),
child: GarphicWidget(
gradientColors: [
Colors.yellow.shade100,
Colors.orange.shade200,
],
),
),
)
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceAround,
// children: [
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(16),
// color: Colors.blue.withOpacity(0.1),
// border: Border.all(
// color: Colors.blue,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('Low',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.blue)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_low,
// // color: Colors.blue,
// // ),
// SizedBox(height: 8.h),
// Text(
// '<20°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// color: Colors.green.withOpacity(0.1),
// borderRadius: BorderRadius.circular(16),
// border: Border.all(
// color: Colors.green,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('Ideal',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.green)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_half,
// // color: Colors.green,
// // ),
// SizedBox(height: 8.h),
// Text(
// '20-30°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// color: Colors.orange.withOpacity(0.1),
// borderRadius: BorderRadius.circular(16),
// border: Border.all(
// color: Colors.orange.shade800,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('high',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.orange)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_high,
// // color: Colors.orange,
// // ),
// SizedBox(height: 8.h),
// Text(
// '>30°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// ],
// )
],
),
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/dht_graphic_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class LumProvider extends ChangeNotifier {
LumProvider() {
getLumData();
}
ResultState dataState = ResultState.initial;
List<Dht> dataFetched = [];
Future<void> getLumData() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGrafikDataDht(metric: 'viciLuminosity');
if (result.data == null || result.data!.dht!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetched = result.data!.dht ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Luminosity Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -0,0 +1,191 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/luminosity/provider/lum_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:provider/provider.dart';
class LightScreen extends StatelessWidget {
const LightScreen({super.key, this.lightIntensity = 0});
final double lightIntensity;
double get value => lightIntensity;
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LumProvider(),
child: Scaffold(
appBar: AppBar(
title: Text('Light', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
BootstrapIcons.sun,
color: Colors.yellow.shade600,
),
)
],
),
body: SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
SizedBox(
height: 240.h,
child: Stack(
fit: StackFit.expand,
children: [
const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(BootstrapIcons.sun,
size: 32, color: Colors.orange),
],
),
),
AnimatedRadialGauge(
duration: const Duration(seconds: 3),
curve: Curves.easeOut,
value: value,
axis: GaugeAxis(
degrees: 360,
min: 0,
max: 1000,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 100,
),
progressBar: GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.yellow.shade100,
Colors.orange.shade200,
]),
),
),
),
],
),
),
const SizedBox(height: 16),
Text(
'${value.toStringAsFixed(0)} lux',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Intensitas Cahaya',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
aspectRatio: 1.8.h,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300, width: 1.w),
borderRadius: BorderRadius.circular(16.w),
),
child: Consumer<LumProvider>(
builder: (context, provider, child) {
switch (provider.dataState) {
case ResultState.loading:
return const Center(
child: CupertinoActivityIndicator(),
);
case ResultState.hasData:
return GarphicWidget(
gradientColors: [
Colors.yellow.shade100,
Colors.orange.shade200,
],
hour: List.generate(provider.dataFetched.length,
(index) => provider.dataFetched[index].hour ?? 0),
data: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].vicihumidityAvg
?.toDouble() ??
0),
);
case ResultState.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.exclamation_circle,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Terjadi Kesalahan',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.noData:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.database_fill_x,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Tidak Ada Data',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.initial:
return const SizedBox.shrink();
default:
return const Center(
child: Text('Default Error'),
);
}
}),
),
)
],
),
),
),
);
}
}

View File

@ -1,47 +1,58 @@
import 'package:agrilink_vocpro/core/state/result_state.dart'; import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/model/npk1_soil_temp_grafik.dart'; import 'package:agrilink_vocpro/data/model/npk1_graphic_response.dart';
import 'package:agrilink_vocpro/features/home/service/home_service.dart'; import 'package:agrilink_vocpro/data/model/npk2_graphic_response.dart';
import 'package:flutter/material.dart'; import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class SoilTempProvider extends ChangeNotifier { class SoilTempProvider extends ChangeNotifier {
SoilTempProvider() { SoilTempProvider() {
getSoilTempData(); getSoilTempNpk1Data();
getSoilTempNpk2Data();
} }
List<Npk1> dataFetched = [];
void setSoilTempData(List<Npk1> data) {
dataFetched = data;
notifyListeners();
}
ResultState dataState = ResultState.initial; ResultState dataState = ResultState.initial;
Future<void> getSoilTempData() async { List<Npk1> dataFetchedNpk1 = [];
List<Npk2> dataFetchedNpk2 = [];
Future<void> getSoilTempNpk1Data() async {
dataState = ResultState.loading; dataState = ResultState.loading;
notifyListeners(); notifyListeners();
try { try {
final result = await HomeService().getNpk1SoilTempGrafik(); final result =
await AppService().getGraphicDataNpk1(metric: 'soilTemperature');
if (result.data == null || result.data!.npk1!.isEmpty) { if (result.data == null || result.data!.npk1!.isEmpty) {
dataState = ResultState.noData; dataState = ResultState.noData;
notifyListeners();
return;
} else { } else {
setSoilTempData(result.data?.npk1 ?? []); dataFetchedNpk1 = result.data!.npk1 ?? [];
dataState = ResultState.hasData; dataState = ResultState.hasData;
notifyListeners();
} }
} catch (e) { } catch (e) {
print('Error: $e'); if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error; dataState = ResultState.error;
notifyListeners();
} }
notifyListeners();
} }
@override Future<void> getSoilTempNpk2Data() async {
void dispose() { dataState = ResultState.loading;
dataState = ResultState.initial; notifyListeners();
dataFetched = []; try {
super.dispose(); final result =
await AppService().getGraphicDataNpk2(metric: 'soilTemperature');
if (result.data == null || result.data!.npk2!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk2 = result.data!.npk2 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
} }
} }

View File

@ -1,288 +1,242 @@
import 'package:agrilink_vocpro/features/home/widgets/graphic_error_widget.dart';
import 'package:flutter/material.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:agrilink_vocpro/core/constant/app_constant.dart'; import 'package:agrilink_vocpro/core/constant/app_constant.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart'; import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart'; import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/core/widgets/show_info.dart'; import 'package:agrilink_vocpro/core/widgets/show_info.dart';
import 'package:agrilink_vocpro/features/home/pages/soil_temperature/provider/soil_temp_provider.dart'; import 'package:agrilink_vocpro/features/home/pages/soil_temperature/provider/soil_temp_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart'; import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart'; import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:provider/provider.dart';
class SoilTemperatureScreen extends StatelessWidget { class SoilTemperatureScreen extends StatelessWidget {
const SoilTemperatureScreen({super.key, this.temperature = 0}); const SoilTemperatureScreen({
super.key,
this.npk1Temperature = 0,
this.npk2Temperature = 0,
});
final double temperature; final double npk1Temperature;
double get value => temperature; final double npk2Temperature;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider( return ChangeNotifierProvider(
create: (context) => SoilTempProvider(), create: (context) => SoilTempProvider(),
child: Scaffold( child: DefaultTabController(
appBar: AppBar( length: 2,
title: Text('Soil Temperature', style: AppTheme.labelMedium), child: Scaffold(
centerTitle: true, appBar: AppBar(
backgroundColor: Colors.white, title: Text('Soil Temperature', style: AppTheme.labelMedium),
scrolledUnderElevation: 0, centerTitle: true,
actions: const [ backgroundColor: Colors.white,
Padding( scrolledUnderElevation: 0,
padding: EdgeInsets.only(right: 16), actions: const [
child: Icon( Padding(
BootstrapIcons.water, padding: EdgeInsets.only(right: 16),
color: Colors.green, child: Icon(
), BootstrapIcons.water,
) color: Colors.green,
],
),
body: SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
SizedBox(
height: 240.h,
child: Stack(
fit: StackFit.expand,
children: [
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 80.h,
),
const Icon(BootstrapIcons.water,
size: 32, color: Colors.green),
Text(
'$value°C', // Animated percentage text
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
),
),
AnimatedRadialGauge(
duration: const Duration(seconds: 2),
curve: Curves.easeOut,
value: value,
axis: GaugeAxis(
degrees: 240,
min: 0,
max: 56.7,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 50,
),
progressBar: const GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.blue,
Colors.orange,
]),
),
),
),
],
),
),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Soil Temperature',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {
showInfo(
context,
'Soil Temperature',
AppConstant.soilTempInfo,
);
},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.blue.withOpacity(0.1),
border: Border.all(
color: Colors.blue,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Low',
style: AppTheme.labelMedium
.copyWith(color: Colors.blue)),
SizedBox(height: 8.h),
Text(
'<20°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.green,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ideal',
style: AppTheme.labelMedium
.copyWith(color: Colors.green)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_half,
// color: Colors.green,
// ),
SizedBox(height: 8.h),
Text(
'20-30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.orange.shade800,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('high',
style: AppTheme.labelMedium
.copyWith(color: Colors.orange)),
SizedBox(height: 8.h),
Text(
'>30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
],
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: Consumer<SoilTempProvider>(
builder: (context, provider, child) {
switch (provider.dataState) {
case ResultState.loading:
return const Center(
child: CircularProgressIndicator(),
);
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [
Colors.cyan,
Colors.amber,
],
hour: List.generate(provider.dataFetched.length,
(index) => provider.dataFetched[index].hour ?? 0),
data: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].soiltemperatureAvg
?.toDouble() ??
0),
);
case ResultState.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.exclamation_circle,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Terjadi Kesalahan',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.noData:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.database_fill_x,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Tidak Ada Data',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.initial:
return const SizedBox.shrink();
default:
return const Center(
child: Text('Default Error'),
);
}
}),
), ),
) )
], ],
bottom: const TabBar(
tabs: [
Tab(text: 'NPK 1'),
Tab(text: 'NPK 2'),
],
),
),
body: TabBarView(
children: [
buildTabContent(context, npk1Temperature, 'NPK 1', true),
buildTabContent(context, npk2Temperature, 'NPK 2', false),
],
), ),
), ),
), ),
); );
} }
// Generalized method for tab content to avoid duplication
SafeArea buildTabContent(
BuildContext context, double value, String label, bool isNpk1) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
buildTemperatureInfo(context, value),
SizedBox(height: 16.h),
buildInfoRow(context),
SizedBox(height: 16.h),
buildTemperatureRange(),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
buildGraphicContent(context, isNpk1),
],
),
);
}
Widget buildTemperatureInfo(BuildContext context, double value) {
return SizedBox(
height: 240.h,
child: Stack(
fit: StackFit.expand,
children: [
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 80.h),
const Icon(BootstrapIcons.water, size: 32, color: Colors.green),
Text(
'$value°C', // Display temperature
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
),
),
AnimatedRadialGauge(
duration: const Duration(seconds: 2),
curve: Curves.easeOut,
value: value,
axis: GaugeAxis(
degrees: 240,
min: 0,
max: 56.7,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 50,
),
progressBar: const GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.blue,
Colors.orange,
]),
),
),
),
],
),
);
}
// Build the row with 'Low', 'Ideal', 'High' temperature containers
Widget buildTemperatureRange() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildInfoContainer('Low', '<20°C', Colors.blue),
_buildInfoContainer('Ideal', '20-30°C', Colors.green),
_buildInfoContainer('High', '>30°C', Colors.orange),
],
);
}
// Reusable container builder
Widget _buildInfoContainer(String label, String tempRange, Color color) {
return Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color, width: 2),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label, style: AppTheme.labelMedium.copyWith(color: color)),
SizedBox(height: 8.h),
Text(tempRange,
style: AppTheme.labelMedium, textAlign: TextAlign.center),
],
),
);
}
// Information row with IconButton
Widget buildInfoRow(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Soil Temperature',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {
showInfo(context, 'Soil Temperature', AppConstant.soilTempInfo);
},
icon: const Icon(BootstrapIcons.info_circle),
),
],
);
}
// Generalized method to build the graphic content based on NPK type
Widget buildGraphicContent(BuildContext context, bool isNpk1) {
return AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: Consumer<SoilTempProvider>(
builder: (context, provider, child) {
final dataState = provider.dataState;
switch (dataState) {
case ResultState.loading:
return const Center(child: CupertinoActivityIndicator());
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [Colors.cyan, Colors.amber],
hour: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].hour ?? 0
: provider.dataFetchedNpk2[index].hour ?? 0,
),
data: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].soiltemperatureAvg ??
0
: provider.dataFetchedNpk2[index].soiltemperatureAvg ??
0,
),
);
case ResultState.error:
return const GraphicErrorWidget(message: 'Terjadi Kesalahan');
case ResultState.noData:
return const GraphicErrorWidget(message: 'Tidak Ada Data');
case ResultState.initial:
default:
return const SizedBox.shrink();
}
},
),
),
);
}
} }

View File

@ -0,0 +1,34 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/dht_graphic_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class TempProvider extends ChangeNotifier {
TempProvider() {
getTempData();
}
ResultState dataState = ResultState.initial;
List<Dht> dataFetched = [];
Future<void> getTempData() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGrafikDataDht(metric: 'viciTemperature');
if (result.data == null || result.data!.dht!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetched = result.data!.dht ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Temperature Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,9 +1,13 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart'; import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/temperature/provider/temp_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart'; import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart'; import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:provider/provider.dart';
class TemperatureScreen extends StatelessWidget { class TemperatureScreen extends StatelessWidget {
const TemperatureScreen({super.key, this.temperature = 0}); const TemperatureScreen({super.key, this.temperature = 0});
@ -13,213 +17,264 @@ class TemperatureScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return ChangeNotifierProvider(
appBar: AppBar( create: (context) => TempProvider(),
title: Text('Temperature', style: AppTheme.labelMedium), child: Scaffold(
centerTitle: true, appBar: AppBar(
backgroundColor: Colors.white, title: Text('Temperature', style: AppTheme.labelMedium),
scrolledUnderElevation: 0, centerTitle: true,
actions: const [ backgroundColor: Colors.white,
Padding( scrolledUnderElevation: 0,
padding: EdgeInsets.only(right: 16), actions: const [
child: Icon( Padding(
BootstrapIcons.thermometer_half, padding: EdgeInsets.only(right: 16),
color: Colors.red, child: Icon(
), BootstrapIcons.thermometer_half,
) color: Colors.red,
], ),
), )
body: SafeArea( ],
child: ListView( ),
padding: EdgeInsets.all(16.w), body: SafeArea(
children: [ child: ListView(
SizedBox( padding: EdgeInsets.all(16.w),
height: MediaQuery.of(context).size.height * 0.05, children: [
), SizedBox(
SizedBox( height: MediaQuery.of(context).size.height * 0.05,
height: 240.h, ),
child: Stack( SizedBox(
fit: StackFit.expand, height: 240.h,
children: [ child: Stack(
Center( fit: StackFit.expand,
child: Column( children: [
mainAxisSize: MainAxisSize.min, Center(
children: [ child: Column(
SizedBox( mainAxisSize: MainAxisSize.min,
height: 80.h, children: [
), SizedBox(
const Icon(BootstrapIcons.thermometer_half, height: 80.h,
size: 32, color: Colors.orange),
Text(
'${value.toStringAsFixed(0)}°C', // Animated percentage text
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
), ),
const Icon(BootstrapIcons.thermometer_half,
size: 32, color: Colors.orange),
Text(
'${value.toStringAsFixed(0)}°C', // Animated percentage text
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
),
),
AnimatedRadialGauge(
duration: const Duration(seconds: 2),
curve: Curves.easeOut,
value: value,
axis: GaugeAxis(
degrees: 240,
min: 0,
max: 56.7,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 50,
),
progressBar: const GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.white12,
Colors.orange,
]),
),
),
),
],
),
),
const SizedBox(height: 16),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Temperature',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.blue.withOpacity(0.1),
border: Border.all(
color: Colors.blue,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Low',
style: AppTheme.labelMedium
.copyWith(color: Colors.blue)),
SizedBox(height: 8.h),
Text(
'<20°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
), ),
], ],
), ),
), ),
AnimatedRadialGauge( Container(
duration: const Duration(seconds: 2), height: 100.h,
curve: Curves.easeOut, width: 100.w,
value: value, decoration: BoxDecoration(
axis: GaugeAxis( color: Colors.green.withOpacity(0.1),
degrees: 240, borderRadius: BorderRadius.circular(16),
min: 0, border: Border.all(
max: 56.7, color: Colors.green,
style: GaugeAxisStyle( width: 2,
background: Colors.grey.shade100,
thickness: 50,
), ),
progressBar: const GaugeBasicProgressBar( ),
gradient: GaugeAxisGradient(colors: [ child: Column(
Colors.white12, mainAxisAlignment: MainAxisAlignment.center,
Colors.orange, children: [
]), Text('Ideal',
style: AppTheme.labelMedium
.copyWith(color: Colors.green)),
SizedBox(height: 8.h),
Text(
'20-30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.orange.shade800,
width: 2,
), ),
), ),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('high',
style: AppTheme.labelMedium
.copyWith(color: Colors.orange)),
SizedBox(height: 8.h),
Text(
'>30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
), ),
], ],
), ),
), SizedBox(height: 16.h),
const SizedBox(height: 16), const Text('Grafik'),
const SizedBox(height: 16), SizedBox(height: 16.h),
Row( AspectRatio(
mainAxisAlignment: MainAxisAlignment.center, aspectRatio: 1.6.h,
children: [ child: Container(
Text(
'Temperature',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), color: Colors.white,
color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(16.w),
border: Border.all( border: Border.all(color: Colors.grey.shade300, width: 1.w),
color: Colors.blue,
width: 2,
),
), ),
child: Column( child: Consumer<TempProvider>(
mainAxisAlignment: MainAxisAlignment.center, builder: (context, provider, child) {
children: [ switch (provider.dataState) {
Text('Low', case ResultState.loading:
style: AppTheme.labelMedium return const Center(
.copyWith(color: Colors.blue)), child: CupertinoActivityIndicator(),
// SizedBox(height: 8.h), );
// const Icon(
// BootstrapIcons.thermometer_low, case ResultState.hasData:
// color: Colors.blue, return GarphicWidget(
// ), gradientColors: const [
SizedBox(height: 8.h), Colors.cyan,
Text( Colors.amber,
'<20°C', ],
style: AppTheme.labelMedium, hour: List.generate(
textAlign: TextAlign.center, provider.dataFetched.length,
), (index) =>
], provider.dataFetched[index].hour ?? 0),
data: List.generate(
provider.dataFetched.length,
(index) =>
provider
.dataFetched[index].vicitemperatureAvg
?.toDouble() ??
0),
);
case ResultState.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.exclamation_circle,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Terjadi Kesalahan',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.noData:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.database_fill_x,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Tidak Ada Data',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.initial:
return const SizedBox.shrink();
default:
return const Center(
child: Text('Default Error'),
);
}
},
), ),
), ),
Container( )
height: 100.h, ],
width: 100.w, ),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.green,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ideal',
style: AppTheme.labelMedium
.copyWith(color: Colors.green)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_half,
// color: Colors.green,
// ),
SizedBox(height: 8.h),
Text(
'20-30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.orange.shade800,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('high',
style: AppTheme.labelMedium
.copyWith(color: Colors.orange)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_high,
// color: Colors.orange,
// ),
SizedBox(height: 8.h),
Text(
'>30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
],
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: const GarphicWidget(
gradientColors: [
Colors.cyan,
Colors.amber,
],
),
),
)
],
), ),
), ),
); );

View File

@ -1,6 +1,6 @@
import 'package:agrilink_vocpro/core/state/result_state.dart'; import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart'; import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart';
class HomeProvider extends ChangeNotifier { class HomeProvider extends ChangeNotifier {
final DateTime currentDate = DateTime.now(); final DateTime currentDate = DateTime.now();
@ -95,8 +95,10 @@ class HomeProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
} catch (e) { } catch (e) {
print('Error: $e'); if (kDebugMode) {
dataState = ResultState.error; print('Get Latest Error: $e');
}
dataState = ResultState.hasData;
notifyListeners(); notifyListeners();
} }
} }

View File

@ -1,29 +1 @@
import 'package:agrilink_vocpro/core/constant/app_constant.dart';
import 'package:agrilink_vocpro/features/home/model/npk1_soil_temp_grafik.dart';
import 'package:dio/dio.dart';
class HomeService {
final Dio _dioWithoutInterceptor = Dio(
BaseOptions(
baseUrl: AppConstant.baseUrl,
),
);
Future<Npk1SoilTempGrafik> getNpk1SoilTempGrafik() async {
try {
final result = await _dioWithoutInterceptor.get(
'/sensor/getData?metric=soilTemperature&range[start]=2024-10-03&range[end]=2024-10-03&range[time_range]=HOURLY&sensor=npk1',
);
if (result.statusCode == 200) {
print(result.data.toString());
final data = Npk1SoilTempGrafik.fromJson(result.data);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
print(e);
rethrow;
}
}
}

View File

@ -0,0 +1,24 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class GraphicErrorWidget extends StatelessWidget {
const GraphicErrorWidget({super.key, required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(BootstrapIcons.exclamation_circle, color: Colors.grey.shade400),
SizedBox(height: 8.h),
Text(message, style: AppTheme.labelSmall),
],
),
);
}
}

View File

@ -58,7 +58,7 @@ class ListDataFromCensorNpk1 extends StatelessWidget {
censorIdentifier: censorIdentifier, censorIdentifier: censorIdentifier,
onTap: () async { onTap: () async {
await context.push( await context.push(
'${AppRoute.soilTemperature}/${provider.npk1Temperature}'); '${AppRoute.soilTemperature}/${provider.npk1Temperature}/${provider.npk2Temperature}');
}, },
), ),
DataDisplayerWidget( DataDisplayerWidget(

View File

@ -31,20 +31,21 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (context) => ControlProvider()), ChangeNotifierProvider(create: (context) => ControlProvider()),
], ],
child: ScreenUtilInit( child: ScreenUtilInit(
designSize: const Size(360, 800), designSize: const Size(360, 800),
minTextAdapt: true, minTextAdapt: true,
builder: (_, context) { builder: (_, context) {
return MaterialApp.router( return MaterialApp.router(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Flutter Demo', title: 'Flutter Demo',
theme: ThemeData( theme: ThemeData(
scaffoldBackgroundColor: Colors.white, scaffoldBackgroundColor: Colors.white,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true, useMaterial3: true,
), ),
routerConfig: AppRoute.router, routerConfig: AppRoute.router,
); );
}), },
),
); );
} }
} }