Compare commits

...

24 Commits

Author SHA1 Message Date
5589e5b587 Refactor booking system: update API endpoint format, add ResetEvents event, and enhance UI components for improved user experience 2025-07-21 15:22:17 +03:00
d12b4c0c65 Sp 1721 fe implement delete space feature (#351)
<!--
  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-1721](https://syncrow.atlassian.net/browse/SP-1721)

## Description

Implemented delete space feature.
Smoothened out the connective lines in the canvas.
Synced state between selection and communities bloc on delete.
Implemented create space feature.

## 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-1721]:
https://syncrow.atlassian.net/browse/SP-1721?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-17 16:49:20 +03:00
22c8c54fab Enhance booking system: update API endpoints, improve event loading w… (#357)
…ith caching, and refine UI components

<!--
  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 booking system: update API endpoints, improve event loading with
caching, and refine UI components

## 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-17 16:23:03 +03:00
95300071e9 [FE] When user navigates to devices page the devices are already listed although no community is selected also when we select a community the API is being called repeatedly too many times (#356)
<!--
  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-1589](https://syncrow.atlassian.net/browse/SP-1589)

## Description
fixed the issue of community selection (if empty no devices should
appear)
## 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-1589]:
https://syncrow.atlassian.net/browse/SP-1589?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-17 15:07:46 +03:00
7876af9756 Refactor memory service implementation: rename MemoryBookableSpaceService to MemoryCalendarService for clarity and consistency 2025-07-17 12:46:07 +03:00
fe2f4a872b Refactor memory event handling: replace MemoryBookableSpaceService with a new implementation and integrate caching logic in CalendarEventsBloc 2025-07-17 12:38:58 +03:00
c9b8fbb0c2 Refactor calendar event loading: replace parameters with LoadEventsParam class and implement memory caching for improved performance 2025-07-17 11:20:32 +03:00
c6729f476f Enhance booking system: update API endpoints, improve event loading with caching, and refine UI components 2025-07-16 15:36:49 +03:00
3e634dc7a2 fix communities filtiring issue 2025-07-16 11:02:32 +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
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
dcf1df9b4a sp1613 delete condition word 2025-05-21 07:25:34 -05:00
36 changed files with 804 additions and 676 deletions

View File

@ -0,0 +1,63 @@
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.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';
class MemoryCalendarService implements CalendarSystemService {
final Map<String, CalendarEventsResponse> _eventsCache = {};
@override
Future<CalendarEventsResponse> getCalendarEvents({
required LoadEventsParam params,
}) async {
final key = params.generateKey();
return _eventsCache[key]!;
}
void setEvents(
LoadEventsParam param,
CalendarEventsResponse events,
) {
final key = param.generateKey();
_eventsCache[key] = events;
}
void addEvent(LoadEventsParam param, CalendarEventsResponse event) {
final key = param.generateKey();
_eventsCache[key] = event;
}
void clear() {
_eventsCache.clear();
}
}
class MemoryCalendarServiceWithRemoteFallback implements CalendarSystemService {
final MemoryCalendarService memoryService;
final RemoteCalendarService remoteService;
MemoryCalendarServiceWithRemoteFallback({
required this.memoryService,
required this.remoteService,
});
@override
Future<CalendarEventsResponse> getCalendarEvents({
required LoadEventsParam params,
}) async {
final key = params.generateKey();
final doesExistInMemory = memoryService._eventsCache.containsKey(key);
if (doesExistInMemory) {
return memoryService.getCalendarEvents(params: params);
} else {
final remoteResult =
await remoteService.getCalendarEvents(params: params);
memoryService.setEvents(params, remoteResult);
return remoteResult;
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.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/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/api_exception.dart';
@ -13,147 +14,21 @@ class RemoteCalendarService implements CalendarSystemService {
@override @override
Future<CalendarEventsResponse> getCalendarEvents({ Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId, required LoadEventsParam params,
}) async { }) async {
final month = params.startDate.month.toString().padLeft(2, '0');
final year = params.startDate.year.toString();
try { try {
final response = await _httpService.get( return await _httpService.get<CalendarEventsResponse>(
path: ApiEndpoints.getCalendarEvents, path: ApiEndpoints.getBookings
queryParameters: { .replaceAll('{mm}', month)
'spaceId': spaceId, .replaceAll('{yyyy}', year)
}, .replaceAll('{space}', params.id),
expectedResponseModel: (json) { expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson( return CalendarEventsResponse.fromJson(json as Map<String, dynamic>);
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) { } on DioException catch (e) {
final responseData = e.response?.data; final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) { if (responseData is Map<String, dynamic>) {

View File

@ -0,0 +1,34 @@
import 'package:equatable/equatable.dart';
class LoadEventsParam extends Equatable {
final DateTime startDate;
final DateTime endDate;
final String id;
const LoadEventsParam({
required this.startDate,
required this.endDate,
required this.id,
});
@override
List<Object?> get props => [startDate, endDate, id];
LoadEventsParam copyWith({
DateTime? startDate,
DateTime? endDate,
String? id,
}) {
return LoadEventsParam(
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
id: id ?? this.id,
);
}
}
extension KeyGenerator on LoadEventsParam {
String generateKey() {
return '$id-${startDate.year}-${startDate.month.toString().padLeft(2, '0')}';
}
}

View File

@ -1,7 +1,8 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.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/models/calendar_event_booking.dart';
abstract class CalendarSystemService { abstract class CalendarSystemService {
Future<CalendarEventsResponse> getCalendarEvents({ Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId, required LoadEventsParam params,
}); });
} }

View File

@ -2,37 +2,48 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.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/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart';
part 'events_event.dart'; part 'events_event.dart';
part 'events_state.dart'; part 'events_state.dart';
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> { class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController(); EventController eventController = EventController();
final CalendarSystemService calendarService; final CalendarSystemService calendarService;
CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) { CalendarEventsBloc({
required this.calendarService,
}) : super(EventsInitial()) {
on<LoadEvents>(_onLoadEvents); on<LoadEvents>(_onLoadEvents);
on<AddEvent>(_onAddEvent); on<AddEvent>(_onAddEvent);
on<StartTimer>(_onStartTimer);
on<DisposeResources>(_onDisposeResources); on<DisposeResources>(_onDisposeResources);
on<GoToWeek>(_onGoToWeek); on<GoToWeek>(_onGoToWeek);
on<ResetEvents>(_onResetEvents);
} }
Future<void> _onLoadEvents( Future<void> _onLoadEvents(
LoadEvents event, LoadEvents event,
Emitter<CalendarEventState> emit, Emitter<CalendarEventState> emit,
) async { ) async {
final param = event.param;
final month = param.endDate.month;
final year = param.endDate.year;
final spaceId = param.id;
emit(EventsLoading()); emit(EventsLoading());
try { try {
final response = await calendarService.getCalendarEvents( final response = await calendarService.getCalendarEvents(params: param);
spaceId: event.spaceId,
); final events = response.data.map(_toCalendarEventData).toList();
final events =
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
eventController.addAll(events); eventController.addAll(events);
emit(EventsLoaded(events: events)); emit(EventsLoaded(
events: events,
spaceId: spaceId,
month: month,
year: year,
));
} catch (e) { } catch (e) {
emit(EventsError('Failed to load events')); emit(EventsError('Failed to load events'));
} }
@ -40,16 +51,19 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) { void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
eventController.add(event.event); eventController.add(event.event);
if (state is EventsLoaded) { if (state is EventsLoaded) {
final loaded = state as EventsLoaded; final loaded = state as EventsLoaded;
emit(EventsLoaded( emit(EventsLoaded(
events: [...eventController.events], events: [...eventController.events],
spaceId: loaded.spaceId,
month: loaded.month,
year: loaded.year,
)); ));
} }
} }
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
void _onDisposeResources( void _onDisposeResources(
DisposeResources event, Emitter<CalendarEventState> emit) { DisposeResources event, Emitter<CalendarEventState> emit) {
eventController.dispose(); eventController.dispose();
@ -61,6 +75,9 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final newWeekDays = _getWeekDays(event.weekDate); final newWeekDays = _getWeekDays(event.weekDate);
emit(EventsLoaded( emit(EventsLoaded(
events: loaded.events, events: loaded.events,
spaceId: loaded.spaceId,
month: loaded.month,
year: loaded.year,
)); ));
} }
} }
@ -90,14 +107,13 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
); );
return CalendarEventData( return CalendarEventData(
date: startTime, date: startTime,
startTime: startTime, startTime: startTime,
endTime: endTime, endTime: endTime,
title: title: '${booking.user.firstName} ${booking.user.lastName}',
'${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}',
description: 'Cost: ${booking.cost}', description: 'Cost: ${booking.cost}',
color: Colors.blue, color: Colors.blue,
event: booking, event: booking,
); );
} }
@ -112,4 +128,18 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
eventController.dispose(); eventController.dispose();
return super.close(); return super.close();
} }
void _onResetEvents(
ResetEvents event,
Emitter<CalendarEventState> emit,
) {
if (calendarService is MemoryCalendarServiceWithRemoteFallback) {
(calendarService as MemoryCalendarServiceWithRemoteFallback)
.memoryService
.clear();
}
eventController.dispose();
eventController = EventController();
emit(EventsInitial());
}
} }

View File

@ -6,17 +6,11 @@ abstract class CalendarEventsEvent {
} }
class LoadEvents extends CalendarEventsEvent { class LoadEvents extends CalendarEventsEvent {
final String spaceId; final LoadEventsParam param;
final DateTime weekStart; const LoadEvents(this.param);
final DateTime weekEnd;
const LoadEvents({
required this.spaceId,
required this.weekStart,
required this.weekEnd,
});
} }
class AddEvent extends CalendarEventsEvent { class AddEvent extends CalendarEventsEvent {
final CalendarEventData event; final CalendarEventData event;
const AddEvent(this.event); const AddEvent(this.event);
@ -35,3 +29,10 @@ class CheckWeekHasEvents extends CalendarEventsEvent {
final DateTime weekStart; final DateTime weekStart;
const CheckWeekHasEvents(this.weekStart); const CheckWeekHasEvents(this.weekStart);
} }
class ResetEvents extends CalendarEventsEvent {
const ResetEvents();
@override
List<Object?> get props => [];
}

View File

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

View File

@ -1,12 +1,15 @@
// booking_page.dart
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:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.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_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_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/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/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.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/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/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/icon_text_button.dart';
@ -24,7 +27,33 @@ class BookingPage extends StatefulWidget {
} }
class _BookingPageState extends State<BookingPage> { class _BookingPageState extends State<BookingPage> {
late final EventController _eventController; @override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()),
BlocProvider(
create: (_) => CalendarEventsBloc(
calendarService: MemoryCalendarServiceWithRemoteFallback(
remoteService: RemoteCalendarService(HTTPService()),
memoryService: MemoryCalendarService(),
),
),
),
],
child: _BookingPageContent(),
);
}
}
class _BookingPageContent extends StatefulWidget {
@override
State<_BookingPageContent> createState() => _BookingPageContentState();
}
class _BookingPageContentState extends State<_BookingPageContent> {
late EventController _eventController;
@override @override
void initState() { void initState() {
@ -38,7 +67,7 @@ class _BookingPageState extends State<BookingPage> {
super.dispose(); super.dispose();
} }
void _dispatchLoadEvents(BuildContext context) { void _loadEvents(BuildContext context) {
final selectedRoom = final selectedRoom =
context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace; context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
final dateState = context.read<DateSelectionBloc>().state; final dateState = context.read<DateSelectionBloc>().state;
@ -46,9 +75,11 @@ class _BookingPageState extends State<BookingPage> {
if (selectedRoom != null) { if (selectedRoom != null) {
context.read<CalendarEventsBloc>().add( context.read<CalendarEventsBloc>().add(
LoadEvents( LoadEvents(
spaceId: selectedRoom.uuid, LoadEventsParam(
weekStart: dateState.weekStart, startDate: dateState.weekStart,
weekEnd: dateState.weekStart.add(const Duration(days: 6)), endDate: dateState.weekStart.add(const Duration(days: 6)),
id: selectedRoom.uuid,
),
), ),
); );
} }
@ -56,182 +87,170 @@ class _BookingPageState extends State<BookingPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return BlocListener<SelectedBookableSpaceBloc, SelectedBookableSpaceState>(
providers: [ listener: (context, state) {
BlocProvider(create: (_) => SelectedBookableSpaceBloc()), if (state.selectedBookableSpace != null) {
BlocProvider(create: (_) => DateSelectionBloc()), // Reset events and clear cache when room changes
BlocProvider( context.read<CalendarEventsBloc>().add(ResetEvents());
create: (_) => CalendarEventsBloc( _loadEvents(context);
calendarService: }
FakeRemoteCalendarService(HTTPService(), useDummy: true), },
), child: BlocListener<DateSelectionBloc, DateSelectionState>(
), listener: (context, state) {
], _loadEvents(context);
child: Builder( },
builder: (context) => child: BlocListener<CalendarEventsBloc, CalendarEventState>(
BlocListener<CalendarEventsBloc, CalendarEventState>(
listenWhen: (prev, curr) => curr is EventsLoaded,
listener: (context, state) { listener: (context, state) {
if (state is EventsLoaded) { if (state is EventsLoaded) {
_eventController.removeWhere((_) => true); _eventController.removeWhere((_) => true);
_eventController.addAll(state.events); _eventController.addAll(state.events);
} }
}, },
child: BlocListener<SelectedBookableSpaceBloc, child: Row(
SelectedBookableSpaceState>( crossAxisAlignment: CrossAxisAlignment.start,
listener: (context, state) => _dispatchLoadEvents(context), children: [
child: BlocListener<DateSelectionBloc, DateSelectionState>( Expanded(
listener: (context, state) => _dispatchLoadEvents(context), child: Container(
child: Row( decoration: BoxDecoration(
crossAxisAlignment: CrossAxisAlignment.start, color: ColorsManager.whiteColors,
children: [ boxShadow: [
Expanded( BoxShadow(
child: Container( color: ColorsManager.blackColor.withOpacity(0.1),
decoration: BoxDecoration( offset: const Offset(3, 0),
color: ColorsManager.whiteColors, blurRadius: 6,
boxShadow: [ spreadRadius: 0,
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));
context.read<DateSelectionBloc>().add(
SelectDateFromSidebarCalendar(newDate));
},
);
},
),
),
],
),
),
), ),
Expanded( child: Column(
flex: 4, children: [
child: Padding( Expanded(
padding: const EdgeInsets.all(20.0), flex: 2,
child: Column( child: BlocBuilder<SelectedBookableSpaceBloc,
crossAxisAlignment: CrossAxisAlignment.start, 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));
context.read<DateSelectionBloc>().add(
SelectDateFromSidebarCalendar(newDate));
},
);
},
),
),
],
),
),
),
Expanded(
flex: 5,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( SvgTextButton(
children: [ svgAsset: Assets.homeIcon,
SvgTextButton( label: 'Manage Bookable Spaces',
svgAsset: Assets.homeIcon, onPressed: () {},
label: 'Manage Bookable Spaces',
onPressed: () {},
),
const SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',
onPressed: () {},
),
],
), ),
BlocBuilder<DateSelectionBloc, const SizedBox(width: 20),
DateSelectionState>( SvgTextButton(
builder: (context, state) { svgAsset: Assets.groupIcon,
final weekStart = state.weekStart; label: 'Manage Users',
final weekEnd = onPressed: () {},
weekStart.add(const Duration(days: 6));
return WeekNavigation(
weekStart: weekStart,
weekEnd: weekEnd,
onPreviousWeek: () {
context
.read<DateSelectionBloc>()
.add(PreviousWeek());
},
onNextWeek: () {
context
.read<DateSelectionBloc>()
.add(NextWeek());
},
);
},
), ),
], ],
), ),
Expanded( BlocBuilder<DateSelectionBloc, DateSelectionState>(
child: BlocBuilder<SelectedBookableSpaceBloc, builder: (context, state) {
SelectedBookableSpaceState>( final weekStart = state.weekStart;
builder: (context, roomState) { final weekEnd =
final selectedRoom = weekStart.add(const Duration(days: 6));
roomState.selectedBookableSpace; return WeekNavigation(
return BlocBuilder<DateSelectionBloc, weekStart: weekStart,
DateSelectionState>( weekEnd: weekEnd,
builder: (context, dateState) { onPreviousWeek: () {
return BlocListener<CalendarEventsBloc, context
CalendarEventState>( .read<DateSelectionBloc>()
listenWhen: (prev, curr) => .add(PreviousWeek());
curr is EventsLoaded, },
listener: (context, state) { onNextWeek: () {
if (state is EventsLoaded) { context
_eventController .read<DateSelectionBloc>()
.removeWhere((_) => true); .add(NextWeek());
_eventController.addAll(state.events); },
} );
}, },
child: WeeklyCalendarPage( ),
startTime: selectedRoom ],
?.bookableConfig.startTime, ),
endTime: selectedRoom const SizedBox(height: 20),
?.bookableConfig.endTime, Expanded(
weekStart: dateState.weekStart, flex: 5,
selectedDate: dateState.selectedDate, child: BlocBuilder<SelectedBookableSpaceBloc,
eventController: _eventController, SelectedBookableSpaceState>(
selectedDateFromSideBarCalender: context builder: (context, roomState) {
.watch<DateSelectionBloc>() final selectedRoom =
.state roomState.selectedBookableSpace;
.selectedDateFromSideBarCalender, return BlocBuilder<DateSelectionBloc,
), DateSelectionState>(
builder: (context, dateState) {
return BlocBuilder<CalendarEventsBloc,
CalendarEventState>(
builder: (context, eventState) {
return WeeklyCalendarPage(
key: ValueKey(
selectedRoom?.uuid ?? 'no-room'),
startTime: selectedRoom
?.bookableConfig.startTime,
endTime:
selectedRoom?.bookableConfig.endTime,
weekStart: dateState.weekStart,
selectedDate: dateState.selectedDate,
eventController: _eventController,
selectedDateFromSideBarCalender: context
.watch<DateSelectionBloc>()
.state
.selectedDateFromSideBarCalender,
// isLoading: eventState is EventsLoading,
); );
}, },
); );
}, },
), );
), },
], ),
), ),
), ],
), ),
], ),
), ),
), ],
), ),
), ),
), ),

View File

@ -72,29 +72,33 @@ class __SidebarContentState extends State<_SidebarContent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<SidebarBloc, SidebarState>( return BlocConsumer<SidebarBloc, SidebarState>(
listener: (context, state) { listener: (context, state) {},
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
searchController.clear();
}
},
builder: (context, state) { builder: (context, state) {
return Column( return Column(
children: [ children: [
const _SidebarHeader(title: 'Spaces'), Padding(
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
child: Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, 4),
blurRadius: 4,
spreadRadius: 0,
),
],
),
child: _SidebarHeader(title: 'Spaces')),
),
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: ColorsManager.whiteColors, color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1), color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, -2), offset: const Offset(0, 4),
blurRadius: 4,
spreadRadius: 0,
),
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, 2),
blurRadius: 4, blurRadius: 4,
spreadRadius: 0, spreadRadius: 0,
), ),
@ -147,6 +151,7 @@ class __SidebarContentState extends State<_SidebarContent> {
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
searchController.clear();
context.read<SidebarBloc>().add(ResetSearch()); context.read<SidebarBloc>().add(ResetSearch());
}, },
), ),
@ -223,7 +228,7 @@ class _SidebarHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(10.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@ -66,18 +66,28 @@ class _CustomCalendarPageState extends State<CustomCalendarPage> {
weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
); );
return CalendarDatePicker2( return Container(
config: config, decoration: const BoxDecoration(
value: [_selectedDate], border: Border(
onValueChanged: (dates) { top: BorderSide(
final picked = dates.first; color: ColorsManager.textGray,
if (picked != null) { width: 1.0,
setState(() { ),
_selectedDate = picked; ),
}); ),
widget.onDateChanged(picked.day, picked.month, picked.year); child: 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);
}
},
),
); );
} }
} }

View File

@ -1,16 +1,15 @@
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.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'; import 'package:syncrow_web/utils/color_manager.dart';
class EventTileWidget extends StatelessWidget { class EventTileWidget extends StatelessWidget {
final List<CalendarEventData<Object?>> events; final List<CalendarEventData<Object?>> events;
const EventTileWidget({ const EventTileWidget({
super.key, super.key,
required this.events, required this.events,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@ -18,39 +17,88 @@ class EventTileWidget extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: events.map((event) { 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()); event.endTime != null && event.endTime!.isBefore(DateTime.now());
final duration = event.endTime!.difference(event.startTime!);
final bool isLongEnough = duration.inMinutes >= 31;
return Expanded( return Expanded(
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isEventEnded color: isEventEnded
? ColorsManager.lightGrayBorderColor ? ColorsManager.grayColor.withOpacity(0.1)
: ColorsManager.blue1.withOpacity(0.25), : ColorsManager.blue1.withOpacity(0.1),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), border: Border(
child: Column( left: BorderSide(
crossAxisAlignment: CrossAxisAlignment.start, color: isEventEnded
children: [ ? ColorsManager.grayColor
Text( : ColorsManager.secondaryColor,
DateFormat('h:mm a').format(event.startTime!), width: 4,
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,
),
),
],
), ),
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(), }).toList(),

View File

@ -21,20 +21,24 @@ class RoomListItem extends StatelessWidget {
groupValue: isSelected ? room.uuid : null, groupValue: isSelected ? room.uuid : null,
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
onChanged: (value) => onTap(), onChanged: (value) => onTap(),
activeColor: ColorsManager.primaryColor, activeColor: ColorsManager.secondaryColor,
title: Text( title: Text(
room.spaceName, room.spaceName,
maxLines: 2,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor, color: ColorsManager.lightGrayColor,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
overflow: TextOverflow.ellipsis,
fontSize: 12), fontSize: 12),
), ),
subtitle: Text( subtitle: Text(
room.virtualLocation, room.virtualLocation,
maxLines: 2,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: ColorsManager.textGray, color: ColorsManager.textGray,
overflow: TextOverflow.ellipsis,
), ),
), ),
); );

View File

@ -14,26 +14,33 @@ class WeekDayHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return ColoredBox(
children: [ color: isSelectedDay
Text( ? ColorsManager.secondaryColor.withOpacity(0.1)
DateFormat('EEE').format(date).toUpperCase(), : Colors.transparent,
style: TextStyle( child: Column(
fontWeight: FontWeight.w400, children: [
fontSize: 14, const SizedBox(
color: isSelectedDay ? Colors.blue : Colors.black, height: 10,
), ),
), Text(
Text( DateFormat('EEE').format(date).toUpperCase(),
DateFormat('d').format(date), style: const TextStyle(
style: TextStyle( fontWeight: FontWeight.w400,
fontWeight: FontWeight.w700, fontSize: 14,
fontSize: 20, color: ColorsManager.blackColor,
color: ),
isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor,
), ),
), Text(
], DateFormat('d').format(date),
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 30,
color: ColorsManager.blackColor,
),
),
],
),
); );
} }
} }

View File

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

View File

@ -132,6 +132,8 @@ class _DynamicTableState extends State<DynamicTable> {
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _horizontalScrollController, controller: _horizontalScrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics:
widget.isEmpty ? const NeverScrollableScrollPhysics() : null,
child: SizedBox( child: SizedBox(
width: _totalTableWidth, width: _totalTableWidth,
child: Column( child: Column(
@ -164,7 +166,6 @@ class _DynamicTableState extends State<DynamicTable> {
], ],
), ),
), ),
Expanded( Expanded(
child: widget.isEmpty child: widget.isEmpty
? _buildEmptyState() ? _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,16 @@ class DeviceManagementBloc
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (spaceBloc.state.selectedCommunities.isEmpty) { if (spaceBloc.state.selectedCommunities.isEmpty) {
devices = await DevicesManagementApi().fetchDevices('', '', projectUuid); devices = await DevicesManagementApi().fetchDevices(
projectUuid,
);
} else { } else {
for (final community in spaceBloc.state.selectedCommunities) { for (var community in spaceBloc.state.selectedCommunities) {
final spacesList = final spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
for (final space in spacesList) { devices.addAll(await DevicesManagementApi().fetchDevices(projectUuid,
devices.addAll(await DevicesManagementApi() spacesId: spacesList,
.fetchDevices(community, space, projectUuid)); communities: spaceBloc.state.selectedCommunities));
}
} }
} }
@ -158,7 +159,8 @@ class DeviceManagementBloc
add(FilterDevices(_getFilterFromIndex(_selectedIndex))); add(FilterDevices(_getFilterFromIndex(_selectedIndex)));
} }
void _onSelectDevice(SelectDevice event, Emitter<DeviceManagementState> emit) { void _onSelectDevice(
SelectDevice event, Emitter<DeviceManagementState> emit) {
final selectedUuid = event.selectedDevice.uuid; final selectedUuid = event.selectedDevice.uuid;
if (_selectedDevices.any((device) => device.uuid == selectedUuid)) { if (_selectedDevices.any((device) => device.uuid == selectedUuid)) {
@ -254,7 +256,8 @@ class DeviceManagementBloc
_onlineCount = _devices.where((device) => device.online == true).length; _onlineCount = _devices.where((device) => device.online == true).length;
_offlineCount = _devices.where((device) => device.online == false).length; _offlineCount = _devices.where((device) => device.online == false).length;
_lowBatteryCount = _devices _lowBatteryCount = _devices
.where((device) => device.batteryLevel != null && device.batteryLevel! < 20) .where((device) =>
device.batteryLevel != null && device.batteryLevel! < 20)
.length; .length;
} }
@ -271,7 +274,8 @@ class DeviceManagementBloc
} }
} }
void _onSearchDevices(SearchDevices event, Emitter<DeviceManagementState> emit) { void _onSearchDevices(
SearchDevices event, Emitter<DeviceManagementState> emit) {
if ((event.community == null || event.community!.isEmpty) && if ((event.community == null || event.community!.isEmpty) &&
(event.unitName == null || event.unitName!.isEmpty) && (event.unitName == null || event.unitName!.isEmpty) &&
(event.deviceNameOrProductName == null || (event.deviceNameOrProductName == null ||
@ -435,8 +439,8 @@ class DeviceManagementBloc
final selectedDevices = loaded.selectedDevice?.map((device) { final selectedDevices = loaded.selectedDevice?.map((device) {
if (device.uuid == event.deviceId) { if (device.uuid == event.deviceId) {
return device.copyWith( return device.copyWith(
subspace: subspace: device.subspace
device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); ?.copyWith(subspaceName: event.newSubSpaceName));
} }
return device; return device;
}).toList(); }).toList();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; 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/water_heater/models/schedule_entry.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ScheduleDialogHelper { class ScheduleDialogHelper {
static const List<String> allDays = [ static const List<String> allDays = [
@ -56,8 +58,9 @@ class ScheduleDialogHelper {
Text( Text(
isEdit ? 'Edit Schedule' : 'Add Schedule', isEdit ? 'Edit Schedule' : 'Add Schedule',
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.blue, color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
fontSize: 30,
), ),
), ),
const SizedBox(), const SizedBox(),
@ -69,9 +72,9 @@ class ScheduleDialogHelper {
height: 40, height: 40,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200], backgroundColor: ColorsManager.boxColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(8),
), ),
), ),
onPressed: () async { onPressed: () async {
@ -110,39 +113,27 @@ class ScheduleDialogHelper {
], ],
), ),
actions: [ actions: [
SizedBox( ScheduleModeButtons(
width: 100, onSave: () {
child: OutlinedButton( dynamic temp;
onPressed: () { if (deviceType == 'CUR_2') {
Navigator.pop(ctx, null); temp = functionOn! ? 'open' : 'close';
}, } else {
child: const Text('Cancel'), 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? jobTitle; // can be empty
final String roleType; // e.g. "ADMIN" final String roleType; // e.g. "ADMIN"
final List<UserSpaceModel> spaces; final List<UserSpaceModel> spaces;
final String? companyName;
EditUserModel({ EditUserModel({
required this.uuid, required this.uuid,
@ -167,6 +168,7 @@ class EditUserModel {
required this.jobTitle, required this.jobTitle,
required this.roleType, required this.roleType,
required this.spaces, required this.spaces,
this.companyName,
}); });
/// Create a [UserData] from JSON data /// Create a [UserData] from JSON data
@ -182,6 +184,7 @@ class EditUserModel {
invitedBy: json['invitedBy'] as String, invitedBy: json['invitedBy'] as String,
phoneNumber: json['phoneNumber'] ?? '', phoneNumber: json['phoneNumber'] ?? '',
jobTitle: json['jobTitle'] ?? '', jobTitle: json['jobTitle'] ?? '',
companyName: json['companyName'] as String?,
roleType: json['roleType'] as String, roleType: json['roleType'] as String,
spaces: (json['spaces'] as List<dynamic>) spaces: (json['spaces'] as List<dynamic>)
.map((e) => UserSpaceModel.fromJson(e as Map<String, dynamic>)) .map((e) => UserSpaceModel.fromJson(e as Map<String, dynamic>))

View File

@ -12,7 +12,7 @@ class RolesUserModel {
final dynamic jobTitle; final dynamic jobTitle;
final dynamic createdDate; final dynamic createdDate;
final dynamic createdTime; final dynamic createdTime;
final String? companyName;
RolesUserModel({ RolesUserModel({
required this.uuid, required this.uuid,
required this.createdAt, required this.createdAt,
@ -27,6 +27,7 @@ class RolesUserModel {
this.jobTitle, this.jobTitle,
required this.createdDate, required this.createdDate,
required this.createdTime, required this.createdTime,
this.companyName,
}); });
factory RolesUserModel.fromJson(Map<String, dynamic> json) { factory RolesUserModel.fromJson(Map<String, dynamic> json) {
@ -47,6 +48,7 @@ class RolesUserModel {
: json['jobTitle'], : json['jobTitle'],
createdDate: json['createdDate'], createdDate: json['createdDate'],
createdTime: json['createdTime'], 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 lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController(); final TextEditingController phoneController = TextEditingController();
final TextEditingController jobTitleController = TextEditingController(); final TextEditingController companyNameController = TextEditingController();
final TextEditingController roleSearchController = TextEditingController(); final TextEditingController roleSearchController = TextEditingController();
bool? isCompleteBasics; bool? isCompleteBasics;
@ -352,7 +352,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().sendInviteUser( bool res = await UserPermissionApi().sendInviteUser(
email: emailController.text, email: emailController.text,
firstName: firstNameController.text, firstName: firstNameController.text,
jobTitle: jobTitleController.text, companyName: companyNameController.text,
lastName: lastNameController.text, lastName: lastNameController.text,
phoneNumber: phoneController.text, phoneNumber: phoneController.text,
roleUuid: roleSelected, roleUuid: roleSelected,
@ -405,7 +405,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().editInviteUser( bool res = await UserPermissionApi().editInviteUser(
userId: event.userId, userId: event.userId,
firstName: firstNameController.text, firstName: firstNameController.text,
jobTitle: jobTitleController.text, companyName: companyNameController.text,
lastName: lastNameController.text, lastName: lastNameController.text,
phoneNumber: phoneController.text, phoneNumber: phoneController.text,
roleUuid: roleSelected, roleUuid: roleSelected,
@ -455,7 +455,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
Future<void> checkEmail( Future<void> checkEmail(
CheckEmailEvent event, Emitter<UsersState> emit) async { CheckEmailEvent event, Emitter<UsersState> emit) async {
emit(UsersLoadingState()); emit(UsersLoadingState());
String? res = await UserPermissionApi().checkEmail( String? res = await UserPermissionApi().checkEmail(
emailController.text, emailController.text,
); );
checkEmailValid = res!; checkEmailValid = res!;
@ -529,7 +529,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.text = res.lastName; lastNameController.text = res.lastName;
emailController.text = res.email; emailController.text = res.email;
phoneController.text = res.phoneNumber ?? ''; phoneController.text = res.phoneNumber ?? '';
jobTitleController.text = res.jobTitle ?? ''; companyNameController.text = res.companyName ?? '';
res.roleType; res.roleType;
res.spaces.map((space) { res.spaces.map((space) {
selectedIds.add(space.uuid); selectedIds.add(space.uuid);
@ -645,7 +645,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.dispose(); lastNameController.dispose();
emailController.dispose(); emailController.dispose();
phoneController.dispose(); phoneController.dispose();
jobTitleController.dispose(); companyNameController.dispose();
roleSearchController.dispose(); roleSearchController.dispose();
return super.close(); return super.close();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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