Compare commits

..

34 Commits

Author SHA1 Message Date
c6729f476f Enhance booking system: update API endpoints, improve event loading with caching, and refine UI components 2025-07-16 15:36:49 +03:00
75b9f4a4e6 changed the title from Routine to Workflow Automation (#354)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1851](https://syncrow.atlassian.net/browse/SP-1851)

## Description

change the Title to Work Flow 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1851]:
https://syncrow.atlassian.net/browse/SP-1851?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-15 11:54:29 +03:00
fe4063ef8f changed the title from Routine to Workflow Automation 2025-07-15 11:20:13 +03:00
029b5d32e0 Adjust table scroll behavior and modify height for improved layout (#353)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Description

<!--- Describe your changes in detail -->
Adjust table scroll behavior and modify height for improved layout

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-15 10:43:22 +03:00
428c81efff Adjust table scroll behavior and modify height for improved layout 2025-07-15 10:05:07 +03:00
288c252f46 SP-1696-fe-edit-user-dialog-enhancements (#347)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1696](https://syncrow.atlassian.net/browse/SP-1696)

## Description

add company Name and replace it with job title

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1696]:
https://syncrow.atlassian.net/browse/SP-1696?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-15 09:08:15 +03:00
7399dee687 [FE] Redundant API calls on Routines page when selecting a community from the tree (#345)
…rom the tree

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1800](https://syncrow.atlassian.net/browse/SP-1800)

## Description

fix Redundant API calls when choosing devices and use Que Par to send
spaces instead of send api for every space

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1800]:
https://syncrow.atlassian.net/browse/SP-1800?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-15 08:32:08 +03:00
08e2ed4b4c Merge branch 'dev' into revert-SP-1589 2025-07-15 08:31:45 +03:00
59e04708cd Merge branch 'main' into revert-SP-1589 2025-07-15 08:30:33 +03:00
338d4f5737 fix typo 2025-07-15 08:19:43 +03:00
5532935a3a Enhance UI components: update color management, adjust button styles,… (#350)
… and improve text formatting for better readability

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->



## Description

<!--- Describe your changes in detail -->
Enhance UI components: update color 
## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-14 16:15:42 +03:00
249cbfc242 Merge branch 'dev' into Build-Schedule-List-View-Support-State-Persistence 2025-07-14 15:20:08 +03:00
8167926620 Enhance UI components: update color management, adjust button styles, and improve text formatting for better readability 2025-07-14 15:14:56 +03:00
559091faa0 Add calendar event management features and UI components and Implemen… (#349)
…t Calendar logic

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->



## Description

<!--- Describe your changes in detail -->
Implement Calendar logic

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-14 14:18:43 +03:00
eba351c9be Sp 1717 fe draw create edit space dialog (#348)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1717](https://syncrow.atlassian.net/browse/SP-1717)

## Description

Implemented Reordering in spaces, but without API integration.
Implemented syncing data between selection and communities bloc.
Implemented Edit community feature.
Implemented button in canvas that opens a create dialog.


## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1717]:
https://syncrow.atlassian.net/browse/SP-1717?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-14 12:57:21 +03:00
c112cde634 Uses Inkwell instead of Gesture Detector for canvas widgets. 2025-07-14 11:04:29 +03:00
7331c8440b Refactor SpaceManagementPage to use StatefulWidget and initialize CommunitiesBloc in initState. Update CommunityStructureHeader to handle community updates and improve state management in CommunitiesTreeSelectionBloc with new event for community state updates. 2025-07-14 10:27:22 +03:00
a409e34643 Merge branch 'SP-1717-FE-Draw-Create-Edit-Space-Dialog' of https://github.com/SyncrowIOT/web into SP-1717-FE-Draw-Create-Edit-Space-Dialog 2025-07-14 10:16:31 +03:00
2681c837f5 add company name and replace it with job title 2025-07-11 12:10:53 +03:00
b6664ec1ba fix Redundant API calls on Routines page when selecting a community from the tree 2025-07-11 10:53:04 +03:00
83202204b0 Remove BlocProvider for UpdateSpaceBloc in SpaceDetailsDialogHelper to streamline dependency management and improve code clarity. 2025-07-09 15:58:17 +03:00
d87739f1fd Refactor JSON Serialization in UpdateSpaceParam: Adjusted the _toJson method for Subspace to ensure 'subspaceName' is always included and 'uuid' is only added when applicable, enhancing clarity and consistency in data representation. 2025-07-09 15:25:41 +03:00
5cd083a37b Refactor Space and Tag Models: Removed unused JSON serialization methods from SpaceDetailsModel, ProductAllocation, and Subspace. Updated Tag model to eliminate unnecessary fields. Enhanced UpdateSpaceParam to streamline JSON conversion for subspaces and product allocations, improving data handling during updates. 2025-07-09 15:08:49 +03:00
2b8d987c69 Add SpaceReorderDataModel and integrate drag-and-drop functionality in CommunityStructureCanvas for improved space management. 2025-07-08 16:00:57 +03:00
707cb4791f Added CreateSpaceButton for improved user interaction and updated layout calculations to utilize context extensions for better responsiveness. 2025-07-08 13:08:43 +03:00
03c45ed8d0 Refactor SpaceCardWidget: Simplified widget structure by removing unnecessary SizedBox. 2025-07-08 13:07:55 +03:00
9e0ea4ad6f Adjust spacer flex in SpaceManagementCommunityStructure widget for improved layout consistency. 2025-07-08 13:07:39 +03:00
9e6b14737f Refactor CreateSpaceButton: Changed from StatelessWidget to StatefulWidget to manage hover state and added tooltip for improved user experience. Enhanced button styling and interaction feedback for better visual cues during space creation. 2025-07-08 13:07:26 +03:00
7c2aed2d58 Refactor RemoteUpdateSpaceService: Improved error handling in updateSpace method by checking API response success before returning the updated space. This enhances robustness and ensures proper error propagation for failed updates. 2025-07-08 12:20:10 +03:00
bcf62027bc Validate UUIDs in RemoteUpdateSpaceService: Added checks for empty space and community UUIDs before constructing the update URL, improving error handling and robustness in the update space process. 2025-07-08 11:12:12 +03:00
b001713ce4 Enhance Community Structure Widgets: Updated SpaceDetailsDialogHelper to accept community UUID for space creation and editing. Refactored CreateSpaceButton and CommunityStructureHeader to pass community UUID, improving data handling and consistency across the community structure features. 2025-07-08 11:10:22 +03:00
bab3226c73 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1717-FE-Draw-Create-Edit-Space-Dialog 2025-07-08 10:02:12 +03:00
fa1eaa570c Refactor Space Update Logic: Introduced UpdateSpaceParam for better parameter handling in update operations. Enhanced SpaceDetailsDialogHelper to manage loading and error states during space updates. Updated RemoteUpdateSpaceService to construct dynamic URLs for space updates based on community UUID. Improved CommunitiesTreeFailureWidget UI with SelectableText and added spacing for better layout. 2025-07-08 10:01:43 +03:00
dcf1df9b4a sp1613 delete condition word 2025-05-21 07:25:34 -05:00
57 changed files with 1139 additions and 667 deletions

View File

@ -14,146 +14,20 @@ class RemoteCalendarService implements CalendarSystemService {
@override
Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
required String month,
required String year,
}) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.getCalendarEvents,
queryParameters: {
'spaceId': spaceId,
},
return await _httpService.get<CalendarEventsResponse>(
path: ApiEndpoints.getBookings
.replaceAll('{mm}', month)
.replaceAll('{yyyy}', year)
.replaceAll('{space}', spaceId),
expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson(
json as Map<String, dynamic>,
);
return CalendarEventsResponse.fromJson(json as Map<String, dynamic>);
},
);
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
} 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()}');
}
}
}
class FakeRemoteCalendarService implements CalendarSystemService {
const FakeRemoteCalendarService(this._httpService, {this.useDummy = false});
final HTTPService _httpService;
final bool useDummy;
static const _defaultErrorMessage = 'Failed to load Calendar';
@override
Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
}) async {
if (useDummy) {
final dummyJson = {
'statusCode': 200,
'message': 'Successfully fetched all bookings',
'data': [
{
'uuid': 'd4553fa6-a0c9-4f42-81c9-99a13a57bf80',
'date': '2025-07-11T10:22:00.626Z',
'startTime': '09:00:00',
'endTime': '12:00:00',
'cost': 10,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
},
{
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
'date': '2025-07-11T10:22:00.626Z',
'startTime': '12:00:00',
'endTime': '13:00:00',
'cost': 10,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
},
{
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
'date': '2025-07-13T10:22:00.626Z',
'startTime': '15:30:00',
'endTime': '19:00:00',
'cost': 20,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
}
],
'success': true
};
final response = CalendarEventsResponse.fromJson(dummyJson);
// Filter events by spaceId
final filteredData = response.data.where((event) {
return event.space.uuid == spaceId;
}).toList();
print('Filtering events for spaceId: $spaceId');
print('Found ${filteredData.length} matching events');
return filteredData.isNotEmpty
? CalendarEventsResponse(
statusCode: response.statusCode,
message: response.message,
data: filteredData,
success: response.success,
)
: CalendarEventsResponse(
statusCode: 404,
message: 'No events found for spaceId: $spaceId',
data: [],
success: false,
);
}
try {
final response = await _httpService.get(
path: ApiEndpoints.getCalendarEvents,
queryParameters: {
'spaceId': spaceId,
},
expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson(
json as Map<String, dynamic>,
);
},
);
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
} on DioException catch (e) {
final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) {

View File

@ -3,5 +3,7 @@ import 'package:syncrow_web/pages/access_management/booking_system/domain/models
abstract class CalendarSystemService {
Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
required String month,
required String year,
});
}

View File

@ -4,35 +4,70 @@ import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
part 'events_event.dart';
part 'events_state.dart';
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController();
final CalendarSystemService calendarService;
final Map<String, List<CalendarEventData>> _eventsCache = {};
String? _lastSpaceId;
int? _lastMonth;
int? _lastYear;
CalendarEventsBloc({required this.calendarService}) : 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 {
final month = event.weekEnd.month;
final year = event.weekEnd.year;
final cacheKey =
'${event.spaceId}-$year-${month.toString().padLeft(2, '0')}';
if (_eventsCache.containsKey(cacheKey)) {
final cachedEvents = _eventsCache[cacheKey]!;
eventController.addAll(cachedEvents);
emit(EventsLoaded(
events: cachedEvents,
spaceId: event.spaceId,
month: month,
year: year,
));
return;
}
if (_lastSpaceId == event.spaceId &&
_lastMonth == month &&
_lastYear == year) {
return;
}
emit(EventsLoading());
try {
final response = await calendarService.getCalendarEvents(
month: month.toString().padLeft(2, '0'),
year: year.toString(),
spaceId: event.spaceId,
);
final events =
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
final events = response.data.map(_toCalendarEventData).toList();
_eventsCache[cacheKey] = events;
eventController.addAll(events);
emit(EventsLoaded(events: events));
_lastSpaceId = event.spaceId;
_lastMonth = month;
_lastYear = year;
emit(EventsLoaded(
events: events,
spaceId: event.spaceId,
month: month,
year: year,
));
} catch (e) {
emit(EventsError('Failed to load events'));
}
@ -40,16 +75,28 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
eventController.add(event.event);
if (state is EventsLoaded) {
final loaded = state as EventsLoaded;
final cacheKey =
'${loaded.spaceId}-${loaded.year}-${loaded.month.toString().padLeft(2, '0')}';
if (_eventsCache.containsKey(cacheKey)) {
final cachedEvents =
List<CalendarEventData>.from(_eventsCache[cacheKey]!);
cachedEvents.add(event.event);
_eventsCache[cacheKey] = cachedEvents;
}
emit(EventsLoaded(
events: [...eventController.events],
spaceId: loaded.spaceId,
month: loaded.month,
year: loaded.year,
));
}
}
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
void _onDisposeResources(
DisposeResources event, Emitter<CalendarEventState> emit) {
eventController.dispose();
@ -61,6 +108,9 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final newWeekDays = _getWeekDays(event.weekDate);
emit(EventsLoaded(
events: loaded.events,
spaceId: loaded.spaceId,
month: loaded.month,
year: loaded.year,
));
}
}
@ -90,14 +140,13 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
);
return CalendarEventData(
date: startTime,
date: startTime,
startTime: startTime,
endTime: endTime,
title:
'${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}',
title: '${booking.user.firstName} ${booking.user.lastName}',
description: 'Cost: ${booking.cost}',
color: Colors.blue,
event: booking,
event: booking,
);
}

View File

@ -7,11 +7,17 @@ class EventsInitial extends CalendarEventState {}
class EventsLoading extends CalendarEventState {}
class EventsLoaded extends CalendarEventState {
final class EventsLoaded extends CalendarEventState {
final List<CalendarEventData> events;
final String spaceId;
final int month;
final int year;
EventsLoaded({
required this.events,
required this.spaceId,
required this.month,
required this.year,
});
}

View File

@ -62,8 +62,9 @@ class _BookingPageState extends State<BookingPage> {
BlocProvider(create: (_) => DateSelectionBloc()),
BlocProvider(
create: (_) => CalendarEventsBloc(
calendarService:
FakeRemoteCalendarService(HTTPService(), useDummy: true),
calendarService: RemoteCalendarService(
HTTPService(),
),
),
),
],
@ -138,7 +139,7 @@ class _BookingPageState extends State<BookingPage> {
),
),
Expanded(
flex: 4,
flex: 5,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
@ -187,6 +188,7 @@ class _BookingPageState extends State<BookingPage> {
],
),
Expanded(
flex: 5,
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, roomState) {

View File

@ -72,11 +72,7 @@ class __SidebarContentState extends State<_SidebarContent> {
@override
Widget build(BuildContext context) {
return BlocConsumer<SidebarBloc, SidebarState>(
listener: (context, state) {
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
searchController.clear();
}
},
listener: (context, state) {},
builder: (context, state) {
return Column(
children: [
@ -147,6 +143,7 @@ class __SidebarContentState extends State<_SidebarContent> {
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
searchController.clear();
context.read<SidebarBloc>().add(ResetSearch());
},
),

View File

@ -1,16 +1,15 @@
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EventTileWidget extends StatelessWidget {
final List<CalendarEventData<Object?>> events;
const EventTileWidget({
super.key,
required this.events,
});
@override
Widget build(BuildContext context) {
return Container(
@ -18,39 +17,86 @@ class EventTileWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events.map((event) {
final bool isEventEnded =
final booking = event.event is CalendarEventBooking
? event.event! as CalendarEventBooking
: null;
final companyName = booking?.user.companyName ?? 'Unknown Company';
final startTime = DateFormat('hh:mm a').format(event.startTime!);
final endTime = DateFormat('hh:mm a').format(event.endTime!);
final isEventEnded =
event.endTime != null && event.endTime!.isBefore(DateTime.now());
final duration = event.endTime!.difference(event.startTime!);
final bool isLongEnough = duration.inMinutes >= 31;
return Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(6),
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: isEventEnded
? ColorsManager.lightGrayBorderColor
: ColorsManager.blue1.withOpacity(0.25),
? ColorsManager.grayColor.withOpacity(0.1)
: ColorsManager.blue1.withOpacity(0.1),
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,
),
border: const Border(
left: BorderSide(
color: ColorsManager.grayColor,
width: 4,
),
const SizedBox(height: 2),
Text(
event.title,
style: const TextStyle(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
],
),
),
child: isLongEnough
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$startTime - $endTime',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: isEventEnded
? ColorsManager.grayColor.withOpacity(0.9)
: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 2),
Text(
event.title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: isEventEnded
? ColorsManager.grayColor
: ColorsManager.blackColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
companyName,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: isEventEnded
? ColorsManager.grayColor.withOpacity(0.9)
: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
],
)
: Text(
event.title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: isEventEnded
? ColorsManager.grayColor
: ColorsManager.blackColor,
fontWeight: FontWeight.bold,
),
),
),
);
}).toList(),

View File

@ -24,17 +24,21 @@ class RoomListItem extends StatelessWidget {
activeColor: ColorsManager.primaryColor,
title: Text(
room.spaceName,
maxLines: 2,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
fontWeight: FontWeight.w700,
overflow: TextOverflow.ellipsis,
fontSize: 12),
),
subtitle: Text(
room.virtualLocation,
maxLines: 2,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w400,
color: ColorsManager.textGray,
overflow: TextOverflow.ellipsis,
),
),
);

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
@ -23,6 +22,12 @@ class WeeklyCalendarPage extends StatelessWidget {
this.endTime,
this.selectedDateFromSideBarCalender,
});
static const double timeLineWidth = 65;
static const int totalDays = 7;
static const double dayColumnWidth = 220; // or any width you want
final double calendarContentWidth =
timeLineWidth + (totalDays * dayColumnWidth);
@override
Widget build(BuildContext context) {
@ -52,154 +57,159 @@ class WeeklyCalendarPage extends StatelessWidget {
);
}
final weekDays = _getWeekDays(weekStart);
final selectedDayIndex =
weekDays.indexWhere((d) => isSameDay(d, selectedDate));
final selectedSidebarIndex = selectedDateFromSideBarCalender == null
? -1
: weekDays
.indexWhere((d) => isSameDay(d, selectedDateFromSideBarCalender!));
const double timeLineWidth = 65;
const double timeLineWidth = 80;
const int totalDays = 7;
final DateTime highlightStart = DateTime(2025, 7, 10);
final DateTime highlightEnd = DateTime(2025, 7, 19);
return LayoutBuilder(
builder: (context, constraints) {
final double calendarWidth = constraints.maxWidth;
final double dayColumnWidth =
(calendarWidth - timeLineWidth) / totalDays - 0.1;
bool isInRange(DateTime date, DateTime start, DateTime end) {
return !date.isBefore(start) && !date.isAfter(end);
!date.isBefore(start) && !date.isAfter(end);
// remove this line and Check if the date is within the range
return false;
}
return Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack(
children: [
WeekView(
weekDetectorBuilder: ({
required date,
required height,
required heightPerMinute,
required minuteSlotSize,
required width,
}) {
return isInRange(date, highlightStart, highlightEnd)
? HatchedColumnBackground(
backgroundColor: ColorsManager.grey800,
lineColor: ColorsManager.textGray,
opacity: 0.3,
stripeSpacing: 12,
borderRadius: BorderRadius.circular(8),
)
: const SizedBox();
},
pageViewPhysics: const NeverScrollableScrollPhysics(),
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) {
return WeekDayHeader(
date: date,
isSelectedDay: isSameDay(date, selectedDate),
);
},
timeLineBuilder: (date) {
return TimeLineWidget(date: date);
},
timeLineWidth: timeLineWidth,
weekPageHeaderBuilder: (start, end) => Container(),
weekTitleHeight: 60,
weekNumberBuilder: (firstDayOfWeek) => Padding(
padding: const EdgeInsets.only(right: 15, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
firstDayOfWeek.timeZoneName.replaceAll(':00', ''),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: calendarContentWidth,
child: Padding(
padding:
const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack(
children: [
Container(
child: WeekView(
minuteSlotSize: MinuteSlotSize.minutes15,
weekDetectorBuilder: ({
required date,
required height,
required heightPerMinute,
required minuteSlotSize,
required width,
}) {
final isSelected = isSameDay(date, selectedDate);
final isSidebarSelected =
selectedDateFromSideBarCalender != null &&
isSameDay(
date, selectedDateFromSideBarCalender!);
if (isSidebarSelected && !isSelected) {
return Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.13),
borderRadius: BorderRadius.circular(8),
),
);
} else if (isSelected) {
return Container(
height: height,
width: width,
decoration: BoxDecoration(
color:
ColorsManager.spaceColor.withOpacity(0.07),
borderRadius: BorderRadius.circular(8),
),
);
}
return const SizedBox.shrink();
},
// weekDetectorBuilder: ({
// required date,
// required height,
// required heightPerMinute,
// required minuteSlotSize,
// required width,
// }) {
// return isInRange(date, highlightStart, highlightEnd)
// ? HatchedColumnBackground(
// backgroundColor: ColorsManager.grey800,
// lineColor: ColorsManager.textGray,
// opacity: 0.3,
// stripeSpacing: 12,
// borderRadius: BorderRadius.circular(8),
// )
// : const SizedBox();
// },
pageViewPhysics: const NeverScrollableScrollPhysics(),
key: ValueKey(weekStart),
controller: eventController,
initialDay: weekStart,
startHour: startHour - 1,
endHour: endHour,
heightPerMinute: 1.7,
showLiveTimeLineInAllDays: false,
showVerticalLines: true,
emulateVerticalOffsetBy: -80,
startDay: WeekDays.monday,
liveTimeIndicatorSettings:
const LiveTimeIndicatorSettings(
showBullet: false,
height: 0,
),
weekDayBuilder: (date) {
return WeekDayHeader(
date: date,
isSelectedDay: isSameDay(date, selectedDate),
);
},
timeLineBuilder: (date) {
return TimeLineWidget(date: date);
},
timeLineWidth: timeLineWidth,
weekPageHeaderBuilder: (start, end) => Container(),
weekTitleHeight: 60,
weekNumberBuilder: (firstDayOfWeek) => Padding(
padding: const EdgeInsets.only(right: 15, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
firstDayOfWeek.timeZoneName
.replaceAll(':00', ''),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
],
),
),
eventTileBuilder: (date, events, boundary, start, end) {
return EventTileWidget(
events: events,
);
},
),
],
),
),
eventTileBuilder: (date, events, boundary, start, end) {
return EventTileWidget(
events: events,
);
},
),
if (selectedDayIndex >= 0)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: ColorsManager.spaceColor.withOpacity(0.07),
),
),
),
if (selectedSidebarIndex >= 0 &&
selectedSidebarIndex != selectedDayIndex)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: Colors.orange.withOpacity(0.14),
Positioned(
right: 0,
top: 50,
bottom: 0,
child: IgnorePointer(
child: Container(
width: 1,
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
),
),
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) {

View File

@ -132,6 +132,8 @@ class _DynamicTableState extends State<DynamicTable> {
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
physics:
widget.isEmpty ? const NeverScrollableScrollPhysics() : null,
child: SizedBox(
width: _totalTableWidth,
child: Column(
@ -164,7 +166,6 @@ class _DynamicTableState extends State<DynamicTable> {
],
),
),
Expanded(
child: widget.isEmpty
? _buildEmptyState()
@ -265,7 +266,7 @@ class _DynamicTableState extends State<DynamicTable> {
),
],
),
SizedBox(height: widget.size.height * 0.5),
SizedBox(height: widget.size.height * 0.2),
],
),
);

View File

@ -46,15 +46,15 @@ class DeviceManagementBloc
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (spaceBloc.state.selectedCommunities.isEmpty) {
devices = await DevicesManagementApi().fetchDevices('', '', projectUuid);
devices = await DevicesManagementApi().fetchDevices(
projectUuid,
);
} else {
for (final community in spaceBloc.state.selectedCommunities) {
for (var community in spaceBloc.state.selectedCommunities) {
final spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
for (final space in spacesList) {
devices.addAll(await DevicesManagementApi()
.fetchDevices(community, space, projectUuid));
}
devices.addAll(await DevicesManagementApi()
.fetchDevices(projectUuid, spacesId: spacesList));
}
}

View File

@ -24,12 +24,12 @@ class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout {
}
class _DeviceManagementPageState extends State<DeviceManagementPage> {
@override
@override
void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@ -90,7 +90,7 @@ class _DeviceManagementPageState extends State<DeviceManagementPage> {
const TriggerSwitchTabsEvent(isRoutineTab: true));
},
child: Text(
'Routines',
'Workflow Automation',
style: context.textTheme.titleMedium?.copyWith(
color: state.routineTab
? ColorsManager.whiteColors

View File

@ -29,7 +29,9 @@ class CountdownModeButtons extends StatelessWidget {
children: [
Expanded(
child: DefaultButton(
elevation: 2.5,
height: 40,
borderRadius: 8,
onPressed: () => Navigator.pop(context),
backgroundColor: ColorsManager.boxColor,
child: Text('Cancel', style: context.textTheme.bodyMedium),
@ -39,6 +41,8 @@ class CountdownModeButtons extends StatelessWidget {
Expanded(
child: isActive
? DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40,
onPressed: () {
context.read<ScheduleBloc>().add(
@ -49,10 +53,12 @@ class CountdownModeButtons extends StatelessWidget {
),
);
},
backgroundColor: Colors.red,
backgroundColor: ColorsManager.red100,
child: const Text('Stop'),
)
: DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40,
onPressed: () {
context.read<ScheduleBloc>().add(
@ -63,7 +69,7 @@ class CountdownModeButtons extends StatelessWidget {
countDownCode: countDownCode),
);
},
backgroundColor: ColorsManager.primaryColor,
backgroundColor: ColorsManager.primaryColorWithOpacity,
child: const Text('Save'),
),
),

View File

@ -226,6 +226,7 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
index.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: isActive ? ColorsManager.grayColor : Colors.black,
),
),
@ -240,7 +241,8 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
label,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 18,
fontSize: 24,
fontWeight: FontWeight.w400,
),
),
],

View File

@ -31,12 +31,11 @@ class BuildScheduleView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ScheduleBloc(
deviceId: deviceUuid,
)
create: (_) => ScheduleBloc(deviceId: deviceUuid,)
..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent(
deviceId: deviceUuid, countdownCode: countdownCode ?? '')),
deviceId: deviceUuid,
countdownCode: countdownCode ?? '')),
child: Dialog(
backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20),
@ -77,7 +76,8 @@ class BuildScheduleView extends StatelessWidget {
category: category,
time: '',
function: Status(
code: code.toString(), value: null),
code: code.toString(),
value: true),
days: [],
),
isEdit: false,

View File

@ -13,9 +13,9 @@ class ScheduleHeader extends StatelessWidget {
Text(
'Scheduling',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
color: ColorsManager.dialogBlueTitle,
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.w700,
fontSize: 30,
),
),
Container(

View File

@ -27,7 +27,7 @@ class ScheduleManagementUI extends StatelessWidget {
width: 170,
height: 40,
child: DefaultButton(
borderColor: ColorsManager.boxColor,
borderColor: ColorsManager.grayColor.withOpacity(0.5),
padding: 2,
backgroundColor: ColorsManager.graysColor,
borderRadius: 15,

View File

@ -19,6 +19,8 @@ class ScheduleModeButtons extends StatelessWidget {
children: [
Expanded(
child: DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40,
onPressed: () {
Navigator.pop(context);
@ -33,9 +35,11 @@ class ScheduleModeButtons extends StatelessWidget {
const SizedBox(width: 20),
Expanded(
child: DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40,
onPressed: onSave,
backgroundColor: ColorsManager.primaryColor,
backgroundColor: ColorsManager.primaryColorWithOpacity,
child: const Text('Save'),
),
),

View File

@ -35,12 +35,12 @@ class ScheduleModeSelector extends StatelessWidget {
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildRadioTile(
context, 'Countdown', ScheduleModes.countdown, currentMode),
_buildRadioTile(
context, 'Schedule', ScheduleModes.schedule, currentMode),
const Spacer(flex: 1),
// _buildRadioTile(
// context, 'Circulate', ScheduleModes.circulate, currentMode),
// _buildRadioTile(
@ -65,6 +65,7 @@ class ScheduleModeSelector extends StatelessWidget {
style: context.textTheme.bodySmall!.copyWith(
fontSize: 13,
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
leading: Radio<ScheduleModes>(

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ScheduleDialogHelper {
static const List<String> allDays = [
@ -56,8 +58,9 @@ class ScheduleDialogHelper {
Text(
isEdit ? 'Edit Schedule' : 'Add Schedule',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.blue,
fontWeight: FontWeight.bold,
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.w700,
fontSize: 30,
),
),
const SizedBox(),
@ -69,9 +72,9 @@ class ScheduleDialogHelper {
height: 40,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200],
backgroundColor: ColorsManager.boxColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () async {
@ -110,39 +113,27 @@ class ScheduleDialogHelper {
],
),
actions: [
SizedBox(
width: 100,
child: OutlinedButton(
onPressed: () {
Navigator.pop(ctx, null);
},
child: const Text('Cancel'),
),
ScheduleModeButtons(
onSave: () {
dynamic temp;
if (deviceType == 'CUR_2') {
temp = functionOn! ? 'open' : 'close';
} else {
temp = functionOn;
}
final entry = ScheduleEntry(
category: schedule?.category ?? 'switch_1',
time: _formatTimeOfDayToISO(selectedTime),
function: Status(
code: code ?? 'switch_1',
value: temp,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule.scheduleId,
);
Navigator.pop(ctx, entry);
},
),
SizedBox(
width: 100,
child: ElevatedButton(
onPressed: () {
dynamic temp;
if (deviceType == 'CUR_2') {
temp = functionOn! ? 'open' : 'close';
} else {
temp = functionOn;
}
final entry = ScheduleEntry(
category: schedule?.category ?? 'switch_1',
time: _formatTimeOfDayToISO(selectedTime),
function: Status(
code: code ?? 'switch_1',
value: temp,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule.scheduleId,
);
Navigator.pop(ctx, entry);
},
child: const Text('Save'),
)),
],
);
},

View File

@ -153,6 +153,7 @@ class EditUserModel {
final String? jobTitle; // can be empty
final String roleType; // e.g. "ADMIN"
final List<UserSpaceModel> spaces;
final String? companyName;
EditUserModel({
required this.uuid,
@ -167,6 +168,7 @@ class EditUserModel {
required this.jobTitle,
required this.roleType,
required this.spaces,
this.companyName,
});
/// Create a [UserData] from JSON data
@ -182,6 +184,7 @@ class EditUserModel {
invitedBy: json['invitedBy'] as String,
phoneNumber: json['phoneNumber'] ?? '',
jobTitle: json['jobTitle'] ?? '',
companyName: json['companyName'] as String?,
roleType: json['roleType'] as String,
spaces: (json['spaces'] as List<dynamic>)
.map((e) => UserSpaceModel.fromJson(e as Map<String, dynamic>))

View File

@ -12,7 +12,7 @@ class RolesUserModel {
final dynamic jobTitle;
final dynamic createdDate;
final dynamic createdTime;
final String? companyName;
RolesUserModel({
required this.uuid,
required this.createdAt,
@ -27,6 +27,7 @@ class RolesUserModel {
this.jobTitle,
required this.createdDate,
required this.createdTime,
this.companyName,
});
factory RolesUserModel.fromJson(Map<String, dynamic> json) {
@ -47,6 +48,7 @@ class RolesUserModel {
: json['jobTitle'],
createdDate: json['createdDate'],
createdTime: json['createdTime'],
companyName: json['companyName'] as String?,
);
}
}

View File

@ -52,7 +52,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
final TextEditingController lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController jobTitleController = TextEditingController();
final TextEditingController companyNameController = TextEditingController();
final TextEditingController roleSearchController = TextEditingController();
bool? isCompleteBasics;
@ -352,7 +352,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().sendInviteUser(
email: emailController.text,
firstName: firstNameController.text,
jobTitle: jobTitleController.text,
companyName: companyNameController.text,
lastName: lastNameController.text,
phoneNumber: phoneController.text,
roleUuid: roleSelected,
@ -405,7 +405,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().editInviteUser(
userId: event.userId,
firstName: firstNameController.text,
jobTitle: jobTitleController.text,
companyName: companyNameController.text,
lastName: lastNameController.text,
phoneNumber: phoneController.text,
roleUuid: roleSelected,
@ -455,7 +455,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
Future<void> checkEmail(
CheckEmailEvent event, Emitter<UsersState> emit) async {
emit(UsersLoadingState());
String? res = await UserPermissionApi().checkEmail(
String? res = await UserPermissionApi().checkEmail(
emailController.text,
);
checkEmailValid = res!;
@ -529,7 +529,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.text = res.lastName;
emailController.text = res.email;
phoneController.text = res.phoneNumber ?? '';
jobTitleController.text = res.jobTitle ?? '';
companyNameController.text = res.companyName ?? '';
res.roleType;
res.spaces.map((space) {
selectedIds.add(space.uuid);
@ -645,7 +645,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.dispose();
emailController.dispose();
phoneController.dispose();
jobTitleController.dispose();
companyNameController.dispose();
roleSearchController.dispose();
return super.close();
}

View File

@ -317,7 +317,7 @@ class BasicsView extends StatelessWidget {
child: Row(
children: [
Text(
'Job Title',
'Company Name',
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 13,
@ -328,11 +328,11 @@ class BasicsView extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: _blocRole.jobTitleController,
controller: _blocRole.companyNameController,
style:
const TextStyle(color: ColorsManager.blackColor),
decoration: inputTextFormDeco(
hintText: "Job Title (Optional)")
hintText: 'Company Name (Optional)')
.copyWith(
hintStyle: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,

View File

@ -411,7 +411,7 @@ class UsersPage extends StatelessWidget {
titles: const [
"Full Name",
"Email Address",
"Job Title",
"Company Name",
"Role",
"Creation Date",
"Creation Time",
@ -424,7 +424,7 @@ class UsersPage extends StatelessWidget {
return [
Text('${user.firstName} ${user.lastName}'),
Text(user.email),
Text(user.jobTitle),
Center(child: Text(user.companyName ?? '-')),
Text(user.roleType ?? ''),
Text(user.createdDate ?? ''),
Text(user.createdTime ?? ''),

View File

@ -170,45 +170,45 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> scenes = [];
try {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
scenes.addAll(
await SceneApi.getScenes(spaceId, communityId, projectUuid));
Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> scenes = [];
try {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
scenes.addAll(
await SceneApi.getScenes(spaceId, communityId, projectUuid));
}
}
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
}
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
}
emit(state.copyWith(
scenes: scenes,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
emit(state.copyWith(
scenes: scenes,
isLoading: false,
loadScenesErrorMessage: 'Failed to load scenes',
errorMessage: '',
loadAutomationErrorMessage: '',
scenes: scenes));
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
loadScenesErrorMessage: 'Failed to load scenes',
errorMessage: '',
loadAutomationErrorMessage: '',
scenes: scenes));
}
}
}
Future<void> _onLoadAutomation(
LoadAutomation event, Emitter<RoutineState> emit) async {
@ -936,16 +936,15 @@ Future<void> _onLoadScenes(
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
devices.addAll(await DevicesManagementApi()
.fetchDevices(communityId, spaceId, projectUuid));
}
devices.addAll(await DevicesManagementApi()
.fetchDevices(projectUuid, spacesId: spacesList));
}
} else {
devices.addAll(await DevicesManagementApi().fetchDevices(
createRoutineBloc.selectedCommunityId,
createRoutineBloc.selectedSpaceId,
projectUuid));
projectUuid,
spacesId: [createRoutineBloc.selectedSpaceId],
));
}
emit(state.copyWith(isLoading: false, devices: devices));

View File

@ -96,9 +96,7 @@ class _WallPresenceSensorState extends State<FlushPresenceSensor> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(widget.dialogType == 'THEN'
? 'Presence Sensor Functions'
: 'Presence Sensor Condition'),
const DialogHeader('Presence Sensor'),
Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state),
],

View File

@ -0,0 +1,14 @@
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/space_model.dart';
class SpaceReorderDataModel {
const SpaceReorderDataModel({
required this.space,
this.parent,
this.community,
});
final SpaceModel space;
final SpaceModel? parent;
final CommunityModel? community;
}

View File

@ -16,21 +16,37 @@ import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class SpaceManagementPage extends StatelessWidget {
class SpaceManagementPage extends StatefulWidget {
const SpaceManagementPage({super.key});
@override
State<SpaceManagementPage> createState() => _SpaceManagementPageState();
}
class _SpaceManagementPageState extends State<SpaceManagementPage> {
late final CommunitiesBloc communitiesBloc;
@override
void initState() {
communitiesBloc = CommunitiesBloc(
communitiesService: DebouncedCommunitiesService(
RemoteCommunitiesService(HTTPService()),
),
)..add(const LoadCommunities(LoadCommunitiesParam()));
super.initState();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: communitiesBloc),
BlocProvider(
create: (context) => CommunitiesBloc(
communitiesService: DebouncedCommunitiesService(
RemoteCommunitiesService(HTTPService()),
),
)..add(const LoadCommunities(LoadCommunitiesParam())),
create: (context) => CommunitiesTreeSelectionBloc(
communitiesBloc: communitiesBloc,
),
),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
BlocProvider(
create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator(

View File

@ -1,13 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.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/space_card_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.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/space_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/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureCanvas extends StatefulWidget {
const CommunityStructureCanvas({
@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0;
late TransformationController _transformationController;
late AnimationController _animationController;
late final TransformationController _transformationController;
late final AnimationController _animationController;
SpaceReorderDataModel? _draggedData;
@override
void initState() {
@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final position = _positions[space.uuid];
if (position == null) return;
const scale = 1.5;
const scale = 1;
final viewSize = context.size;
if (viewSize == null) return;
@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_runAnimation(matrix);
}
void _onReorder(SpaceReorderDataModel data, int newIndex) {
final newCommunity = widget.community.copyWith();
final children = data.parent?.children ?? newCommunity.spaces;
final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid);
if (oldIndex != -1) {
final item = children.removeAt(oldIndex);
if (newIndex > oldIndex) {
children.insert(newIndex - 1, item);
} else {
children.insert(newIndex, item);
}
}
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(newCommunity),
);
}
void _onSpaceTapped(SpaceModel? space) {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(community: widget.community, space: space),
);
}
void _resetSelectionAndZoom() {
void _resetSelectionAndZoom([CommunityModel? community]) {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(
community: widget.community,
community: community ?? widget.community,
space: null,
),
);
@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_positions.clear();
final community = widget.community;
_calculateLayout(community.spaces, 0, {});
final levelXOffset = <int, double>{};
_calculateLayout(community.spaces, 0, levelXOffset);
final selectedSpace = widget.selectedSpace;
final highlightedUuids = <String>{};
@ -193,7 +216,24 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final widgets = <Widget>[];
final connections = <SpaceConnectionModel>[];
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
_generateWidgets(
widget.community.spaces,
widgets,
connections,
highlightedUuids,
community: widget.community,
);
final createButtonX = levelXOffset[0] ?? 0.0;
const createButtonY = 0.0;
widgets.add(
Positioned(
left: createButtonX,
top: createButtonY,
child: CreateSpaceButton(communityUuid: widget.community.uuid),
),
);
return [
CustomPaint(
@ -211,58 +251,178 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
List<SpaceModel> spaces,
List<Widget> widgets,
List<SpaceConnectionModel> connections,
Set<String> highlightedUuids,
) {
for (final space in spaces) {
Set<String> highlightedUuids, {
CommunityModel? community,
SpaceModel? parent,
}) {
if (spaces.isNotEmpty) {
final firstChildPos = _positions[spaces.first.uuid]!;
final targetPos = Offset(
firstChildPos.dx - (_horizontalSpacing / 4),
firstChildPos.dy,
);
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
}
for (var i = 0; i < spaces.length; i++) {
final space = spaces[i];
final position = _positions[space.uuid];
if (position == null) continue;
if (position == null) {
continue;
}
final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null;
final spaceCard = SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.community.uuid,
),
);
final reorderData = SpaceReorderDataModel(
space: space,
parent: parent,
community: community,
);
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
child: Draggable<SpaceReorderDataModel>(
data: reorderData,
feedback: Material(
color: Colors.transparent,
child: Opacity(
opacity: 0.2,
child: SizedBox(
width: _cardWidth,
height: _cardHeight,
child: spaceCard,
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
),
),
onDragStarted: () => setState(() => _draggedData = reorderData),
onDragEnd: (_) => setState(() => _draggedData = null),
onDraggableCanceled: (_, __) => setState(() => _draggedData = null),
childWhenDragging: Opacity(opacity: 0.4, child: spaceCard),
child: spaceCard,
),
),
);
final targetPos = Offset(
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
position.dy,
);
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
for (final child in space.children) {
connections.add(
SpaceConnectionModel(from: space.uuid, to: child.uuid),
connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
}
if (space.children.isNotEmpty) {
_generateWidgets(
space.children,
widgets,
connections,
highlightedUuids,
parent: space,
);
}
_generateWidgets(space.children, widgets, connections, highlightedUuids);
}
}
Widget _buildDropTarget(
SpaceModel? parent,
CommunityModel? community,
int index,
Offset position,
) {
return Positioned(
left: position.dx,
top: position.dy,
width: 40,
height: _cardHeight,
child: DragTarget<SpaceReorderDataModel>(
builder: (context, candidateData, rejectedData) {
if (_draggedData == null) {
return const SizedBox();
}
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
_draggedData?.community == null) ||
(_draggedData?.community?.uuid == community?.uuid &&
_draggedData?.parent == null);
if (!isTargetForDragged) {
return const SizedBox();
}
return Container(
width: 40,
height: _cardHeight,
decoration: BoxDecoration(
color: context.theme.colorScheme.primary.withValues(
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.add,
color: context.theme.colorScheme.onPrimary,
),
);
},
onWillAcceptWithDetails: (data) {
final children = parent?.children ?? community?.spaces ?? [];
final isSameParent = (data.data.parent?.uuid == parent?.uuid &&
data.data.community == null) ||
(data.data.community?.uuid == community?.uuid &&
data.data.parent == null);
if (!isSameParent) {
return false;
}
final oldIndex =
children.indexWhere((s) => s.uuid == data.data.space.uuid);
if (oldIndex == index || oldIndex == index - 1) {
return false;
}
return true;
},
onAcceptWithDetails: (data) => _onReorder(data.data, index),
),
);
}
@override
Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets();
return InteractiveViewer(
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: MediaQuery.sizeOf(context).width * 0.3,
vertical: MediaQuery.sizeOf(context).height * 0.3,
horizontal: context.screenWidth * 0.3,
vertical: context.screenHeight * 0.3,
),
minScale: 0.5,
maxScale: 3.0,
@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: GestureDetector(
onTap: _resetSelectionAndZoom,
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 5,
height: MediaQuery.sizeOf(context).height * 5,
width: context.screenWidth * 5,
height: context.screenHeight * 5,
child: Stack(children: treeWidgets),
),
),

View File

@ -3,7 +3,10 @@ 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/domain/models/space_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/space_details/domain/models/space_details_model.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';
@ -11,6 +14,26 @@ import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeader extends StatelessWidget {
const CommunityStructureHeader({super.key});
List<SpaceModel> _updateRecursive(
List<SpaceModel> spaces,
SpaceDetailsModel updatedSpace,
) {
return spaces.map((space) {
if (space.uuid == updatedSpace.uuid) {
return space.copyWith(
spaceName: updatedSpace.spaceName,
icon: updatedSpace.icon,
);
}
if (space.children.isNotEmpty) {
return space.copyWith(
children: _updateRecursive(space.children, updatedSpace),
);
}
return space;
}).toList();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -55,8 +78,9 @@ class CommunityStructureHeader extends StatelessWidget {
children: [
Text(
'Community Structure',
style: theme.textTheme.headlineLarge
?.copyWith(color: ColorsManager.blackColor),
style: theme.textTheme.headlineLarge?.copyWith(
color: ColorsManager.blackColor,
),
),
if (selectedCommunity != null)
Row(
@ -67,8 +91,9 @@ class CommunityStructureHeader extends StatelessWidget {
Flexible(
child: SelectableText(
selectedCommunity.name,
style: theme.textTheme.bodyLarge
?.copyWith(color: ColorsManager.blackColor),
style: theme.textTheme.bodyLarge?.copyWith(
color: ColorsManager.blackColor,
),
maxLines: 1,
),
),
@ -93,12 +118,24 @@ class CommunityStructureHeader extends StatelessWidget {
CommunityStructureHeaderActionButtons(
onDelete: (space) {},
onDuplicate: (space) {},
onEdit: (space) {
SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
);
},
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
communityUuid: selectedCommunity.uuid,
onSuccess: (updatedSpaceDetails) {
final communitiesBloc = context.read<CommunitiesBloc>();
final updatedSpaces = _updateRecursive(
selectedCommunity.spaces,
updatedSpaceDetails,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
communitiesBloc.add(CommunitiesUpdateCommunity(community));
},
),
selectedSpace: selectedSpace,
),
],

View File

@ -2,38 +2,66 @@ import 'package:flutter/material.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';
class CreateSpaceButton extends StatelessWidget {
const CreateSpaceButton({super.key});
class CreateSpaceButton extends StatefulWidget {
const CreateSpaceButton({
required this.communityUuid,
super.key,
});
final String communityUuid;
@override
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
}
class _CreateSpaceButtonState extends State<CreateSpaceButton> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
child: Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
return Tooltip(
margin: const EdgeInsets.symmetric(vertical: 24),
message: 'Create a new space',
child: InkWell(
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.communityUuid,
),
child: Center(
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: Colors.blue,
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: _isHovered ? 1.0 : 0.45,
child: Container(
width: 150,
height: 90,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.2),
spreadRadius: 3,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.borderColor, width: 2),
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.add,
color: Colors.blue,
),
),
),
),
),
),

View File

@ -22,22 +22,20 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
return MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: SizedBox(
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: 0,
child: PlusButtonWidget(
offset: Offset.zero,
onButtonTap: widget.onTap,
),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: 0,
child: PlusButtonWidget(
offset: Offset.zero,
onButtonTap: widget.onTap,
),
],
),
),
],
),
);
}

View File

@ -17,7 +17,7 @@ class SpaceCell extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
return InkWell(
onTap: onTap,
child: Container(
width: 150,

View File

@ -13,11 +13,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace;
const spacer = Spacer(flex: 10);
const spacer = Spacer(flex: 6);
return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty,
replacement: const Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
replacement: Row(
children: [
spacer,
Expanded(
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
),
spacer
],
),
child: Column(
mainAxisSize: MainAxisSize.min,

View File

@ -46,6 +46,25 @@ class SpaceModel extends Equatable {
);
}
SpaceModel copyWith({
String? uuid,
DateTime? createdAt,
DateTime? updatedAt,
String? spaceName,
String? icon,
List<SpaceModel>? children,
}) {
return SpaceModel(
uuid: uuid ?? this.uuid,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
spaceName: spaceName ?? this.spaceName,
icon: icon ?? this.icon,
children: children ?? this.children,
parent: parent,
);
}
@override
List<Object?> get props => [uuid, spaceName, icon, children];
}

View File

@ -1,17 +1,39 @@
import 'dart:async';
import 'package:bloc/bloc.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/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
part 'communities_tree_selection_event.dart';
part 'communities_tree_selection_state.dart';
class CommunitiesTreeSelectionBloc
extends Bloc<CommunitiesTreeSelectionEvent, CommunitiesTreeSelectionState> {
CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) {
CommunitiesTreeSelectionBloc({
required CommunitiesBloc communitiesBloc,
}) : _communitiesBloc = communitiesBloc,
super(const CommunitiesTreeSelectionState()) {
on<SelectCommunityEvent>(_onSelectCommunity);
on<SelectSpaceEvent>(_onSelectSpace);
on<ClearCommunitiesTreeSelectionEvent>(_onClearSelection);
on<_CommunitiesStateUpdated>(_onCommunitiesStateUpdated);
_communitiesSubscription = _communitiesBloc.stream.listen((communitiesState) {
if (state.selectedCommunity != null) {
add(_CommunitiesStateUpdated(communitiesState));
}
});
}
final CommunitiesBloc _communitiesBloc;
late final StreamSubscription<CommunitiesState> _communitiesSubscription;
@override
Future<void> close() {
_communitiesSubscription.cancel();
return super.close();
}
void _onSelectCommunity(
@ -44,4 +66,59 @@ class CommunitiesTreeSelectionBloc
) {
emit(const CommunitiesTreeSelectionState());
}
void _onCommunitiesStateUpdated(
_CommunitiesStateUpdated event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
if (state.selectedCommunity == null) return;
final communities = event.communitiesState.communities;
try {
final updatedCommunity = communities.firstWhere(
(c) => c.uuid == state.selectedCommunity!.uuid,
);
var updatedSelectedSpace = state.selectedSpace;
if (state.selectedSpace != null) {
updatedSelectedSpace = _findSpaceInCommunity(
updatedCommunity,
state.selectedSpace!.uuid,
);
}
emit(
state.copyWith(
selectedCommunity: updatedCommunity,
selectedSpace: updatedSelectedSpace,
clearSelectedSpace: updatedSelectedSpace == null,
),
);
} catch (_) {
add(const ClearCommunitiesTreeSelectionEvent());
}
}
SpaceModel? _findSpaceInCommunity(CommunityModel community, String spaceUuid) {
try {
return _findSpaceRecursive(community.spaces, spaceUuid);
} catch (_) {
return null;
}
}
SpaceModel _findSpaceRecursive(List<SpaceModel> spaces, String spaceUuid) {
for (final space in spaces) {
if (space.uuid == spaceUuid) {
return space;
}
if (space.children.isNotEmpty) {
try {
return _findSpaceRecursive(space.children, spaceUuid);
} catch (_) {
// not found in this branch
}
}
}
throw Exception('Space not found');
}
}

View File

@ -29,3 +29,12 @@ final class ClearCommunitiesTreeSelectionEvent
extends CommunitiesTreeSelectionEvent {
const ClearCommunitiesTreeSelectionEvent();
}
final class _CommunitiesStateUpdated extends CommunitiesTreeSelectionEvent {
const _CommunitiesStateUpdated(this.communitiesState);
final CommunitiesState communitiesState;
@override
List<Object> get props => [communitiesState];
}

View File

@ -12,18 +12,14 @@ final class CommunitiesTreeSelectionState extends Equatable {
CommunitiesTreeSelectionState copyWith({
CommunityModel? selectedCommunity,
SpaceModel? selectedSpace,
List<CommunityModel>? expandedCommunities,
List<SpaceModel>? expandedSpaces,
bool clearSelectedSpace = false,
}) {
return CommunitiesTreeSelectionState(
selectedCommunity: selectedCommunity ?? this.selectedCommunity,
selectedSpace: selectedSpace ?? this.selectedSpace,
selectedSpace: clearSelectedSpace ? null : selectedSpace ?? this.selectedSpace,
);
}
@override
List<Object?> get props => [
selectedCommunity,
selectedSpace,
];
}
List<Object?> get props => [selectedCommunity, selectedSpace];
}

View File

@ -13,14 +13,14 @@ class CommunitiesTreeFailureWidget extends StatelessWidget {
return Expanded(
child: Center(
child: Column(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
SelectableText(
errorMessage ?? 'Something went wrong',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
FilledButton(
onPressed: () => context.read<CommunitiesBloc>().add(
LoadCommunities(
LoadCommunitiesParam(

View File

@ -40,16 +40,6 @@ class SpaceDetailsModel extends Equatable {
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'spaceName': spaceName,
'icon': icon,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
'subspaces': subspaces.map((e) => e.toJson()).toList(),
};
}
SpaceDetailsModel copyWith({
String? uuid,
String? spaceName,
@ -89,14 +79,6 @@ class ProductAllocation extends Equatable {
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'product': product.toJson(),
'tag': tag.toJson(),
};
}
ProductAllocation copyWith({
String? uuid,
Product? product,
@ -134,14 +116,6 @@ class Subspace extends Equatable {
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'name': name,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
};
}
Subspace copyWith({
String? uuid,
String? name,

View File

@ -2,23 +2,37 @@ 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/domain/models/space_details_model.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/update_space/data/services/remote_update_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) {
static void showCreate(
BuildContext context, {
required String communityUuid,
}) {
showDialog<void>(
context: context,
builder: (_) => BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
child: SpaceDetailsDialog(
context: context,
title: const SelectableText('Create Space'),
spaceModel: SpaceModel.empty(),
onSave: (space) {},
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
],
child: Builder(
builder: (context) => SpaceDetailsDialog(
context: context,
title: const SelectableText('Create Space'),
spaceModel: SpaceModel.empty(),
onSave: (space) {},
communityUuid: communityUuid,
),
),
),
);
@ -27,20 +41,98 @@ abstract final class SpaceDetailsDialogHelper {
static void showEdit(
BuildContext context, {
required SpaceModel spaceModel,
required String communityUuid,
required void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
}) {
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) {},
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
BlocProvider(
create: (context) => UpdateSpaceBloc(
RemoteUpdateSpaceService(HTTPService()),
),
),
],
child: Builder(
builder: (context) => BlocListener<UpdateSpaceBloc, UpdateSpaceState>(
listener: (context, state) => _updateListener(
context,
state,
onSuccess,
),
child: SpaceDetailsDialog(
context: context,
title: const SelectableText('Edit Space'),
spaceModel: spaceModel,
onSave: (space) => context.read<UpdateSpaceBloc>().add(
UpdateSpace(
UpdateSpaceParam(
communityUuid: communityUuid,
space: space,
),
),
),
communityUuid: communityUuid,
),
),
),
),
);
}
static void _updateListener(
BuildContext context,
UpdateSpaceState state,
void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
) {
return switch (state) {
UpdateSpaceInitial() => null,
UpdateSpaceLoading() => _onLoading(context),
UpdateSpaceSuccess(:final space) =>
_onUpdateSuccess(context, space, onSuccess),
UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage),
};
}
static void _onUpdateSuccess(
BuildContext context,
SpaceDetailsModel space,
void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
) {
Navigator.of(context).pop();
Navigator.of(context).pop();
onSuccess?.call(space);
}
static void _onLoading(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
}
static void _onError(BuildContext context, String errorMessage) {
Navigator.of(context).pop();
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('OK'),
),
],
),
);
}
}

View File

@ -1,7 +1,6 @@
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';
@ -15,6 +14,7 @@ class SpaceDetailsDialog extends StatefulWidget {
required this.spaceModel,
required this.onSave,
required this.context,
required this.communityUuid,
super.key,
});
@ -22,6 +22,7 @@ class SpaceDetailsDialog extends StatefulWidget {
final SpaceModel spaceModel;
final void Function(SpaceDetailsModel space) onSave;
final BuildContext context;
final String communityUuid;
@override
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
@ -35,11 +36,7 @@ class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
if (!isCreateMode) {
final param = LoadSpaceDetailsParam(
spaceUuid: widget.spaceModel.uuid,
communityUuid: widget.context
.read<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity!
.uuid,
communityUuid: widget.communityUuid,
);
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
}

View File

@ -37,7 +37,7 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
..._subspaces,
Subspace(
name: name,
uuid: const Uuid().v4(),
uuid: '${const Uuid().v4()}-NewTag',
productAllocations: const [],
),
];

View File

@ -3,41 +3,19 @@ import 'package:equatable/equatable.dart';
class Tag extends Equatable {
final String uuid;
final String name;
final String createdAt;
final String updatedAt;
const Tag({
required this.uuid,
required this.name,
required this.createdAt,
required this.updatedAt,
});
factory Tag.empty() => const Tag(
uuid: '',
name: '',
createdAt: '',
updatedAt: '',
);
factory Tag.fromJson(Map<String, dynamic> json) {
return Tag(
uuid: json['uuid'] as String,
name: json['name'] as String,
createdAt: json['createdAt'] as String,
updatedAt: json['updatedAt'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'name': name,
'createdAt': createdAt,
'updatedAt': updatedAt,
};
}
@override
List<Object?> get props => [uuid, name, createdAt, updatedAt];
List<Object?> get props => [uuid, name];
}

View File

@ -214,9 +214,12 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
for (final product in newProducts) {
_space.productAllocations.add(
ProductAllocation(
uuid: const Uuid().v4(),
uuid: '${const Uuid().v4()}-NewProductUuid',
product: product,
tag: Tag.empty(),
tag: Tag(
uuid: '${const Uuid().v4()}-NewTag',
name: '',
),
),
);
}

View File

@ -2,6 +2,7 @@ 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';
import 'package:uuid/uuid.dart';
class ProductTagField extends StatefulWidget {
final List<Tag> items;
@ -53,13 +54,8 @@ class _ProductTagFieldState extends State<ProductTagField> {
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: '',
),
(e) => e.name.toLowerCase() == lowerCaseValue,
orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value),
);
widget.onSelected(selectedTag);
_closeDropdown();

View File

@ -1,5 +1,7 @@
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/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -12,17 +14,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
static const _defaultErrorMessage = 'Failed to update space';
@override
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space) async {
Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param) async {
try {
final response = await _httpService.put(
path: 'endpoint',
body: space.toJson(),
expectedResponseModel: (data) => SpaceDetailsModel.fromJson(
data as Map<String, dynamic>,
),
final path = await _makeUrl(param);
await _httpService.put(
path: path,
body: param.toJson(),
expectedResponseModel: (data) {
final response = data as Map<String, dynamic>;
final isSuccess = response['success'] as bool;
if (!isSuccess) {
throw APIException(response['error'] as String);
}
return isSuccess;
},
);
return response;
return param.space;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
@ -37,4 +45,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeUrl(UpdateSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
final spaceUuid = param.space.uuid;
if (spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
final communityUuid = param.communityUuid;
if (communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
return '/projects/$projectUuid/communities/$communityUuid/spaces/$spaceUuid';
}
}

View File

@ -0,0 +1,42 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
class UpdateSpaceParam {
UpdateSpaceParam({
required this.space,
required this.communityUuid,
});
final SpaceDetailsModel space;
final String communityUuid;
Map<String, dynamic> toJson() {
return {
'spaceName': space.spaceName,
'icon': space.icon,
'subspaces': space.subspaces.map((e) => e._toJson()).toList(),
'productAllocations':
space.productAllocations.map((e) => e._toJson()).toList(),
};
}
}
extension _ProductAllocationToJson on ProductAllocation {
Map<String, dynamic> _toJson() {
final isNewTag = tag.uuid.isEmpty;
return <String, dynamic>{
if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid,
'productUuid': product.uuid,
};
}
}
extension _SubspaceToJson on Subspace {
Map<String, dynamic> _toJson() {
final isNewSubspace = uuid.endsWith('-NewTag');
return <String, dynamic>{
if (!isNewSubspace) 'uuid': uuid,
'subspaceName': name,
'productAllocations': productAllocations.map((e) => e._toJson()).toList(),
};
}
}

View File

@ -1,5 +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/update_space/domain/params/update_space_param.dart';
abstract class UpdateSpaceService {
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space);
abstract interface class UpdateSpaceService {
Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param);
}

View File

@ -1,6 +1,7 @@
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';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -20,7 +21,7 @@ class UpdateSpaceBloc extends Bloc<UpdateSpaceEvent, UpdateSpaceState> {
) async {
emit(UpdateSpaceLoading());
try {
final updatedSpace = await _updateSpaceService.updateSpace(event.space);
final updatedSpace = await _updateSpaceService.updateSpace(event.param);
emit(UpdateSpaceSuccess(updatedSpace));
} on APIException catch (e) {
emit(UpdateSpaceFailure(e.message));

View File

@ -8,10 +8,10 @@ sealed class UpdateSpaceEvent extends Equatable {
}
final class UpdateSpace extends UpdateSpaceEvent {
const UpdateSpace(this.space);
const UpdateSpace(this.param);
final SpaceDetailsModel space;
final UpdateSpaceParam param;
@override
List<Object> get props => [space];
List<Object> get props => [param];
}

View File

@ -21,10 +21,10 @@ final class UpdateSpaceSuccess extends UpdateSpaceState {
}
final class UpdateSpaceFailure extends UpdateSpaceState {
final String message;
final String errorMessage;
const UpdateSpaceFailure(this.message);
const UpdateSpaceFailure(this.errorMessage);
@override
List<Object> get props => [message];
List<Object> get props => [errorMessage];
}

View File

@ -12,20 +12,16 @@ import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class DevicesManagementApi {
Future<List<AllDevicesModel>> fetchDevices(
String communityId, String spaceId, String projectId) async {
Future<List<AllDevicesModel>> fetchDevices(String projectId,
{List<String>? spacesId}) async {
try {
final response = await HTTPService().get(
path: communityId.isNotEmpty && spaceId.isNotEmpty
? ApiEndpoints.getSpaceDevices
.replaceAll('{spaceUuid}', spaceId)
.replaceAll('{communityUuid}', communityId)
.replaceAll('{projectId}', projectId)
: ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId),
path: ApiEndpoints.getSpaceDevices.replaceAll('{projectId}', projectId),
queryParameters: {if (spacesId != null) 'spaces': spacesId},
showServerMessage: true,
expectedResponseModel: (json) {
List<dynamic> jsonData = json['data'];
List<AllDevicesModel> devicesList = jsonData.map((jsonItem) {
final List<dynamic> jsonData = json['data'] as List<dynamic>;
final List<AllDevicesModel> devicesList = jsonData.map((jsonItem) {
return AllDevicesModel.fromJson(jsonItem);
}).toList();
return devicesList;
@ -416,5 +412,4 @@ class DevicesManagementApi {
);
return response;
}
}

View File

@ -34,8 +34,9 @@ class UserPermissionApi {
path: ApiEndpoints.roleTypes,
showServerMessage: true,
expectedResponseModel: (json) {
final List<RoleTypeModel> fetchedRoles =
(json['data'] as List).map((item) => RoleTypeModel.fromJson(item)).toList();
final List<RoleTypeModel> fetchedRoles = (json['data'] as List)
.map((item) => RoleTypeModel.fromJson(item))
.toList();
return fetchedRoles;
},
);
@ -47,7 +48,9 @@ class UserPermissionApi {
path: ApiEndpoints.permission.replaceAll("roleUuid", roleUuid),
showServerMessage: true,
expectedResponseModel: (json) {
return (json as List).map((data) => PermissionOption.fromJson(data)).toList();
return (json as List)
.map((data) => PermissionOption.fromJson(data))
.toList();
},
);
return response ?? [];
@ -57,7 +60,7 @@ class UserPermissionApi {
String? firstName,
String? lastName,
String? email,
String? jobTitle,
String? companyName,
String? phoneNumber,
String? roleUuid,
List<String>? spaceUuids,
@ -68,7 +71,7 @@ class UserPermissionApi {
"firstName": firstName,
"lastName": lastName,
"email": email,
"jobTitle": jobTitle != '' ? jobTitle : null,
"companyName": companyName != '' ? companyName : null,
"phoneNumber": phoneNumber != '' ? phoneNumber : null,
"roleUuid": roleUuid,
"projectUuid": projectUuid,
@ -140,7 +143,7 @@ class UserPermissionApi {
String? firstName,
String? userId,
String? lastName,
String? jobTitle,
String? companyName,
String? phoneNumber,
String? roleUuid,
List<String>? spaceUuids,
@ -150,8 +153,8 @@ class UserPermissionApi {
final body = <String, dynamic>{
"firstName": firstName,
"lastName": lastName,
"jobTitle": jobTitle != '' ? jobTitle : " ",
"phoneNumber": phoneNumber != '' ? phoneNumber : " ",
"companyName": companyName != '' ? companyName : ' ',
"phoneNumber": phoneNumber != '' ? phoneNumber : ' ',
"roleUuid": roleUuid,
"projectUuid": projectUuid,
"spaceUuids": spaceUuids,
@ -190,12 +193,17 @@ class UserPermissionApi {
}
}
Future<bool> changeUserStatusById(userUuid, status, String projectUuid) async {
Future<bool> changeUserStatusById(
userUuid, status, String projectUuid) async {
try {
Map<String, dynamic> bodya = {"disable": status, "projectUuid": projectUuid};
Map<String, dynamic> bodya = {
"disable": status,
"projectUuid": projectUuid
};
final response = await _httpService.put(
path: ApiEndpoints.changeUserStatus.replaceAll("{invitedUserUuid}", userUuid),
path: ApiEndpoints.changeUserStatus
.replaceAll("{invitedUserUuid}", userUuid),
body: bodya,
expectedResponseModel: (json) {
return json['success'];

View File

@ -84,5 +84,6 @@ abstract class ColorsManager {
static const Color minBlueDot = Color(0xFF023DFE);
static const Color grey25 = Color(0xFFF9F9F9);
static const Color grey50 = Color(0xFF718096);
static const Color red100 = Color(0xFFFE0202);
static const Color grey800 = Color(0xffF8F8F8);
}

View File

@ -17,8 +17,7 @@ abstract class ApiEndpoints {
////// Devices Management ////////////////
static const String getAllDevices = '/projects/{projectId}/devices';
static const String getSpaceDevices =
'/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices';
static const String getSpaceDevices = '/projects/{projectId}/devices';
static const String getDeviceStatus = '/devices/{uuid}/functions/status';
static const String getBatchStatus = '/devices/batch';
@ -141,5 +140,6 @@ abstract class ApiEndpoints {
static const String saveSchedule = '/schedule/{deviceUuid}';
static const String getBookableSpaces = '/bookable-spaces';
static const String getCalendarEvents = '/api';
static const String getBookings =
'/bookings?month={mm}%2F{yyyy}&space={space}';
}