mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
Compare commits
72 Commits
main
...
2d16bda61d
Author | SHA1 | Date | |
---|---|---|---|
2d16bda61d | |||
5c90d5f6b9 | |||
d6a48850a7 | |||
6cac94a1c4 | |||
9f28e1ccef | |||
6534bfae5b | |||
4cfb984d2c | |||
4c06479469 | |||
3101960201 | |||
ddfd4ee153 | |||
7f0484eec6 | |||
dc7064d142 | |||
e523a83912 | |||
e917225c3d | |||
66ed30b50c | |||
47bd6ff89e | |||
138390496c | |||
df87e41d61 | |||
f0bfe085a4 | |||
bb846f797f | |||
e234c9f3b2 | |||
bcd0ae4a2a | |||
cebce2ce7f | |||
97e3fb68bf | |||
46a7add90d | |||
73de1e6ff9 | |||
826dea8054 | |||
fdea4b1cd0 | |||
823d86fd80 | |||
dd735032ea | |||
6dcc851d97 | |||
15b36fd052 | |||
a4024067c7 | |||
95cded4bf5 | |||
757a96ed9f | |||
b857736e10 | |||
1fccd51440 | |||
c07ddb0ccd | |||
58e99f95b2 | |||
227df6fe3d | |||
9451ec0cc4 | |||
fc797c2646 | |||
318e1d9af7 | |||
d47dc349bc | |||
c221c8499f | |||
71cf4b9feb | |||
c43cf9347f | |||
9990b1805e | |||
50f8158830 | |||
009b7c0316 | |||
72af55ef98 | |||
779c0fe916 | |||
e448eabda6 | |||
9dfb3ed369 | |||
63353af38b | |||
68b6c9b18c | |||
fa6ee9a0af | |||
3601b02bc3 | |||
fdd0526c78 | |||
b888f516e2 | |||
bdeec7d325 | |||
50ff17a0c1 | |||
87c2e3261d | |||
62a6f9c993 | |||
c1e61ee61d | |||
7750290be4 | |||
f7e4d6ff07 | |||
7f26c773a7 | |||
1adbae6735 | |||
0847cb8a41 | |||
818bdee745 | |||
f33b3e8bd2 |
15
assets/icons/group_icon.svg
Normal file
15
assets/icons/group_icon.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_9717_7433)">
|
||||||
|
<path d="M17.1131 10.6766H15.5664C15.7241 11.1083 15.8102 11.5741 15.8102 12.0596V17.9053C15.8102 18.1077 15.775 18.302 15.7109 18.4827H18.2679C19.2231 18.4827 20.0002 17.7056 20.0002 16.7505V13.5637C20.0002 11.9718 18.7051 10.6766 17.1131 10.6766Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M4.19005 12.0596C4.19005 11.5741 4.27618 11.1083 4.43384 10.6766H2.88712C1.29516 10.6766 0 11.9718 0 13.5637V16.7505C0 17.7057 0.777072 18.4828 1.73227 18.4828H4.28938C4.22528 18.302 4.19005 18.1077 4.19005 17.9053V12.0596Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M11.7679 9.17249H8.23184C6.63989 9.17249 5.34473 10.4676 5.34473 12.0596V17.9053C5.34473 18.2242 5.60324 18.4827 5.92215 18.4827H14.0776C14.3965 18.4827 14.655 18.2242 14.655 17.9053V12.0596C14.655 10.4676 13.3598 9.17249 11.7679 9.17249Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M9.99995 1.51721C8.08541 1.51721 6.52783 3.07479 6.52783 4.98937C6.52783 6.288 7.24459 7.42218 8.30311 8.01765C8.80518 8.30008 9.38401 8.46148 9.99995 8.46148C10.6159 8.46148 11.1947 8.30008 11.6968 8.01765C12.7553 7.42218 13.4721 6.28796 13.4721 4.98937C13.4721 3.07483 11.9145 1.51721 9.99995 1.51721Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M3.90284 4.75354C2.471 4.75354 1.30615 5.91839 1.30615 7.35022C1.30615 8.78206 2.471 9.94691 3.90284 9.94691C4.26604 9.94691 4.6119 9.87168 4.92608 9.73644C5.46929 9.50257 5.91718 9.08859 6.19433 8.57003C6.38886 8.20609 6.49952 7.79089 6.49952 7.35022C6.49952 5.91843 5.33468 4.75354 3.90284 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M16.0972 4.75354C14.6653 4.75354 13.5005 5.91839 13.5005 7.35022C13.5005 7.79093 13.6112 8.20612 13.8057 8.57003C14.0828 9.08863 14.5307 9.50261 15.0739 9.73644C15.3881 9.87168 15.734 9.94691 16.0972 9.94691C17.529 9.94691 18.6939 8.78206 18.6939 7.35022C18.6939 5.91839 17.529 4.75354 16.0972 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_9717_7433">
|
||||||
|
<rect width="20" height="20" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
4
assets/icons/home_icon.svg
Normal file
4
assets/icons/home_icon.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.0002 5.97498L3.12109 11.2683V18.3601H8.64871V13.163H11.5852V18.3601H16.8794V11.2683L10.0002 5.97498Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M17.1673 7.15356V3.52759H14.2702V4.92485L10 1.63989L0 9.33274L1.38043 11.1271L10 4.49458L18.6196 11.1272L20 9.33278L17.1673 7.15356Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 433 B |
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||||
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
|
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
|
||||||
import 'package:syncrow_web/services/access_mang_api.dart';
|
import 'package:syncrow_web/services/access_mang_api.dart';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||||
|
|
||||||
abstract class AccessState extends Equatable {
|
abstract class AccessState extends Equatable {
|
||||||
const AccessState();
|
const AccessState();
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart';
|
||||||
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/api_const.dart';
|
||||||
|
|
||||||
|
class BookableSpacesService implements BookingSystemService {
|
||||||
|
const BookableSpacesService(this._httpService);
|
||||||
|
|
||||||
|
final HTTPService _httpService;
|
||||||
|
static const _defaultErrorMessage = 'Failed to load bookable spaces';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||||
|
required int page,
|
||||||
|
required int size,
|
||||||
|
required String search,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _httpService.get(
|
||||||
|
path: ApiEndpoints.getBookableSpaces,
|
||||||
|
queryParameters: {
|
||||||
|
'page': page,
|
||||||
|
'size': size,
|
||||||
|
'active': true,
|
||||||
|
'configured': true,
|
||||||
|
if (search.isNotEmpty && search != 'null') 'search': search,
|
||||||
|
},
|
||||||
|
expectedResponseModel: (json) {
|
||||||
|
return PaginatedBookableSpaces.fromJson(
|
||||||
|
json as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
final responseData = e.response?.data;
|
||||||
|
if (responseData is Map<String, dynamic>) {
|
||||||
|
final errorMessage = responseData['error']?['message'] as String? ??
|
||||||
|
responseData['message'] as String? ??
|
||||||
|
_defaultErrorMessage;
|
||||||
|
throw APIException(errorMessage);
|
||||||
|
}
|
||||||
|
throw APIException(_defaultErrorMessage);
|
||||||
|
} catch (e) {
|
||||||
|
throw APIException('$_defaultErrorMessage: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
class BookableSpaceModel {
|
||||||
|
final String uuid;
|
||||||
|
final String spaceName;
|
||||||
|
final String virtualLocation;
|
||||||
|
final BookableConfig bookableConfig;
|
||||||
|
|
||||||
|
BookableSpaceModel({
|
||||||
|
required this.uuid,
|
||||||
|
required this.spaceName,
|
||||||
|
required this.virtualLocation,
|
||||||
|
required this.bookableConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory BookableSpaceModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return BookableSpaceModel(
|
||||||
|
uuid: json['uuid'] as String,
|
||||||
|
spaceName: json['spaceName'] as String,
|
||||||
|
virtualLocation: json['virtualLocation'] as String,
|
||||||
|
bookableConfig: BookableConfig.fromJson(
|
||||||
|
json['bookableConfig'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookableConfig {
|
||||||
|
final String uuid;
|
||||||
|
final List<String> daysAvailable;
|
||||||
|
final String startTime;
|
||||||
|
final String endTime;
|
||||||
|
final bool active;
|
||||||
|
final int points;
|
||||||
|
|
||||||
|
BookableConfig({
|
||||||
|
required this.uuid,
|
||||||
|
required this.daysAvailable,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
required this.active,
|
||||||
|
required this.points,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory BookableConfig.fromJson(Map<String, dynamic> json) {
|
||||||
|
return BookableConfig(
|
||||||
|
uuid: json['uuid'] as String,
|
||||||
|
daysAvailable: (json['daysAvailable'] as List).cast<String>(),
|
||||||
|
startTime: json['startTime'] as String,
|
||||||
|
endTime: json['endTime'] as String,
|
||||||
|
active: json['active'] as bool,
|
||||||
|
points: json['points'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||||
|
|
||||||
|
class PaginatedBookableSpaces {
|
||||||
|
final List<BookableSpaceModel> data;
|
||||||
|
final String message;
|
||||||
|
final int page;
|
||||||
|
final int size;
|
||||||
|
final int totalItem;
|
||||||
|
final int totalPage;
|
||||||
|
final bool hasNext;
|
||||||
|
final bool hasPrevious;
|
||||||
|
|
||||||
|
PaginatedBookableSpaces({
|
||||||
|
required this.data,
|
||||||
|
required this.message,
|
||||||
|
required this.page,
|
||||||
|
required this.size,
|
||||||
|
required this.totalItem,
|
||||||
|
required this.totalPage,
|
||||||
|
required this.hasNext,
|
||||||
|
required this.hasPrevious,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PaginatedBookableSpaces.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PaginatedBookableSpaces(
|
||||||
|
data: (json['data'] as List)
|
||||||
|
.map((item) => BookableSpaceModel.fromJson(item))
|
||||||
|
.toList(),
|
||||||
|
message: json['message'] as String,
|
||||||
|
page: json['page'] as int,
|
||||||
|
size: json['size'] as int,
|
||||||
|
totalItem: json['totalItem'] as int,
|
||||||
|
totalPage: json['totalPage'] as int,
|
||||||
|
hasNext: json['hasNext'] as bool,
|
||||||
|
hasPrevious: json['hasPrevious'] as bool,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart';
|
||||||
|
|
||||||
|
class DebouncedBookingSystemService implements BookingSystemService {
|
||||||
|
final BookingSystemService _inner;
|
||||||
|
final Duration debounceDuration;
|
||||||
|
|
||||||
|
Timer? _debounceTimer;
|
||||||
|
Completer<PaginatedBookableSpaces>? _lastCompleter;
|
||||||
|
|
||||||
|
int? _lastPage;
|
||||||
|
int? _lastSize;
|
||||||
|
bool? _lastIncludeSpaces;
|
||||||
|
String? _lastSearch;
|
||||||
|
|
||||||
|
DebouncedBookingSystemService(
|
||||||
|
this._inner, {
|
||||||
|
this.debounceDuration = const Duration(milliseconds: 500),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||||
|
required int page,
|
||||||
|
required int size,
|
||||||
|
required String search,
|
||||||
|
}) {
|
||||||
|
_debounceTimer?.cancel();
|
||||||
|
_lastCompleter?.completeError(StateError("Cancelled by new search"));
|
||||||
|
|
||||||
|
final completer = Completer<PaginatedBookableSpaces>();
|
||||||
|
_lastCompleter = completer;
|
||||||
|
|
||||||
|
_lastPage = page;
|
||||||
|
_lastSize = size;
|
||||||
|
_lastSearch = search;
|
||||||
|
|
||||||
|
_debounceTimer = Timer(debounceDuration, () async {
|
||||||
|
try {
|
||||||
|
final result = await _inner.getBookableSpaces(
|
||||||
|
page: _lastPage!,
|
||||||
|
size: _lastSize!,
|
||||||
|
search: _lastSearch!,
|
||||||
|
);
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(result);
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
|
||||||
|
|
||||||
|
abstract class BookingSystemService {
|
||||||
|
Future<PaginatedBookableSpaces> getBookableSpaces({
|
||||||
|
required int page,
|
||||||
|
required int size,
|
||||||
|
required String search,
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:calendar_view/calendar_view.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
part 'events_event.dart';
|
||||||
|
part 'events_state.dart';
|
||||||
|
|
||||||
|
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||||
|
final EventController eventController = EventController();
|
||||||
|
|
||||||
|
CalendarEventsBloc() : super(EventsInitial()) {
|
||||||
|
on<LoadEvents>(_onLoadEvents);
|
||||||
|
on<AddEvent>(_onAddEvent);
|
||||||
|
on<StartTimer>(_onStartTimer);
|
||||||
|
on<DisposeResources>(_onDisposeResources);
|
||||||
|
on<GoToWeek>(_onGoToWeek);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadEvents(
|
||||||
|
LoadEvents event,
|
||||||
|
Emitter<CalendarEventState> emit,
|
||||||
|
) async {
|
||||||
|
emit(EventsLoading());
|
||||||
|
try {
|
||||||
|
final events = _generateDummyEventsForWeek(event.weekStart);
|
||||||
|
eventController.addAll(events);
|
||||||
|
emit(EventsLoaded(
|
||||||
|
events: events,
|
||||||
|
initialDate: event.weekStart,
|
||||||
|
weekDays: _getWeekDays(event.weekStart),
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(EventsError('Failed to load events'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
|
||||||
|
final events = <CalendarEventData>[];
|
||||||
|
|
||||||
|
for (int i = 0; i < 7; i++) {
|
||||||
|
final date = weekStart.add(Duration(days: i));
|
||||||
|
|
||||||
|
events.add(CalendarEventData(
|
||||||
|
date: date,
|
||||||
|
startTime: date.copyWith(hour: 9, minute: 0),
|
||||||
|
endTime: date.copyWith(hour: 10, minute: 30),
|
||||||
|
title: 'Team Meeting',
|
||||||
|
description: 'Daily standup',
|
||||||
|
color: Colors.blue,
|
||||||
|
));
|
||||||
|
events.add(CalendarEventData(
|
||||||
|
date: date,
|
||||||
|
startTime: date.copyWith(hour: 14, minute: 0),
|
||||||
|
endTime: date.copyWith(hour: 15, minute: 0),
|
||||||
|
title: 'Client Call',
|
||||||
|
description: 'Project discussion',
|
||||||
|
color: Colors.green,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
|
||||||
|
eventController.add(event.event);
|
||||||
|
if (state is EventsLoaded) {
|
||||||
|
final loaded = state as EventsLoaded;
|
||||||
|
emit(EventsLoaded(
|
||||||
|
events: [...eventController.events],
|
||||||
|
initialDate: loaded.initialDate,
|
||||||
|
weekDays: loaded.weekDays,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
|
||||||
|
|
||||||
|
void _onDisposeResources(
|
||||||
|
DisposeResources event, Emitter<CalendarEventState> emit) {
|
||||||
|
eventController.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onGoToWeek(GoToWeek event, Emitter<CalendarEventState> emit) {
|
||||||
|
if (state is EventsLoaded) {
|
||||||
|
final loaded = state as EventsLoaded;
|
||||||
|
final newWeekDays = _getWeekDays(event.weekDate);
|
||||||
|
emit(EventsLoaded(
|
||||||
|
events: loaded.events,
|
||||||
|
initialDate: event.weekDate,
|
||||||
|
weekDays: newWeekDays,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CalendarEventData> _generateDummyEvents() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return [
|
||||||
|
CalendarEventData(
|
||||||
|
date: now,
|
||||||
|
startTime: now.copyWith(hour: 8, minute: 00, second: 0),
|
||||||
|
endTime: now.copyWith(hour: 9, minute: 00, second: 0),
|
||||||
|
title: 'Team Meeting',
|
||||||
|
description: 'Weekly team sync',
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
CalendarEventData(
|
||||||
|
date: now,
|
||||||
|
startTime: now.copyWith(hour: 9, minute: 00, second: 0),
|
||||||
|
endTime: now.copyWith(hour: 10, minute: 30, second: 0),
|
||||||
|
title: 'Team Meeting',
|
||||||
|
description: 'Weekly team sync',
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
CalendarEventData(
|
||||||
|
date: now.add(const Duration(days: 1)),
|
||||||
|
startTime: now.copyWith(hour: 14, day: now.day + 1),
|
||||||
|
endTime: now.copyWith(hour: 15, day: now.day + 1),
|
||||||
|
title: 'Client Call',
|
||||||
|
description: 'Project discussion',
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
CalendarEventData(
|
||||||
|
date: now.add(const Duration(days: 2)),
|
||||||
|
startTime: now.copyWith(hour: 11, day: now.day + 2),
|
||||||
|
endTime: now.copyWith(hour: 12, day: now.day + 2),
|
||||||
|
title: 'Lunch with Team',
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DateTime> _getWeekDays(DateTime date) {
|
||||||
|
final int weekday = date.weekday;
|
||||||
|
final DateTime monday = date.subtract(Duration(days: weekday - 1));
|
||||||
|
return List.generate(7, (i) => monday.add(Duration(days: i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
eventController.dispose();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
part of 'events_bloc.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
abstract class CalendarEventsEvent {
|
||||||
|
const CalendarEventsEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadEvents extends CalendarEventsEvent {
|
||||||
|
final DateTime weekStart;
|
||||||
|
const LoadEvents({required this.weekStart});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddEvent extends CalendarEventsEvent {
|
||||||
|
final CalendarEventData event;
|
||||||
|
AddEvent(this.event);
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartTimer extends CalendarEventsEvent {}
|
||||||
|
|
||||||
|
class DisposeResources extends CalendarEventsEvent {}
|
||||||
|
|
||||||
|
class GoToWeek extends CalendarEventsEvent {
|
||||||
|
final DateTime weekDate;
|
||||||
|
GoToWeek(this.weekDate);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
part of 'events_bloc.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
abstract class CalendarEventState {}
|
||||||
|
|
||||||
|
class EventsInitial extends CalendarEventState {}
|
||||||
|
|
||||||
|
class EventsLoading extends CalendarEventState {}
|
||||||
|
|
||||||
|
class EventsLoaded extends CalendarEventState {
|
||||||
|
final List<CalendarEventData> events;
|
||||||
|
final DateTime initialDate;
|
||||||
|
final List<DateTime> weekDays;
|
||||||
|
|
||||||
|
EventsLoaded({
|
||||||
|
required this.events,
|
||||||
|
required this.initialDate,
|
||||||
|
required this.weekDays,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventsError extends CalendarEventState {
|
||||||
|
final String message;
|
||||||
|
EventsError(this.message);
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
|
||||||
|
import 'date_selection_state.dart';
|
||||||
|
|
||||||
|
class DateSelectionBloc extends Bloc<DateSelectionEvent, DateSelectionState> {
|
||||||
|
DateSelectionBloc() : super(DateSelectionState.initial()) {
|
||||||
|
on<SelectDate>((event, emit) {
|
||||||
|
final newWeekStart = _getStartOfWeek(event.selectedDate);
|
||||||
|
emit(DateSelectionState(
|
||||||
|
selectedDate: event.selectedDate,
|
||||||
|
weekStart: newWeekStart,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
on<NextWeek>((event, emit) {
|
||||||
|
final newWeekStart = state.weekStart.add(const Duration(days: 7));
|
||||||
|
final inNewWeek = state.selectedDate
|
||||||
|
.isAfter(newWeekStart.subtract(const Duration(days: 1))) &&
|
||||||
|
state.selectedDate
|
||||||
|
.isBefore(newWeekStart.add(const Duration(days: 7)));
|
||||||
|
emit(DateSelectionState(
|
||||||
|
selectedDate: state.selectedDate,
|
||||||
|
weekStart: newWeekStart,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
on<PreviousWeek>((event, emit) {
|
||||||
|
emit(DateSelectionState(
|
||||||
|
selectedDate: state.selectedDate!.subtract(const Duration(days: 7)),
|
||||||
|
weekStart: state.weekStart.subtract(const Duration(days: 7)),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static DateTime _getStartOfWeek(DateTime date) {
|
||||||
|
return date.subtract(Duration(days: date.weekday - 1));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
abstract class DateSelectionEvent {
|
||||||
|
const DateSelectionEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectDate extends DateSelectionEvent {
|
||||||
|
final DateTime selectedDate;
|
||||||
|
const SelectDate(this.selectedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NextWeek extends DateSelectionEvent {}
|
||||||
|
|
||||||
|
class PreviousWeek extends DateSelectionEvent {}
|
@ -0,0 +1,21 @@
|
|||||||
|
class DateSelectionState {
|
||||||
|
final DateTime selectedDate;
|
||||||
|
final DateTime weekStart;
|
||||||
|
|
||||||
|
const DateSelectionState({
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.weekStart,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DateSelectionState.initial() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return DateSelectionState(
|
||||||
|
selectedDate: now,
|
||||||
|
weekStart: _getStartOfWeek(now),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DateTime _getStartOfWeek(DateTime date) {
|
||||||
|
return date.subtract(Duration(days: date.weekday - 1));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||||
|
part 'selected_bookable_space_event.dart';
|
||||||
|
part 'selected_bookable_space_state.dart';
|
||||||
|
|
||||||
|
class SelectedBookableSpaceBloc
|
||||||
|
extends Bloc<SelectedBookableSpaceEvent, SelectedBookableSpaceState> {
|
||||||
|
SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) {
|
||||||
|
on<SelectBookableSpace>((event, emit) {
|
||||||
|
emit(SelectedBookableSpaceState(
|
||||||
|
selectedBookableSpace: event.bookableSpace));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
part of 'selected_bookable_space_bloc.dart';
|
||||||
|
|
||||||
|
abstract class SelectedBookableSpaceEvent {
|
||||||
|
const SelectedBookableSpaceEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectBookableSpace extends SelectedBookableSpaceEvent {
|
||||||
|
final BookableSpaceModel bookableSpace;
|
||||||
|
|
||||||
|
const SelectBookableSpace(this.bookableSpace);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
part of 'selected_bookable_space_bloc.dart';
|
||||||
|
|
||||||
|
class SelectedBookableSpaceState {
|
||||||
|
final BookableSpaceModel? selectedBookableSpace;
|
||||||
|
|
||||||
|
const SelectedBookableSpaceState(
|
||||||
|
{ this.selectedBookableSpace,}
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
|
||||||
|
|
||||||
|
class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
|
||||||
|
final BookingSystemService _bookingService;
|
||||||
|
Timer? _searchDebounce;
|
||||||
|
int _currentPage = 1;
|
||||||
|
final int _pageSize = 20;
|
||||||
|
String _currentSearch = '';
|
||||||
|
|
||||||
|
SidebarBloc(this._bookingService)
|
||||||
|
: super(SidebarState(
|
||||||
|
allRooms: [],
|
||||||
|
displayedRooms: [],
|
||||||
|
isLoading: true,
|
||||||
|
hasMore: true,
|
||||||
|
)) {
|
||||||
|
on<LoadBookableSpaces>(_onLoadBookableSpaces);
|
||||||
|
on<LoadMoreSpaces>(_onLoadMoreSpaces);
|
||||||
|
on<SelectRoomEvent>(_onSelectRoom);
|
||||||
|
on<SearchRoomsEvent>(_onSearchRooms);
|
||||||
|
on<ResetSearch>(_onResetSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadBookableSpaces(
|
||||||
|
LoadBookableSpaces event,
|
||||||
|
Emitter<SidebarState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||||
|
_currentPage = 1;
|
||||||
|
_currentSearch = '';
|
||||||
|
|
||||||
|
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||||
|
page: _currentPage,
|
||||||
|
size: _pageSize,
|
||||||
|
search: _currentSearch,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
allRooms: paginatedSpaces.data,
|
||||||
|
displayedRooms: paginatedSpaces.data,
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: paginatedSpaces.hasNext,
|
||||||
|
totalPages: paginatedSpaces.totalPage,
|
||||||
|
currentPage: _currentPage,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: 'Failed to load rooms: ${e.toString()}',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadMoreSpaces(
|
||||||
|
LoadMoreSpaces event,
|
||||||
|
Emitter<SidebarState> emit,
|
||||||
|
) async {
|
||||||
|
if (!state.hasMore || state.isLoadingMore) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
emit(state.copyWith(isLoadingMore: true));
|
||||||
|
_currentPage++;
|
||||||
|
|
||||||
|
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||||
|
page: _currentPage,
|
||||||
|
size: _pageSize,
|
||||||
|
search: _currentSearch,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedRooms = [...state.allRooms, ...paginatedSpaces.data];
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
allRooms: updatedRooms,
|
||||||
|
displayedRooms: updatedRooms,
|
||||||
|
isLoadingMore: false,
|
||||||
|
hasMore: paginatedSpaces.hasNext,
|
||||||
|
currentPage: _currentPage,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
_currentPage--;
|
||||||
|
emit(state.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
errorMessage: 'Failed to load more rooms: ${e.toString()}',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSearchRooms(
|
||||||
|
SearchRoomsEvent event,
|
||||||
|
Emitter<SidebarState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
_currentSearch = event.query;
|
||||||
|
_currentPage = 1;
|
||||||
|
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||||
|
final paginatedSpaces = await _bookingService.getBookableSpaces(
|
||||||
|
page: _currentPage,
|
||||||
|
size: _pageSize,
|
||||||
|
search: _currentSearch,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
allRooms: paginatedSpaces.data,
|
||||||
|
displayedRooms: paginatedSpaces.data,
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: paginatedSpaces.hasNext,
|
||||||
|
totalPages: paginatedSpaces.totalPage,
|
||||||
|
currentPage: _currentPage,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: 'Search failed: ${e.toString()}',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onResetSearch(
|
||||||
|
ResetSearch event,
|
||||||
|
Emitter<SidebarState> emit,
|
||||||
|
) {
|
||||||
|
_currentSearch = '';
|
||||||
|
add(LoadBookableSpaces());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectRoom(
|
||||||
|
SelectRoomEvent event,
|
||||||
|
Emitter<SidebarState> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(selectedRoomId: event.roomId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
abstract class SidebarEvent {}
|
||||||
|
|
||||||
|
class LoadBookableSpaces extends SidebarEvent {}
|
||||||
|
|
||||||
|
class SelectRoomEvent extends SidebarEvent {
|
||||||
|
final String roomId;
|
||||||
|
|
||||||
|
SelectRoomEvent(this.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchRoomsEvent extends SidebarEvent {
|
||||||
|
final String query;
|
||||||
|
|
||||||
|
SearchRoomsEvent(this.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadMoreSpaces extends SidebarEvent {}
|
||||||
|
|
||||||
|
class ResetSearch extends SidebarEvent {}
|
||||||
|
|
||||||
|
class ExecuteSearch extends SidebarEvent {
|
||||||
|
final String query;
|
||||||
|
|
||||||
|
ExecuteSearch(this.query);
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||||
|
|
||||||
|
class SidebarState {
|
||||||
|
final List<BookableSpaceModel> allRooms;
|
||||||
|
final List<BookableSpaceModel> displayedRooms;
|
||||||
|
final bool isLoading;
|
||||||
|
final bool isLoadingMore;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String? selectedRoomId;
|
||||||
|
final bool hasMore;
|
||||||
|
final int totalPages;
|
||||||
|
final int currentPage;
|
||||||
|
|
||||||
|
SidebarState({
|
||||||
|
required this.allRooms,
|
||||||
|
required this.displayedRooms,
|
||||||
|
required this.isLoading,
|
||||||
|
this.isLoadingMore = false,
|
||||||
|
this.errorMessage,
|
||||||
|
this.selectedRoomId,
|
||||||
|
this.hasMore = true,
|
||||||
|
this.totalPages = 0,
|
||||||
|
this.currentPage = 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
SidebarState copyWith({
|
||||||
|
List<BookableSpaceModel>? allRooms,
|
||||||
|
List<BookableSpaceModel>? displayedRooms,
|
||||||
|
bool? isLoading,
|
||||||
|
bool? isLoadingMore,
|
||||||
|
String? errorMessage,
|
||||||
|
String? selectedRoomId,
|
||||||
|
bool? hasMore,
|
||||||
|
int? totalPages,
|
||||||
|
int? currentPage,
|
||||||
|
}) {
|
||||||
|
return SidebarState(
|
||||||
|
allRooms: allRooms ?? this.allRooms,
|
||||||
|
displayedRooms: displayedRooms ?? this.displayedRooms,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
selectedRoomId: selectedRoomId ?? this.selectedRoomId,
|
||||||
|
hasMore: hasMore ?? this.hasMore,
|
||||||
|
totalPages: totalPages ?? this.totalPages,
|
||||||
|
currentPage: currentPage ?? this.currentPage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,259 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:calendar_view/calendar_view.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class BookingPage extends StatefulWidget {
|
||||||
|
const BookingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BookingPage> createState() => _BookingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BookingPageState extends State<BookingPage> {
|
||||||
|
late final EventController _eventController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_eventController = EventController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_eventController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
|
||||||
|
final List<CalendarEventData> events = [];
|
||||||
|
for (int i = 0; i < 7; i++) {
|
||||||
|
final date = weekStart.add(Duration(days: i));
|
||||||
|
events.add(CalendarEventData(
|
||||||
|
date: date,
|
||||||
|
startTime: date.copyWith(hour: 9, minute: 0),
|
||||||
|
endTime: date.copyWith(hour: 10, minute: 30),
|
||||||
|
title: 'Team Meeting',
|
||||||
|
description: 'Daily standup',
|
||||||
|
color: Colors.blue,
|
||||||
|
));
|
||||||
|
events.add(CalendarEventData(
|
||||||
|
date: date,
|
||||||
|
startTime: date.copyWith(hour: 14, minute: 0),
|
||||||
|
endTime: date.copyWith(hour: 15, minute: 0),
|
||||||
|
title: 'Client Call',
|
||||||
|
description: 'Project discussion',
|
||||||
|
color: Colors.green,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadEventsForWeek(DateTime weekStart) {
|
||||||
|
_eventController.removeWhere((_) => true);
|
||||||
|
_eventController.addAll(_generateDummyEventsForWeek(weekStart));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
|
||||||
|
BlocProvider(create: (_) => DateSelectionBloc()),
|
||||||
|
],
|
||||||
|
child: BlocListener<DateSelectionBloc, DateSelectionState>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous.weekStart != current.weekStart,
|
||||||
|
listener: (context, state) {
|
||||||
|
_loadEventsForWeek(state.weekStart);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||||
|
offset: const Offset(3, 0),
|
||||||
|
blurRadius: 6,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||||
|
SelectedBookableSpaceState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return BookingSidebar(
|
||||||
|
onRoomSelected: (selectedRoom) {
|
||||||
|
context
|
||||||
|
.read<SelectedBookableSpaceBloc>()
|
||||||
|
.add(SelectBookableSpace(selectedRoom));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<DateSelectionBloc, DateSelectionState>(
|
||||||
|
builder: (context, dateState) {
|
||||||
|
return CustomCalendarPage(
|
||||||
|
selectedDate: dateState.selectedDate,
|
||||||
|
onDateChanged: (day, month, year) {
|
||||||
|
final newDate = DateTime(year, month, day);
|
||||||
|
context
|
||||||
|
.read<DateSelectionBloc>()
|
||||||
|
.add(SelectDate(newDate));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 4,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SvgTextButton(
|
||||||
|
svgAsset: Assets.homeIcon,
|
||||||
|
label: 'Manage Bookable Spaces',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
SvgTextButton(
|
||||||
|
svgAsset: Assets.groupIcon,
|
||||||
|
label: 'Manage Users',
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
BlocBuilder<DateSelectionBloc, DateSelectionState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final weekStart = state.weekStart;
|
||||||
|
final weekEnd =
|
||||||
|
weekStart.add(const Duration(days: 6));
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.circleRolesBackground,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
iconSize: 15,
|
||||||
|
icon: const Icon(Icons.arrow_back_ios,
|
||||||
|
color: ColorsManager.lightGrayColor),
|
||||||
|
onPressed: () {
|
||||||
|
context
|
||||||
|
.read<DateSelectionBloc>()
|
||||||
|
.add(PreviousWeek());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
_getMonthYearText(weekStart, weekEnd),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
IconButton(
|
||||||
|
iconSize: 15,
|
||||||
|
icon: const Icon(Icons.arrow_forward_ios,
|
||||||
|
color: ColorsManager.lightGrayColor),
|
||||||
|
onPressed: () {
|
||||||
|
context
|
||||||
|
.read<DateSelectionBloc>()
|
||||||
|
.add(NextWeek());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||||
|
SelectedBookableSpaceState>(
|
||||||
|
builder: (context, roomState) {
|
||||||
|
final selectedRoom = roomState.selectedBookableSpace;
|
||||||
|
return BlocBuilder<DateSelectionBloc,
|
||||||
|
DateSelectionState>(
|
||||||
|
builder: (context, dateState) {
|
||||||
|
return WeeklyCalendarPage(
|
||||||
|
startTime:
|
||||||
|
selectedRoom?.bookableConfig.startTime,
|
||||||
|
endTime: selectedRoom?.bookableConfig.endTime,
|
||||||
|
weekStart: dateState.weekStart,
|
||||||
|
selectedDate: dateState.selectedDate,
|
||||||
|
eventController: _eventController,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getMonthYearText(DateTime start, DateTime end) {
|
||||||
|
final startMonth = DateFormat('MMM').format(start);
|
||||||
|
final endMonth = DateFormat('MMM').format(end);
|
||||||
|
final year = start.year == end.year
|
||||||
|
? start.year.toString()
|
||||||
|
: '${start.year}-${end.year}';
|
||||||
|
|
||||||
|
if (start.month == end.month) {
|
||||||
|
return '$startMonth $year';
|
||||||
|
} else {
|
||||||
|
return '$startMonth - $endMonth $year';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,248 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/data/services/bookable_spaces_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class BookingSidebar extends StatelessWidget {
|
||||||
|
final void Function(BookableSpaceModel) onRoomSelected;
|
||||||
|
|
||||||
|
const BookingSidebar({
|
||||||
|
super.key,
|
||||||
|
required this.onRoomSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => SidebarBloc(BookableSpacesService(
|
||||||
|
HTTPService(),
|
||||||
|
))
|
||||||
|
..add(LoadBookableSpaces()),
|
||||||
|
child: _SidebarContent(onRoomSelected: onRoomSelected),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SidebarContent extends StatefulWidget {
|
||||||
|
final void Function(BookableSpaceModel) onRoomSelected;
|
||||||
|
|
||||||
|
const _SidebarContent({
|
||||||
|
required this.onRoomSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SidebarContent> createState() => __SidebarContentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class __SidebarContentState extends State<_SidebarContent> {
|
||||||
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
Timer? _searchDebounce;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController.addListener(_scrollListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollListener() {
|
||||||
|
if (_scrollController.position.pixels ==
|
||||||
|
_scrollController.position.maxScrollExtent) {
|
||||||
|
context.read<SidebarBloc>().add(LoadMoreSpaces());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSearch(String value) {
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||||
|
context.read<SidebarBloc>().add(SearchRoomsEvent(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocConsumer<SidebarBloc, SidebarState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
|
||||||
|
searchController.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const _SidebarHeader(title: 'Spaces'),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
blurRadius: 4,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
blurRadius: 4,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Container(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.counterBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: _handleSearch,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search',
|
||||||
|
suffixIcon: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.searchIconUser,
|
||||||
|
color: ColorsManager.primaryTextColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8, horizontal: 12),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderSide: BorderSide.none),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (searchController.text.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
context.read<SidebarBloc>().add(ResetSearch());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (state.isLoading)
|
||||||
|
const Expanded(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
)
|
||||||
|
else if (state.errorMessage != null)
|
||||||
|
Expanded(
|
||||||
|
child: Center(child: Text(state.errorMessage!)),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
itemCount:
|
||||||
|
state.displayedRooms.length + (state.hasMore ? 1 : 0),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == state.displayedRooms.length) {
|
||||||
|
return _buildLoadMoreIndicator(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
final room = state.displayedRooms[index];
|
||||||
|
return RoomListItem(
|
||||||
|
room: room,
|
||||||
|
isSelected: state.selectedRoomId == room.uuid,
|
||||||
|
onTap: () {
|
||||||
|
context
|
||||||
|
.read<SidebarBloc>()
|
||||||
|
.add(SelectRoomEvent(room.uuid));
|
||||||
|
widget.onRoomSelected(room);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadMoreIndicator(SidebarState state) {
|
||||||
|
if (state.isLoadingMore) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
} else if (state.hasMore) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: Center(child: Text('Scroll to load more')),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SidebarHeader extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const _SidebarHeader({
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: ColorsManager.primaryTextColor,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class CustomCalendarPage extends StatefulWidget {
|
||||||
|
final DateTime selectedDate;
|
||||||
|
final Function(int day, int month, int year) onDateChanged;
|
||||||
|
|
||||||
|
const CustomCalendarPage({
|
||||||
|
super.key,
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.onDateChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomCalendarPage> createState() => _CustomCalendarPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomCalendarPageState extends State<CustomCalendarPage> {
|
||||||
|
late DateTime _selectedDate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedDate = widget.selectedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(CustomCalendarPage oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.selectedDate != oldWidget.selectedDate) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDate = widget.selectedDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final config = CalendarDatePicker2Config(
|
||||||
|
calendarType: CalendarDatePicker2Type.single,
|
||||||
|
selectedDayHighlightColor: const Color(0xFF3B82F6),
|
||||||
|
selectedDayTextStyle: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
dayTextStyle: const TextStyle(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
weekdayLabelTextStyle: const TextStyle(
|
||||||
|
color: ColorsManager.grey50,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
controlsTextStyle: const TextStyle(
|
||||||
|
color: Color(0xFF232D3A),
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
centerAlignModePicker: false,
|
||||||
|
disableMonthPicker: true,
|
||||||
|
firstDayOfWeek: 1,
|
||||||
|
weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return CalendarDatePicker2(
|
||||||
|
config: config,
|
||||||
|
value: [_selectedDate],
|
||||||
|
onValueChanged: (dates) {
|
||||||
|
final picked = dates.first;
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDate = picked;
|
||||||
|
});
|
||||||
|
widget.onDateChanged(picked.day, picked.month, picked.year);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class SvgTextButton extends StatelessWidget {
|
||||||
|
final String svgAsset;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Color svgColor;
|
||||||
|
final Color labelColor;
|
||||||
|
final double borderRadius;
|
||||||
|
final List<BoxShadow> boxShadow;
|
||||||
|
final double svgSize;
|
||||||
|
|
||||||
|
const SvgTextButton({
|
||||||
|
super.key,
|
||||||
|
required this.svgAsset,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
this.backgroundColor = ColorsManager.circleRolesBackground,
|
||||||
|
this.svgColor = const Color(0xFF496EFF),
|
||||||
|
this.labelColor = Colors.black,
|
||||||
|
this.borderRadius = 10.0,
|
||||||
|
this.boxShadow = const [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
this.svgSize = 24.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
boxShadow: boxShadow,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
svgAsset,
|
||||||
|
width: svgSize,
|
||||||
|
height: svgSize,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: labelColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class RoomListItem extends StatelessWidget {
|
||||||
|
final BookableSpaceModel room;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const RoomListItem({
|
||||||
|
required this.room,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RadioListTile(
|
||||||
|
value: room.uuid,
|
||||||
|
contentPadding: const EdgeInsetsDirectional.symmetric(horizontal: 16),
|
||||||
|
groupValue: isSelected ? room.uuid : null,
|
||||||
|
visualDensity: const VisualDensity(vertical: -4),
|
||||||
|
onChanged: (value) => onTap(),
|
||||||
|
activeColor: ColorsManager.primaryColor,
|
||||||
|
title: Text(
|
||||||
|
room.spaceName,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 12),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
room.virtualLocation,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: ColorsManager.textGray,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,260 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:calendar_view/calendar_view.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class WeeklyCalendarPage extends StatelessWidget {
|
||||||
|
final DateTime weekStart;
|
||||||
|
final DateTime selectedDate;
|
||||||
|
final EventController eventController;
|
||||||
|
final String? startTime;
|
||||||
|
final String? endTime;
|
||||||
|
|
||||||
|
const WeeklyCalendarPage({
|
||||||
|
super.key,
|
||||||
|
required this.weekStart,
|
||||||
|
required this.selectedDate,
|
||||||
|
required this.eventController,
|
||||||
|
this.startTime,
|
||||||
|
this.endTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final startHour = _parseHour(startTime, defaultValue: 0);
|
||||||
|
final endHour = _parseHour(endTime, defaultValue: 24);
|
||||||
|
if (endTime == null || endTime!.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'Please select a bookable space to view the calendar.',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final weekDays = _getWeekDays(weekStart);
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final double calendarWidth = constraints.maxWidth;
|
||||||
|
const double timeLineWidth = 80;
|
||||||
|
const int totalDays = 7;
|
||||||
|
final double dayColumnWidth =
|
||||||
|
(calendarWidth - timeLineWidth) / totalDays;
|
||||||
|
final selectedDayIndex =
|
||||||
|
weekDays.indexWhere((d) => isSameDay(d, selectedDate));
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
WeekView(
|
||||||
|
key: ValueKey(weekStart),
|
||||||
|
controller: eventController,
|
||||||
|
initialDay: weekStart,
|
||||||
|
startHour: startHour - 1,
|
||||||
|
endHour: endHour,
|
||||||
|
heightPerMinute: 1.1,
|
||||||
|
showLiveTimeLineInAllDays: false,
|
||||||
|
showVerticalLines: true,
|
||||||
|
emulateVerticalOffsetBy: -80,
|
||||||
|
startDay: WeekDays.monday,
|
||||||
|
liveTimeIndicatorSettings: const LiveTimeIndicatorSettings(
|
||||||
|
showBullet: false,
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
weekDayBuilder: (date) {
|
||||||
|
final weekDays = _getWeekDays(weekStart);
|
||||||
|
final selectedDayIndex =
|
||||||
|
weekDays.indexWhere((d) => isSameDay(d, selectedDate));
|
||||||
|
final index = weekDays.indexWhere((d) => isSameDay(d, date));
|
||||||
|
final isSelectedDay = index == selectedDayIndex;
|
||||||
|
final isToday = isSameDay(date, DateTime.now());
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: isSelectedDay
|
||||||
|
? BoxDecoration(
|
||||||
|
color: ColorsManager.blue1.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
)
|
||||||
|
: isToday
|
||||||
|
? BoxDecoration(
|
||||||
|
color: ColorsManager.blue1.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat('EEE').format(date).toUpperCase(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 14,
|
||||||
|
color: isSelectedDay ? Colors.blue : Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
DateFormat('d').format(date),
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 20,
|
||||||
|
color: isSelectedDay
|
||||||
|
? ColorsManager.blue1
|
||||||
|
: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
timeLineBuilder: (date) {
|
||||||
|
int hour = date.hour == 0
|
||||||
|
? 12
|
||||||
|
: (date.hour > 12 ? date.hour - 12 : date.hour);
|
||||||
|
String period = date.hour >= 12 ? 'PM' : 'AM';
|
||||||
|
return Container(
|
||||||
|
height: 60,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '$hour',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 24,
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
WidgetSpan(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 2, top: 6),
|
||||||
|
child: Text(
|
||||||
|
period,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 12,
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alignment: PlaceholderAlignment.baseline,
|
||||||
|
baseline: TextBaseline.alphabetic,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
timeLineWidth: timeLineWidth,
|
||||||
|
weekPageHeaderBuilder: (start, end) => Container(),
|
||||||
|
weekTitleHeight: 60,
|
||||||
|
weekNumberBuilder: (firstDayOfWeek) => Text(
|
||||||
|
firstDayOfWeek.timeZoneName,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Colors.black,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventTileBuilder: (date, events, boundary, start, end) {
|
||||||
|
return Container(
|
||||||
|
margin:
|
||||||
|
const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: events.map((event) {
|
||||||
|
final bool isEventEnded = event.endTime != null &&
|
||||||
|
event.endTime!.isBefore(DateTime.now());
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isEventEnded
|
||||||
|
? ColorsManager.grayColor
|
||||||
|
: ColorsManager.lightGrayColor
|
||||||
|
.withOpacity(0.25),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat('h:mm a').format(event.startTime!),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
event.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (selectedDayIndex >= 0)
|
||||||
|
Positioned(
|
||||||
|
left: timeLineWidth + dayColumnWidth * selectedDayIndex,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: dayColumnWidth,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 0, horizontal: 2),
|
||||||
|
color: ColorsManager.blue1.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
top: 50,
|
||||||
|
bottom: 0,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
width: 1,
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DateTime> _getWeekDays(DateTime date) {
|
||||||
|
final int weekday = date.weekday;
|
||||||
|
final DateTime monday = date.subtract(Duration(days: weekday - 1));
|
||||||
|
return List.generate(7, (i) => monday.add(Duration(days: i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSameDay(DateTime d1, DateTime d2) {
|
||||||
|
return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _parseHour(String? time, {required int defaultValue}) {
|
||||||
|
if (time == null || time.isEmpty || !time.contains(':')) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return int.parse(time.split(':')[0]);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
@ -2,302 +2,86 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
|
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart';
|
||||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
|
||||||
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/custom_table.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/date_time_widget.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
|
|
||||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||||
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
|
|
||||||
// import 'package:syncrow_web/utils/color_manager.dart';
|
|
||||||
import 'package:syncrow_web/utils/constants/app_enum.dart';
|
|
||||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||||
import 'package:syncrow_web/utils/style.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';
|
||||||
|
|
||||||
class AccessManagementPage extends StatelessWidget with HelperResponsiveLayout {
|
class AccessManagementPage extends StatefulWidget {
|
||||||
const AccessManagementPage({super.key});
|
const AccessManagementPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<AccessManagementPage> createState() => _AccessManagementPageState();
|
||||||
final isLargeScreen = isLargeScreenSize(context);
|
}
|
||||||
final isSmallScreen = isSmallScreenSize(context);
|
|
||||||
final isHalfMediumScreen = isHafMediumScreenSize(context);
|
|
||||||
final padding =
|
|
||||||
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
|
|
||||||
|
|
||||||
return WebScaffold(
|
class _AccessManagementPageState extends State<AccessManagementPage>
|
||||||
|
with HelperResponsiveLayout {
|
||||||
|
final PageController _pageController = PageController(initialPage: 0);
|
||||||
|
int _currentPageIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
|
||||||
|
child: WebScaffold(
|
||||||
enableMenuSidebar: false,
|
enableMenuSidebar: false,
|
||||||
appBarTitle: Text(
|
appBarTitle: Text(
|
||||||
'Access Management',
|
'Access Management',
|
||||||
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
||||||
),
|
),
|
||||||
rightBody: const NavigateHomeGridView(),
|
centerBody: Row(
|
||||||
scaffoldBody: BlocProvider(
|
|
||||||
create: (BuildContext context) =>
|
|
||||||
AccessBloc()..add(FetchTableData()),
|
|
||||||
child: BlocConsumer<AccessBloc, AccessState>(
|
|
||||||
listener: (context, state) {},
|
|
||||||
builder: (context, state) {
|
|
||||||
final accessBloc = BlocProvider.of<AccessBloc>(context);
|
|
||||||
final filteredData = accessBloc.filteredData;
|
|
||||||
return state is AccessLoaded
|
|
||||||
? const Center(child: CircularProgressIndicator())
|
|
||||||
: Container(
|
|
||||||
padding: padding,
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
FilterWidget(
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
tabs: accessBloc.tabs,
|
|
||||||
selectedIndex: accessBloc.selectedIndex,
|
|
||||||
onTabChanged: (index) {
|
|
||||||
accessBloc.add(TabChangedEvent(index));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
if (isSmallScreen || isHalfMediumScreen)
|
|
||||||
_buildSmallSearchFilters(context, accessBloc)
|
|
||||||
else
|
|
||||||
_buildNormalSearchWidgets(context, accessBloc),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildVisitorAdminPasswords(context, accessBloc),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Expanded(
|
|
||||||
child: DynamicTable(
|
|
||||||
tableName: 'AccessManagement',
|
|
||||||
uuidIndex: 1,
|
|
||||||
withSelectAll: true,
|
|
||||||
isEmpty: filteredData.isEmpty,
|
|
||||||
withCheckBox: false,
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
cellDecoration: containerDecoration,
|
|
||||||
headers: const [
|
|
||||||
'Name',
|
|
||||||
'Access Type',
|
|
||||||
'Access Start',
|
|
||||||
'Access End',
|
|
||||||
'Accessible Device',
|
|
||||||
'Authorizer',
|
|
||||||
'Authorization Date & Time',
|
|
||||||
'Access Status'
|
|
||||||
],
|
|
||||||
data: filteredData.map((item) {
|
|
||||||
return [
|
|
||||||
item.passwordName,
|
|
||||||
item.passwordType.value,
|
|
||||||
accessBloc
|
|
||||||
.timestampToDate(item.effectiveTime),
|
|
||||||
accessBloc
|
|
||||||
.timestampToDate(item.invalidTime),
|
|
||||||
item.deviceName.toString(),
|
|
||||||
item.authorizerEmail.toString(),
|
|
||||||
accessBloc
|
|
||||||
.timestampToDate(item.invalidTime),
|
|
||||||
item.passwordStatus.value,
|
|
||||||
];
|
|
||||||
}).toList(),
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
Wrap _buildVisitorAdminPasswords(
|
|
||||||
BuildContext context, AccessBloc accessBloc) {
|
|
||||||
return Wrap(
|
|
||||||
spacing: 10,
|
|
||||||
runSpacing: 10,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 205,
|
|
||||||
height: 42,
|
|
||||||
decoration: containerDecoration,
|
|
||||||
child: DefaultButton(
|
|
||||||
onPressed: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return const VisitorPasswordDialog();
|
|
||||||
},
|
|
||||||
).then((v) {
|
|
||||||
if (v != null) {
|
|
||||||
accessBloc.add(FetchTableData());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
borderRadius: 8,
|
|
||||||
child: Text(
|
|
||||||
'Create Visitor Password ',
|
|
||||||
style: context.textTheme.titleSmall!
|
|
||||||
.copyWith(color: Colors.white, fontSize: 12),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
// Container(
|
|
||||||
// width: 133,
|
|
||||||
// height: 42,
|
|
||||||
// decoration: containerDecoration,
|
|
||||||
// child: DefaultButton(
|
|
||||||
// borderRadius: 8,
|
|
||||||
// backgroundColor: ColorsManager.whiteColors,
|
|
||||||
// child: Text(
|
|
||||||
// 'Admin Password',
|
|
||||||
// style: context.textTheme.titleSmall!
|
|
||||||
// .copyWith(color: Colors.black, fontSize: 12),
|
|
||||||
// )),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
|
|
||||||
// TimeOfDay _selectedTime = TimeOfDay.now();
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
textBaseline: TextBaseline.ideographic,
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
TextButton(
|
||||||
width: 250,
|
onPressed: () => _switchPage(0),
|
||||||
child: CustomWebTextField(
|
child: Text(
|
||||||
controller: accessBloc.passwordName,
|
'Access Overview',
|
||||||
height: 43,
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
isRequired: false,
|
color: _currentPageIndex == 0 ? Colors.white : Colors.grey,
|
||||||
textFieldName: 'Name',
|
fontWeight: _currentPageIndex == 0
|
||||||
description: '',
|
? FontWeight.w700
|
||||||
onSubmitted: (value) {
|
: FontWeight.w400,
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer:
|
|
||||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 15),
|
),
|
||||||
SizedBox(
|
TextButton(
|
||||||
width: 250,
|
onPressed: () => _switchPage(1),
|
||||||
child: CustomWebTextField(
|
child: Text(
|
||||||
controller: accessBloc.emailAuthorizer,
|
'Booking System',
|
||||||
height: 43,
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
isRequired: false,
|
color: _currentPageIndex == 1 ? Colors.white : Colors.grey,
|
||||||
textFieldName: 'Authorizer',
|
fontWeight: _currentPageIndex == 1
|
||||||
description: '',
|
? FontWeight.w700
|
||||||
onSubmitted: (value) {
|
: FontWeight.w400,
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer:
|
|
||||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 15),
|
|
||||||
SizedBox(
|
|
||||||
child: DateTimeWebWidget(
|
|
||||||
icon: Assets.calendarIcon,
|
|
||||||
isRequired: false,
|
|
||||||
title: 'Access Time',
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
endTime: () {
|
|
||||||
accessBloc.add(SelectTime(context: context, isStart: false));
|
|
||||||
},
|
|
||||||
startTime: () {
|
|
||||||
accessBloc.add(SelectTime(context: context, isStart: true));
|
|
||||||
},
|
|
||||||
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
|
||||||
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
SearchResetButtons(
|
|
||||||
onSearch: () {
|
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
},
|
|
||||||
onReset: () {
|
|
||||||
accessBloc.add(ResetSearch());
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
rightBody: const NavigateHomeGridView(),
|
||||||
|
scaffoldBody: PageView(
|
||||||
|
controller: _pageController,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: const [
|
||||||
|
AccessOverviewContent(),
|
||||||
|
BookingPage(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
|
void _switchPage(int index) {
|
||||||
return Wrap(
|
setState(() => _currentPageIndex = index);
|
||||||
spacing: 20,
|
_pageController.jumpToPage(index);
|
||||||
runSpacing: 10,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 300,
|
|
||||||
child: CustomWebTextField(
|
|
||||||
controller: accessBloc.passwordName,
|
|
||||||
isRequired: true,
|
|
||||||
height: 40,
|
|
||||||
textFieldName: 'Name',
|
|
||||||
description: '',
|
|
||||||
onSubmitted: (value) {
|
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer:
|
|
||||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
DateTimeWebWidget(
|
|
||||||
icon: Assets.calendarIcon,
|
|
||||||
isRequired: false,
|
|
||||||
title: 'Access Time',
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
endTime: () {
|
|
||||||
accessBloc.add(SelectTime(context: context, isStart: false));
|
|
||||||
},
|
|
||||||
startTime: () {
|
|
||||||
accessBloc.add(SelectTime(context: context, isStart: true));
|
|
||||||
},
|
|
||||||
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
|
||||||
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
|
||||||
),
|
|
||||||
SearchResetButtons(
|
|
||||||
onSearch: () {
|
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
},
|
|
||||||
onReset: () {
|
|
||||||
accessBloc.add(ResetSearch());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
289
lib/pages/access_management/view/access_overview_content.dart
Normal file
289
lib/pages/access_management/view/access_overview_content.dart
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/custom_table.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/date_time_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
||||||
|
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/app_enum.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
|
class AccessOverviewContent extends StatelessWidget
|
||||||
|
with HelperResponsiveLayout {
|
||||||
|
const AccessOverviewContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isLargeScreen = isLargeScreenSize(context);
|
||||||
|
final isSmallScreen = isSmallScreenSize(context);
|
||||||
|
final isHalfMediumScreen = isHafMediumScreenSize(context);
|
||||||
|
final padding =
|
||||||
|
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
|
||||||
|
|
||||||
|
return BlocProvider(
|
||||||
|
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
|
||||||
|
child: BlocConsumer<AccessBloc, AccessState>(
|
||||||
|
listener: (context, state) {},
|
||||||
|
builder: (context, state) {
|
||||||
|
final accessBloc = BlocProvider.of<AccessBloc>(context);
|
||||||
|
final filteredData = accessBloc.filteredData;
|
||||||
|
return state is AccessLoaded
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: Container(
|
||||||
|
padding: padding,
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FilterWidget(
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
tabs: accessBloc.tabs,
|
||||||
|
selectedIndex: accessBloc.selectedIndex,
|
||||||
|
onTabChanged: (index) {
|
||||||
|
accessBloc.add(TabChangedEvent(index));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (isSmallScreen || isHalfMediumScreen)
|
||||||
|
_buildSmallSearchFilters(context, accessBloc)
|
||||||
|
else
|
||||||
|
_buildNormalSearchWidgets(context, accessBloc),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildVisitorAdminPasswords(context, accessBloc),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: DynamicTable(
|
||||||
|
tableName: 'AccessManagement',
|
||||||
|
uuidIndex: 1,
|
||||||
|
withSelectAll: true,
|
||||||
|
isEmpty: filteredData.isEmpty,
|
||||||
|
withCheckBox: false,
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
cellDecoration: containerDecoration,
|
||||||
|
headers: const [
|
||||||
|
'Name',
|
||||||
|
'Access Type',
|
||||||
|
'Access Start',
|
||||||
|
'Access End',
|
||||||
|
'Accessible Device',
|
||||||
|
'Authorizer',
|
||||||
|
'Authorization Date & Time',
|
||||||
|
'Access Status'
|
||||||
|
],
|
||||||
|
data: filteredData.map((item) {
|
||||||
|
return [
|
||||||
|
item.passwordName,
|
||||||
|
item.passwordType.value,
|
||||||
|
accessBloc.timestampToDate(item.effectiveTime),
|
||||||
|
accessBloc.timestampToDate(item.invalidTime),
|
||||||
|
item.deviceName.toString(),
|
||||||
|
item.authorizerEmail.toString(),
|
||||||
|
accessBloc.timestampToDate(item.invalidTime),
|
||||||
|
item.passwordStatus.value,
|
||||||
|
];
|
||||||
|
}).toList(),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Wrap _buildVisitorAdminPasswords(
|
||||||
|
BuildContext context, AccessBloc accessBloc) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 10,
|
||||||
|
runSpacing: 10,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 205,
|
||||||
|
height: 42,
|
||||||
|
decoration: containerDecoration,
|
||||||
|
child: DefaultButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const VisitorPasswordDialog();
|
||||||
|
},
|
||||||
|
).then((v) {
|
||||||
|
if (v != null) {
|
||||||
|
accessBloc.add(FetchTableData());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
borderRadius: 8,
|
||||||
|
child: Text(
|
||||||
|
'Create Visitor Password ',
|
||||||
|
style: context.textTheme.titleSmall!
|
||||||
|
.copyWith(color: Colors.white, fontSize: 12),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
// Container(
|
||||||
|
// width: 133,
|
||||||
|
// height: 42,
|
||||||
|
// decoration: containerDecoration,
|
||||||
|
// child: DefaultButton(
|
||||||
|
// borderRadius: 8,
|
||||||
|
// backgroundColor: ColorsManager.whiteColors,
|
||||||
|
// child: Text(
|
||||||
|
// 'Admin Password',
|
||||||
|
// style: context.textTheme.titleSmall!
|
||||||
|
// .copyWith(color: Colors.black, fontSize: 12),
|
||||||
|
// )),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
|
||||||
|
// TimeOfDay _selectedTime = TimeOfDay.now();
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
textBaseline: TextBaseline.ideographic,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: CustomWebTextField(
|
||||||
|
controller: accessBloc.passwordName,
|
||||||
|
height: 43,
|
||||||
|
isRequired: false,
|
||||||
|
textFieldName: 'Name',
|
||||||
|
description: '',
|
||||||
|
onSubmitted: (value) {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer:
|
||||||
|
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: CustomWebTextField(
|
||||||
|
controller: accessBloc.emailAuthorizer,
|
||||||
|
height: 43,
|
||||||
|
isRequired: false,
|
||||||
|
textFieldName: 'Authorizer',
|
||||||
|
description: '',
|
||||||
|
onSubmitted: (value) {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer:
|
||||||
|
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
SizedBox(
|
||||||
|
child: DateTimeWebWidget(
|
||||||
|
icon: Assets.calendarIcon,
|
||||||
|
isRequired: false,
|
||||||
|
title: 'Access Time',
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
endTime: () {
|
||||||
|
accessBloc.add(SelectTime(context: context, isStart: false));
|
||||||
|
},
|
||||||
|
startTime: () {
|
||||||
|
accessBloc.add(SelectTime(context: context, isStart: true));
|
||||||
|
},
|
||||||
|
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
||||||
|
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
SearchResetButtons(
|
||||||
|
onSearch: () {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
},
|
||||||
|
onReset: () {
|
||||||
|
accessBloc.add(ResetSearch());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 20,
|
||||||
|
runSpacing: 10,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: CustomWebTextField(
|
||||||
|
controller: accessBloc.passwordName,
|
||||||
|
isRequired: true,
|
||||||
|
height: 40,
|
||||||
|
textFieldName: 'Name',
|
||||||
|
description: '',
|
||||||
|
onSubmitted: (value) {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer:
|
||||||
|
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
DateTimeWebWidget(
|
||||||
|
icon: Assets.calendarIcon,
|
||||||
|
isRequired: false,
|
||||||
|
title: 'Access Time',
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
endTime: () {
|
||||||
|
accessBloc.add(SelectTime(context: context, isStart: false));
|
||||||
|
},
|
||||||
|
startTime: () {
|
||||||
|
accessBloc.add(SelectTime(context: context, isStart: true));
|
||||||
|
},
|
||||||
|
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
||||||
|
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
||||||
|
),
|
||||||
|
SearchResetButtons(
|
||||||
|
onSearch: () {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
},
|
||||||
|
onReset: () {
|
||||||
|
accessBloc.add(ResetSearch());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -50,6 +50,9 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
bool _selectAll = false;
|
bool _selectAll = false;
|
||||||
final ScrollController _verticalScrollController = ScrollController();
|
final ScrollController _verticalScrollController = ScrollController();
|
||||||
final ScrollController _horizontalScrollController = ScrollController();
|
final ScrollController _horizontalScrollController = ScrollController();
|
||||||
|
static const double _fixedRowHeight = 60;
|
||||||
|
static const double _checkboxColumnWidth = 50;
|
||||||
|
static const double _settingsColumnWidth = 100;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -67,7 +70,6 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
|
|
||||||
bool _compareListOfLists(
|
bool _compareListOfLists(
|
||||||
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
||||||
// Check if the old and new lists are the same
|
|
||||||
if (oldList.length != newList.length) return false;
|
if (oldList.length != newList.length) return false;
|
||||||
|
|
||||||
for (int i = 0; i < oldList.length; i++) {
|
for (int i = 0; i < oldList.length; i++) {
|
||||||
@ -104,73 +106,130 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
|
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double get _totalTableWidth {
|
||||||
|
final hasSettings = widget.headers.contains('Settings');
|
||||||
|
final base = (widget.withCheckBox ? _checkboxColumnWidth : 0) +
|
||||||
|
(hasSettings ? _settingsColumnWidth : 0);
|
||||||
|
final regularCount = widget.headers.length - (hasSettings ? 1 : 0);
|
||||||
|
final regularWidth = (widget.size.width - base) / regularCount;
|
||||||
|
return base + regularCount * regularWidth;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
|
width: widget.size.width,
|
||||||
|
height: widget.size.height,
|
||||||
decoration: widget.cellDecoration,
|
decoration: widget.cellDecoration,
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: const ScrollBehavior().copyWith(scrollbars: false),
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
controller: _verticalScrollController,
|
|
||||||
thumbVisibility: true,
|
|
||||||
trackVisibility: true,
|
|
||||||
child: Scrollbar(
|
|
||||||
//fixed the horizontal scrollbar issue
|
|
||||||
controller: _horizontalScrollController,
|
controller: _horizontalScrollController,
|
||||||
thumbVisibility: true,
|
thumbVisibility: true,
|
||||||
trackVisibility: true,
|
trackVisibility: true,
|
||||||
notificationPredicate: (notif) => notif.depth == 1,
|
notificationPredicate: (notif) =>
|
||||||
child: SingleChildScrollView(
|
notif.metrics.axis == Axis.horizontal,
|
||||||
controller: _verticalScrollController,
|
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: _horizontalScrollController,
|
controller: _horizontalScrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: widget.size.width,
|
width: _totalTableWidth,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
|
height: _fixedRowHeight,
|
||||||
decoration: widget.headerDecoration ??
|
decoration: widget.headerDecoration ??
|
||||||
const BoxDecoration(
|
const BoxDecoration(color: ColorsManager.boxColor),
|
||||||
color: ColorsManager.boxColor,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.withCheckBox) _buildSelectAllCheckbox(),
|
if (widget.withCheckBox)
|
||||||
...List.generate(widget.headers.length, (index) {
|
_buildSelectAllCheckbox(_checkboxColumnWidth),
|
||||||
return _buildTableHeaderCell(
|
for (var i = 0; i < widget.headers.length; i++)
|
||||||
widget.headers[index], index);
|
_buildTableHeaderCell(
|
||||||
})
|
widget.headers[i],
|
||||||
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
|
widget.headers[i] == 'Settings'
|
||||||
|
? _settingsColumnWidth
|
||||||
|
: (_totalTableWidth -
|
||||||
|
(widget.withCheckBox
|
||||||
|
? _checkboxColumnWidth
|
||||||
|
: 0) -
|
||||||
|
(widget.headers.contains('Settings')
|
||||||
|
? _settingsColumnWidth
|
||||||
|
: 0)) /
|
||||||
|
(widget.headers.length -
|
||||||
|
(widget.headers.contains('Settings')
|
||||||
|
? 1
|
||||||
|
: 0)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
|
||||||
width: widget.size.width,
|
Expanded(
|
||||||
child: widget.isEmpty
|
child: widget.isEmpty
|
||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
: Column(
|
: Scrollbar(
|
||||||
children:
|
controller: _verticalScrollController,
|
||||||
List.generate(widget.data.length, (rowIndex) {
|
thumbVisibility: true,
|
||||||
|
trackVisibility: true,
|
||||||
|
notificationPredicate: (notif) =>
|
||||||
|
notif.metrics.axis == Axis.vertical,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _verticalScrollController,
|
||||||
|
itemCount: widget.data.length,
|
||||||
|
itemBuilder: (_, rowIndex) {
|
||||||
final row = widget.data[rowIndex];
|
final row = widget.data[rowIndex];
|
||||||
return Row(
|
return SizedBox(
|
||||||
|
height: _fixedRowHeight,
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.withCheckBox)
|
if (widget.withCheckBox)
|
||||||
_buildRowCheckbox(
|
_buildRowCheckbox(
|
||||||
rowIndex, widget.size.height * 0.08),
|
rowIndex,
|
||||||
...row.asMap().entries.map((entry) {
|
_checkboxColumnWidth,
|
||||||
return _buildTableCell(
|
),
|
||||||
entry.value.toString(),
|
for (var colIndex = 0;
|
||||||
widget.size.height * 0.08,
|
colIndex < row.length;
|
||||||
|
colIndex++)
|
||||||
|
widget.headers[colIndex] == 'Settings'
|
||||||
|
? buildSettingsIcon(
|
||||||
|
width: _settingsColumnWidth,
|
||||||
|
onTap: () => widget
|
||||||
|
.onSettingsPressed
|
||||||
|
?.call(rowIndex),
|
||||||
|
)
|
||||||
|
: _buildTableCell(
|
||||||
|
row[colIndex].toString(),
|
||||||
|
width: widget.headers[
|
||||||
|
colIndex] ==
|
||||||
|
'Settings'
|
||||||
|
? _settingsColumnWidth
|
||||||
|
: (_totalTableWidth -
|
||||||
|
(widget.withCheckBox
|
||||||
|
? _checkboxColumnWidth
|
||||||
|
: 0) -
|
||||||
|
(widget.headers
|
||||||
|
.contains(
|
||||||
|
'Settings')
|
||||||
|
? _settingsColumnWidth
|
||||||
|
: 0)) /
|
||||||
|
(widget.headers.length -
|
||||||
|
(widget.headers
|
||||||
|
.contains(
|
||||||
|
'Settings')
|
||||||
|
? 1
|
||||||
|
: 0)),
|
||||||
rowIndex: rowIndex,
|
rowIndex: rowIndex,
|
||||||
columnIndex: entry.key,
|
columnIndex: colIndex,
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -210,9 +269,10 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Widget _buildSelectAllCheckbox() {
|
|
||||||
|
Widget _buildSelectAllCheckbox(double width) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 50,
|
width: width,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border.symmetric(
|
border: Border.symmetric(
|
||||||
vertical: BorderSide(color: ColorsManager.boxDivider),
|
vertical: BorderSide(color: ColorsManager.boxDivider),
|
||||||
@ -227,11 +287,11 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRowCheckbox(int index, double size) {
|
Widget _buildRowCheckbox(int index, double width) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 50,
|
width: width,
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
height: size,
|
height: _fixedRowHeight,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
@ -253,20 +313,18 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTableHeaderCell(String title, int index) {
|
Widget _buildTableHeaderCell(String title, double width) {
|
||||||
return Expanded(
|
return Container(
|
||||||
child: Container(
|
width: width,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border.symmetric(
|
border: Border.symmetric(
|
||||||
vertical: BorderSide(color: ColorsManager.boxDivider),
|
vertical: BorderSide(color: ColorsManager.boxDivider),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
constraints: const BoxConstraints.expand(height: 40),
|
constraints: BoxConstraints(minHeight: 40, maxHeight: _fixedRowHeight),
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||||
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
|
|
||||||
vertical: 4),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: context.textTheme.titleSmall!.copyWith(
|
style: context.textTheme.titleSmall!.copyWith(
|
||||||
@ -275,28 +333,27 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTableCell(String content, double size,
|
Widget _buildTableCell(String content,
|
||||||
{required int rowIndex, required int columnIndex}) {
|
{required double width,
|
||||||
|
required int rowIndex,
|
||||||
|
required int columnIndex}) {
|
||||||
bool isBatteryLevel = content.endsWith('%');
|
bool isBatteryLevel = content.endsWith('%');
|
||||||
double? batteryLevel;
|
double? batteryLevel;
|
||||||
|
|
||||||
if (isBatteryLevel) {
|
if (isBatteryLevel) {
|
||||||
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
|
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
|
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
|
||||||
if (isSettingsColumn) {
|
if (isSettingsColumn) {
|
||||||
return buildSettingsIcon(
|
return buildSettingsIcon(
|
||||||
width: 120,
|
width: width, onTap: () => widget.onSettingsPressed?.call(rowIndex));
|
||||||
height: 60,
|
|
||||||
iconSize: 40,
|
|
||||||
onTap: () => widget.onSettingsPressed?.call(rowIndex),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Color? statusColor;
|
Color? statusColor;
|
||||||
@ -320,10 +377,10 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
statusColor = Colors.black;
|
statusColor = Colors.black;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Expanded(
|
return Container(
|
||||||
child: Container(
|
width: width,
|
||||||
height: size,
|
height: _fixedRowHeight,
|
||||||
padding: const EdgeInsets.all(5.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
@ -343,23 +400,19 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
? ColorsManager.green
|
? ColorsManager.green
|
||||||
: statusColor,
|
: statusColor,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w400),
|
fontWeight: FontWeight.w400,
|
||||||
maxLines: 2,
|
|
||||||
),
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSettingsIcon(
|
Widget buildSettingsIcon({required double width, VoidCallback? onTap}) {
|
||||||
{double width = 120,
|
return Container(
|
||||||
double height = 60,
|
width: width,
|
||||||
double iconSize = 40,
|
height: _fixedRowHeight,
|
||||||
VoidCallback? onTap}) {
|
padding: const EdgeInsets.only(left: 15, top: 10, bottom: 10),
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10),
|
|
||||||
margin: const EdgeInsets.only(right: 15),
|
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: ColorsManager.whiteColors,
|
color: ColorsManager.whiteColors,
|
||||||
border: Border(
|
border: Border(
|
||||||
@ -369,17 +422,13 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
width: width,
|
child: Align(
|
||||||
child: Padding(
|
alignment: Alignment.centerLeft,
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
right: 16.0,
|
|
||||||
left: 17.0,
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 50,
|
width: 50,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF7F8FA),
|
color: const Color(0xFFF7F8FA),
|
||||||
borderRadius: BorderRadius.circular(height / 2),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.17),
|
color: Colors.black.withOpacity(0.17),
|
||||||
@ -391,12 +440,12 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
Assets.settings,
|
Assets.settings,
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 22,
|
height: 20,
|
||||||
color: ColorsManager.primaryColor,
|
color: ColorsManager.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -404,8 +453,6 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ class CalibrateCompletedDialog extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(_) {
|
Widget build(_) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
height: 250,
|
height: 250,
|
||||||
|
@ -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) {
|
||||||
|
@ -32,15 +32,12 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
color: ColorsManager.blackColor,
|
color: ColorsManager.blackColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
DropdownButton2<String>(
|
||||||
child: Container(
|
|
||||||
height: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: ColorsManager.whiteColors,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: DropdownButton2<String>(
|
|
||||||
underline: const SizedBox(),
|
underline: const SizedBox(),
|
||||||
|
buttonStyleData: ButtonStyleData(
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
value: selectedValue,
|
value: selectedValue,
|
||||||
items: spaces.map((space) {
|
items: spaces.map((space) {
|
||||||
return DropdownMenuItem<String>(
|
return DropdownMenuItem<String>(
|
||||||
@ -51,17 +48,21 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
' ${space.name}',
|
' ${space.name}',
|
||||||
style:
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
fontSize: 16,
|
||||||
fontSize: 12,
|
fontWeight: FontWeight.bold,
|
||||||
color: ColorsManager.blackColor,
|
color: selectedValue == space.uuid
|
||||||
|
? ColorsManager.dialogBlueTitle
|
||||||
|
: ColorsManager.blackColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
' ${space.lastThreeParents}',
|
' ${space.lastThreeParents}',
|
||||||
style:
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
color: selectedValue == space.uuid
|
||||||
|
? ColorsManager.dialogBlueTitle
|
||||||
|
: ColorsManager.blackColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -69,7 +70,10 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
style: TextStyle(color: Colors.black),
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
hint: Padding(
|
hint: Padding(
|
||||||
padding: const EdgeInsets.only(left: 10),
|
padding: const EdgeInsets.only(left: 10),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -80,10 +84,9 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
customButton: Container(
|
customButton: Container(
|
||||||
height: 45,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border:
|
border: Border.all(color: ColorsManager.textGray, width: 1.0),
|
||||||
Border.all(color: ColorsManager.textGray, width: 1.0),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -99,8 +102,8 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
.firstWhere((e) => e.uuid == selectedValue)
|
.firstWhere((e) => e.uuid == selectedValue)
|
||||||
.name
|
.name
|
||||||
: hintMessage,
|
: hintMessage,
|
||||||
style:
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
fontSize: 13,
|
||||||
color: selectedValue != null
|
color: selectedValue != null
|
||||||
? Colors.black
|
? Colors.black
|
||||||
: ColorsManager.textGray,
|
: ColorsManager.textGray,
|
||||||
@ -139,8 +142,6 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
height: 60,
|
height: 60,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -121,7 +121,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child:
|
||||||
|
CircularProgressIndicator(strokeWidth: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -159,7 +160,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
|||||||
height: iconSize,
|
height: iconSize,
|
||||||
width: iconSize,
|
width: iconSize,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (context, error, stackTrace) =>
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) =>
|
||||||
Image.asset(
|
Image.asset(
|
||||||
Assets.logo,
|
Assets.logo,
|
||||||
height: iconSize,
|
height: iconSize,
|
||||||
@ -203,7 +205,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
color: ColorsManager.blackColor,
|
color: ColorsManager.blackColor,
|
||||||
fontSize: widget.isSmallScreenSize(context) ? 10 : 12,
|
fontSize:
|
||||||
|
widget.isSmallScreenSize(context) ? 10 : 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.spaceName != '')
|
if (widget.spaceName != '')
|
||||||
@ -222,8 +225,9 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
color: ColorsManager.blackColor,
|
color: ColorsManager.blackColor,
|
||||||
fontSize:
|
fontSize: widget.isSmallScreenSize(context)
|
||||||
widget.isSmallScreenSize(context) ? 10 : 12,
|
? 10
|
||||||
|
: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,24 +1,39 @@
|
|||||||
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/community_model.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/create_community/presentation/create_community_dialog.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/update_community/presentation/edit_community_dialog.dart';
|
||||||
|
|
||||||
abstract final class SpaceManagementCommunityDialogHelper {
|
abstract final class SpaceManagementCommunityDialogHelper {
|
||||||
static void showCreateDialog(BuildContext context) {
|
static void showCreateDialog(BuildContext context) => showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const CreateCommunityDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static void showEditDialog(
|
||||||
|
BuildContext context,
|
||||||
|
CommunityModel community,
|
||||||
|
) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => CreateCommunityDialog(
|
builder: (_) => EditCommunityDialog(
|
||||||
title: const SelectableText('Community Name'),
|
community: community,
|
||||||
onCreateCommunity: (community) {
|
parentContext: context,
|
||||||
context.read<CommunitiesBloc>().add(
|
|
||||||
InsertCommunity(community),
|
|
||||||
);
|
|
||||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
|
||||||
SelectCommunityEvent(community: community),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void showLoadingDialog(BuildContext context) => showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static void showSuccessSnackBar(BuildContext context, String message) =>
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,29 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.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/pages/common/buttons/default_button.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.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';
|
||||||
|
|
||||||
class CreateCommunityDialogWidget extends StatefulWidget {
|
class CommunityDialog extends StatefulWidget {
|
||||||
final String? initialName;
|
final String? initialName;
|
||||||
final Widget title;
|
final Widget title;
|
||||||
|
final void Function(String name) onSubmit;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
const CreateCommunityDialogWidget({
|
const CommunityDialog({
|
||||||
super.key,
|
|
||||||
required this.title,
|
required this.title,
|
||||||
|
required this.onSubmit,
|
||||||
this.initialName,
|
this.initialName,
|
||||||
|
this.errorMessage,
|
||||||
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CreateCommunityDialogWidget> createState() =>
|
State<CommunityDialog> createState() => _CommunityDialogState();
|
||||||
_CreateCommunityDialogWidgetState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidget> {
|
class _CommunityDialogState extends State<CommunityDialog> {
|
||||||
late final TextEditingController _nameController;
|
late final TextEditingController _nameController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -63,9 +64,7 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
|
|||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: BlocBuilder<CreateCommunityBloc, CreateCommunityState>(
|
child: Column(
|
||||||
builder: (context, state) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -74,24 +73,11 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
|
|||||||
child: widget.title,
|
child: widget.title,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
CreateCommunityNameTextField(
|
CreateCommunityNameTextField(nameController: _nameController),
|
||||||
nameController: _nameController,
|
_buildErrorMessage(),
|
||||||
),
|
|
||||||
if (state case CreateCommunityFailure(:final message))
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 18),
|
|
||||||
child: SelectableText(
|
|
||||||
'* $message',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildActionButtons(context),
|
_buildActionButtons(context),
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -132,13 +118,22 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
|
|||||||
|
|
||||||
void _onSubmit(BuildContext context) {
|
void _onSubmit(BuildContext context) {
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
context.read<CreateCommunityBloc>().add(
|
widget.onSubmit.call(_nameController.text.trim());
|
||||||
CreateCommunity(
|
}
|
||||||
CreateCommunityParam(
|
}
|
||||||
name: _nameController.text.trim(),
|
|
||||||
|
Widget _buildErrorMessage() {
|
||||||
|
return Visibility(
|
||||||
|
visible: widget.errorMessage != null,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(vertical: 18),
|
||||||
|
child: SelectableText(
|
||||||
|
'* ${widget.errorMessage}',
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.theme.colorScheme.error,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
@ -7,6 +7,11 @@ 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/products/data/services/remote_products_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_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/data/services/unique_subspaces_decorator.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 +31,18 @@ class SpaceManagementPage extends StatelessWidget {
|
|||||||
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
||||||
),
|
),
|
||||||
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsBloc(
|
||||||
|
UniqueSubspacesDecorator(
|
||||||
|
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => ProductsBloc(
|
||||||
|
RemoteProductsService(HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: WebScaffold(
|
child: WebScaffold(
|
||||||
appBarTitle: Text(
|
appBarTitle: Text(
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
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/shared/helpers/space_management_community_dialog_helper.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/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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: () {
|
||||||
|
SpaceManagementCommunityDialogHelper.showEditDialog(
|
||||||
|
context,
|
||||||
|
selectedCommunity,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,10 +19,18 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
|
|||||||
replacement: const Row(
|
replacement: const Row(
|
||||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||||
),
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CommunityStructureHeader(),
|
||||||
|
Expanded(
|
||||||
child: CommunityStructureCanvas(
|
child: CommunityStructureCanvas(
|
||||||
community: selectedCommunity,
|
community: selectedCommunity,
|
||||||
selectedSpace: selectedSpace,
|
selectedSpace: selectedSpace,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,26 @@ class CommunityModel extends Equatable {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CommunityModel copyWith({
|
||||||
|
String? uuid,
|
||||||
|
String? name,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? description,
|
||||||
|
String? externalId,
|
||||||
|
List<SpaceModel>? spaces,
|
||||||
|
}) {
|
||||||
|
return CommunityModel(
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
|
name: name ?? this.name,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
description: description ?? this.description,
|
||||||
|
externalId: externalId ?? this.externalId,
|
||||||
|
spaces: spaces ?? this.spaces,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [uuid, name, spaces];
|
List<Object?> get props => [uuid, name, spaces];
|
||||||
}
|
}
|
||||||
|
@ -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? ?? '',
|
||||||
|
@ -16,6 +16,7 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
|
|||||||
on<LoadCommunities>(_onLoadCommunities);
|
on<LoadCommunities>(_onLoadCommunities);
|
||||||
on<LoadMoreCommunities>(_onLoadMoreCommunities);
|
on<LoadMoreCommunities>(_onLoadMoreCommunities);
|
||||||
on<InsertCommunity>(_onInsertCommunity);
|
on<InsertCommunity>(_onInsertCommunity);
|
||||||
|
on<CommunitiesUpdateCommunity>(_onCommunitiesUpdateCommunity);
|
||||||
}
|
}
|
||||||
|
|
||||||
final CommunitiesService _communitiesService;
|
final CommunitiesService _communitiesService;
|
||||||
@ -114,4 +115,18 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
|
|||||||
) {
|
) {
|
||||||
emit(state.copyWith(communities: [event.community, ...state.communities]));
|
emit(state.copyWith(communities: [event.community, ...state.communities]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onCommunitiesUpdateCommunity(
|
||||||
|
CommunitiesUpdateCommunity event,
|
||||||
|
Emitter<CommunitiesState> emit,
|
||||||
|
) {
|
||||||
|
final updatedCommunities = state.communities
|
||||||
|
.map((e) => e.uuid == event.community.uuid ? event.community : e)
|
||||||
|
.toList();
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
communities: updatedCommunities,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,3 +31,12 @@ final class InsertCommunity extends CommunitiesEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [community];
|
List<Object?> get props => [community];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class CommunitiesUpdateCommunity extends CommunitiesEvent {
|
||||||
|
const CommunitiesUpdateCommunity(this.community);
|
||||||
|
|
||||||
|
final CommunityModel community;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [community];
|
||||||
|
}
|
||||||
|
@ -1,57 +1,58 @@
|
|||||||
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/modules/communities/domain/models/community_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.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/create_community/data/services/remote_create_community_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart';
|
|
||||||
import 'package:syncrow_web/services/api/http_service.dart';
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
|
||||||
class CreateCommunityDialog extends StatelessWidget {
|
class CreateCommunityDialog extends StatelessWidget {
|
||||||
final void Function(CommunityModel community) onCreateCommunity;
|
const CreateCommunityDialog({super.key});
|
||||||
final String? initialName;
|
|
||||||
final Widget title;
|
|
||||||
|
|
||||||
const CreateCommunityDialog({
|
|
||||||
super.key,
|
|
||||||
required this.onCreateCommunity,
|
|
||||||
required this.title,
|
|
||||||
this.initialName,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())),
|
create: (_) => CreateCommunityBloc(
|
||||||
child: BlocListener<CreateCommunityBloc, CreateCommunityState>(
|
RemoteCreateCommunityService(HTTPService()),
|
||||||
|
),
|
||||||
|
child: BlocConsumer<CreateCommunityBloc, CreateCommunityState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case CreateCommunityLoading():
|
case CreateCommunityLoading() || CreateCommunityInitial():
|
||||||
showDialog<void>(
|
SpaceManagementCommunityDialogHelper.showLoadingDialog(context);
|
||||||
context: context,
|
|
||||||
builder: (context) => const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case CreateCommunitySuccess(:final community):
|
case CreateCommunitySuccess(:final community):
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
SpaceManagementCommunityDialogHelper.showSuccessSnackBar(
|
||||||
const SnackBar(content: Text('Community created successfully')),
|
context,
|
||||||
|
'${community.name} community created successfully',
|
||||||
|
);
|
||||||
|
context.read<CommunitiesBloc>().add(
|
||||||
|
InsertCommunity(community),
|
||||||
|
);
|
||||||
|
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||||
|
SelectCommunityEvent(community: community),
|
||||||
);
|
);
|
||||||
onCreateCommunity.call(community);
|
|
||||||
break;
|
break;
|
||||||
case CreateCommunityFailure():
|
case CreateCommunityFailure():
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: CreateCommunityDialogWidget(
|
builder: (BuildContext context, CreateCommunityState state) {
|
||||||
title: title,
|
return CommunityDialog(
|
||||||
initialName: initialName,
|
title: const Text('Create Community'),
|
||||||
|
initialName: null,
|
||||||
|
onSubmit: (name) => context.read<CreateCommunityBloc>().add(
|
||||||
|
CreateCommunity(CreateCommunityParam(name: name)),
|
||||||
),
|
),
|
||||||
|
errorMessage: state is CreateCommunityFailure ? state.message : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.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/products/domain/params/load_products_param.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_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';
|
||||||
|
import 'package:syncrow_web/utils/constants/api_const.dart';
|
||||||
|
|
||||||
class RemoteProductsService implements ProductsService {
|
class RemoteProductsService implements ProductsService {
|
||||||
const RemoteProductsService(this._httpService);
|
const RemoteProductsService(this._httpService);
|
||||||
@ -13,17 +13,14 @@ class RemoteProductsService implements ProductsService {
|
|||||||
static const _defaultErrorMessage = 'Failed to load devices';
|
static const _defaultErrorMessage = 'Failed to load devices';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Product>> getProducts(LoadProductsParam param) async {
|
Future<List<Product>> getProducts() async {
|
||||||
try {
|
try {
|
||||||
final response = await _httpService.get(
|
final response = await _httpService.get(
|
||||||
path: 'devices',
|
path: ApiEndpoints.listProducts,
|
||||||
queryParameters: {
|
|
||||||
'spaceUuid': param.spaceUuid,
|
|
||||||
if (param.type != null) 'type': param.type,
|
|
||||||
if (param.status != null) 'status': param.status,
|
|
||||||
},
|
|
||||||
expectedResponseModel: (data) {
|
expectedResponseModel: (data) {
|
||||||
return (data as List)
|
final json = data as Map<String, dynamic>;
|
||||||
|
final products = json['data'] as List<dynamic>;
|
||||||
|
return products
|
||||||
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
class Product extends Equatable {
|
class Product extends Equatable {
|
||||||
final String uuid;
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
const Product({
|
const Product({
|
||||||
required this.uuid,
|
required this.uuid,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.productType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String uuid;
|
||||||
|
final String name;
|
||||||
|
final String productType;
|
||||||
|
|
||||||
|
String get icon => _mapIconToProduct(productType);
|
||||||
|
|
||||||
factory Product.fromJson(Map<String, dynamic> json) {
|
factory Product.fromJson(Map<String, dynamic> json) {
|
||||||
return Product(
|
return Product(
|
||||||
uuid: json['uuid'] as String,
|
uuid: json['uuid'] as String? ?? '',
|
||||||
name: json['name'] as String,
|
name: json['name'] as String? ?? '',
|
||||||
|
productType: json['prodType'] as String? ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,9 +26,37 @@ class Product extends Equatable {
|
|||||||
return {
|
return {
|
||||||
'uuid': uuid,
|
'uuid': uuid,
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'productType': productType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
static String _mapIconToProduct(String prodType) {
|
||||||
List<Object?> get props => [uuid, name];
|
const iconMapping = {
|
||||||
|
'1G': Assets.Gang1SwitchIcon,
|
||||||
|
'1GT': Assets.oneTouchSwitch,
|
||||||
|
'2G': Assets.Gang2SwitchIcon,
|
||||||
|
'2GT': Assets.twoTouchSwitch,
|
||||||
|
'3G': Assets.Gang3SwitchIcon,
|
||||||
|
'3GT': Assets.threeTouchSwitch,
|
||||||
|
'CUR': Assets.curtain,
|
||||||
|
'CUR_2': Assets.curtain,
|
||||||
|
'GD': Assets.garageDoor,
|
||||||
|
'GW': Assets.SmartGatewayIcon,
|
||||||
|
'DL': Assets.DoorLockIcon,
|
||||||
|
'WL': Assets.waterLeakSensor,
|
||||||
|
'WH': Assets.waterHeater,
|
||||||
|
'WM': Assets.waterLeakSensor,
|
||||||
|
'SOS': Assets.sos,
|
||||||
|
'AC': Assets.ac,
|
||||||
|
'CPS': Assets.presenceSensor,
|
||||||
|
'PC': Assets.powerClamp,
|
||||||
|
'WPS': Assets.presenceSensor,
|
||||||
|
'DS': Assets.doorSensor
|
||||||
|
};
|
||||||
|
|
||||||
|
return iconMapping[prodType] ?? Assets.presenceSensor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [uuid, name, productType];
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
class LoadProductsParam {
|
|
||||||
final String spaceUuid;
|
|
||||||
final String? type;
|
|
||||||
final String? status;
|
|
||||||
|
|
||||||
const LoadProductsParam({
|
|
||||||
required this.spaceUuid,
|
|
||||||
this.type,
|
|
||||||
this.status,
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
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/products/domain/params/load_products_param.dart';
|
|
||||||
|
|
||||||
abstract class ProductsService {
|
abstract class ProductsService {
|
||||||
Future<List<Product>> getProducts(LoadProductsParam param);
|
Future<List<Product>> getProducts();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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/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/products/domain/params/load_products_param.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
|
||||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
|
|
||||||
@ -9,20 +8,20 @@ part 'products_event.dart';
|
|||||||
part 'products_state.dart';
|
part 'products_state.dart';
|
||||||
|
|
||||||
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
|
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
|
||||||
final ProductsService _deviceService;
|
ProductsBloc(this._productsService) : super(ProductsInitial()) {
|
||||||
|
|
||||||
ProductsBloc(this._deviceService) : super(ProductsInitial()) {
|
|
||||||
on<LoadProducts>(_onLoadProducts);
|
on<LoadProducts>(_onLoadProducts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final ProductsService _productsService;
|
||||||
|
|
||||||
Future<void> _onLoadProducts(
|
Future<void> _onLoadProducts(
|
||||||
LoadProducts event,
|
LoadProducts event,
|
||||||
Emitter<ProductsState> emit,
|
Emitter<ProductsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(ProductsLoading());
|
emit(ProductsLoading());
|
||||||
try {
|
try {
|
||||||
final devices = await _deviceService.getProducts(event.param);
|
final products = await _productsService.getProducts();
|
||||||
emit(ProductsLoaded(devices));
|
emit(ProductsLoaded(products));
|
||||||
} on APIException catch (e) {
|
} on APIException catch (e) {
|
||||||
emit(ProductsFailure(e.message));
|
emit(ProductsFailure(e.message));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class LoadProducts extends ProductsEvent {
|
final class LoadProducts extends ProductsEvent {
|
||||||
const LoadProducts(this.param);
|
const LoadProducts();
|
||||||
|
|
||||||
final LoadProductsParam param;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [param];
|
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class ProductsFailure extends ProductsState {
|
final class ProductsFailure extends ProductsState {
|
||||||
final String message;
|
final String errorMessage;
|
||||||
|
|
||||||
const ProductsFailure(this.message);
|
const ProductsFailure(this.errorMessage);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [message];
|
List<Object> get props => [errorMessage];
|
||||||
}
|
}
|
||||||
|
@ -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}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
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/domain/services/space_details_service.dart';
|
||||||
|
|
||||||
|
class UniqueSubspacesDecorator implements SpaceDetailsService {
|
||||||
|
final SpaceDetailsService _decoratee;
|
||||||
|
|
||||||
|
const UniqueSubspacesDecorator(this._decoratee);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
|
||||||
|
final response = await _decoratee.getSpaceDetails(param);
|
||||||
|
|
||||||
|
final uniqueSubspaces = <String, Subspace>{};
|
||||||
|
|
||||||
|
for (final subspace in response.subspaces) {
|
||||||
|
final normalizedName = subspace.name.trim().toLowerCase();
|
||||||
|
if (!uniqueSubspaces.containsKey(normalizedName)) {
|
||||||
|
uniqueSubspaces[normalizedName] = subspace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.copyWith(
|
||||||
|
subspaces: uniqueSubspaces.values.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
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';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class SpaceDetailsModel extends Equatable {
|
class SpaceDetailsModel extends Equatable {
|
||||||
final String uuid;
|
final String uuid;
|
||||||
@ -17,6 +19,13 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
required this.subspaces,
|
required this.subspaces,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
|
||||||
|
uuid: '',
|
||||||
|
spaceName: '',
|
||||||
|
icon: Assets.location,
|
||||||
|
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,
|
||||||
@ -41,23 +50,40 @@ 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];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProductAllocation extends Equatable {
|
class ProductAllocation extends Equatable {
|
||||||
|
final String uuid;
|
||||||
final Product product;
|
final Product product;
|
||||||
final Tag tag;
|
final Tag tag;
|
||||||
final String? location;
|
|
||||||
|
|
||||||
const ProductAllocation({
|
const ProductAllocation({
|
||||||
|
required this.uuid,
|
||||||
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) {
|
||||||
return ProductAllocation(
|
return ProductAllocation(
|
||||||
|
uuid: json['uuid'] as String? ?? const Uuid().v4(),
|
||||||
product: Product.fromJson(json['product'] as Map<String, dynamic>),
|
product: Product.fromJson(json['product'] as Map<String, dynamic>),
|
||||||
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
|
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
@ -65,13 +91,26 @@ class ProductAllocation extends Equatable {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
|
'uuid': uuid,
|
||||||
'product': product.toJson(),
|
'product': product.toJson(),
|
||||||
'tag': tag.toJson(),
|
'tag': tag.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProductAllocation copyWith({
|
||||||
|
String? uuid,
|
||||||
|
Product? product,
|
||||||
|
Tag? tag,
|
||||||
|
}) {
|
||||||
|
return ProductAllocation(
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
|
product: product ?? this.product,
|
||||||
|
tag: tag ?? this.tag,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [product, tag];
|
List<Object?> get props => [uuid, product, tag];
|
||||||
}
|
}
|
||||||
|
|
||||||
class Subspace extends Equatable {
|
class Subspace extends Equatable {
|
||||||
@ -88,7 +127,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 +142,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];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
class LoadSpaceDetailsParam {
|
||||||
|
const LoadSpaceDetailsParam({
|
||||||
|
required this.spaceUuid,
|
||||||
|
required this.communityUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String spaceUuid;
|
||||||
|
final String communityUuid;
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
class LoadSpacesParam {
|
|
||||||
const LoadSpacesParam();
|
|
||||||
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 => [];
|
||||||
|
}
|
@ -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];
|
||||||
}
|
}
|
||||||
|
@ -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 SelectableText('Create Space'),
|
||||||
|
spaceModel: SpaceModel.empty(),
|
||||||
|
onSave: (space) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 SelectableText('Edit Space'),
|
||||||
|
spaceModel: spaceModel,
|
||||||
|
onSave: (space) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
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,
|
||||||
|
this.saveButtonLabel = 'OK',
|
||||||
|
this.cancelButtonLabel = 'Cancel',
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
final VoidCallback? onSave;
|
||||||
|
final String saveButtonLabel;
|
||||||
|
final String cancelButtonLabel;
|
||||||
|
|
||||||
|
@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: cancelButtonLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSaveButton() {
|
||||||
|
return DefaultButton(
|
||||||
|
onPressed: onSave,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: ColorsManager.secondaryColor,
|
||||||
|
foregroundColor: ColorsManager.whiteColors,
|
||||||
|
child: Text(saveButtonLabel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/svg.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/tags/presentation/widgets/assign_tags_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/enum/device_types.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||||
|
const SpaceDetailsDevicesBox({
|
||||||
|
required this.space,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SpaceDetailsModel space;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final allAllocations = [
|
||||||
|
...space.productAllocations,
|
||||||
|
...space.subspaces.expand((s) => s.productAllocations),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (allAllocations.isNotEmpty) {
|
||||||
|
final productCounts = <String, int>{};
|
||||||
|
for (final allocation in allAllocations) {
|
||||||
|
final productType = allocation.product.productType;
|
||||||
|
productCounts[productType] = (productCounts[productType] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: [
|
||||||
|
...productCounts.entries.map((entry) {
|
||||||
|
final productType = entry.key;
|
||||||
|
final count = entry.value;
|
||||||
|
return Chip(
|
||||||
|
avatar: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
_getDeviceIcon(productType),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
'x$count',
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.spaceColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: ColorsManager.spaceColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
EditChip(onTap: () => _showAssignTagsDialog(context)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () => _showAssignTagsDialog(context),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ButtonContentWidget(
|
||||||
|
svgAssets: Assets.addIcon,
|
||||||
|
label: 'Add Devices',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAssignTagsDialog(BuildContext context) {
|
||||||
|
showDialog<SpaceDetailsModel>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AssignTagsDialog(space: space),
|
||||||
|
).then((resultSpace) {
|
||||||
|
if (resultSpace != null) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<SpaceDetailsModelBloc>().add(UpdateSpaceDetails(resultSpace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getDeviceIcon(String productType) =>
|
||||||
|
switch (devicesTypesMap[productType]) {
|
||||||
|
DeviceType.LightBulb => Assets.lightBulb,
|
||||||
|
DeviceType.CeilingSensor => Assets.sensors,
|
||||||
|
DeviceType.AC => Assets.ac,
|
||||||
|
DeviceType.DoorLock => Assets.doorLock,
|
||||||
|
DeviceType.Curtain => Assets.curtain,
|
||||||
|
DeviceType.ThreeGang => Assets.gangSwitch,
|
||||||
|
DeviceType.Gateway => Assets.gateway,
|
||||||
|
DeviceType.OneGang => Assets.oneGang,
|
||||||
|
DeviceType.TwoGang => Assets.twoGang,
|
||||||
|
DeviceType.WH => Assets.waterHeater,
|
||||||
|
DeviceType.DoorSensor => Assets.openCloseDoor,
|
||||||
|
DeviceType.GarageDoor => Assets.openedDoor,
|
||||||
|
DeviceType.WaterLeak => Assets.waterLeakNormal,
|
||||||
|
DeviceType.Curtain2 => Assets.curtainIcon,
|
||||||
|
DeviceType.Blind => Assets.curtainIcon,
|
||||||
|
DeviceType.WallSensor => Assets.sensors,
|
||||||
|
DeviceType.DS => Assets.openCloseDoor,
|
||||||
|
DeviceType.OneTouch => Assets.gangSwitch,
|
||||||
|
DeviceType.TowTouch => Assets.gangSwitch,
|
||||||
|
DeviceType.ThreeTouch => Assets.gangSwitch,
|
||||||
|
DeviceType.NCPS => Assets.sensors,
|
||||||
|
DeviceType.PC => Assets.powerClamp,
|
||||||
|
DeviceType.Other => Assets.blackLogo,
|
||||||
|
null => Assets.blackLogo,
|
||||||
|
};
|
||||||
|
}
|
@ -1,12 +1,105 @@
|
|||||||
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: SizedBox(
|
||||||
|
height: context.screenHeight * 0.3,
|
||||||
|
width: context.screenWidth * 0.5,
|
||||||
|
child: 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
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, space) {
|
||||||
|
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: space.icon)),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
SpaceNameTextField(
|
||||||
|
initialValue: space.spaceName,
|
||||||
|
isNameFieldExist: (value) => space.subspaces.any(
|
||||||
|
(subspace) => subspace.name == value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
SpaceSubSpacesBox(
|
||||||
|
subspaces: space.subspaces,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SpaceDetailsDevicesBox(space: space),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
SpaceDetailsActionButtons(
|
||||||
|
onSave: () => onSave(space),
|
||||||
|
onCancel: Navigator.of(context).pop,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
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 SelectableText('Create Sub Spaces'),
|
||||||
|
content: Column(
|
||||||
|
spacing: 12,
|
||||||
|
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 SelectableText(
|
||||||
|
'Error: Duplicate subspace names are not allowed.',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
SpaceDetailsActionButtons(
|
||||||
|
onSave: _hasDuplicateNames ? null : _handleSave,
|
||||||
|
onCancel: Navigator.of(context).pop,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
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/tags/domain/models/tag.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/params/load_tags_param.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_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';
|
||||||
import 'package:syncrow_web/utils/constants/api_const.dart';
|
|
||||||
|
|
||||||
final class RemoteTagsService implements TagsService {
|
final class RemoteTagsService implements TagsService {
|
||||||
const RemoteTagsService(this._httpService);
|
const RemoteTagsService(this._httpService);
|
||||||
@ -14,17 +13,10 @@ final class RemoteTagsService implements TagsService {
|
|||||||
static const _defaultErrorMessage = 'Failed to load tags';
|
static const _defaultErrorMessage = 'Failed to load tags';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Tag>> loadTags(LoadTagsParam param) async {
|
Future<List<Tag>> loadTags() async {
|
||||||
if (param.projectUuid == null) {
|
|
||||||
throw Exception('Project UUID is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _httpService.get(
|
final response = await _httpService.get(
|
||||||
path: ApiEndpoints.listTags.replaceAll(
|
path: await _makeUrl(),
|
||||||
'{projectUuid}',
|
|
||||||
param.projectUuid!,
|
|
||||||
),
|
|
||||||
expectedResponseModel: (json) {
|
expectedResponseModel: (json) {
|
||||||
final result = json as Map<String, dynamic>;
|
final result = json as Map<String, dynamic>;
|
||||||
final data = result['data'] as List<dynamic>;
|
final data = result['data'] as List<dynamic>;
|
||||||
@ -46,4 +38,12 @@ final class RemoteTagsService implements TagsService {
|
|||||||
throw APIException(formattedErrorMessage);
|
throw APIException(formattedErrorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> _makeUrl() async {
|
||||||
|
final projectUuid = await ProjectManager.getProjectUUID();
|
||||||
|
if (projectUuid == null || projectUuid.isEmpty) {
|
||||||
|
throw APIException('Project UUID is required');
|
||||||
|
}
|
||||||
|
return '/projects/$projectUuid/tags';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,13 @@ class Tag extends Equatable {
|
|||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory Tag.empty() => const Tag(
|
||||||
|
uuid: '',
|
||||||
|
name: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
);
|
||||||
|
|
||||||
factory Tag.fromJson(Map<String, dynamic> json) {
|
factory Tag.fromJson(Map<String, dynamic> json) {
|
||||||
return Tag(
|
return Tag(
|
||||||
uuid: json['uuid'] as String,
|
uuid: json['uuid'] as String,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
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/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
|
|
||||||
|
|
||||||
abstract interface class TagsService {
|
abstract interface class TagsService {
|
||||||
Future<List<Tag>> loadTags(LoadTagsParam param);
|
Future<List<Tag>> loadTags();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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/tags/domain/models/tag.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/params/load_tags_param.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
|
||||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ class TagsBloc extends Bloc<TagsEvent, TagsState> {
|
|||||||
) async {
|
) async {
|
||||||
emit(TagsLoading());
|
emit(TagsLoading());
|
||||||
try {
|
try {
|
||||||
final tags = await _tagsService.loadTags(event.param);
|
final tags = await _tagsService.loadTags();
|
||||||
emit(TagsLoaded(tags));
|
emit(TagsLoaded(tags));
|
||||||
} on APIException catch (e) {
|
} on APIException catch (e) {
|
||||||
emit(TagsFailure(e.message));
|
emit(TagsFailure(e.message));
|
||||||
|
@ -8,10 +8,5 @@ abstract class TagsEvent extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LoadTags extends TagsEvent {
|
class LoadTags extends TagsEvent {
|
||||||
final LoadTagsParam param;
|
const LoadTags();
|
||||||
|
|
||||||
const LoadTags(this.param);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [param];
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.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/presentation/bloc/products_bloc.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/tags/presentation/widgets/products_grid.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class AddDeviceTypeWidget extends StatefulWidget {
|
||||||
|
const AddDeviceTypeWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
|
||||||
|
final Map<Product, int> _selectedProducts = {};
|
||||||
|
|
||||||
|
void _onIncrement(Product product) {
|
||||||
|
setState(() {
|
||||||
|
_selectedProducts[product] = (_selectedProducts[product] ?? 0) + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDecrement(Product product) {
|
||||||
|
setState(() {
|
||||||
|
if ((_selectedProducts[product] ?? 0) > 0) {
|
||||||
|
_selectedProducts[product] = _selectedProducts[product]! - 1;
|
||||||
|
if (_selectedProducts[product] == 0) {
|
||||||
|
_selectedProducts.remove(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => ProductsBloc(RemoteProductsService(HTTPService()))
|
||||||
|
..add(const LoadProducts()),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const SelectableText('Add Devices'),
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: BlocBuilder<ProductsBloc, ProductsState>(
|
||||||
|
builder: (context, state) => switch (state) {
|
||||||
|
ProductsInitial() || ProductsLoading() => _buildLoading(context),
|
||||||
|
ProductsLoaded(:final products) => ProductsGrid(
|
||||||
|
products: products,
|
||||||
|
selectedProducts: _selectedProducts,
|
||||||
|
onIncrement: _onIncrement,
|
||||||
|
onDecrement: _onDecrement,
|
||||||
|
),
|
||||||
|
ProductsFailure(:final errorMessage) => _buildFailure(
|
||||||
|
context,
|
||||||
|
errorMessage,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
SpaceDetailsActionButtons(
|
||||||
|
onSave: () {
|
||||||
|
final result = _selectedProducts.entries
|
||||||
|
.expand((entry) => List.generate(entry.value, (_) => entry.key))
|
||||||
|
.toList();
|
||||||
|
Navigator.of(context).pop(result);
|
||||||
|
},
|
||||||
|
onCancel: Navigator.of(context).pop,
|
||||||
|
saveButtonLabel: 'Next',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoading(BuildContext context) => SizedBox(
|
||||||
|
width: context.screenWidth * 0.9,
|
||||||
|
height: context.screenHeight * 0.65,
|
||||||
|
child: const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildFailure(BuildContext context, String errorMessage) {
|
||||||
|
return SizedBox(
|
||||||
|
width: context.screenWidth * 0.9,
|
||||||
|
height: context.screenHeight * 0.65,
|
||||||
|
child: Center(
|
||||||
|
child: SelectableText(
|
||||||
|
errorMessage,
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,231 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.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/tags/domain/models/tag.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class AssignTagsDialog extends StatefulWidget {
|
||||||
|
const AssignTagsDialog({required this.space, super.key});
|
||||||
|
|
||||||
|
final SpaceDetailsModel space;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AssignTagsDialog> createState() => _AssignTagsDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssignTagsDialogState extends State<AssignTagsDialog> {
|
||||||
|
late SpaceDetailsModel _space;
|
||||||
|
final Map<String, String> _validationErrors = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_space = widget.space.copyWith(
|
||||||
|
productAllocations:
|
||||||
|
widget.space.productAllocations.map((e) => e.copyWith()).toList(),
|
||||||
|
subspaces: widget.space.subspaces
|
||||||
|
.map(
|
||||||
|
(s) => s.copyWith(
|
||||||
|
productAllocations:
|
||||||
|
s.productAllocations.map((e) => e.copyWith()).toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
_validateAllTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _validateAllTags() {
|
||||||
|
final newErrors = <String, String>{};
|
||||||
|
final allAllocations = [
|
||||||
|
..._space.productAllocations,
|
||||||
|
..._space.subspaces.expand((s) => s.productAllocations),
|
||||||
|
];
|
||||||
|
|
||||||
|
final allocationsByProductType = <String, List<ProductAllocation>>{};
|
||||||
|
for (final allocation in allAllocations) {
|
||||||
|
(allocationsByProductType[allocation.product.productType] ??= [])
|
||||||
|
.add(allocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final productType in allocationsByProductType.keys) {
|
||||||
|
final allocations = allocationsByProductType[productType]!;
|
||||||
|
final tagCounts = <String, int>{};
|
||||||
|
|
||||||
|
for (final allocation in allocations) {
|
||||||
|
final tagName = allocation.tag.name.trim().toLowerCase();
|
||||||
|
if (tagName.isEmpty) {
|
||||||
|
newErrors[allocation.uuid] =
|
||||||
|
'Tag for ${allocation.product.name} cannot be empty.';
|
||||||
|
} else {
|
||||||
|
tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final allocation in allocations) {
|
||||||
|
final tagName = allocation.tag.name.trim().toLowerCase();
|
||||||
|
if (tagName.isNotEmpty && (tagCounts[tagName] ?? 0) > 1) {
|
||||||
|
newErrors[allocation.uuid] =
|
||||||
|
'Tag "${allocation.tag.name}" is used by multiple $productType devices.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_validationErrors
|
||||||
|
..clear()
|
||||||
|
..addAll(newErrors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTagChange(String allocationUuid, Tag newTag) {
|
||||||
|
setState(() {
|
||||||
|
var index =
|
||||||
|
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
if (index != -1) {
|
||||||
|
final allocation = _space.productAllocations[index];
|
||||||
|
_space.productAllocations[index] = allocation.copyWith(tag: newTag);
|
||||||
|
} else {
|
||||||
|
for (final subspace in _space.subspaces) {
|
||||||
|
index = subspace.productAllocations
|
||||||
|
.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
if (index != -1) {
|
||||||
|
final allocation = subspace.productAllocations[index];
|
||||||
|
subspace.productAllocations[index] = allocation.copyWith(tag: newTag);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_validateAllTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleLocationChange(String allocationUuid, String? newSubspaceUuid) {
|
||||||
|
setState(() {
|
||||||
|
ProductAllocation? allocationToMove;
|
||||||
|
|
||||||
|
var index =
|
||||||
|
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
if (index != -1) {
|
||||||
|
allocationToMove = _space.productAllocations.removeAt(index);
|
||||||
|
} else {
|
||||||
|
for (final subspace in _space.subspaces) {
|
||||||
|
index = subspace.productAllocations
|
||||||
|
.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
if (index != -1) {
|
||||||
|
allocationToMove = subspace.productAllocations.removeAt(index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allocationToMove == null) return;
|
||||||
|
|
||||||
|
if (newSubspaceUuid == null) {
|
||||||
|
_space.productAllocations.add(allocationToMove);
|
||||||
|
} else {
|
||||||
|
_space.subspaces
|
||||||
|
.firstWhere((s) => s.uuid == newSubspaceUuid)
|
||||||
|
.productAllocations
|
||||||
|
.add(allocationToMove);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleProductDelete(String allocationUuid) {
|
||||||
|
setState(() {
|
||||||
|
_space.productAllocations.removeWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
|
||||||
|
for (final subspace in _space.subspaces) {
|
||||||
|
subspace.productAllocations.removeWhere(
|
||||||
|
(pa) => pa.uuid == allocationUuid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_validateAllTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final allProductAllocations = [
|
||||||
|
..._space.productAllocations,
|
||||||
|
..._space.subspaces.expand((s) => s.productAllocations),
|
||||||
|
];
|
||||||
|
|
||||||
|
final productLocations = <String, String?>{};
|
||||||
|
for (final pa in _space.productAllocations) {
|
||||||
|
productLocations[pa.uuid] = null;
|
||||||
|
}
|
||||||
|
for (final subspace in _space.subspaces) {
|
||||||
|
for (final pa in subspace.productAllocations) {
|
||||||
|
productLocations[pa.uuid] = subspace.uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasErrors = _validationErrors.isNotEmpty;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const SelectableText('Assign Tags'),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: context.screenWidth * 0.6,
|
||||||
|
minWidth: context.screenWidth * 0.6,
|
||||||
|
maxHeight: context.screenHeight * 0.8,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: AssignTagsTable(
|
||||||
|
productAllocations: allProductAllocations,
|
||||||
|
subspaces: _space.subspaces,
|
||||||
|
productLocations: productLocations,
|
||||||
|
onTagSelected: _handleTagChange,
|
||||||
|
onLocationSelected: _handleLocationChange,
|
||||||
|
onProductDeleted: _handleProductDelete,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasErrors)
|
||||||
|
AssignTagsErrorMessages(
|
||||||
|
errorMessages: _validationErrors.values.toSet().toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
SpaceDetailsActionButtons(
|
||||||
|
onSave: hasErrors ? null : () => Navigator.of(context).pop(_space),
|
||||||
|
onCancel: () async {
|
||||||
|
final newProducts = await showDialog<List<Product>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const AddDeviceTypeWidget(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newProducts == null || newProducts.isEmpty) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
for (final product in newProducts) {
|
||||||
|
_space.productAllocations.add(
|
||||||
|
ProductAllocation(
|
||||||
|
uuid: const Uuid().v4(),
|
||||||
|
product: product,
|
||||||
|
tag: Tag.empty(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_validateAllTags();
|
||||||
|
},
|
||||||
|
cancelButtonLabel: 'Add New Device',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class AssignTagsErrorMessages extends StatelessWidget {
|
||||||
|
const AssignTagsErrorMessages({super.key, required this.errorMessages});
|
||||||
|
|
||||||
|
final List<String> errorMessages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: errorMessages
|
||||||
|
.map(
|
||||||
|
(error) => Text(
|
||||||
|
'- $error',
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/common/dialog_dropdown.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/tags/data/services/remote_tags_service.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/presentation/bloc/tags_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class AssignTagsTable extends StatelessWidget {
|
||||||
|
const AssignTagsTable({
|
||||||
|
required this.productAllocations,
|
||||||
|
required this.subspaces,
|
||||||
|
required this.productLocations,
|
||||||
|
required this.onTagSelected,
|
||||||
|
required this.onLocationSelected,
|
||||||
|
required this.onProductDeleted,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<ProductAllocation> productAllocations;
|
||||||
|
final List<Subspace> subspaces;
|
||||||
|
final Map<String, String?> productLocations;
|
||||||
|
final void Function(String, Tag) onTagSelected;
|
||||||
|
final void Function(String, String?) onLocationSelected;
|
||||||
|
final void Function(String) onProductDeleted;
|
||||||
|
|
||||||
|
DataColumn _buildDataColumn(BuildContext context, String label) {
|
||||||
|
return DataColumn(
|
||||||
|
label: SelectableText(label, style: context.textTheme.bodyMedium),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider<TagsBloc>(
|
||||||
|
create: (BuildContext context) => TagsBloc(
|
||||||
|
RemoteTagsService(HTTPService()),
|
||||||
|
)..add(const LoadTags()),
|
||||||
|
child: BlocBuilder<TagsBloc, TagsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return switch (state) {
|
||||||
|
TagsLoading() || TagsInitial() => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
TagsFailure(:final message) => Center(
|
||||||
|
child: Text(message),
|
||||||
|
),
|
||||||
|
TagsLoaded(:final tags) => ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: DataTable(
|
||||||
|
headingRowColor: WidgetStateProperty.all(
|
||||||
|
ColorsManager.dataHeaderGrey,
|
||||||
|
),
|
||||||
|
key: ValueKey(productAllocations.length),
|
||||||
|
border: TableBorder.all(
|
||||||
|
color: ColorsManager.dataHeaderGrey,
|
||||||
|
width: 1,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
columns: [
|
||||||
|
_buildDataColumn(context, '#'),
|
||||||
|
_buildDataColumn(context, 'Device'),
|
||||||
|
_buildDataColumn(context, 'Tag'),
|
||||||
|
_buildDataColumn(context, 'Location'),
|
||||||
|
],
|
||||||
|
rows: productAllocations.isEmpty
|
||||||
|
? [
|
||||||
|
DataRow(
|
||||||
|
cells: [
|
||||||
|
DataCell(
|
||||||
|
Center(
|
||||||
|
child: SelectableText(
|
||||||
|
'No Devices Available',
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell.empty,
|
||||||
|
DataCell.empty,
|
||||||
|
DataCell.empty,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: List.generate(productAllocations.length, (index) {
|
||||||
|
final productAllocation = productAllocations[index];
|
||||||
|
final allocationUuid = productAllocation.uuid;
|
||||||
|
|
||||||
|
final availableTags = tags
|
||||||
|
.where(
|
||||||
|
(tag) =>
|
||||||
|
!productAllocations
|
||||||
|
.where((p) =>
|
||||||
|
p.product.productType ==
|
||||||
|
productAllocation.product.productType)
|
||||||
|
.map((p) => p.tag.name.toLowerCase())
|
||||||
|
.contains(tag.name.toLowerCase()) ||
|
||||||
|
tag.uuid == productAllocation.tag.uuid,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final currentLocationUuid =
|
||||||
|
productLocations[allocationUuid];
|
||||||
|
final currentLocationName = currentLocationUuid == null
|
||||||
|
? 'Main Space'
|
||||||
|
: subspaces
|
||||||
|
.firstWhere((s) => s.uuid == currentLocationUuid)
|
||||||
|
.name;
|
||||||
|
|
||||||
|
return DataRow(
|
||||||
|
key: ValueKey(allocationUuid),
|
||||||
|
cells: [
|
||||||
|
DataCell(Text((index + 1).toString())),
|
||||||
|
DataCell(
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
productAllocation.product.name,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: ColorsManager.lightGreyColor,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
onProductDeleted(allocationUuid);
|
||||||
|
},
|
||||||
|
tooltip: 'Delete Tag',
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
width: double.infinity,
|
||||||
|
child: ProductTagField(
|
||||||
|
key: ValueKey('dropdown_$allocationUuid'),
|
||||||
|
productName: productAllocation.product.uuid,
|
||||||
|
initialValue: productAllocation.tag,
|
||||||
|
onSelected: (newTag) {
|
||||||
|
onTagSelected(allocationUuid, newTag);
|
||||||
|
},
|
||||||
|
items: availableTags,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DataCell(
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: DialogDropdown(
|
||||||
|
items: [
|
||||||
|
'Main Space',
|
||||||
|
...subspaces.map((s) => s.name)
|
||||||
|
],
|
||||||
|
selectedValue: currentLocationName,
|
||||||
|
onSelected: (newLocationName) {
|
||||||
|
final newSubspaceUuid = newLocationName ==
|
||||||
|
'Main Space'
|
||||||
|
? null
|
||||||
|
: subspaces
|
||||||
|
.firstWhere(
|
||||||
|
(s) => s.name == newLocationName)
|
||||||
|
.uuid;
|
||||||
|
onLocationSelected(
|
||||||
|
allocationUuid, newSubspaceUuid);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => const SizedBox.shrink(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,186 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class ProductTagField extends StatefulWidget {
|
||||||
|
final List<Tag> items;
|
||||||
|
final ValueChanged<Tag> onSelected;
|
||||||
|
final Tag? initialValue;
|
||||||
|
final String productName;
|
||||||
|
|
||||||
|
const ProductTagField({
|
||||||
|
super.key,
|
||||||
|
required this.items,
|
||||||
|
required this.onSelected,
|
||||||
|
this.initialValue,
|
||||||
|
required this.productName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProductTagField> createState() => _ProductTagFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProductTagFieldState extends State<ProductTagField> {
|
||||||
|
bool _isOpen = false;
|
||||||
|
OverlayEntry? _overlayEntry;
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.text = widget.initialValue?.name ?? '';
|
||||||
|
_focusNode.addListener(_handleFocusChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode.removeListener(_handleFocusChange);
|
||||||
|
_controller.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
_overlayEntry = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleFocusChange() {
|
||||||
|
if (!_focusNode.hasFocus) {
|
||||||
|
_submit(_controller.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submit(String value) {
|
||||||
|
final lowerCaseValue = value.toLowerCase();
|
||||||
|
final selectedTag = widget.items.firstWhere(
|
||||||
|
(tag) => tag.name.toLowerCase() == lowerCaseValue,
|
||||||
|
orElse: () => Tag(
|
||||||
|
name: value,
|
||||||
|
uuid: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
widget.onSelected(selectedTag);
|
||||||
|
_closeDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleDropdown() {
|
||||||
|
if (_isOpen) {
|
||||||
|
_closeDropdown();
|
||||||
|
} else {
|
||||||
|
_openDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openDropdown() {
|
||||||
|
_overlayEntry = _createOverlayEntry();
|
||||||
|
Overlay.of(context).insert(_overlayEntry!);
|
||||||
|
setState(() => _isOpen = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _closeDropdown() {
|
||||||
|
if (_isOpen) {
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
_overlayEntry = null;
|
||||||
|
setState(() => _isOpen = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => FocusScope.of(context).unfocus(),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: ColorsManager.transparentColor),
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
onFieldSubmitted: _submit,
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Enter or Select a tag',
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _toggleDropdown,
|
||||||
|
child: const Icon(Icons.arrow_drop_down),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OverlayEntry _createOverlayEntry() {
|
||||||
|
final renderBox = context.findRenderObject()! as RenderBox;
|
||||||
|
final size = renderBox.size;
|
||||||
|
final offset = renderBox.localToGlobal(Offset.zero);
|
||||||
|
|
||||||
|
return OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _closeDropdown,
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: offset.dx,
|
||||||
|
top: offset.dy + size.height,
|
||||||
|
width: size.width,
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
child: Container(
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
constraints: const BoxConstraints(maxHeight: 200.0),
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: widget.items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final tag = widget.items[index];
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: ColorsManager.lightGrayBorderColor,
|
||||||
|
width: 1.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
tag.name,
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: ColorsManager.textPrimaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_controller.text = tag.name;
|
||||||
|
_submit(tag.name);
|
||||||
|
_closeDropdown();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import 'package:flutter/material.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/presentation/widgets/product_type_card_counter.dart';
|
||||||
|
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class ProductTypeCard extends StatelessWidget {
|
||||||
|
const ProductTypeCard({
|
||||||
|
required this.product,
|
||||||
|
required this.count,
|
||||||
|
required this.onIncrement,
|
||||||
|
required this.onDecrement,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Product product;
|
||||||
|
final int count;
|
||||||
|
final void Function() onIncrement;
|
||||||
|
final void Function() onDecrement;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: Column(
|
||||||
|
spacing: 8,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: DeviceIconWidget(icon: product.icon)),
|
||||||
|
_buildName(context, product.name),
|
||||||
|
ProductTypeCardCounter(
|
||||||
|
onIncrement: onIncrement,
|
||||||
|
onDecrement: onDecrement,
|
||||||
|
count: count,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildName(BuildContext context, String name) {
|
||||||
|
return Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 35,
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class ProductTypeCardCounter extends StatelessWidget {
|
||||||
|
const ProductTypeCardCounter({
|
||||||
|
super.key,
|
||||||
|
required this.onIncrement,
|
||||||
|
required this.onDecrement,
|
||||||
|
required this.count,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int count;
|
||||||
|
final void Function() onIncrement;
|
||||||
|
final void Function() onDecrement;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.counterBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
_buildCounterButton(
|
||||||
|
Icons.remove,
|
||||||
|
onDecrement,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
count.toString(),
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: ColorsManager.spaceColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildCounterButton(Icons.add, onIncrement),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCounterButton(
|
||||||
|
IconData icon,
|
||||||
|
VoidCallback onPressed,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Icon(
|
||||||
|
icon,
|
||||||
|
color: ColorsManager.spaceColor.withValues(alpha: 0.3),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.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/presentation/widgets/product_type_card.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class ProductsGrid extends StatelessWidget {
|
||||||
|
const ProductsGrid({
|
||||||
|
required this.products,
|
||||||
|
required this.selectedProducts,
|
||||||
|
required this.onIncrement,
|
||||||
|
required this.onDecrement,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Product> products;
|
||||||
|
final Map<Product, int> selectedProducts;
|
||||||
|
final void Function(Product) onIncrement;
|
||||||
|
final void Function(Product) onDecrement;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
final crossAxisCount = switch (context.screenWidth) {
|
||||||
|
> 1200 => 8,
|
||||||
|
> 800 => 5,
|
||||||
|
_ => 3,
|
||||||
|
};
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Container(
|
||||||
|
width: size.width * 0.9,
|
||||||
|
height: size.height * 0.65,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: GridView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
|
shrinkWrap: true,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
mainAxisSpacing: 6,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
childAspectRatio: 0.8,
|
||||||
|
),
|
||||||
|
itemCount: products.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = products[index];
|
||||||
|
return ProductTypeCard(
|
||||||
|
product: product,
|
||||||
|
count: selectedProducts[product] ?? 0,
|
||||||
|
onIncrement: () => onIncrement(product),
|
||||||
|
onDecrement: () => onDecrement(product),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
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/communities/domain/models/community_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_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';
|
||||||
@ -13,15 +13,15 @@ class RemoteUpdateCommunityService implements UpdateCommunityService {
|
|||||||
static const _defaultErrorMessage = 'Failed to update community';
|
static const _defaultErrorMessage = 'Failed to update community';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CommunityModel> updateCommunity(UpdateCommunityParam param) async {
|
Future<CommunityModel> updateCommunity(CommunityModel param) async {
|
||||||
|
final endpoint = await _makeUrl(param.uuid);
|
||||||
try {
|
try {
|
||||||
final response = await _httpService.put(
|
await _httpService.put(
|
||||||
path: 'endpoint',
|
path: endpoint,
|
||||||
expectedResponseModel: (data) => CommunityModel.fromJson(
|
body: {'name': param.name},
|
||||||
data as Map<String, dynamic>,
|
expectedResponseModel: (data) => null,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return response;
|
return param;
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
final message = e.response?.data as Map<String, dynamic>?;
|
final message = e.response?.data as Map<String, dynamic>?;
|
||||||
final error = message?['error'] as Map<String, dynamic>?;
|
final error = message?['error'] as Map<String, dynamic>?;
|
||||||
@ -36,4 +36,12 @@ class RemoteUpdateCommunityService implements UpdateCommunityService {
|
|||||||
throw APIException(formattedErrorMessage);
|
throw APIException(formattedErrorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> _makeUrl(String communityUuid) async {
|
||||||
|
final projectUuid = await ProjectManager.getProjectUUID();
|
||||||
|
if (projectUuid == null) {
|
||||||
|
throw APIException('Project UUID is not set');
|
||||||
|
}
|
||||||
|
return '/projects/$projectUuid/communities/$communityUuid';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
|
||||||
|
|
||||||
class UpdateCommunityParam extends Equatable {
|
|
||||||
const UpdateCommunityParam({required this.name});
|
|
||||||
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [name];
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart';
|
|
||||||
|
|
||||||
abstract class UpdateCommunityService {
|
abstract class UpdateCommunityService {
|
||||||
Future<CommunityModel> updateCommunity(UpdateCommunityParam param);
|
Future<CommunityModel> updateCommunity(CommunityModel community);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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/communities/domain/models/community_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart';
|
||||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ class UpdateCommunityBloc extends Bloc<UpdateCommunityEvent, UpdateCommunityStat
|
|||||||
emit(UpdateCommunityLoading());
|
emit(UpdateCommunityLoading());
|
||||||
try {
|
try {
|
||||||
final updatedCommunity = await _updateCommunityService.updateCommunity(
|
final updatedCommunity = await _updateCommunityService.updateCommunity(
|
||||||
event.param,
|
event.communityModel,
|
||||||
);
|
);
|
||||||
emit(UpdateCommunitySuccess(updatedCommunity));
|
emit(UpdateCommunitySuccess(updatedCommunity));
|
||||||
} on APIException catch (e) {
|
} on APIException catch (e) {
|
||||||
|
@ -8,10 +8,10 @@ sealed class UpdateCommunityEvent extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class UpdateCommunity extends UpdateCommunityEvent {
|
final class UpdateCommunity extends UpdateCommunityEvent {
|
||||||
const UpdateCommunity(this.param);
|
const UpdateCommunity(this.communityModel);
|
||||||
|
|
||||||
final UpdateCommunityParam param;
|
final CommunityModel communityModel ;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [param];
|
List<Object> get props => [communityModel];
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,10 @@ final class UpdateCommunitySuccess extends UpdateCommunityState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class UpdateCommunityFailure extends UpdateCommunityState {
|
final class UpdateCommunityFailure extends UpdateCommunityState {
|
||||||
final String message;
|
final String errorMessage;
|
||||||
|
|
||||||
const UpdateCommunityFailure(this.message);
|
const UpdateCommunityFailure(this.errorMessage);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [message];
|
List<Object> get props => [errorMessage];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.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/update_community/data/services/remote_update_community_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
|
||||||
|
class EditCommunityDialog extends StatelessWidget {
|
||||||
|
const EditCommunityDialog({
|
||||||
|
required this.community,
|
||||||
|
required this.parentContext,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CommunityModel community;
|
||||||
|
final BuildContext parentContext;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => UpdateCommunityBloc(
|
||||||
|
RemoteUpdateCommunityService(HTTPService()),
|
||||||
|
),
|
||||||
|
child: BlocConsumer<UpdateCommunityBloc, UpdateCommunityState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
switch (state) {
|
||||||
|
case UpdateCommunityInitial() || UpdateCommunityLoading():
|
||||||
|
SpaceManagementCommunityDialogHelper.showLoadingDialog(context);
|
||||||
|
break;
|
||||||
|
case UpdateCommunitySuccess(:final community):
|
||||||
|
_onUpdateCommunitySuccess(context, community);
|
||||||
|
break;
|
||||||
|
case UpdateCommunityFailure():
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) => CommunityDialog(
|
||||||
|
title: const Text('Edit Community'),
|
||||||
|
initialName: community.name,
|
||||||
|
errorMessage: state is UpdateCommunityFailure ? state.errorMessage : null,
|
||||||
|
onSubmit: (name) => context.read<UpdateCommunityBloc>().add(
|
||||||
|
UpdateCommunity(community.copyWith(name: name)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateCommunitySuccess(
|
||||||
|
BuildContext context,
|
||||||
|
CommunityModel community,
|
||||||
|
) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
SpaceManagementCommunityDialogHelper.showSuccessSnackBar(
|
||||||
|
context,
|
||||||
|
'${community.name} community updated successfully',
|
||||||
|
);
|
||||||
|
parentContext.read<CommunitiesBloc>().add(
|
||||||
|
CommunitiesUpdateCommunity(community),
|
||||||
|
);
|
||||||
|
parentContext.read<CommunitiesTreeSelectionBloc>().add(
|
||||||
|
SelectCommunityEvent(community: community),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
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);
|
||||||
|
on<UpdateSpaceDetails>(_onUpdateSpaceDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetails(
|
||||||
|
UpdateSpaceDetails event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(event.space);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetails extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetails(this.space);
|
||||||
|
|
||||||
|
final SpaceDetailsModel space;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [space];
|
||||||
|
}
|
@ -21,7 +21,6 @@ import 'package:syncrow_web/utils/snack_bar.dart';
|
|||||||
|
|
||||||
class VisitorPasswordBloc
|
class VisitorPasswordBloc
|
||||||
extends Bloc<VisitorPasswordEvent, VisitorPasswordState> {
|
extends Bloc<VisitorPasswordEvent, VisitorPasswordState> {
|
||||||
|
|
||||||
VisitorPasswordBloc() : super(VisitorPasswordInitial()) {
|
VisitorPasswordBloc() : super(VisitorPasswordInitial()) {
|
||||||
on<SelectUsageFrequency>(selectUsageFrequency);
|
on<SelectUsageFrequency>(selectUsageFrequency);
|
||||||
on<FetchDevice>(_onFetchDevice);
|
on<FetchDevice>(_onFetchDevice);
|
||||||
@ -87,6 +86,9 @@ class VisitorPasswordBloc
|
|||||||
SelectTimeVisitorPassword event,
|
SelectTimeVisitorPassword event,
|
||||||
Emitter<VisitorPasswordState> emit,
|
Emitter<VisitorPasswordState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
// Ensure expirationTimeTimeStamp has a value
|
||||||
|
effectiveTimeTimeStamp ??= DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
final DateTime? picked = await showDatePicker(
|
final DateTime? picked = await showDatePicker(
|
||||||
context: event.context,
|
context: event.context,
|
||||||
initialDate: DateTime.now(),
|
initialDate: DateTime.now(),
|
||||||
@ -94,7 +96,8 @@ class VisitorPasswordBloc
|
|||||||
lastDate: DateTime.now().add(const Duration(days: 5095)),
|
lastDate: DateTime.now().add(const Duration(days: 5095)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (picked != null) {
|
if (picked == null) return;
|
||||||
|
|
||||||
final TimeOfDay? timePicked = await showTimePicker(
|
final TimeOfDay? timePicked = await showTimePicker(
|
||||||
context: event.context,
|
context: event.context,
|
||||||
initialTime: TimeOfDay.now(),
|
initialTime: TimeOfDay.now(),
|
||||||
@ -105,18 +108,14 @@ class VisitorPasswordBloc
|
|||||||
primary: ColorsManager.primaryColor,
|
primary: ColorsManager.primaryColor,
|
||||||
onSurface: Colors.black,
|
onSurface: Colors.black,
|
||||||
),
|
),
|
||||||
buttonTheme: const ButtonThemeData(
|
|
||||||
colorScheme: ColorScheme.light(
|
|
||||||
primary: Colors.green,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: child!,
|
child: child!,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (timePicked != null) {
|
if (timePicked == null) return;
|
||||||
|
|
||||||
final selectedDateTime = DateTime(
|
final selectedDateTime = DateTime(
|
||||||
picked.year,
|
picked.year,
|
||||||
picked.month,
|
picked.month,
|
||||||
@ -124,57 +123,98 @@ class VisitorPasswordBloc
|
|||||||
timePicked.hour,
|
timePicked.hour,
|
||||||
timePicked.minute,
|
timePicked.minute,
|
||||||
);
|
);
|
||||||
|
final selectedTimestamp = selectedDateTime.millisecondsSinceEpoch ~/ 1000;
|
||||||
final selectedTimestamp =
|
final currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
selectedDateTime.millisecondsSinceEpoch ~/ 1000;
|
|
||||||
|
|
||||||
if (event.isStart) {
|
if (event.isStart) {
|
||||||
|
// START TIME VALIDATION
|
||||||
if (expirationTimeTimeStamp != null &&
|
if (expirationTimeTimeStamp != null &&
|
||||||
selectedTimestamp > expirationTimeTimeStamp!) {
|
selectedTimestamp > expirationTimeTimeStamp!) {
|
||||||
CustomSnackBar.displaySnackBar(
|
|
||||||
'Effective Time cannot be later than Expiration Time.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
|
|
||||||
if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
|
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: event.context,
|
context: event.context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Effective Time cannot be earlier than current time.'),
|
title: const Text(
|
||||||
|
'Effective Time cannot be later than Expiration Time.',
|
||||||
|
),
|
||||||
actionsAlignment: MainAxisAlignment.center,
|
actionsAlignment: MainAxisAlignment.center,
|
||||||
content:
|
content: FilledButton(
|
||||||
FilledButton(
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(event.context).pop();
|
Navigator.of(event.context).pop();
|
||||||
add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false));
|
add(SelectTimeVisitorPassword(
|
||||||
|
context: event.context,
|
||||||
|
isStart: true,
|
||||||
|
isRepeat: false,
|
||||||
|
));
|
||||||
},
|
},
|
||||||
child: const Text('OK'),
|
child: const Text('OK'),
|
||||||
),
|
),
|
||||||
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedTimestamp < currentTimestamp) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: event.context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text(
|
||||||
|
'Effective Time cannot be earlier than current time.',
|
||||||
|
),
|
||||||
|
actionsAlignment: MainAxisAlignment.center,
|
||||||
|
content: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(event.context).pop();
|
||||||
|
add(SelectTimeVisitorPassword(
|
||||||
|
context: event.context,
|
||||||
|
isStart: true,
|
||||||
|
isRepeat: false,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save effective time
|
||||||
effectiveTimeTimeStamp = selectedTimestamp;
|
effectiveTimeTimeStamp = selectedTimestamp;
|
||||||
startTimeAccess = selectedDateTime.toString().split('.').first;
|
startTimeAccess = selectedDateTime.toString().split('.').first;
|
||||||
} else {
|
} else {
|
||||||
|
// END TIME VALIDATION
|
||||||
if (effectiveTimeTimeStamp != null &&
|
if (effectiveTimeTimeStamp != null &&
|
||||||
selectedTimestamp < effectiveTimeTimeStamp!) {
|
selectedTimestamp < effectiveTimeTimeStamp!) {
|
||||||
CustomSnackBar.displaySnackBar(
|
await showDialog<void>(
|
||||||
|
context: event.context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text(
|
||||||
'Expiration Time cannot be earlier than Effective Time.',
|
'Expiration Time cannot be earlier than Effective Time.',
|
||||||
|
),
|
||||||
|
actionsAlignment: MainAxisAlignment.center,
|
||||||
|
content: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(event.context).pop();
|
||||||
|
add(SelectTimeVisitorPassword(
|
||||||
|
context: event.context,
|
||||||
|
isStart: false,
|
||||||
|
isRepeat: false,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save expiration time
|
||||||
expirationTimeTimeStamp = selectedTimestamp;
|
expirationTimeTimeStamp = selectedTimestamp;
|
||||||
endTimeAccess = selectedDateTime.toString().split('.').first;
|
endTimeAccess = selectedDateTime.toString().split('.').first;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(ChangeTimeState());
|
emit(ChangeTimeState());
|
||||||
emit(VisitorPasswordInitial());
|
emit(VisitorPasswordInitial());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool toggleRepeat(
|
bool toggleRepeat(
|
||||||
ToggleRepeatEvent event, Emitter<VisitorPasswordState> emit) {
|
ToggleRepeatEvent event, Emitter<VisitorPasswordState> emit) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user