mirror of
https://github.com/SyncrowIOT/web.git
synced 2026-10-26 10:27:56 +00:00
Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95cded4bf5 | ||
|
|
757a96ed9f | ||
|
|
b857736e10 | ||
|
|
1fccd51440 | ||
|
|
c07ddb0ccd | ||
|
|
58e99f95b2 | ||
|
|
227df6fe3d | ||
|
|
9451ec0cc4 | ||
|
|
fc797c2646 | ||
|
|
318e1d9af7 | ||
|
|
d47dc349bc | ||
|
|
c221c8499f | ||
|
|
71cf4b9feb | ||
|
|
c43cf9347f | ||
|
|
9990b1805e | ||
|
|
009b7c0316 | ||
|
|
779c0fe916 | ||
|
|
e448eabda6 | ||
|
|
9dfb3ed369 | ||
|
|
63353af38b | ||
|
|
68b6c9b18c | ||
|
|
fa6ee9a0af | ||
|
|
3601b02bc3 | ||
|
|
fdd0526c78 | ||
|
|
bdeec7d325 | ||
|
|
50ff17a0c1 | ||
|
|
87c2e3261d | ||
|
|
62a6f9c993 | ||
|
|
f7e4d6ff07 | ||
|
|
95907661d2 | ||
|
|
9c9b7d99dc | ||
|
|
c07bae5cbc | ||
|
|
8cb6c13cd5 | ||
|
|
949c27938a | ||
|
|
4c582b865d | ||
|
|
d7467adeda | ||
|
|
5486f0832d | ||
|
|
fd239a3907 | ||
|
|
e2d6f5eea8 | ||
|
|
289922071a | ||
|
|
8594168548 | ||
|
|
bd9a74b380 | ||
|
|
15ee79688d | ||
|
|
e5e88385e9 | ||
|
|
62d5bbce7e |
@@ -20,6 +20,7 @@ class AqiDistributionChart extends StatelessWidget {
|
|||||||
return BarChart(
|
return BarChart(
|
||||||
BarChartData(
|
BarChartData(
|
||||||
maxY: 100.1,
|
maxY: 100.1,
|
||||||
|
alignment: BarChartAlignment.start,
|
||||||
gridData: EnergyManagementChartsHelper.gridData(
|
gridData: EnergyManagementChartsHelper.gridData(
|
||||||
horizontalInterval: 20,
|
horizontalInterval: 20,
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: _padding,
|
padding: _padding,
|
||||||
height: MediaQuery.sizeOf(context).height * 1,
|
height: MediaQuery.sizeOf(context).height * 1.05,
|
||||||
child: const Column(
|
child: const Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class OccupancyChart extends StatelessWidget {
|
|||||||
return BarChart(
|
return BarChart(
|
||||||
BarChartData(
|
BarChartData(
|
||||||
maxY: 100.001,
|
maxY: 100.001,
|
||||||
|
alignment: BarChartAlignment.start,
|
||||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||||
checkToShowHorizontalLine: (value) => true,
|
checkToShowHorizontalLine: (value) => true,
|
||||||
horizontalInterval: 20,
|
horizontalInterval: 20,
|
||||||
|
|||||||
+2
-2
@@ -17,8 +17,8 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
|
|||||||
'reverse',
|
'reverse',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
'format': 'json',
|
'format': 'json',
|
||||||
'lat': param.latitude,
|
'lat': 25.1880567,
|
||||||
'lon': param.longitude,
|
'lon': 55.266608,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ class Status {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory Status.fromJson(String source) => Status.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
Status copyWith({
|
Status copyWith({
|
||||||
String? code,
|
String? code,
|
||||||
dynamic value,
|
dynamic value,
|
||||||
@@ -66,8 +69,4 @@ class Status {
|
|||||||
value: value ?? this.value,
|
value: value ?? this.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Status.fromJson(String source) => Status.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,9 +62,10 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout {
|
|||||||
BlocProvider.of<CurtainModuleBloc>(context),
|
BlocProvider.of<CurtainModuleBloc>(context),
|
||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'CUR_2',
|
category: 'Timer',
|
||||||
code: 'control',
|
code: 'control',
|
||||||
|
countdownCode: 'Timer',
|
||||||
|
deviceType: 'CUR_2',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
+23
-31
@@ -40,7 +40,7 @@ class OneGangGlassSwitchBloc
|
|||||||
emit(OneGangGlassSwitchLoading());
|
emit(OneGangGlassSwitchLoading());
|
||||||
try {
|
try {
|
||||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||||
_listenToChanges(event.deviceId, emit);
|
_listenToChanges(event.deviceId);
|
||||||
deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||||
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
|
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -48,42 +48,28 @@ class OneGangGlassSwitchBloc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenToChanges(
|
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||||
String deviceId,
|
|
||||||
Emitter<OneGangGlassSwitchState> emit,
|
void _listenToChanges(String deviceId) {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||||
final stream = ref.onValue;
|
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||||
|
if (event.snapshot.value == null) return;
|
||||||
|
|
||||||
stream.listen((DatabaseEvent event) {
|
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||||
final data = event.snapshot.value as Map<dynamic, dynamic>?;
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
final statusList = <Status>[];
|
final statusList = <Status>[];
|
||||||
if (data['status'] != null) {
|
|
||||||
for (var element in data['status']) {
|
usersMap['status'].forEach((element) {
|
||||||
statusList.add(
|
statusList.add(Status(code: element['code'], value: element['value']));
|
||||||
Status(
|
});
|
||||||
code: element['code'].toString(),
|
|
||||||
value: element['value'].toString(),
|
deviceStatus =
|
||||||
),
|
OneGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||||
);
|
|
||||||
}
|
add(StatusUpdated(deviceStatus));
|
||||||
}
|
|
||||||
if (statusList.isNotEmpty) {
|
|
||||||
final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList);
|
|
||||||
if (newStatus != deviceStatus) {
|
|
||||||
deviceStatus = newStatus;
|
|
||||||
if (!isClosed) {
|
|
||||||
add(StatusUpdated(deviceStatus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (_) {}
|
||||||
emit(OneGangGlassSwitchError('Failed to listen to changes: $e'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStatusUpdated(
|
void _onStatusUpdated(
|
||||||
@@ -174,4 +160,10 @@ class OneGangGlassSwitchBloc
|
|||||||
deviceStatus = deviceStatus.copyWith(switch1: value);
|
deviceStatus = deviceStatus.copyWith(switch1: value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_deviceStatusSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -90,6 +90,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '1GT',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ class WallLightDeviceControl extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '1G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
final success = await RemoteControlDeviceService().controlDevice(
|
final success = await RemoteControlDeviceService().controlDevice(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
status: Status(
|
status: Status(
|
||||||
code: 'countdown_1',
|
code: event.countdownCode,
|
||||||
value: 0,
|
value: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -80,15 +80,18 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
) {
|
) {
|
||||||
if (state is ScheduleLoaded) {
|
if (state is ScheduleLoaded) {
|
||||||
final currentState = state as ScheduleLoaded;
|
final currentState = state as ScheduleLoaded;
|
||||||
|
|
||||||
emit(currentState.copyWith(
|
emit(currentState.copyWith(
|
||||||
|
countdownSeconds: currentState.countdownSeconds,
|
||||||
|
selectedTime: currentState.selectedTime,
|
||||||
|
deviceId: deviceId,
|
||||||
scheduleMode: event.scheduleMode,
|
scheduleMode: event.scheduleMode,
|
||||||
countdownRemaining: Duration.zero,
|
countdownHours: currentState.countdownHours,
|
||||||
countdownHours: 0,
|
countdownMinutes: currentState.countdownMinutes,
|
||||||
countdownMinutes: 0,
|
inchingHours: currentState.inchingHours,
|
||||||
inchingHours: 0,
|
inchingMinutes: currentState.inchingMinutes,
|
||||||
inchingMinutes: 0,
|
|
||||||
isCountdownActive: false,
|
|
||||||
isInchingActive: false,
|
isInchingActive: false,
|
||||||
|
isCountdownActive: currentState.countdownRemaining > Duration.zero,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +224,6 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
deviceId,
|
deviceId,
|
||||||
event.category,
|
event.category,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state is ScheduleLoaded) {
|
if (state is ScheduleLoaded) {
|
||||||
final currentState = state as ScheduleLoaded;
|
final currentState = state as ScheduleLoaded;
|
||||||
emit(currentState.copyWith(
|
emit(currentState.copyWith(
|
||||||
@@ -285,9 +287,8 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
if (state is ScheduleLoaded) {
|
if (state is ScheduleLoaded) {
|
||||||
final dateTime = DateTime.parse(event.time);
|
|
||||||
Status status = Status(code: '', value: '');
|
Status status = Status(code: '', value: '');
|
||||||
if (event.category == 'CUR_2') {
|
if (event.deviceType == 'CUR_2') {
|
||||||
status = status.copyWith(
|
status = status.copyWith(
|
||||||
code: 'control',
|
code: 'control',
|
||||||
value: event.functionOn == true ? 'open' : 'close');
|
value: event.functionOn == true ? 'open' : 'close');
|
||||||
@@ -295,6 +296,8 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
status =
|
status =
|
||||||
status.copyWith(code: event.category, value: event.functionOn);
|
status.copyWith(code: event.category, value: event.functionOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final dateTime = DateTime.parse(event.time);
|
||||||
final updatedSchedule = ScheduleEntry(
|
final updatedSchedule = ScheduleEntry(
|
||||||
scheduleId: event.scheduleId,
|
scheduleId: event.scheduleId,
|
||||||
category: event.category,
|
category: event.category,
|
||||||
@@ -405,7 +408,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
final totalSeconds =
|
final totalSeconds =
|
||||||
Duration(hours: event.hours, minutes: event.minutes).inSeconds;
|
Duration(hours: event.hours, minutes: event.minutes).inSeconds;
|
||||||
final code = event.mode == ScheduleModes.countdown
|
final code = event.mode == ScheduleModes.countdown
|
||||||
? 'countdown_1'
|
? event.countDownCode
|
||||||
: 'switch_inching';
|
: 'switch_inching';
|
||||||
final currentState = state as ScheduleLoaded;
|
final currentState = state as ScheduleLoaded;
|
||||||
final duration = Duration(seconds: totalSeconds);
|
final duration = Duration(seconds: totalSeconds);
|
||||||
@@ -432,7 +435,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
if (code == 'countdown_1') {
|
if (code == event.countDownCode) {
|
||||||
final countdownDuration = Duration(seconds: totalSeconds);
|
final countdownDuration = Duration(seconds: totalSeconds);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
@@ -446,7 +449,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (countdownDuration.inSeconds > 0) {
|
if (countdownDuration.inSeconds > 0) {
|
||||||
_startCountdownTimer(emit, countdownDuration);
|
_startCountdownTimer(emit, countdownDuration, event.countDownCode);
|
||||||
} else {
|
} else {
|
||||||
_countdownTimer?.cancel();
|
_countdownTimer?.cancel();
|
||||||
emit(
|
emit(
|
||||||
@@ -476,9 +479,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _startCountdownTimer(
|
void _startCountdownTimer(
|
||||||
Emitter<ScheduleState> emit,
|
Emitter<ScheduleState> emit, Duration duration, String countdownCode) {
|
||||||
Duration duration,
|
|
||||||
) {
|
|
||||||
_countdownTimer?.cancel();
|
_countdownTimer?.cancel();
|
||||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
if (_currentCountdown != null && _currentCountdown! > Duration.zero) {
|
if (_currentCountdown != null && _currentCountdown! > Duration.zero) {
|
||||||
@@ -488,6 +489,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
} else {
|
} else {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
add(StopScheduleEvent(
|
add(StopScheduleEvent(
|
||||||
|
countdownCode: countdownCode,
|
||||||
mode: _currentCountdown == null
|
mode: _currentCountdown == null
|
||||||
? ScheduleModes.countdown
|
? ScheduleModes.countdown
|
||||||
: ScheduleModes.inching,
|
: ScheduleModes.inching,
|
||||||
@@ -524,70 +526,75 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
try {
|
try {
|
||||||
final status =
|
final status =
|
||||||
await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||||
print(status.status);
|
int totalSeconds = 0;
|
||||||
|
final countdownItem = status.status.firstWhere(
|
||||||
|
(item) => item.code == event.countdownCode,
|
||||||
|
orElse: () => Status(code: '', value: 0),
|
||||||
|
);
|
||||||
|
totalSeconds = (countdownItem.value as int?) ?? 0;
|
||||||
|
final countdownHours = totalSeconds ~/ 3600;
|
||||||
|
final countdownMinutes = (totalSeconds % 3600) ~/ 60;
|
||||||
|
final countdownSeconds = totalSeconds % 60;
|
||||||
|
|
||||||
final deviceStatus =
|
final deviceStatus =
|
||||||
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
|
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
|
||||||
|
final isCountdownActive = totalSeconds > 0;
|
||||||
|
final isInchingActive = !isCountdownActive &&
|
||||||
|
(deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0);
|
||||||
|
|
||||||
final scheduleMode =
|
final newState = state is ScheduleLoaded
|
||||||
deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0
|
? (state as ScheduleLoaded).copyWith(
|
||||||
? ScheduleModes.countdown
|
scheduleMode: ScheduleModes.schedule,
|
||||||
: deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0
|
countdownHours: countdownHours,
|
||||||
? ScheduleModes.inching
|
countdownMinutes: countdownMinutes,
|
||||||
: ScheduleModes.schedule;
|
countdownSeconds: countdownSeconds,
|
||||||
final isCountdown = scheduleMode == ScheduleModes.countdown;
|
inchingHours: deviceStatus.inchingHours,
|
||||||
final isInching = scheduleMode == ScheduleModes.inching;
|
inchingMinutes: deviceStatus.inchingMinutes,
|
||||||
|
isCountdownActive: isCountdownActive,
|
||||||
|
isInchingActive: isInchingActive,
|
||||||
|
countdownRemaining: isCountdownActive
|
||||||
|
? Duration(seconds: totalSeconds)
|
||||||
|
: Duration.zero,
|
||||||
|
)
|
||||||
|
: ScheduleLoaded(
|
||||||
|
scheduleMode: ScheduleModes.schedule,
|
||||||
|
schedules: const [],
|
||||||
|
selectedTime: null,
|
||||||
|
selectedDays: List.filled(7, false),
|
||||||
|
functionOn: false,
|
||||||
|
isEditing: false,
|
||||||
|
deviceId: event.deviceId,
|
||||||
|
countdownHours: countdownHours,
|
||||||
|
countdownMinutes: countdownMinutes,
|
||||||
|
countdownSeconds: countdownSeconds,
|
||||||
|
inchingHours: deviceStatus.inchingHours,
|
||||||
|
inchingMinutes: deviceStatus.inchingMinutes,
|
||||||
|
isCountdownActive: isCountdownActive,
|
||||||
|
isInchingActive: isInchingActive,
|
||||||
|
countdownRemaining: isCountdownActive
|
||||||
|
? Duration(seconds: totalSeconds)
|
||||||
|
: Duration.zero,
|
||||||
|
);
|
||||||
|
emit(newState);
|
||||||
|
|
||||||
Duration? countdownRemaining;
|
if (isCountdownActive) {
|
||||||
var isCountdownActive = false;
|
_countdownTimer?.cancel();
|
||||||
var isInchingActive = false;
|
_currentCountdown = Duration(seconds: totalSeconds);
|
||||||
|
countdownRemaining = _currentCountdown!;
|
||||||
|
|
||||||
if (isCountdown) {
|
if (totalSeconds > 0) {
|
||||||
countdownRemaining = Duration(
|
_startCountdownTimer(
|
||||||
hours: deviceStatus.countdownHours,
|
emit, Duration(seconds: totalSeconds), event.countdownCode);
|
||||||
minutes: deviceStatus.countdownMinutes,
|
} else {
|
||||||
);
|
add(StopScheduleEvent(
|
||||||
isCountdownActive = countdownRemaining > Duration.zero;
|
countdownCode: event.countdownCode,
|
||||||
} else if (isInching) {
|
mode: ScheduleModes.countdown,
|
||||||
isInchingActive = Duration(
|
deviceId: event.deviceId,
|
||||||
hours: deviceStatus.inchingHours,
|
));
|
||||||
minutes: deviceStatus.inchingMinutes,
|
}
|
||||||
) >
|
|
||||||
Duration.zero;
|
|
||||||
}
|
|
||||||
if (state is ScheduleLoaded) {
|
|
||||||
final currentState = state as ScheduleLoaded;
|
|
||||||
emit(currentState.copyWith(
|
|
||||||
scheduleMode: scheduleMode,
|
|
||||||
countdownHours: deviceStatus.countdownHours,
|
|
||||||
countdownMinutes: deviceStatus.countdownMinutes,
|
|
||||||
inchingHours: deviceStatus.inchingHours,
|
|
||||||
inchingMinutes: deviceStatus.inchingMinutes,
|
|
||||||
isCountdownActive: isCountdownActive,
|
|
||||||
isInchingActive: isInchingActive,
|
|
||||||
countdownRemaining: countdownRemaining ?? Duration.zero,
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
emit(ScheduleLoaded(
|
_countdownTimer?.cancel();
|
||||||
schedules: const [],
|
|
||||||
selectedTime: null,
|
|
||||||
selectedDays: List.filled(7, false),
|
|
||||||
functionOn: false,
|
|
||||||
isEditing: false,
|
|
||||||
deviceId: deviceId,
|
|
||||||
scheduleMode: scheduleMode,
|
|
||||||
countdownHours: deviceStatus.countdownHours,
|
|
||||||
countdownMinutes: deviceStatus.countdownMinutes,
|
|
||||||
inchingHours: deviceStatus.inchingHours,
|
|
||||||
inchingMinutes: deviceStatus.inchingMinutes,
|
|
||||||
isCountdownActive: isCountdownActive,
|
|
||||||
isInchingActive: isInchingActive,
|
|
||||||
countdownRemaining: countdownRemaining ?? Duration.zero,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (isCountdownActive && countdownRemaining != null) {
|
|
||||||
// _startCountdownTimer(emit, countdownRemaining);
|
|
||||||
// }
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(ScheduleError('Failed to fetch device status: $e'));
|
emit(ScheduleError('Failed to fetch device status: $e'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
|||||||
final String time;
|
final String time;
|
||||||
final List<String> selectedDays;
|
final List<String> selectedDays;
|
||||||
final bool functionOn;
|
final bool functionOn;
|
||||||
|
final String deviceType;
|
||||||
|
|
||||||
const ScheduleEditEvent({
|
const ScheduleEditEvent({
|
||||||
required this.scheduleId,
|
required this.scheduleId,
|
||||||
@@ -98,6 +99,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
|||||||
required this.time,
|
required this.time,
|
||||||
required this.selectedDays,
|
required this.selectedDays,
|
||||||
required this.functionOn,
|
required this.functionOn,
|
||||||
|
required this.deviceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -107,6 +109,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
|||||||
time,
|
time,
|
||||||
selectedDays,
|
selectedDays,
|
||||||
functionOn,
|
functionOn,
|
||||||
|
deviceType,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +141,13 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent {
|
|||||||
|
|
||||||
class UpdateScheduleModeEvent extends ScheduleEvent {
|
class UpdateScheduleModeEvent extends ScheduleEvent {
|
||||||
final ScheduleModes scheduleMode;
|
final ScheduleModes scheduleMode;
|
||||||
|
final String countdownCode;
|
||||||
|
|
||||||
const UpdateScheduleModeEvent({required this.scheduleMode});
|
const UpdateScheduleModeEvent(
|
||||||
|
{required this.scheduleMode, required this.countdownCode});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [scheduleMode];
|
List<Object> get props => [scheduleMode, countdownCode!];
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdateCountdownTimeEvent extends ScheduleEvent {
|
class UpdateCountdownTimeEvent extends ScheduleEvent {
|
||||||
@@ -177,28 +182,32 @@ class StartScheduleEvent extends ScheduleEvent {
|
|||||||
final ScheduleModes mode;
|
final ScheduleModes mode;
|
||||||
final int hours;
|
final int hours;
|
||||||
final int minutes;
|
final int minutes;
|
||||||
|
final String countDownCode;
|
||||||
|
|
||||||
const StartScheduleEvent({
|
const StartScheduleEvent({
|
||||||
required this.mode,
|
required this.mode,
|
||||||
required this.hours,
|
required this.hours,
|
||||||
required this.minutes,
|
required this.minutes,
|
||||||
|
required this.countDownCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [mode, hours, minutes];
|
List<Object?> get props => [mode, hours, minutes, countDownCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
class StopScheduleEvent extends ScheduleEvent {
|
class StopScheduleEvent extends ScheduleEvent {
|
||||||
final ScheduleModes mode;
|
final ScheduleModes mode;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
|
final String countdownCode;
|
||||||
|
|
||||||
const StopScheduleEvent({
|
const StopScheduleEvent({
|
||||||
required this.mode,
|
required this.mode,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
|
required this.countdownCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [mode, deviceId];
|
List<Object?> get props => [mode, deviceId, countdownCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
|
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
|
||||||
@@ -210,11 +219,13 @@ class ScheduleDecrementCountdownEvent extends ScheduleEvent {
|
|||||||
|
|
||||||
class ScheduleFetchStatusEvent extends ScheduleEvent {
|
class ScheduleFetchStatusEvent extends ScheduleEvent {
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
|
final String countdownCode;
|
||||||
|
|
||||||
const ScheduleFetchStatusEvent(this.deviceId);
|
const ScheduleFetchStatusEvent(
|
||||||
|
{required this.deviceId, required this.countdownCode});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [deviceId];
|
List<Object> get props => [deviceId, countdownCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeleteScheduleEvent extends ScheduleEvent {
|
class DeleteScheduleEvent extends ScheduleEvent {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class ScheduleLoaded extends ScheduleState {
|
|||||||
final int inchingSeconds;
|
final int inchingSeconds;
|
||||||
final bool isInchingActive;
|
final bool isInchingActive;
|
||||||
final ScheduleModes scheduleMode;
|
final ScheduleModes scheduleMode;
|
||||||
final Duration? countdownRemaining;
|
final Duration countdownRemaining;
|
||||||
final int? countdownSeconds;
|
final int? countdownSeconds;
|
||||||
|
|
||||||
const ScheduleLoaded({
|
const ScheduleLoaded({
|
||||||
@@ -48,7 +48,7 @@ class ScheduleLoaded extends ScheduleState {
|
|||||||
this.inchingMinutes = 0,
|
this.inchingMinutes = 0,
|
||||||
this.isInchingActive = false,
|
this.isInchingActive = false,
|
||||||
this.scheduleMode = ScheduleModes.countdown,
|
this.scheduleMode = ScheduleModes.countdown,
|
||||||
this.countdownRemaining,
|
this.countdownRemaining = Duration.zero,
|
||||||
});
|
});
|
||||||
|
|
||||||
ScheduleLoaded copyWith({
|
ScheduleLoaded copyWith({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
final String deviceId;
|
final String deviceId;
|
||||||
final int hours;
|
final int hours;
|
||||||
final int minutes;
|
final int minutes;
|
||||||
|
final String countDownCode;
|
||||||
|
|
||||||
const CountdownModeButtons({
|
const CountdownModeButtons({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -18,6 +19,7 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.hours,
|
required this.hours,
|
||||||
required this.minutes,
|
required this.minutes,
|
||||||
|
required this.countDownCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -43,6 +45,7 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
StopScheduleEvent(
|
StopScheduleEvent(
|
||||||
mode: ScheduleModes.countdown,
|
mode: ScheduleModes.countdown,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
|
countdownCode: countDownCode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -54,10 +57,10 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
StartScheduleEvent(
|
StartScheduleEvent(
|
||||||
mode: ScheduleModes.countdown,
|
mode: ScheduleModes.countdown,
|
||||||
hours: hours,
|
hours: hours,
|
||||||
minutes: minutes,
|
minutes: minutes,
|
||||||
),
|
countDownCode: countDownCode),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
backgroundColor: ColorsManager.primaryColor,
|
backgroundColor: ColorsManager.primaryColor,
|
||||||
|
|||||||
+21
-11
@@ -75,23 +75,33 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
|||||||
final isCountDown = state.scheduleMode == ScheduleModes.countdown;
|
final isCountDown = state.scheduleMode == ScheduleModes.countdown;
|
||||||
final isActive =
|
final isActive =
|
||||||
isCountDown ? state.isCountdownActive : state.isInchingActive;
|
isCountDown ? state.isCountdownActive : state.isInchingActive;
|
||||||
final displayHours = isActive && state.countdownRemaining != null
|
|
||||||
? state.countdownRemaining!.inHours
|
|
||||||
: (isCountDown ? state.countdownHours : state.inchingHours);
|
|
||||||
final displayMinutes = isActive && state.countdownRemaining != null
|
|
||||||
? state.countdownRemaining!.inMinutes.remainder(60)
|
|
||||||
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
|
|
||||||
final displaySeconds = isActive && state.countdownRemaining != null
|
|
||||||
? state.countdownRemaining!.inSeconds.remainder(60)
|
|
||||||
: (isCountDown ? state.countdownSeconds : state.inchingSeconds);
|
|
||||||
|
|
||||||
_updateControllers(displayHours, displayMinutes, displaySeconds!);
|
final displayHours =
|
||||||
|
isActive && state.countdownRemaining != Duration.zero
|
||||||
|
? state.countdownRemaining.inHours
|
||||||
|
: (isCountDown ? state.countdownHours : state.inchingHours);
|
||||||
|
|
||||||
if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) {
|
final displayMinutes =
|
||||||
|
isActive && state.countdownRemaining != Duration.zero
|
||||||
|
? state.countdownRemaining.inMinutes.remainder(60)
|
||||||
|
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
|
||||||
|
|
||||||
|
final displaySeconds =
|
||||||
|
isActive && state.countdownRemaining != Duration.zero
|
||||||
|
? state.countdownRemaining.inSeconds.remainder(60)
|
||||||
|
: (isCountDown ? (state.countdownSeconds ?? 0) : 0);
|
||||||
|
|
||||||
|
_updateControllers(displayHours, displayMinutes, displaySeconds);
|
||||||
|
|
||||||
|
if (isActive &&
|
||||||
|
displayHours == 0 &&
|
||||||
|
displayMinutes == 0 &&
|
||||||
|
displaySeconds == 0) {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
StopScheduleEvent(
|
StopScheduleEvent(
|
||||||
mode: ScheduleModes.countdown,
|
mode: ScheduleModes.countdown,
|
||||||
deviceId: widget.deviceId,
|
deviceId: widget.deviceId,
|
||||||
|
countdownCode: '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -43,7 +43,9 @@ class InchingModeButtons extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
StopScheduleEvent(
|
StopScheduleEvent(
|
||||||
deviceId: deviceId, mode: ScheduleModes.inching),
|
deviceId: deviceId,
|
||||||
|
mode: ScheduleModes.inching,
|
||||||
|
countdownCode: ''),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
|
|||||||
@@ -18,11 +18,15 @@ class BuildScheduleView extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.deviceUuid,
|
required this.deviceUuid,
|
||||||
required this.category,
|
required this.category,
|
||||||
|
required this.countdownCode,
|
||||||
this.code,
|
this.code,
|
||||||
|
required this.deviceType,
|
||||||
});
|
});
|
||||||
final String deviceUuid;
|
final String deviceUuid;
|
||||||
final String category;
|
final String category;
|
||||||
final String? code;
|
final String? code;
|
||||||
|
final String? countdownCode;
|
||||||
|
final String deviceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -31,7 +35,8 @@ class BuildScheduleView extends StatelessWidget {
|
|||||||
deviceId: deviceUuid,
|
deviceId: deviceUuid,
|
||||||
)
|
)
|
||||||
..add(ScheduleGetEvent(category: category))
|
..add(ScheduleGetEvent(category: category))
|
||||||
..add(ScheduleFetchStatusEvent(deviceUuid)),
|
..add(ScheduleFetchStatusEvent(
|
||||||
|
deviceId: deviceUuid, countdownCode: countdownCode ?? '')),
|
||||||
child: Dialog(
|
child: Dialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
insetPadding: const EdgeInsets.all(20),
|
insetPadding: const EdgeInsets.all(20),
|
||||||
@@ -52,31 +57,32 @@ class BuildScheduleView extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const ScheduleHeader(),
|
const ScheduleHeader(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (category == 'CUR_2')
|
if (deviceType == 'CUR_2')
|
||||||
const SizedBox()
|
const SizedBox()
|
||||||
else
|
else
|
||||||
ScheduleModeSelector(
|
ScheduleModeSelector(
|
||||||
|
countdownCode: countdownCode ?? '',
|
||||||
currentMode: state.scheduleMode,
|
currentMode: state.scheduleMode,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (state.scheduleMode == ScheduleModes.schedule)
|
if (state.scheduleMode == ScheduleModes.schedule)
|
||||||
ScheduleManagementUI(
|
ScheduleManagementUI(
|
||||||
|
deviceType: deviceType,
|
||||||
category: category,
|
category: category,
|
||||||
deviceUuid: deviceUuid,
|
deviceUuid: deviceUuid,
|
||||||
onAddSchedule: () async {
|
onAddSchedule: () async {
|
||||||
final entry = await ScheduleDialogHelper
|
final entry = await ScheduleDialogHelper
|
||||||
.showAddScheduleDialog(
|
.showAddScheduleDialog(context,
|
||||||
context,
|
schedule: ScheduleEntry(
|
||||||
schedule: ScheduleEntry(
|
category: category,
|
||||||
category: category,
|
time: '',
|
||||||
time: '',
|
function: Status(
|
||||||
function: Status(
|
code: code.toString(), value: null),
|
||||||
code: code.toString(), value: null),
|
days: [],
|
||||||
days: [],
|
),
|
||||||
),
|
isEdit: false,
|
||||||
isEdit: false,
|
code: code,
|
||||||
code: code,
|
deviceType: deviceType);
|
||||||
);
|
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
ScheduleAddEvent(
|
ScheduleAddEvent(
|
||||||
@@ -90,14 +96,16 @@ class BuildScheduleView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (state.scheduleMode == ScheduleModes.countdown ||
|
if (deviceType != 'CUR_2')
|
||||||
state.scheduleMode == ScheduleModes.inching)
|
if (state.scheduleMode == ScheduleModes.countdown ||
|
||||||
CountdownInchingView(
|
state.scheduleMode == ScheduleModes.inching)
|
||||||
deviceId: deviceUuid,
|
CountdownInchingView(
|
||||||
),
|
deviceId: deviceUuid,
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (state.scheduleMode == ScheduleModes.countdown)
|
if (state.scheduleMode == ScheduleModes.countdown)
|
||||||
CountdownModeButtons(
|
CountdownModeButtons(
|
||||||
|
countDownCode: countdownCode ?? '',
|
||||||
isActive: state.isCountdownActive,
|
isActive: state.isCountdownActive,
|
||||||
deviceId: deviceUuid,
|
deviceId: deviceUuid,
|
||||||
hours: state.countdownHours,
|
hours: state.countdownHours,
|
||||||
|
|||||||
+7
-1
@@ -8,11 +8,13 @@ class ScheduleManagementUI extends StatelessWidget {
|
|||||||
final String deviceUuid;
|
final String deviceUuid;
|
||||||
final VoidCallback onAddSchedule;
|
final VoidCallback onAddSchedule;
|
||||||
final String category;
|
final String category;
|
||||||
|
final String deviceType;
|
||||||
|
|
||||||
const ScheduleManagementUI({
|
const ScheduleManagementUI({
|
||||||
super.key,
|
super.key,
|
||||||
required this.deviceUuid,
|
required this.deviceUuid,
|
||||||
required this.onAddSchedule,
|
required this.onAddSchedule,
|
||||||
|
required this.deviceType,
|
||||||
this.category = 'switch_1',
|
this.category = 'switch_1',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,7 +46,11 @@ class ScheduleManagementUI extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
ScheduleTableWidget(deviceUuid: deviceUuid, category: category),
|
ScheduleTableWidget(
|
||||||
|
deviceUuid: deviceUuid,
|
||||||
|
category: category,
|
||||||
|
deviceType: deviceType,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -7,10 +7,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
|||||||
|
|
||||||
class ScheduleModeSelector extends StatelessWidget {
|
class ScheduleModeSelector extends StatelessWidget {
|
||||||
final ScheduleModes currentMode;
|
final ScheduleModes currentMode;
|
||||||
|
final String countdownCode;
|
||||||
|
|
||||||
const ScheduleModeSelector({
|
const ScheduleModeSelector({
|
||||||
super.key,
|
super.key,
|
||||||
required this.currentMode,
|
required this.currentMode,
|
||||||
|
required this.countdownCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -71,7 +73,8 @@ class ScheduleModeSelector extends StatelessWidget {
|
|||||||
onChanged: (ScheduleModes? value) {
|
onChanged: (ScheduleModes? value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
UpdateScheduleModeEvent(scheduleMode: value),
|
UpdateScheduleModeEvent(
|
||||||
|
scheduleMode: value, countdownCode: countdownCode),
|
||||||
);
|
);
|
||||||
if (value == ScheduleModes.schedule) {
|
if (value == ScheduleModes.schedule) {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import 'package:syncrow_web/utils/format_date_time.dart';
|
|||||||
class ScheduleTableWidget extends StatelessWidget {
|
class ScheduleTableWidget extends StatelessWidget {
|
||||||
final String deviceUuid;
|
final String deviceUuid;
|
||||||
final String category;
|
final String category;
|
||||||
|
final String deviceType;
|
||||||
|
|
||||||
const ScheduleTableWidget({
|
const ScheduleTableWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.deviceUuid,
|
required this.deviceUuid,
|
||||||
this.category = 'switch_1',
|
this.category = 'switch_1',
|
||||||
|
required this.deviceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -25,13 +27,14 @@ class ScheduleTableWidget extends StatelessWidget {
|
|||||||
create: (_) => ScheduleBloc(
|
create: (_) => ScheduleBloc(
|
||||||
deviceId: deviceUuid,
|
deviceId: deviceUuid,
|
||||||
)..add(ScheduleGetEvent(category: category)),
|
)..add(ScheduleGetEvent(category: category)),
|
||||||
child: _ScheduleTableView(),
|
child: _ScheduleTableView(deviceType),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ScheduleTableView extends StatelessWidget {
|
class _ScheduleTableView extends StatelessWidget {
|
||||||
const _ScheduleTableView();
|
final String deviceType;
|
||||||
|
const _ScheduleTableView(this.deviceType);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -81,7 +84,7 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
bottomLeft: Radius.circular(20),
|
bottomLeft: Radius.circular(20),
|
||||||
bottomRight: Radius.circular(20)),
|
bottomRight: Radius.circular(20)),
|
||||||
),
|
),
|
||||||
child: _buildTableBody(state.schedules, context));
|
child: _buildTableBody(state.schedules, context, deviceType));
|
||||||
}
|
}
|
||||||
if (state is ScheduleError) {
|
if (state is ScheduleError) {
|
||||||
return Center(child: Text(state.error));
|
return Center(child: Text(state.error));
|
||||||
@@ -123,7 +126,8 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTableBody(List<ScheduleModel> schedules, BuildContext context) {
|
Widget _buildTableBody(
|
||||||
|
List<ScheduleModel> schedules, BuildContext context, String deviceType) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 200,
|
height: 200,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -132,7 +136,8 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < schedules.length; i++)
|
for (int i = 0; i < schedules.length; i++)
|
||||||
_buildScheduleRow(schedules[i], i, context),
|
_buildScheduleRow(schedules[i], i, context,
|
||||||
|
deviceType: deviceType),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -155,25 +160,19 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TableRow _buildScheduleRow(
|
TableRow _buildScheduleRow(
|
||||||
ScheduleModel schedule, int index, BuildContext context) {
|
ScheduleModel schedule, int index, BuildContext context,
|
||||||
|
{required String deviceType}) {
|
||||||
return TableRow(
|
return TableRow(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
bool temp;
|
|
||||||
if (schedule.category == 'CUR_2') {
|
|
||||||
temp = schedule.function.value == 'open' ? true : false;
|
|
||||||
} else {
|
|
||||||
temp = schedule.function.value as bool;
|
|
||||||
}
|
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
ScheduleUpdateEntryEvent(
|
ScheduleUpdateEntryEvent(
|
||||||
category: schedule.category,
|
category: schedule.category,
|
||||||
scheduleId: schedule.scheduleId,
|
scheduleId: schedule.scheduleId,
|
||||||
functionOn: temp,
|
functionOn: schedule.function.value,
|
||||||
// schedule.function.value,
|
|
||||||
enable: !schedule.enable,
|
enable: !schedule.enable,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -195,8 +194,9 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
child: Text(_getSelectedDays(
|
child: Text(_getSelectedDays(
|
||||||
ScheduleModel.parseSelectedDays(schedule.days)))),
|
ScheduleModel.parseSelectedDays(schedule.days)))),
|
||||||
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
|
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
|
||||||
if (schedule.category == 'CUR_2')
|
if (deviceType == 'CUR_2')
|
||||||
Center(child: Text(schedule.function.value))
|
Center(
|
||||||
|
child: Text(schedule.function.value == true ? 'open' : 'close'))
|
||||||
else
|
else
|
||||||
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
|
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
|
||||||
Center(
|
Center(
|
||||||
@@ -206,14 +206,14 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScheduleDialogHelper.showAddScheduleDialog(
|
ScheduleDialogHelper.showAddScheduleDialog(context,
|
||||||
context,
|
schedule: ScheduleEntry.fromScheduleModel(schedule),
|
||||||
schedule: ScheduleEntry.fromScheduleModel(schedule),
|
isEdit: true,
|
||||||
isEdit: true,
|
deviceType: deviceType)
|
||||||
).then((updatedSchedule) {
|
.then((updatedSchedule) {
|
||||||
if (updatedSchedule != null) {
|
if (updatedSchedule != null) {
|
||||||
bool temp;
|
bool temp;
|
||||||
if (schedule.category == 'CUR_2') {
|
if (deviceType == 'CUR_2') {
|
||||||
updatedSchedule.function.value == 'open'
|
updatedSchedule.function.value == 'open'
|
||||||
? temp = true
|
? temp = true
|
||||||
: temp = false;
|
: temp = false;
|
||||||
@@ -222,6 +222,7 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
ScheduleEditEvent(
|
ScheduleEditEvent(
|
||||||
|
deviceType: deviceType,
|
||||||
scheduleId: schedule.scheduleId,
|
scheduleId: schedule.scheduleId,
|
||||||
category: schedule.category,
|
category: schedule.category,
|
||||||
time: updatedSchedule.time,
|
time: updatedSchedule.time,
|
||||||
|
|||||||
+23
-31
@@ -41,7 +41,7 @@ class ThreeGangGlassSwitchBloc
|
|||||||
emit(ThreeGangGlassSwitchLoading());
|
emit(ThreeGangGlassSwitchLoading());
|
||||||
try {
|
try {
|
||||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||||
_listenToChanges(event.deviceId, emit);
|
_listenToChanges(event.deviceId);
|
||||||
deviceStatus =
|
deviceStatus =
|
||||||
ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||||
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
|
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
|
||||||
@@ -50,42 +50,28 @@ class ThreeGangGlassSwitchBloc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenToChanges(
|
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||||
String deviceId,
|
|
||||||
Emitter<ThreeGangGlassSwitchState> emit,
|
void _listenToChanges(String deviceId) {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||||
final stream = ref.onValue;
|
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||||
|
if (event.snapshot.value == null) return;
|
||||||
|
|
||||||
stream.listen((DatabaseEvent event) {
|
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||||
final data = event.snapshot.value as Map<dynamic, dynamic>?;
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
final statusList = <Status>[];
|
final statusList = <Status>[];
|
||||||
if (data['status'] != null) {
|
|
||||||
for (var element in data['status']) {
|
usersMap['status'].forEach((element) {
|
||||||
statusList.add(
|
statusList.add(Status(code: element['code'], value: element['value']));
|
||||||
Status(
|
});
|
||||||
code: element['code'].toString(),
|
|
||||||
value: element['value'].toString(),
|
deviceStatus =
|
||||||
),
|
ThreeGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||||
);
|
|
||||||
}
|
add(StatusUpdated(deviceStatus));
|
||||||
}
|
|
||||||
if (statusList.isNotEmpty) {
|
|
||||||
final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList);
|
|
||||||
if (newStatus != deviceStatus) {
|
|
||||||
deviceStatus = newStatus;
|
|
||||||
if (!isClosed) {
|
|
||||||
add(StatusUpdated(deviceStatus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (_) {}
|
||||||
emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStatusUpdated(
|
void _onStatusUpdated(
|
||||||
@@ -184,4 +170,10 @@ class ThreeGangGlassSwitchBloc
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_deviceStatusSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -111,6 +111,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '3GT',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -127,6 +129,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
|
deviceType: '3GT',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -143,6 +147,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_3',
|
category: 'switch_3',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_3',
|
||||||
|
deviceType: '3GT',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '3G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -118,6 +120,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
|
deviceType: '3G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -134,6 +138,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_3',
|
category: 'switch_3',
|
||||||
|
countdownCode: 'countdown_3',
|
||||||
|
deviceType: '3G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
+163
-159
@@ -1,173 +1,177 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:firebase_database/firebase_database.dart';
|
import 'package:firebase_database/firebase_database.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
|
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
|
||||||
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
|
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
|
||||||
import 'package:syncrow_web/services/batch_control_devices_service.dart';
|
import 'package:syncrow_web/services/batch_control_devices_service.dart';
|
||||||
import 'package:syncrow_web/services/control_device_service.dart';
|
import 'package:syncrow_web/services/control_device_service.dart';
|
||||||
import 'package:syncrow_web/services/devices_mang_api.dart';
|
import 'package:syncrow_web/services/devices_mang_api.dart';
|
||||||
|
|
||||||
part 'two_gang_glass_switch_event.dart';
|
part 'two_gang_glass_switch_event.dart';
|
||||||
part 'two_gang_glass_switch_state.dart';
|
part 'two_gang_glass_switch_state.dart';
|
||||||
|
|
||||||
class TwoGangGlassSwitchBloc
|
class TwoGangGlassSwitchBloc
|
||||||
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
|
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final ControlDeviceService controlDeviceService;
|
final ControlDeviceService controlDeviceService;
|
||||||
final BatchControlDevicesService batchControlDevicesService;
|
final BatchControlDevicesService batchControlDevicesService;
|
||||||
|
|
||||||
late TwoGangGlassStatusModel deviceStatus;
|
late TwoGangGlassStatusModel deviceStatus;
|
||||||
|
|
||||||
TwoGangGlassSwitchBloc({
|
TwoGangGlassSwitchBloc({
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.controlDeviceService,
|
required this.controlDeviceService,
|
||||||
required this.batchControlDevicesService,
|
required this.batchControlDevicesService,
|
||||||
}) : super(TwoGangGlassSwitchInitial()) {
|
}) : super(TwoGangGlassSwitchInitial()) {
|
||||||
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
|
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
|
||||||
on<TwoGangGlassSwitchControl>(_onControl);
|
on<TwoGangGlassSwitchControl>(_onControl);
|
||||||
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
|
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
|
||||||
on<TwoGangGlassSwitchFetchBatchStatusEvent>(_onFetchBatchStatus);
|
on<TwoGangGlassSwitchFetchBatchStatusEvent>(_onFetchBatchStatus);
|
||||||
on<TwoGangGlassFactoryReset>(_onFactoryReset);
|
on<TwoGangGlassFactoryReset>(_onFactoryReset);
|
||||||
on<StatusUpdated>(_onStatusUpdated);
|
on<StatusUpdated>(_onStatusUpdated);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onFetchDeviceStatus(
|
|
||||||
TwoGangGlassSwitchFetchDeviceEvent event,
|
|
||||||
Emitter<TwoGangGlassSwitchState> emit,
|
|
||||||
) async {
|
|
||||||
emit(TwoGangGlassSwitchLoading());
|
|
||||||
try {
|
|
||||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
|
||||||
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
|
||||||
_listenToChanges(event.deviceId);
|
|
||||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
|
||||||
} catch (e) {
|
|
||||||
emit(TwoGangGlassSwitchError(e.toString()));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void _listenToChanges(String deviceId) {
|
Future<void> _onFetchDeviceStatus(
|
||||||
try {
|
TwoGangGlassSwitchFetchDeviceEvent event,
|
||||||
final ref = FirebaseDatabase.instance.ref(
|
Emitter<TwoGangGlassSwitchState> emit,
|
||||||
'device-status/$deviceId',
|
) async {
|
||||||
);
|
emit(TwoGangGlassSwitchLoading());
|
||||||
|
try {
|
||||||
ref.onValue.listen((event) {
|
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||||
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
|
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||||
|
_listenToChanges(event.deviceId);
|
||||||
List<Status> statusList = [];
|
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||||
eventsMap['status'].forEach((element) {
|
} catch (e) {
|
||||||
statusList.add(Status(code: element['code'], value: element['value']));
|
emit(TwoGangGlassSwitchError(e.toString()));
|
||||||
});
|
|
||||||
|
|
||||||
deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList);
|
|
||||||
add(StatusUpdated(deviceStatus));
|
|
||||||
});
|
|
||||||
} catch (_) {
|
|
||||||
log(
|
|
||||||
'Error listening to changes',
|
|
||||||
name: 'TwoGangGlassSwitchBloc._listenToChanges',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onControl(
|
|
||||||
TwoGangGlassSwitchControl event,
|
|
||||||
Emitter<TwoGangGlassSwitchState> emit,
|
|
||||||
) async {
|
|
||||||
emit(TwoGangGlassSwitchLoading());
|
|
||||||
_updateLocalValue(event.code, event.value);
|
|
||||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await controlDeviceService.controlDevice(
|
|
||||||
deviceUuid: event.deviceId,
|
|
||||||
status: Status(code: event.code, value: event.value),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_updateLocalValue(event.code, !event.value);
|
|
||||||
emit(TwoGangGlassSwitchError(e.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onBatchControl(
|
|
||||||
TwoGangGlassSwitchBatchControl event,
|
|
||||||
Emitter<TwoGangGlassSwitchState> emit,
|
|
||||||
) async {
|
|
||||||
emit(TwoGangGlassSwitchLoading());
|
|
||||||
_updateLocalValue(event.code, event.value);
|
|
||||||
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await batchControlDevicesService.batchControlDevices(
|
|
||||||
uuids: event.deviceIds,
|
|
||||||
code: event.code,
|
|
||||||
value: event.value,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
_updateLocalValue(event.code, !event.value);
|
|
||||||
emit(TwoGangGlassSwitchError(e.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onFetchBatchStatus(
|
|
||||||
TwoGangGlassSwitchFetchBatchStatusEvent event,
|
|
||||||
Emitter<TwoGangGlassSwitchState> emit,
|
|
||||||
) async {
|
|
||||||
emit(TwoGangGlassSwitchLoading());
|
|
||||||
try {
|
|
||||||
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
|
|
||||||
deviceStatus = TwoGangGlassStatusModel.fromJson(
|
|
||||||
event.deviceIds.first,
|
|
||||||
status.status,
|
|
||||||
);
|
|
||||||
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
|
|
||||||
} catch (e) {
|
|
||||||
emit(TwoGangGlassSwitchError(e.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onFactoryReset(
|
|
||||||
TwoGangGlassFactoryReset event,
|
|
||||||
Emitter<TwoGangGlassSwitchState> emit,
|
|
||||||
) async {
|
|
||||||
emit(TwoGangGlassSwitchLoading());
|
|
||||||
try {
|
|
||||||
final response = await DevicesManagementApi().factoryReset(
|
|
||||||
event.factoryReset,
|
|
||||||
event.deviceId,
|
|
||||||
);
|
|
||||||
if (!response) {
|
|
||||||
emit(TwoGangGlassSwitchError('Failed to reset device'));
|
|
||||||
} else {
|
|
||||||
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
emit(TwoGangGlassSwitchError(e.toString()));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void _onStatusUpdated(
|
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||||
StatusUpdated event,
|
|
||||||
Emitter<TwoGangGlassSwitchState> emit,
|
|
||||||
) {
|
|
||||||
deviceStatus = event.deviceStatus;
|
|
||||||
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateLocalValue(String code, bool value) {
|
void _listenToChanges(String deviceId) {
|
||||||
switch (code) {
|
try {
|
||||||
case 'switch_1':
|
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||||
deviceStatus = deviceStatus.copyWith(switch1: value);
|
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||||
break;
|
if (event.snapshot.value == null) return;
|
||||||
case 'switch_2':
|
|
||||||
deviceStatus = deviceStatus.copyWith(switch2: value);
|
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||||
break;
|
|
||||||
|
final statusList = <Status>[];
|
||||||
|
|
||||||
|
usersMap['status'].forEach((element) {
|
||||||
|
statusList.add(Status(code: element['code'], value: element['value']));
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceStatus =
|
||||||
|
TwoGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||||
|
|
||||||
|
add(StatusUpdated(deviceStatus));
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onControl(
|
||||||
|
TwoGangGlassSwitchControl event,
|
||||||
|
Emitter<TwoGangGlassSwitchState> emit,
|
||||||
|
) async {
|
||||||
|
emit(TwoGangGlassSwitchLoading());
|
||||||
|
_updateLocalValue(event.code, event.value);
|
||||||
|
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await controlDeviceService.controlDevice(
|
||||||
|
deviceUuid: event.deviceId,
|
||||||
|
status: Status(code: event.code, value: event.value),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_updateLocalValue(event.code, !event.value);
|
||||||
|
emit(TwoGangGlassSwitchError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onBatchControl(
|
||||||
|
TwoGangGlassSwitchBatchControl event,
|
||||||
|
Emitter<TwoGangGlassSwitchState> emit,
|
||||||
|
) async {
|
||||||
|
emit(TwoGangGlassSwitchLoading());
|
||||||
|
_updateLocalValue(event.code, event.value);
|
||||||
|
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await batchControlDevicesService.batchControlDevices(
|
||||||
|
uuids: event.deviceIds,
|
||||||
|
code: event.code,
|
||||||
|
value: event.value,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
_updateLocalValue(event.code, !event.value);
|
||||||
|
emit(TwoGangGlassSwitchError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFetchBatchStatus(
|
||||||
|
TwoGangGlassSwitchFetchBatchStatusEvent event,
|
||||||
|
Emitter<TwoGangGlassSwitchState> emit,
|
||||||
|
) async {
|
||||||
|
emit(TwoGangGlassSwitchLoading());
|
||||||
|
try {
|
||||||
|
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
|
||||||
|
deviceStatus = TwoGangGlassStatusModel.fromJson(
|
||||||
|
event.deviceIds.first,
|
||||||
|
status.status,
|
||||||
|
);
|
||||||
|
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
|
||||||
|
} catch (e) {
|
||||||
|
emit(TwoGangGlassSwitchError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFactoryReset(
|
||||||
|
TwoGangGlassFactoryReset event,
|
||||||
|
Emitter<TwoGangGlassSwitchState> emit,
|
||||||
|
) async {
|
||||||
|
emit(TwoGangGlassSwitchLoading());
|
||||||
|
try {
|
||||||
|
final response = await DevicesManagementApi().factoryReset(
|
||||||
|
event.factoryReset,
|
||||||
|
event.deviceId,
|
||||||
|
);
|
||||||
|
if (!response) {
|
||||||
|
emit(TwoGangGlassSwitchError('Failed to reset device'));
|
||||||
|
} else {
|
||||||
|
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(TwoGangGlassSwitchError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStatusUpdated(
|
||||||
|
StatusUpdated event,
|
||||||
|
Emitter<TwoGangGlassSwitchState> emit,
|
||||||
|
) {
|
||||||
|
deviceStatus = event.deviceStatus;
|
||||||
|
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateLocalValue(String code, bool value) {
|
||||||
|
switch (code) {
|
||||||
|
case 'switch_1':
|
||||||
|
deviceStatus = deviceStatus.copyWith(switch1: value);
|
||||||
|
break;
|
||||||
|
case 'switch_2':
|
||||||
|
deviceStatus = deviceStatus.copyWith(switch2: value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_deviceStatusSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -102,6 +102,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
|
|||||||
builder: (ctx) => BlocProvider.value(
|
builder: (ctx) => BlocProvider.value(
|
||||||
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
|
deviceType: '2GT',
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
),
|
),
|
||||||
@@ -118,6 +120,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
|
|||||||
builder: (ctx) => BlocProvider.value(
|
builder: (ctx) => BlocProvider.value(
|
||||||
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
|
deviceType: '2GT',
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ class TwoGangBatchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
deviceUuid: deviceIds.first,
|
deviceUuid: deviceIds.first,
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '2G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -114,6 +116,8 @@ class TwoGangBatchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
deviceUuid: deviceIds.first,
|
deviceUuid: deviceIds.first,
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
|
deviceType: '2G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -121,10 +125,7 @@ class TwoGangBatchControlView extends StatelessWidget
|
|||||||
subtitle: 'Scheduling',
|
subtitle: 'Scheduling',
|
||||||
iconPath: Assets.scheduling,
|
iconPath: Assets.scheduling,
|
||||||
),
|
),
|
||||||
// FirmwareUpdateWidget(
|
|
||||||
// deviceId: deviceIds.first,
|
|
||||||
// version: 12,
|
|
||||||
// ),
|
|
||||||
FactoryResetWidget(callFactoryReset: () {
|
FactoryResetWidget(callFactoryReset: () {
|
||||||
context.read<TwoGangSwitchBloc>().add(
|
context.read<TwoGangSwitchBloc>().add(
|
||||||
TwoGangFactoryReset(
|
TwoGangFactoryReset(
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ class TwoGangDeviceControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '2G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -125,6 +127,8 @@ class TwoGangDeviceControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
|
deviceType: '2G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ class ScheduleDialogHelper {
|
|||||||
ScheduleEntry? schedule,
|
ScheduleEntry? schedule,
|
||||||
bool isEdit = false,
|
bool isEdit = false,
|
||||||
String? code,
|
String? code,
|
||||||
|
required String deviceType,
|
||||||
}) {
|
}) {
|
||||||
bool temp;
|
bool temp;
|
||||||
if (schedule?.category == 'CUR_2') {
|
if (deviceType == 'CUR_2') {
|
||||||
temp = schedule!.function.value == 'open' ? true : false;
|
temp = schedule!.function.value == 'open' ? true : false;
|
||||||
} else {
|
} else {
|
||||||
temp = schedule!.function.value;
|
temp = schedule!.function.value;
|
||||||
@@ -103,8 +104,7 @@ class ScheduleDialogHelper {
|
|||||||
setState(() => selectedDays[i] = v);
|
setState(() => selectedDays[i] = v);
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildFunctionSwitch(schedule!.category, ctx, functionOn!,
|
_buildFunctionSwitch(deviceType, ctx, functionOn!, (v) {
|
||||||
(v) {
|
|
||||||
setState(() => functionOn = v);
|
setState(() => functionOn = v);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -120,32 +120,29 @@ class ScheduleDialogHelper {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 100,
|
width: 100,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
dynamic temp;
|
dynamic temp;
|
||||||
if (schedule?.category == 'CUR_2') {
|
if (deviceType == 'CUR_2') {
|
||||||
temp = functionOn! ? 'open' : 'close';
|
temp = functionOn! ? 'open' : 'close';
|
||||||
} else {
|
} else {
|
||||||
temp = functionOn;
|
temp = functionOn;
|
||||||
}
|
}
|
||||||
print(temp);
|
final entry = ScheduleEntry(
|
||||||
final entry = ScheduleEntry(
|
category: schedule?.category ?? 'switch_1',
|
||||||
category: schedule?.category ?? 'switch_1',
|
time: _formatTimeOfDayToISO(selectedTime),
|
||||||
time: _formatTimeOfDayToISO(selectedTime),
|
function: Status(
|
||||||
function: Status(
|
code: code ?? 'switch_1',
|
||||||
code: code ?? 'switch_1',
|
value: temp,
|
||||||
value: temp,
|
),
|
||||||
// functionOn,
|
days: _convertSelectedDaysToStrings(selectedDays),
|
||||||
),
|
scheduleId: schedule.scheduleId,
|
||||||
days: _convertSelectedDaysToStrings(selectedDays),
|
);
|
||||||
scheduleId: schedule?.scheduleId,
|
Navigator.pop(ctx, entry);
|
||||||
);
|
},
|
||||||
Navigator.pop(ctx, entry);
|
child: const Text('Save'),
|
||||||
},
|
)),
|
||||||
child: const Text('Save'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: device.uuid ?? '',
|
deviceUuid: device.uuid ?? '',
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: 'WH',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
|||||||
color: const Color(0xFF0026A2),
|
color: const Color(0xFF0026A2),
|
||||||
),
|
),
|
||||||
HomeItemModel(
|
HomeItemModel(
|
||||||
title: 'Devices Management',
|
title: 'Device Management',
|
||||||
icon: Assets.devicesIcon,
|
icon: Assets.devicesIcon,
|
||||||
active: true,
|
active: true,
|
||||||
onPress: (context) {
|
onPress: (context) {
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
|
|||||||
Future<void> checkEmail(
|
Future<void> checkEmail(
|
||||||
CheckEmailEvent event, Emitter<UsersState> emit) async {
|
CheckEmailEvent event, Emitter<UsersState> emit) async {
|
||||||
emit(UsersLoadingState());
|
emit(UsersLoadingState());
|
||||||
String? res = await UserPermissionApi().checkEmail(
|
String? res = await UserPermissionApi().checkEmail(
|
||||||
emailController.text,
|
emailController.text,
|
||||||
);
|
);
|
||||||
checkEmailValid = res!;
|
checkEmailValid = res!;
|
||||||
|
|||||||
+45
-14
@@ -34,7 +34,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
return Dialog(
|
return Dialog(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))),
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20))),
|
||||||
width: 900,
|
width: 900,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -63,7 +64,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
children: [
|
children: [
|
||||||
_buildStep1Indicator(1, "Basics", _blocRole),
|
_buildStep1Indicator(1, "Basics", _blocRole),
|
||||||
_buildStep2Indicator(2, "Spaces", _blocRole),
|
_buildStep2Indicator(2, "Spaces", _blocRole),
|
||||||
_buildStep3Indicator(3, "Role & Permissions", _blocRole),
|
_buildStep3Indicator(
|
||||||
|
3, "Role & Permissions", _blocRole),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -105,18 +107,32 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
),
|
),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
final isBasicsStep = currentStep == 1;
|
||||||
|
|
||||||
|
if (isBasicsStep) {
|
||||||
|
// Validate the form first
|
||||||
|
final isValid = _blocRole.formKey.currentState
|
||||||
|
?.validate() ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
return; // Stop if form is not valid
|
||||||
|
}
|
||||||
_blocRole.add(const CheckEmailEvent());
|
_blocRole.add(const CheckEmailEvent());
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
if (currentStep < 3) {
|
if (currentStep < 3) {
|
||||||
currentStep++;
|
currentStep++;
|
||||||
if (currentStep == 2) {
|
if (currentStep == 2) {
|
||||||
_blocRole.add(const CheckStepStatus(isEditUser: false));
|
_blocRole.add(const CheckStepStatus(
|
||||||
|
isEditUser: false));
|
||||||
} else if (currentStep == 3) {
|
} else if (currentStep == 3) {
|
||||||
_blocRole.add(const CheckSpacesStepStatus());
|
_blocRole
|
||||||
|
.add(const CheckSpacesStepStatus());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_blocRole.add(SendInviteUsers(context: context));
|
_blocRole
|
||||||
|
.add(SendInviteUsers(context: context));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -124,8 +140,11 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
currentStep < 3 ? "Next" : "Save",
|
currentStep < 3 ? "Next" : "Save",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: (_blocRole.isCompleteSpaces == false ||
|
color: (_blocRole.isCompleteSpaces == false ||
|
||||||
_blocRole.isCompleteBasics == false ||
|
_blocRole.isCompleteBasics ==
|
||||||
_blocRole.isCompleteRolePermissions == false) &&
|
false ||
|
||||||
|
_blocRole
|
||||||
|
.isCompleteRolePermissions ==
|
||||||
|
false) &&
|
||||||
currentStep == 3
|
currentStep == 3
|
||||||
? ColorsManager.grayColor
|
? ColorsManager.grayColor
|
||||||
: ColorsManager.secondaryColor),
|
: ColorsManager.secondaryColor),
|
||||||
@@ -143,7 +162,7 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
Widget _getFormContent() {
|
Widget _getFormContent() {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case 1:
|
case 1:
|
||||||
return const BasicsView(
|
return BasicsView(
|
||||||
userId: '',
|
userId: '',
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
@@ -196,8 +215,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
color: currentStep == step
|
||||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
? ColorsManager.blackColor
|
||||||
|
: ColorsManager.greyColor,
|
||||||
|
fontWeight: currentStep == step
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -260,8 +283,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
color: currentStep == step
|
||||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
? ColorsManager.blackColor
|
||||||
|
: ColorsManager.greyColor,
|
||||||
|
fontWeight: currentStep == step
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -318,8 +345,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
color: currentStep == step
|
||||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
? ColorsManager.blackColor
|
||||||
|
: ColorsManager.greyColor,
|
||||||
|
fontWeight: currentStep == step
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl_phone_field/countries.dart';
|
import 'package:intl_phone_field/countries.dart';
|
||||||
import 'package:intl_phone_field/country_picker_dialog.dart';
|
import 'package:intl_phone_field/country_picker_dialog.dart';
|
||||||
import 'package:intl_phone_field/intl_phone_field.dart';
|
import 'package:intl_phone_field/intl_phone_field.dart';
|
||||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
|
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart';
|
||||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
|
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
|
||||||
import 'package:syncrow_web/utils/color_manager.dart';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
@@ -11,7 +14,9 @@ import 'package:syncrow_web/utils/style.dart';
|
|||||||
|
|
||||||
class BasicsView extends StatelessWidget {
|
class BasicsView extends StatelessWidget {
|
||||||
final String? userId;
|
final String? userId;
|
||||||
const BasicsView({super.key, this.userId = ''});
|
Timer? _debounce;
|
||||||
|
|
||||||
|
BasicsView({super.key, this.userId = ''});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<UsersBloc, UsersState>(builder: (context, state) {
|
return BlocBuilder<UsersBloc, UsersState>(builder: (context, state) {
|
||||||
@@ -21,6 +26,7 @@ class BasicsView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return Form(
|
return Form(
|
||||||
key: _blocRole.formKey,
|
key: _blocRole.formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
@@ -208,6 +214,14 @@ class BasicsView extends StatelessWidget {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: ColorsManager.textGray),
|
color: ColorsManager.textGray),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
onChanged: (value) {
|
||||||
|
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||||
|
_debounce = Timer(const Duration(milliseconds: 800), () {
|
||||||
|
_blocRole.add(const CheckEmailEvent());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Enter Email Address';
|
return 'Enter Email Address';
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
|
|||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||||
import 'package:syncrow_web/services/api/http_service.dart';
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||||
@@ -26,6 +28,11 @@ class SpaceManagementPage extends StatelessWidget {
|
|||||||
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
||||||
),
|
),
|
||||||
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsBloc(
|
||||||
|
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: WebScaffold(
|
child: WebScaffold(
|
||||||
appBarTitle: Text(
|
appBarTitle: Text(
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeader extends StatelessWidget {
|
||||||
|
const CommunityStructureHeader({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.shadowBlackColor,
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildCommunityInfo(context, theme, screenWidth),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateCommunityDialog(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CreateCommunityDialog(
|
||||||
|
title: const Text('Edit Community'),
|
||||||
|
onCreateCommunity: (community) {
|
||||||
|
// TODO(FarisArmoush): Implement
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommunityInfo(
|
||||||
|
BuildContext context, ThemeData theme, double screenWidth) {
|
||||||
|
final selectedCommunity =
|
||||||
|
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
|
||||||
|
final selectedSpace =
|
||||||
|
context.watch<CommunitiesTreeSelectionBloc>().state.selectedSpace;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Community Structure',
|
||||||
|
style: theme.textTheme.headlineLarge
|
||||||
|
?.copyWith(color: ColorsManager.blackColor),
|
||||||
|
),
|
||||||
|
if (selectedCommunity != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: SelectableText(
|
||||||
|
selectedCommunity.name,
|
||||||
|
style: theme.textTheme.bodyLarge
|
||||||
|
?.copyWith(color: ColorsManager.blackColor),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showCreateCommunityDialog(context),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.iconEdit,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CommunityStructureHeaderActionButtons(
|
||||||
|
onDelete: (space) {},
|
||||||
|
onDuplicate: (space) {},
|
||||||
|
onEdit: (space) {
|
||||||
|
SpaceDetailsDialogHelper.showEdit(
|
||||||
|
context,
|
||||||
|
spaceModel: selectedSpace!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selectedSpace: selectedSpace,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeaderActionButtons extends StatelessWidget {
|
||||||
|
const CommunityStructureHeaderActionButtons({
|
||||||
|
super.key,
|
||||||
|
required this.onDelete,
|
||||||
|
required this.selectedSpace,
|
||||||
|
required this.onDuplicate,
|
||||||
|
required this.onEdit,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function(SpaceModel space) onDelete;
|
||||||
|
final void Function(SpaceModel space) onDuplicate;
|
||||||
|
final void Function(SpaceModel space) onEdit;
|
||||||
|
final SpaceModel? selectedSpace;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Wrap(
|
||||||
|
alignment: WrapAlignment.end,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
if (selectedSpace != null) ...[
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Edit',
|
||||||
|
svgAsset: Assets.editSpace,
|
||||||
|
onPressed: () => onEdit(selectedSpace!),
|
||||||
|
),
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Duplicate',
|
||||||
|
svgAsset: Assets.duplicate,
|
||||||
|
onPressed: () => onDuplicate(selectedSpace!),
|
||||||
|
),
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Delete',
|
||||||
|
svgAsset: Assets.spaceDelete,
|
||||||
|
onPressed: () => onDelete(selectedSpace!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeaderButton extends StatelessWidget {
|
||||||
|
const CommunityStructureHeaderButton({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
this.svgAsset,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final String? svgAsset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const double buttonHeight = 40;
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 130,
|
||||||
|
minHeight: buttonHeight,
|
||||||
|
),
|
||||||
|
child: DefaultButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
borderWidth: 2,
|
||||||
|
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||||
|
foregroundColor: ColorsManager.blackColor,
|
||||||
|
borderRadius: 12.0,
|
||||||
|
padding: 2.0,
|
||||||
|
height: buttonHeight,
|
||||||
|
elevation: 0,
|
||||||
|
borderColor: ColorsManager.lightGrayColor,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (svgAsset != null)
|
||||||
|
SvgPicture.asset(
|
||||||
|
svgAsset!,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: context.textTheme.bodySmall
|
||||||
|
?.copyWith(color: ColorsManager.blackColor, fontSize: 14),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-3
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
|
||||||
@@ -18,9 +19,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
|
|||||||
replacement: const Row(
|
replacement: const Row(
|
||||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||||
),
|
),
|
||||||
child: CommunityStructureCanvas(
|
child: Column(
|
||||||
community: selectedCommunity,
|
mainAxisSize: MainAxisSize.min,
|
||||||
selectedSpace: selectedSpace,
|
children: [
|
||||||
|
const CommunityStructureHeader(),
|
||||||
|
Expanded(
|
||||||
|
child: CommunityStructureCanvas(
|
||||||
|
community: selectedCommunity,
|
||||||
|
selectedSpace: selectedSpace,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ class SpaceModel extends Equatable {
|
|||||||
required this.parent,
|
required this.parent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory SpaceModel.empty() => const SpaceModel(
|
||||||
|
uuid: '',
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
spaceName: '',
|
||||||
|
icon: '',
|
||||||
|
children: [],
|
||||||
|
parent: null,
|
||||||
|
);
|
||||||
|
|
||||||
factory SpaceModel.fromJson(Map<String, dynamic> json) {
|
factory SpaceModel.fromJson(Map<String, dynamic> json) {
|
||||||
return SpaceModel(
|
return SpaceModel(
|
||||||
uuid: json['uuid'] as String? ?? '',
|
uuid: json['uuid'] as String? ?? '',
|
||||||
|
|||||||
+17
-4
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
||||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
import 'package:syncrow_web/services/api/http_service.dart';
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
@@ -15,12 +16,15 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
|
|||||||
static const _defaultErrorMessage = 'Failed to load space details';
|
static const _defaultErrorMessage = 'Failed to load space details';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param) async {
|
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
|
||||||
try {
|
try {
|
||||||
final response = await _httpService.get(
|
final response = await _httpService.get(
|
||||||
path: 'endpoint',
|
path: await _makeEndpoint(param),
|
||||||
expectedResponseModel: (data) {
|
expectedResponseModel: (data) {
|
||||||
return SpaceDetailsModel.fromJson(data as Map<String, dynamic>);
|
final response = data as Map<String, dynamic>;
|
||||||
|
return SpaceDetailsModel.fromJson(
|
||||||
|
response['data'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
@@ -37,4 +41,13 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
|
|||||||
throw APIException(formattedErrorMessage);
|
throw APIException(formattedErrorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> _makeEndpoint(LoadSpaceDetailsParam param) async {
|
||||||
|
final projectUuid = await ProjectManager.getProjectUUID();
|
||||||
|
if (projectUuid == null || projectUuid.isEmpty) {
|
||||||
|
throw APIException('Project UUID is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-5
@@ -1,6 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
class SpaceDetailsModel extends Equatable {
|
class SpaceDetailsModel extends Equatable {
|
||||||
final String uuid;
|
final String uuid;
|
||||||
@@ -17,14 +18,21 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
required this.subspaces,
|
required this.subspaces,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
|
||||||
|
uuid: '',
|
||||||
|
spaceName: '',
|
||||||
|
icon: Assets.villa,
|
||||||
|
productAllocations: [],
|
||||||
|
subspaces: [],
|
||||||
|
);
|
||||||
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
|
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
|
||||||
return SpaceDetailsModel(
|
return SpaceDetailsModel(
|
||||||
uuid: json['uuid'] as String,
|
uuid: json['uuid'] as String,
|
||||||
spaceName: json['spaceName'] as String,
|
spaceName: json['spaceName'] as String,
|
||||||
icon: json['icon'] as String,
|
icon: json['icon'] as String,
|
||||||
productAllocations: (json['productAllocations'] as List)
|
productAllocations: (json['productAllocations'] as List)
|
||||||
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
subspaces: (json['subspaces'] as List)
|
subspaces: (json['subspaces'] as List)
|
||||||
.map((e) => Subspace.fromJson(e as Map<String, dynamic>))
|
.map((e) => Subspace.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -41,6 +49,22 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpaceDetailsModel copyWith({
|
||||||
|
String? uuid,
|
||||||
|
String? spaceName,
|
||||||
|
String? icon,
|
||||||
|
List<ProductAllocation>? productAllocations,
|
||||||
|
List<Subspace>? subspaces,
|
||||||
|
}) {
|
||||||
|
return SpaceDetailsModel(
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
|
spaceName: spaceName ?? this.spaceName,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
productAllocations: productAllocations ?? this.productAllocations,
|
||||||
|
subspaces: subspaces ?? this.subspaces,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces];
|
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces];
|
||||||
}
|
}
|
||||||
@@ -48,12 +72,10 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
class ProductAllocation extends Equatable {
|
class ProductAllocation extends Equatable {
|
||||||
final Product product;
|
final Product product;
|
||||||
final Tag tag;
|
final Tag tag;
|
||||||
final String? location;
|
|
||||||
|
|
||||||
const ProductAllocation({
|
const ProductAllocation({
|
||||||
required this.product,
|
required this.product,
|
||||||
required this.tag,
|
required this.tag,
|
||||||
this.location,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
|
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -70,6 +92,16 @@ class ProductAllocation extends Equatable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProductAllocation copyWith({
|
||||||
|
Product? product,
|
||||||
|
Tag? tag,
|
||||||
|
}) {
|
||||||
|
return ProductAllocation(
|
||||||
|
product: product ?? this.product,
|
||||||
|
tag: tag ?? this.tag,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [product, tag];
|
List<Object?> get props => [product, tag];
|
||||||
}
|
}
|
||||||
@@ -88,7 +120,7 @@ class Subspace extends Equatable {
|
|||||||
factory Subspace.fromJson(Map<String, dynamic> json) {
|
factory Subspace.fromJson(Map<String, dynamic> json) {
|
||||||
return Subspace(
|
return Subspace(
|
||||||
uuid: json['uuid'] as String,
|
uuid: json['uuid'] as String,
|
||||||
name: json['name'] as String,
|
name: json['subspaceName'] as String,
|
||||||
productAllocations: (json['productAllocations'] as List)
|
productAllocations: (json['productAllocations'] as List)
|
||||||
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -103,6 +135,18 @@ class Subspace extends Equatable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Subspace copyWith({
|
||||||
|
String? uuid,
|
||||||
|
String? name,
|
||||||
|
List<ProductAllocation>? productAllocations,
|
||||||
|
}) {
|
||||||
|
return Subspace(
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
|
name: name ?? this.name,
|
||||||
|
productAllocations: productAllocations ?? this.productAllocations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [uuid, name, productAllocations];
|
List<Object?> get props => [uuid, name, productAllocations];
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
class LoadSpaceDetailsParam {
|
||||||
|
const LoadSpaceDetailsParam({
|
||||||
|
required this.spaceUuid,
|
||||||
|
required this.communityUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String spaceUuid;
|
||||||
|
final String communityUuid;
|
||||||
|
}
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
class LoadSpacesParam {
|
|
||||||
const LoadSpacesParam();
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||||
|
|
||||||
abstract class SpaceDetailsService {
|
abstract class SpaceDetailsService {
|
||||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param);
|
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -1,7 +1,7 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
||||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
|
|
||||||
@@ -9,12 +9,13 @@ part 'space_details_event.dart';
|
|||||||
part 'space_details_state.dart';
|
part 'space_details_state.dart';
|
||||||
|
|
||||||
class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
|
class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
|
||||||
final SpaceDetailsService _spaceDetailsService;
|
|
||||||
|
|
||||||
SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) {
|
SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) {
|
||||||
on<LoadSpaceDetails>(_onLoadSpaceDetails);
|
on<LoadSpaceDetails>(_onLoadSpaceDetails);
|
||||||
|
on<ClearSpaceDetails>(_onClearSpaceDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final SpaceDetailsService _spaceDetailsService;
|
||||||
|
|
||||||
Future<void> _onLoadSpaceDetails(
|
Future<void> _onLoadSpaceDetails(
|
||||||
LoadSpaceDetails event,
|
LoadSpaceDetails event,
|
||||||
Emitter<SpaceDetailsState> emit,
|
Emitter<SpaceDetailsState> emit,
|
||||||
@@ -31,4 +32,11 @@ class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
|
|||||||
emit(SpaceDetailsFailure(e.toString()));
|
emit(SpaceDetailsFailure(e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onClearSpaceDetails(
|
||||||
|
ClearSpaceDetails event,
|
||||||
|
Emitter<SpaceDetailsState> emit,
|
||||||
|
) {
|
||||||
|
emit(SpaceDetailsInitial());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -7,11 +7,18 @@ sealed class SpaceDetailsEvent extends Equatable {
|
|||||||
List<Object> get props => [];
|
List<Object> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoadSpaceDetails extends SpaceDetailsEvent {
|
final class LoadSpaceDetails extends SpaceDetailsEvent {
|
||||||
const LoadSpaceDetails(this.param);
|
const LoadSpaceDetails(this.param);
|
||||||
|
|
||||||
final LoadSpacesParam param;
|
final LoadSpaceDetailsParam param;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [param];
|
List<Object> get props => [param];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ClearSpaceDetails extends SpaceDetailsEvent {
|
||||||
|
const ClearSpaceDetails();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
+3
-3
@@ -21,10 +21,10 @@ final class SpaceDetailsLoaded extends SpaceDetailsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class SpaceDetailsFailure extends SpaceDetailsState {
|
final class SpaceDetailsFailure extends SpaceDetailsState {
|
||||||
final String message;
|
final String errorMessage;
|
||||||
|
|
||||||
const SpaceDetailsFailure(this.message);
|
const SpaceDetailsFailure(this.errorMessage);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [message];
|
List<Object> get props => [errorMessage];
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -1,11 +1,46 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
|
||||||
abstract final class SpaceDetailsDialogHelper {
|
abstract final class SpaceDetailsDialogHelper {
|
||||||
static void showCreate(BuildContext context) {
|
static void showCreate(BuildContext context) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const SpaceDetailsDialog(),
|
builder: (_) => BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsBloc(
|
||||||
|
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||||
|
),
|
||||||
|
child: SpaceDetailsDialog(
|
||||||
|
context: context,
|
||||||
|
title: const Text('Create Space'),
|
||||||
|
spaceModel: SpaceModel.empty(),
|
||||||
|
onSave: print,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void showEdit(
|
||||||
|
BuildContext context, {
|
||||||
|
required SpaceModel spaceModel,
|
||||||
|
}) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsBloc(
|
||||||
|
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||||
|
),
|
||||||
|
child: SpaceDetailsDialog(
|
||||||
|
context: context,
|
||||||
|
title: const Text('Edit Space'),
|
||||||
|
spaceModel: spaceModel,
|
||||||
|
onSave: (space) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class ButtonContentWidget extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String? svgAssets;
|
||||||
|
final bool disabled;
|
||||||
|
|
||||||
|
const ButtonContentWidget({
|
||||||
|
required this.label,
|
||||||
|
this.svgAssets,
|
||||||
|
this.disabled = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
return Opacity(
|
||||||
|
opacity: disabled ? 0.5 : 1.0,
|
||||||
|
child: Container(
|
||||||
|
width: screenWidth * 0.25,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.neutralGray,
|
||||||
|
width: 3.0,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (svgAssets != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 6.0),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
svgAssets!,
|
||||||
|
width: screenWidth * 0.015,
|
||||||
|
height: screenWidth * 0.015,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class SpaceDetailsActionButtons extends StatelessWidget {
|
||||||
|
const SpaceDetailsActionButtons({
|
||||||
|
super.key,
|
||||||
|
required this.onSave,
|
||||||
|
required this.onCancel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
final VoidCallback? onSave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildCancelButton(context)),
|
||||||
|
Expanded(child: _buildSaveButton()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCancelButton(BuildContext context) {
|
||||||
|
return CancelButton(
|
||||||
|
onPressed: onCancel,
|
||||||
|
label: 'Cancel',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSaveButton() {
|
||||||
|
return DefaultButton(
|
||||||
|
onPressed: onSave,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: ColorsManager.secondaryColor,
|
||||||
|
foregroundColor: ColorsManager.whiteColors,
|
||||||
|
child: const Text('OK'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/common/edit_chip.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||||
|
const SpaceDetailsDevicesBox({
|
||||||
|
required this.space,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SpaceDetailsModel space;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final productAllocations = space.productAllocations;
|
||||||
|
final subspaces = space.subspaces;
|
||||||
|
final isAnySubspaceHasProductAllocations =
|
||||||
|
subspaces.any((subspace) => subspace.productAllocations.isNotEmpty);
|
||||||
|
if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
width: 3.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
// Combine tags from spaceModel and subspaces
|
||||||
|
// ...TagHelper.groupTags([
|
||||||
|
// ...?tags,
|
||||||
|
// ...?subspaces?.expand((subspace) => subspace.tags ?? [])
|
||||||
|
// ]).entries.map(
|
||||||
|
// (entry) => Chip(
|
||||||
|
// avatar: SizedBox(
|
||||||
|
// width: 24,
|
||||||
|
// height: 24,
|
||||||
|
// child: SvgPicture.asset(
|
||||||
|
// entry.key.icon ?? 'assets/icons/gateway.svg',
|
||||||
|
// fit: BoxFit.contain,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// label: Text(
|
||||||
|
// 'x${entry.value}',
|
||||||
|
// style: Theme.of(context)
|
||||||
|
// .textTheme
|
||||||
|
// .bodySmall
|
||||||
|
// ?.copyWith(color: ColorsManager.spaceColor),
|
||||||
|
// ),
|
||||||
|
// backgroundColor: ColorsManager.whiteColors,
|
||||||
|
// shape: RoundedRectangleBorder(
|
||||||
|
// borderRadius: BorderRadius.circular(16),
|
||||||
|
// side: const BorderSide(
|
||||||
|
// color: ColorsManager.spaceColor,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
EditChip(
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () {},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ButtonContentWidget(
|
||||||
|
svgAssets: Assets.addIcon,
|
||||||
|
label: 'Add Devices',
|
||||||
|
// disabled: isTagsAndSubspaceModelDisabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
-4
@@ -1,12 +1,101 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
class SpaceDetailsDialog extends StatelessWidget {
|
class SpaceDetailsDialog extends StatefulWidget {
|
||||||
const SpaceDetailsDialog({super.key});
|
const SpaceDetailsDialog({
|
||||||
|
required this.title,
|
||||||
|
required this.spaceModel,
|
||||||
|
required this.onSave,
|
||||||
|
required this.context,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final SpaceModel spaceModel;
|
||||||
|
final void Function(SpaceDetailsModel space) onSave;
|
||||||
|
final BuildContext context;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final isCreateMode = widget.spaceModel.uuid.isEmpty;
|
||||||
|
|
||||||
|
if (!isCreateMode) {
|
||||||
|
final param = LoadSpaceDetailsParam(
|
||||||
|
spaceUuid: widget.spaceModel.uuid,
|
||||||
|
communityUuid: widget.context
|
||||||
|
.read<CommunitiesTreeSelectionBloc>()
|
||||||
|
.state
|
||||||
|
.selectedCommunity!
|
||||||
|
.uuid,
|
||||||
|
);
|
||||||
|
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Dialog(
|
final isCreateMode = widget.spaceModel.uuid.isEmpty;
|
||||||
child: Text('Create Space'),
|
if (isCreateMode) {
|
||||||
|
return SpaceDetailsForm(
|
||||||
|
title: widget.title,
|
||||||
|
space: SpaceDetailsModel.empty(),
|
||||||
|
onSave: widget.onSave,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlocBuilder<SpaceDetailsBloc, SpaceDetailsState>(
|
||||||
|
bloc: widget.context.read<SpaceDetailsBloc>(),
|
||||||
|
builder: (context, state) => switch (state) {
|
||||||
|
SpaceDetailsInitial() => _buildLoadingDialog(),
|
||||||
|
SpaceDetailsLoading() => _buildLoadingDialog(),
|
||||||
|
SpaceDetailsLoaded(:final spaceDetails) => SpaceDetailsForm(
|
||||||
|
title: widget.title,
|
||||||
|
space: spaceDetails,
|
||||||
|
onSave: widget.onSave,
|
||||||
|
),
|
||||||
|
SpaceDetailsFailure(:final errorMessage) => _buildErrorDialog(
|
||||||
|
errorMessage,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingDialog() {
|
||||||
|
return AlertDialog(
|
||||||
|
title: widget.title,
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorDialog(String errorMessage) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: widget.title,
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: Center(
|
||||||
|
child: SelectableText(
|
||||||
|
errorMessage,
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: ColorsManager.red,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceDetailsForm extends StatelessWidget {
|
||||||
|
const SpaceDetailsForm({
|
||||||
|
required this.title,
|
||||||
|
required this.space,
|
||||||
|
required this.onSave,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final SpaceDetailsModel space;
|
||||||
|
final void Function(SpaceDetailsModel space) onSave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsModelBloc(initialState: space),
|
||||||
|
child: BlocBuilder<SpaceDetailsModelBloc, SpaceDetailsModel>(
|
||||||
|
buildWhen: (previous, current) => previous != current,
|
||||||
|
builder: (context, state) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: title,
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: SizedBox(
|
||||||
|
height: context.screenHeight * 0.3,
|
||||||
|
width: context.screenWidth * 0.5,
|
||||||
|
child: Row(
|
||||||
|
spacing: 20,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: SpaceIconPicker(iconPath: state.icon)),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SpaceNameTextField(
|
||||||
|
initialValue: state.spaceName,
|
||||||
|
isNameFieldExist: (value) => state.subspaces.any(
|
||||||
|
(subspace) => subspace.name == value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
SpaceSubSpacesBox(
|
||||||
|
subspaces: state.subspaces,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SpaceDetailsDevicesBox(space: state),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
SpaceDetailsActionButtons(
|
||||||
|
onSave: () => onSave(state),
|
||||||
|
onCancel: Navigator.of(context).pop,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceIconPicker extends StatelessWidget {
|
||||||
|
const SpaceIconPicker({
|
||||||
|
required this.iconPath,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String iconPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Stack(
|
||||||
|
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: context.screenWidth * 0.175,
|
||||||
|
height: context.screenHeight * 0.175,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: ColorsManager.boxColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
iconPath,
|
||||||
|
width: context.screenWidth * 0.08,
|
||||||
|
height: context.screenHeight * 0.08,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.directional(
|
||||||
|
top: 12,
|
||||||
|
start: context.screenHeight * 0.06,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
showDialog<String?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SpaceIconSelectionDialog(
|
||||||
|
selectedIcon: iconPath,
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<SpaceDetailsModelBloc>().add(
|
||||||
|
UpdateSpaceDetailsIcon(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.iconEdit,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceIconSelectionDialog extends StatelessWidget {
|
||||||
|
const SpaceIconSelectionDialog({super.key, required this.selectedIcon});
|
||||||
|
final String selectedIcon;
|
||||||
|
|
||||||
|
static const List<String> _icons = [
|
||||||
|
Assets.location,
|
||||||
|
Assets.villa,
|
||||||
|
Assets.gym,
|
||||||
|
Assets.sauna,
|
||||||
|
Assets.bbq,
|
||||||
|
Assets.building,
|
||||||
|
Assets.desk,
|
||||||
|
Assets.door,
|
||||||
|
Assets.parking,
|
||||||
|
Assets.pool,
|
||||||
|
Assets.stair,
|
||||||
|
Assets.steamRoom,
|
||||||
|
Assets.street,
|
||||||
|
Assets.unit,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: SelectableText(
|
||||||
|
'Space Icon',
|
||||||
|
style: context.textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: Container(
|
||||||
|
width: context.screenWidth * 0.45,
|
||||||
|
height: context.screenHeight * 0.275,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.boxColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: GridView.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 7,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
),
|
||||||
|
itemCount: _icons.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final isSelected = selectedIcon == _icons[index];
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsetsDirectional.all(2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: isSelected
|
||||||
|
? Border.all(color: ColorsManager.vividBlue, width: 2)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(_icons[index]),
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
_icons[index],
|
||||||
|
width: context.screenWidth * 0.03,
|
||||||
|
height: context.screenHeight * 0.08,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceNameTextField extends StatefulWidget {
|
||||||
|
const SpaceNameTextField({
|
||||||
|
required this.initialValue,
|
||||||
|
required this.isNameFieldExist,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? initialValue;
|
||||||
|
final bool Function(String value) isNameFieldExist;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpaceNameTextField> createState() => _SpaceNameTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_controller = TextEditingController(text: widget.initialValue);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
String? _validateName(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '*Space name should not be empty.';
|
||||||
|
}
|
||||||
|
if (widget.isNameFieldExist(value)) {
|
||||||
|
return '*Name already exists';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: (value) => context.read<SpaceDetailsModelBloc>().add(
|
||||||
|
UpdateSpaceDetailsName(value),
|
||||||
|
),
|
||||||
|
validator: _validateName,
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Please enter the name',
|
||||||
|
hintStyle: context.textTheme.bodyMedium!.copyWith(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: ColorsManager.boxColor,
|
||||||
|
enabledBorder: _buildBorder(context, ColorsManager.vividBlue),
|
||||||
|
focusedBorder: _buildBorder(context, ColorsManager.primaryColor),
|
||||||
|
errorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||||
|
focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||||
|
errorStyle: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: context.theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlineInputBorder _buildBorder(BuildContext context, [Color? color]) {
|
||||||
|
return OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(width: 1, color: color ?? ColorsManager.boxColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/common/edit_chip.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class SpaceSubSpacesBox extends StatelessWidget {
|
||||||
|
const SpaceSubSpacesBox({super.key, required this.subspaces});
|
||||||
|
|
||||||
|
final List<Subspace> subspaces;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (subspaces.isEmpty) {
|
||||||
|
return TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
overlayColor: ColorsManager.transparentColor,
|
||||||
|
),
|
||||||
|
onPressed: () => _showSubSpacesDialog(context),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ButtonContentWidget(
|
||||||
|
svgAssets: Assets.addIcon,
|
||||||
|
label: 'Create Sub Spaces',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
width: 3.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
...subspaces.map((e) => SubspaceNameDisplayWidget(subSpace: e)),
|
||||||
|
EditChip(
|
||||||
|
onTap: () => _showSubSpacesDialog(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSubSpacesDialog(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => SpaceSubSpacesDialog(
|
||||||
|
subspaces: subspaces,
|
||||||
|
onSave: (subspaces) {
|
||||||
|
context.read<SpaceDetailsModelBloc>().add(
|
||||||
|
UpdateSpaceDetailsSubspaces(subspaces),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class SpaceSubSpacesDialog extends StatefulWidget {
|
||||||
|
const SpaceSubSpacesDialog({
|
||||||
|
required this.subspaces,
|
||||||
|
required this.onSave,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Subspace> subspaces;
|
||||||
|
final void Function(List<Subspace> subspaces) onSave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpaceSubSpacesDialog> createState() => _SpaceSubSpacesDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
||||||
|
late List<Subspace> _subspaces;
|
||||||
|
|
||||||
|
bool get _hasDuplicateNames =>
|
||||||
|
_subspaces.map((subspace) => subspace.name.toLowerCase()).toSet().length !=
|
||||||
|
_subspaces.length;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_subspaces = List.from(widget.subspaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSubspaceAdded(String name) {
|
||||||
|
setState(() {
|
||||||
|
_subspaces = [
|
||||||
|
..._subspaces,
|
||||||
|
Subspace(
|
||||||
|
name: name,
|
||||||
|
uuid: const Uuid().v4(),
|
||||||
|
productAllocations: const [],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSubspaceDeleted(String uuid) => setState(
|
||||||
|
() => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _handleSave() {
|
||||||
|
widget.onSave(_subspaces);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Create Sub Spaces'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SubSpacesInput(
|
||||||
|
subSpaces: _subspaces,
|
||||||
|
onSubspaceAdded: _handleSubspaceAdded,
|
||||||
|
onSubspaceDeleted: _handleSubspaceDeleted,
|
||||||
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
child: Visibility(
|
||||||
|
key: ValueKey(_hasDuplicateNames),
|
||||||
|
visible: _hasDuplicateNames,
|
||||||
|
child: const Text(
|
||||||
|
'Error: Duplicate subspace names are not allowed.',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
SpaceDetailsActionButtons(
|
||||||
|
onSave: _hasDuplicateNames ? null : _handleSave,
|
||||||
|
onCancel: Navigator.of(context).pop,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SubSpacesInput extends StatefulWidget {
|
||||||
|
const SubSpacesInput({
|
||||||
|
super.key,
|
||||||
|
required this.subSpaces,
|
||||||
|
required this.onSubspaceAdded,
|
||||||
|
required this.onSubspaceDeleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Subspace> subSpaces;
|
||||||
|
final void Function(String name) onSubspaceAdded;
|
||||||
|
final void Function(String uuid) onSubspaceDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SubSpacesInput> createState() => _SubSpacesInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SubSpacesInputState extends State<SubSpacesInput> {
|
||||||
|
late final TextEditingController _subspaceNameController;
|
||||||
|
late final FocusNode _focusNode;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_subspaceNameController = TextEditingController();
|
||||||
|
_focusNode = FocusNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subspaceNameController.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: context.screenWidth * 0.35,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.boxColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
...widget.subSpaces.asMap().entries.map(
|
||||||
|
(entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final subSpace = entry.value;
|
||||||
|
|
||||||
|
final lowerName = subSpace.name.toLowerCase();
|
||||||
|
|
||||||
|
final duplicateIndices = widget.subSpaces
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.where((e) => e.value.name.toLowerCase() == lowerName)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
final isDuplicate = duplicateIndices.length > 1 &&
|
||||||
|
duplicateIndices.indexOf(index) != 0;
|
||||||
|
return SubspaceChip(
|
||||||
|
subSpace: subSpace,
|
||||||
|
isDuplicate: isDuplicate,
|
||||||
|
onDeleted: () => widget.onSubspaceDeleted(subSpace.uuid),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: TextField(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
controller: _subspaceNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
|
||||||
|
hintStyle: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
final trimmedValue = value.trim();
|
||||||
|
if (trimmedValue.isNotEmpty) {
|
||||||
|
widget.onSubspaceAdded(trimmedValue);
|
||||||
|
_subspaceNameController.clear();
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SubspaceChip extends StatelessWidget {
|
||||||
|
const SubspaceChip({
|
||||||
|
required this.subSpace,
|
||||||
|
required this.isDuplicate,
|
||||||
|
required this.onDeleted,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Subspace subSpace;
|
||||||
|
final bool isDuplicate;
|
||||||
|
final void Function() onDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Chip(
|
||||||
|
label: Text(
|
||||||
|
subSpace.name,
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
side: BorderSide(
|
||||||
|
color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor,
|
||||||
|
width: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
deleteIcon: Container(
|
||||||
|
padding: const EdgeInsetsDirectional.all(1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDeleted: onDeleted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SubspaceNameDisplayWidget extends StatefulWidget {
|
||||||
|
const SubspaceNameDisplayWidget({super.key, required this.subSpace});
|
||||||
|
|
||||||
|
final Subspace subSpace;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SubspaceNameDisplayWidget> createState() =>
|
||||||
|
_SubspaceNameDisplayWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SubspaceNameDisplayWidgetState extends State<SubspaceNameDisplayWidget> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
late final FocusNode _focusNode;
|
||||||
|
bool _isEditing = false;
|
||||||
|
bool _hasDuplicateName = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_controller = TextEditingController(text: widget.subSpace.name);
|
||||||
|
_focusNode = FocusNode();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _checkForDuplicateName(String name) {
|
||||||
|
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||||
|
return bloc.state.subspaces
|
||||||
|
.where((s) => s.uuid != widget.subSpace.uuid)
|
||||||
|
.any((s) => s.name.toLowerCase() == name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleNameChange(String value) {
|
||||||
|
setState(() {
|
||||||
|
_hasDuplicateName = _checkForDuplicateName(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tryToFinishEditing() {
|
||||||
|
if (!_hasDuplicateName) {
|
||||||
|
_onFinishEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tryToSubmit(String value) {
|
||||||
|
if (_hasDuplicateName) return;
|
||||||
|
|
||||||
|
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||||
|
bloc.add(
|
||||||
|
UpdateSpaceDetailsSubspaces(
|
||||||
|
bloc.state.subspaces
|
||||||
|
.map(
|
||||||
|
(e) => e.uuid == widget.subSpace.uuid ? e.copyWith(name: value) : e,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_onFinishEditing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.spaceColor,
|
||||||
|
);
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _isEditing = true);
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
},
|
||||||
|
child: Chip(
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
side: const BorderSide(color: ColorsManager.transparentColor),
|
||||||
|
),
|
||||||
|
onDeleted: () {
|
||||||
|
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||||
|
bloc.add(
|
||||||
|
UpdateSpaceDetailsSubspaces(
|
||||||
|
bloc.state.subspaces
|
||||||
|
.where((s) => s.uuid != widget.subSpace.uuid)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
deleteIcon: Container(
|
||||||
|
padding: const EdgeInsetsDirectional.all(1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const FittedBox(
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label: Visibility(
|
||||||
|
visible: _isEditing,
|
||||||
|
replacement: Text(
|
||||||
|
widget.subSpace.name,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: context.screenWidth * 0.065,
|
||||||
|
height: context.screenHeight * 0.025,
|
||||||
|
child: TextField(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
controller: _controller,
|
||||||
|
style: textStyle?.copyWith(
|
||||||
|
color: _hasDuplicateName ? Colors.red : null,
|
||||||
|
),
|
||||||
|
decoration: const InputDecoration.collapsed(
|
||||||
|
hintText: '',
|
||||||
|
),
|
||||||
|
onChanged: _handleNameChange,
|
||||||
|
onTapOutside: (_) => _tryToFinishEditing(),
|
||||||
|
onSubmitted: _tryToSubmit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_hasDuplicateName)
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
child: Visibility(
|
||||||
|
key: ValueKey(_hasDuplicateName),
|
||||||
|
visible: _hasDuplicateName,
|
||||||
|
child: Text(
|
||||||
|
'Name already exists',
|
||||||
|
style: textStyle?.copyWith(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFinishEditing() {
|
||||||
|
setState(() {
|
||||||
|
_isEditing = false;
|
||||||
|
_hasDuplicateName = false;
|
||||||
|
});
|
||||||
|
_focusNode.unfocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
|
||||||
|
part 'space_details_model_event.dart';
|
||||||
|
|
||||||
|
class SpaceDetailsModelBloc extends Bloc<SpaceDetailsModelEvent, SpaceDetailsModel> {
|
||||||
|
SpaceDetailsModelBloc({
|
||||||
|
required SpaceDetailsModel initialState,
|
||||||
|
}) : super(initialState) {
|
||||||
|
on<UpdateSpaceDetailsIcon>(_onUpdateSpaceDetailsIcon);
|
||||||
|
on<UpdateSpaceDetailsName>(_onUpdateSpaceDetailsName);
|
||||||
|
on<UpdateSpaceDetailsSubspaces>(_onUpdateSpaceDetailsSubspaces);
|
||||||
|
on<UpdateSpaceDetailsProductAllocations>(
|
||||||
|
_onUpdateSpaceDetailsProductAllocations);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetailsIcon(
|
||||||
|
UpdateSpaceDetailsIcon event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(icon: event.icon));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetailsName(
|
||||||
|
UpdateSpaceDetailsName event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(spaceName: event.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetailsSubspaces(
|
||||||
|
UpdateSpaceDetailsSubspaces event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(subspaces: event.subspaces));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetailsProductAllocations(
|
||||||
|
UpdateSpaceDetailsProductAllocations event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(productAllocations: event.productAllocations));
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
part of 'space_details_model_bloc.dart';
|
||||||
|
|
||||||
|
sealed class SpaceDetailsModelEvent extends Equatable {
|
||||||
|
const SpaceDetailsModelEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetailsIcon extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetailsIcon(this.icon);
|
||||||
|
|
||||||
|
final String icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [icon];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetailsName extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetailsName(this.name);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [name];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetailsSubspaces extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetailsSubspaces(this.subspaces);
|
||||||
|
|
||||||
|
final List<Subspace> subspaces;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [subspaces];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetailsProductAllocations(this.productAllocations);
|
||||||
|
|
||||||
|
final List<ProductAllocation> productAllocations;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [productAllocations];
|
||||||
|
}
|
||||||
@@ -52,4 +52,7 @@ final myTheme = ThemeData(
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
dialogTheme: const DialogThemeData(
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user