Compare commits

..

24 Commits

Author SHA1 Message Date
d65f9ceea9 Enhance AddDeviceTypeWidget to support initial product counts and update selection logic. Modify AssignTagsDialog to pass initial products from space allocations, improving user experience and maintainability. 2025-07-15 14:53:04 +03:00
f539b0ac8d Rename UniqueSubspacesDecorator to UniqueSpaceDetailsSpacesDecoratorService 2025-07-15 14:38:44 +03:00
5a3cf93748 Improved UniqueSubspacesDecorator implementation to improve handling of duplicate subspace names. 2025-07-15 14:37:16 +03:00
e740652507 Refactor PlusButtonWidget and SpaceCardWidget to improve widget structure and interaction handling. Replace GestureDetector with IconButton for better usability and update positioning logic for the PlusButtonWidget, enhancing maintainability and readability. 2025-07-15 13:11:22 +03:00
c60078c96a Refactor CommunityStructureCanvas to improve widget structure by rearranging the InteractiveViewer and GestureDetector hierarchy. This change enhances readability and maintainability while ensuring proper interaction handling. 2025-07-15 13:04:37 +03:00
903c5dd29b Refactor SpacesRecursiveHelper to improve variable naming and enhance readability. Update mapping logic to clarify the distinction between updated and non-null spaces, ensuring better maintainability of the recursive space handling. 2025-07-15 12:55:49 +03:00
df39fca050 Refactor CommunityStructureHeaderActionButtons to simplify null handling for selectedSpace and improve widget structure. Ensure buttons are always displayed when selectedSpace is not null, enhancing readability and maintainability. 2025-07-15 12:49:37 +03:00
f832c5d884 Refactor SpaceManagementCommunityStructure to improve widget structure and visibility handling. Introduce separate methods for building the canvas and empty state, enhancing readability and maintainability. 2025-07-15 12:30:24 +03:00
fa930571dc Ensure proper handling of null selectedSpace in CommunityStructureCanvas during widget updates to prevent unnecessary processing. 2025-07-15 12:27:45 +03:00
acefe7b355 Refactor RemoteDeleteSpaceService to use a private HTTPService instance and update URL construction with ApiEndpoints for improved maintainability. Update DeleteSpaceDialog to reflect changes in service initialization. 2025-07-15 11:09:04 +03:00
b223194950 Add loading and status widgets for delete space dialog; refactor dialog to utilize new components for improved user feedback during space deletion process. 2025-07-15 10:07:12 +03:00
466f5b89c7 Enhanced SpacesConnectionsArrowPainter and CommunityStructureCanvas to support dynamic card widths; enhance SpaceCell widget layout and shadow properties for improved UI consistency. 2025-07-14 16:56:51 +03:00
de5d8df01c Update SpaceCell widget shadow properties for improved visual appearance 2025-07-14 16:19:57 +03:00
5218641705 Refactor didUpdateWidget in CommunityStructureCanvas to ensure proper widget lifecycle management 2025-07-14 16:05:44 +03:00
ab6a6851f2 Update control points in SpacesConnectionsArrowPainter for smoother arrow rendering 2025-07-14 15:23:01 +03:00
beb33e37fa Add SpacesRecursiveHelper for recursive space updates and deletions; refactor CommunityStructureHeader to use CommunityStructureHeaderActionButtonsComposer for improved action handling. 2025-07-14 14:59:58 +03:00
3bee17c574 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1721-FE-Implement-Delete-Space-Feature 2025-07-14 14:44:55 +03:00
f4b5c6fb52 Implement delete space functionality in CommunityStructureHeader: Integrate DeleteSpaceDialog for space deletion confirmation and update routing for space management page. 2025-07-14 14:44:08 +03:00
086f3cedf8 Refactor RemoteDeleteSpaceService: Simplify success response handling and improve error message formatting 2025-07-14 14:30:17 +03:00
035c03c6b2 Fix error handling in DeleteSpaceBloc: update failure message to include exception details 2025-07-14 14:17:01 +03:00
cf1b34ee0a Add x_delete icon asset. 2025-07-14 12:17:07 +03:00
5663e2084e Created DeleteSpaceBloc. 2025-07-14 10:57:34 +03:00
3cd0125310 Refactor space deletion: Introduce DeleteSpaceParam and DeleteSpaceService for enhanced space management functionality 2025-07-14 10:54:13 +03:00
e0980b324c Add DeleteSpaceParam and DeleteSpaceService for space deletion functionality 2025-07-14 10:54:07 +03:00
48 changed files with 862 additions and 314 deletions

View File

@ -0,0 +1,5 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 34.9999C27.1649 34.9999 34.9999 27.1649 34.9999 17.4999C34.9999 7.83499 27.1649 0 17.5 0C7.83499 0 0 7.83499 0 17.5C0 27.1651 7.83499 34.9999 17.5 34.9999Z" fill="#FF6465"/>
<path opacity="0.1" d="M4.70804 17.5C4.70804 8.63343 11.3024 1.30805 19.854 0.158115C19.0839 0.0545507 18.2984 0 17.5 0C7.835 0 0 7.835 0 17.5C0 27.1651 7.83499 35 17.4999 35C18.2983 35 19.0839 34.9455 19.8539 34.8419C11.3024 33.6919 4.70804 26.3665 4.70804 17.5Z" fill="black"/>
<path d="M21.4229 17.5003L26.0301 12.8931C26.365 12.5582 26.365 12.0152 26.0301 11.6804L23.3197 8.96992C22.9848 8.63503 22.4418 8.63503 22.107 8.96992L17.4997 13.5772L12.8924 8.96992C12.5576 8.63503 12.0146 8.63503 11.6798 8.96992L8.96931 11.6804C8.63442 12.0153 8.63442 12.5582 8.96931 12.8931L13.5766 17.5003L8.96931 22.1076C8.63442 22.4425 8.63442 22.9855 8.96931 23.3204L11.6798 26.0308C12.0146 26.3657 12.5576 26.3657 12.8924 26.0308L17.4997 21.4235L22.1071 26.0308C22.442 26.3657 22.9849 26.3657 23.3198 26.0308L26.0302 23.3204C26.3651 22.9855 26.3651 22.4425 26.0302 22.1076L21.4229 17.5003Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -132,8 +132,6 @@ 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(
@ -166,6 +164,7 @@ class _DynamicTableState extends State<DynamicTable> {
], ],
), ),
), ),
Expanded( Expanded(
child: widget.isEmpty child: widget.isEmpty
? _buildEmptyState() ? _buildEmptyState()
@ -266,7 +265,7 @@ class _DynamicTableState extends State<DynamicTable> {
), ),
], ],
), ),
SizedBox(height: widget.size.height * 0.2), SizedBox(height: widget.size.height * 0.5),
], ],
), ),
); );

View File

@ -46,15 +46,15 @@ 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( devices = await DevicesManagementApi().fetchDevices('', '', projectUuid);
projectUuid,
);
} else { } else {
for (var community in spaceBloc.state.selectedCommunities) { for (final community in spaceBloc.state.selectedCommunities) {
final spacesList = final spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
devices.addAll(await DevicesManagementApi() for (final space in spacesList) {
.fetchDevices(projectUuid, spacesId: spacesList)); devices.addAll(await DevicesManagementApi()
.fetchDevices(community, space, projectUuid));
}
} }
} }

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(
'Workflow Automation', 'Routines',
style: context.textTheme.titleMedium?.copyWith( style: context.textTheme.titleMedium?.copyWith(
color: state.routineTab color: state.routineTab
? ColorsManager.whiteColors ? ColorsManager.whiteColors

View File

@ -29,9 +29,7 @@ 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),
@ -41,8 +39,6 @@ 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(
@ -53,12 +49,10 @@ class CountdownModeButtons extends StatelessWidget {
), ),
); );
}, },
backgroundColor: ColorsManager.red100, backgroundColor: Colors.red,
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(
@ -69,7 +63,7 @@ class CountdownModeButtons extends StatelessWidget {
countDownCode: countDownCode), countDownCode: countDownCode),
); );
}, },
backgroundColor: ColorsManager.primaryColorWithOpacity, backgroundColor: ColorsManager.primaryColor,
child: const Text('Save'), child: const Text('Save'),
), ),
), ),

View File

@ -226,7 +226,6 @@ 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,
), ),
), ),
@ -241,8 +240,7 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
label, label,
style: const TextStyle( style: const TextStyle(
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
fontSize: 24, fontSize: 18,
fontWeight: FontWeight.w400,
), ),
), ),
], ],

View File

@ -31,11 +31,12 @@ class BuildScheduleView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (_) => ScheduleBloc(deviceId: deviceUuid,) create: (_) => ScheduleBloc(
deviceId: deviceUuid,
)
..add(ScheduleGetEvent(category: category)) ..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent( ..add(ScheduleFetchStatusEvent(
deviceId: deviceUuid, deviceId: deviceUuid, countdownCode: countdownCode ?? '')),
countdownCode: countdownCode ?? '')),
child: Dialog( child: Dialog(
backgroundColor: Colors.white, backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20), insetPadding: const EdgeInsets.all(20),
@ -76,8 +77,7 @@ class BuildScheduleView extends StatelessWidget {
category: category, category: category,
time: '', time: '',
function: Status( function: Status(
code: code.toString(), code: code.toString(), value: null),
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(
color: ColorsManager.primaryColorWithOpacity, fontWeight: FontWeight.bold,
fontWeight: FontWeight.w700, fontSize: 22,
fontSize: 30, color: ColorsManager.dialogBlueTitle,
), ),
), ),
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.grayColor.withOpacity(0.5), borderColor: ColorsManager.boxColor,
padding: 2, padding: 2,
backgroundColor: ColorsManager.graysColor, backgroundColor: ColorsManager.graysColor,
borderRadius: 15, borderRadius: 15,

View File

@ -19,8 +19,6 @@ 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);
@ -35,11 +33,9 @@ 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.primaryColorWithOpacity, backgroundColor: ColorsManager.primaryColor,
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,7 +65,6 @@ 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,8 +1,6 @@
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 = [
@ -58,9 +56,8 @@ 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: ColorsManager.primaryColorWithOpacity, color: Colors.blue,
fontWeight: FontWeight.w700, fontWeight: FontWeight.bold,
fontSize: 30,
), ),
), ),
const SizedBox(), const SizedBox(),
@ -72,9 +69,9 @@ class ScheduleDialogHelper {
height: 40, height: 40,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: ColorsManager.boxColor, backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(15),
), ),
), ),
onPressed: () async { onPressed: () async {
@ -113,27 +110,39 @@ class ScheduleDialogHelper {
], ],
), ),
actions: [ actions: [
ScheduleModeButtons( SizedBox(
onSave: () { width: 100,
dynamic temp; child: OutlinedButton(
if (deviceType == 'CUR_2') { onPressed: () {
temp = functionOn! ? 'open' : 'close'; Navigator.pop(ctx, null);
} else { },
temp = functionOn; child: const Text('Cancel'),
} ),
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,7 +153,6 @@ 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,
@ -168,7 +167,6 @@ 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
@ -184,7 +182,6 @@ 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,7 +27,6 @@ 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) {
@ -48,7 +47,6 @@ 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 companyNameController = TextEditingController(); final TextEditingController jobTitleController = 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,
companyName: companyNameController.text, jobTitle: jobTitleController.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,
companyName: companyNameController.text, jobTitle: jobTitleController.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 ?? '';
companyNameController.text = res.companyName ?? ''; jobTitleController.text = res.jobTitle ?? '';
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();
companyNameController.dispose(); jobTitleController.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(
'Company Name', 'Job Title',
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.companyNameController, controller: _blocRole.jobTitleController,
style: style:
const TextStyle(color: ColorsManager.blackColor), const TextStyle(color: ColorsManager.blackColor),
decoration: inputTextFormDeco( decoration: inputTextFormDeco(
hintText: 'Company Name (Optional)') hintText: "Job Title (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",
"Company Name", "Job Title",
"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),
Center(child: Text(user.companyName ?? '-')), Text(user.jobTitle),
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 {
emit(state.copyWith( scenes.addAll(await SceneApi.getScenes(
scenes: scenes, createRoutineBloc.selectedSpaceId,
isLoading: false, createRoutineBloc.selectedCommunityId,
)); projectUuid));
} catch (e) {
emit(state.copyWith(
isLoading: false,
loadScenesErrorMessage: 'Failed to load scenes',
errorMessage: '',
loadAutomationErrorMessage: '',
scenes: scenes));
} }
emit(state.copyWith(
scenes: scenes,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
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,15 +936,16 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
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(projectUuid, spacesId: spacesList)); .fetchDevices(communityId, spaceId, projectUuid));
}
} }
} else { } else {
devices.addAll(await DevicesManagementApi().fetchDevices( devices.addAll(await DevicesManagementApi().fetchDevices(
projectUuid, createRoutineBloc.selectedCommunityId,
spacesId: [createRoutineBloc.selectedSpaceId], createRoutineBloc.selectedSpaceId,
)); projectUuid));
} }
emit(state.copyWith(isLoading: false, devices: devices)); emit(state.copyWith(isLoading: false, devices: devices));

View File

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

View File

@ -0,0 +1,43 @@
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/domain/models/space_details_model.dart';
abstract final class SpacesRecursiveHelper {
static List<SpaceModel> recusrivelyUpdate(
List<SpaceModel> spaces,
SpaceDetailsModel updatedSpace,
) {
return spaces.map((space) {
final isUpdatedSpace = space.uuid == updatedSpace.uuid;
if (isUpdatedSpace) {
return space.copyWith(
spaceName: updatedSpace.spaceName,
icon: updatedSpace.icon,
);
}
final hasChildren = space.children.isNotEmpty;
if (hasChildren) {
return space.copyWith(
children: recusrivelyUpdate(space.children, updatedSpace),
);
}
return space;
}).toList();
}
static List<SpaceModel> recusrivelyDelete(
List<SpaceModel> spaces,
String spaceUuid,
) {
final updatedSpaces = spaces.map((space) {
if (space.uuid == spaceUuid) return null;
if (space.children.isNotEmpty) {
return space.copyWith(
children: recusrivelyDelete(space.children, spaceUuid),
);
}
return space;
}).toList();
final nonNullSpaces = updatedSpaces.whereType<SpaceModel>().toList();
return nonNullSpaces;
}
}

View File

@ -5,13 +5,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
class SpacesConnectionsArrowPainter extends CustomPainter { class SpacesConnectionsArrowPainter extends CustomPainter {
final List<SpaceConnectionModel> connections; final List<SpaceConnectionModel> connections;
final Map<String, Offset> positions; final Map<String, Offset> positions;
final double cardWidth = 150.0; final Map<String, double> cardWidths;
final double cardHeight = 90.0; final double cardHeight = 90.0;
final Set<String> highlightedUuids; final Set<String> highlightedUuids;
SpacesConnectionsArrowPainter({ SpacesConnectionsArrowPainter({
required this.connections, required this.connections,
required this.positions, required this.positions,
required this.cardWidths,
required this.highlightedUuids, required this.highlightedUuids,
}); });
@ -29,19 +30,30 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
final from = positions[connection.from]; final from = positions[connection.from];
final to = positions[connection.to]; final to = positions[connection.to];
final fromWidth = cardWidths[connection.from] ?? 150.0;
final toWidth = cardWidths[connection.to] ?? 150.0;
if (from != null && to != null) { if (from != null && to != null) {
final startPoint = final startPoint =
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10); Offset(from.dx + fromWidth / 2, from.dy + cardHeight - 10);
final endPoint = Offset(to.dx + cardWidth / 2, to.dy); final endPoint = Offset(to.dx + toWidth / 2, to.dy);
final path = Path()..moveTo(startPoint.dx, startPoint.dy); final path = Path()..moveTo(startPoint.dx, startPoint.dy);
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20); if ((startPoint.dx - endPoint.dx).abs() < 1.0) {
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); path.lineTo(endPoint.dx, endPoint.dy);
} else {
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 100);
controlPoint2.dy, endPoint.dx, endPoint.dy); final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 100);
path.cubicTo(
controlPoint1.dx,
controlPoint1.dy,
controlPoint2.dx,
controlPoint2.dy,
endPoint.dx,
endPoint.dy,
);
}
canvas.drawPath(path, paint); canvas.drawPath(path, paint);
@ -51,7 +63,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
: ColorsManager.blackColor.withValues(alpha: 0.5) : ColorsManager.blackColor.withValues(alpha: 0.5)
..style = PaintingStyle.fill ..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn; ..blendMode = BlendMode.srcIn;
canvas.drawCircle(endPoint, 4, circlePaint); canvas.drawCircle(endPoint, 6, circlePaint);
} }
} }
} }

View File

@ -10,7 +10,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
@ -49,7 +49,7 @@ class _SpaceManagementPageState extends State<SpaceManagementPage> {
), ),
BlocProvider( BlocProvider(
create: (context) => SpaceDetailsBloc( create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator( UniqueSpaceDetailsSpacesDecoratorService(
RemoteSpaceDetailsService(httpService: HTTPService()), RemoteSpaceDetailsService(httpService: HTTPService()),
), ),
), ),

View File

@ -30,10 +30,11 @@ class CommunityStructureCanvas extends StatefulWidget {
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas> class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
final Map<String, Offset> _positions = {}; final Map<String, Offset> _positions = {};
final double _cardWidth = 150.0; final Map<String, double> _cardWidths = {};
final double _cardHeight = 90.0; final double _cardHeight = 90.0;
final double _horizontalSpacing = 150.0; final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0; final double _verticalSpacing = 120.0;
static const double _minCardWidth = 150.0;
late final TransformationController _transformationController; late final TransformationController _transformationController;
late final AnimationController _animationController; late final AnimationController _animationController;
@ -52,6 +53,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
@override @override
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) { void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.selectedSpace == null) return;
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { if (mounted) {
@ -68,6 +70,34 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
super.dispose(); super.dispose();
} }
double _calculateCardWidth(String text) {
final style = context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
);
final textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: 1,
textDirection: TextDirection.ltr,
)..layout();
const iconWidth = 40.0;
const horizontalPadding = 10.0;
const contentPadding = 10.0;
final calculatedWidth =
iconWidth + horizontalPadding + textPainter.width + contentPadding;
return calculatedWidth.clamp(_minCardWidth, double.infinity);
}
void _calculateAllCardWidths(List<SpaceModel> spaces) {
for (final space in spaces) {
_cardWidths[space.uuid] = _calculateCardWidth(space.spaceName);
if (space.children.isNotEmpty) {
_calculateAllCardWidths(space.children);
}
}
}
Set<String> _getAllDescendantUuids(SpaceModel space) { Set<String> _getAllDescendantUuids(SpaceModel space) {
final uuids = <String>{}; final uuids = <String>{};
for (final child in space.children) { for (final child in space.children) {
@ -102,11 +132,12 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final position = _positions[space.uuid]; final position = _positions[space.uuid];
if (position == null) return; if (position == null) return;
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
const scale = 1; const scale = 1;
final viewSize = context.size; final viewSize = context.size;
if (viewSize == null) return; if (viewSize == null) return;
final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2); final x = -position.dx * scale + (viewSize.width / 2) - (cardWidth * scale / 2);
final y = final y =
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2); -position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
@ -155,13 +186,16 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
Map<int, double> levelXOffset, Map<int, double> levelXOffset,
) { ) {
for (final space in spaces) { for (final space in spaces) {
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
double childSubtreeWidth = 0; double childSubtreeWidth = 0;
if (space.children.isNotEmpty) { if (space.children.isNotEmpty) {
_calculateLayout(space.children, depth + 1, levelXOffset); _calculateLayout(space.children, depth + 1, levelXOffset);
final firstChildPos = _positions[space.children.first.uuid]; final firstChildPos = _positions[space.children.first.uuid];
final lastChildPos = _positions[space.children.last.uuid]; final lastChildPos = _positions[space.children.last.uuid];
if (firstChildPos != null && lastChildPos != null) { if (firstChildPos != null && lastChildPos != null) {
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx; final lastChildWidth =
_cardWidths[space.children.last.uuid] ?? _minCardWidth;
childSubtreeWidth = (lastChildPos.dx + lastChildWidth) - firstChildPos.dx;
} }
} }
@ -170,7 +204,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
if (space.children.isNotEmpty) { if (space.children.isNotEmpty) {
final firstChildPos = _positions[space.children.first.uuid]!; final firstChildPos = _positions[space.children.first.uuid]!;
x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2; x = firstChildPos.dx + (childSubtreeWidth - cardWidth) / 2;
} else { } else {
x = currentX; x = currentX;
} }
@ -187,7 +221,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final y = depth * (_verticalSpacing + _cardHeight); final y = depth * (_verticalSpacing + _cardHeight);
_positions[space.uuid] = Offset(x, y); _positions[space.uuid] = Offset(x, y);
levelXOffset[depth] = x + _cardWidth + _horizontalSpacing; levelXOffset[depth] = x + cardWidth + _horizontalSpacing;
} }
} }
@ -202,8 +236,11 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
List<Widget> _buildTreeWidgets() { List<Widget> _buildTreeWidgets() {
_positions.clear(); _positions.clear();
_cardWidths.clear();
final community = widget.community; final community = widget.community;
_calculateAllCardWidths(community.spaces);
final levelXOffset = <int, double>{}; final levelXOffset = <int, double>{};
_calculateLayout(community.spaces, 0, levelXOffset); _calculateLayout(community.spaces, 0, levelXOffset);
@ -240,6 +277,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
painter: SpacesConnectionsArrowPainter( painter: SpacesConnectionsArrowPainter(
connections: connections, connections: connections,
positions: _positions, positions: _positions,
cardWidths: _cardWidths,
highlightedUuids: highlightedUuids, highlightedUuids: highlightedUuids,
), ),
child: Stack(alignment: AlignmentDirectional.center, children: widgets), child: Stack(alignment: AlignmentDirectional.center, children: widgets),
@ -271,6 +309,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
continue; continue;
} }
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
final isHighlighted = highlightedUuids.contains(space.uuid); final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null; final hasNoSelectedSpace = widget.selectedSpace == null;
@ -278,14 +317,10 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
buildSpaceContainer: () { buildSpaceContainer: () {
return Opacity( return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip( child: SpaceCell(
message: space.spaceName, onTap: () => _onSpaceTapped(space),
preferBelow: false, icon: space.icon,
child: SpaceCell( name: space.spaceName,
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
), ),
); );
}, },
@ -305,7 +340,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
Positioned( Positioned(
left: position.dx, left: position.dx,
top: position.dy, top: position.dy,
width: _cardWidth, width: cardWidth,
height: _cardHeight, height: _cardHeight,
child: Draggable<SpaceReorderDataModel>( child: Draggable<SpaceReorderDataModel>(
data: reorderData, data: reorderData,
@ -314,7 +349,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: Opacity( child: Opacity(
opacity: 0.2, opacity: 0.2,
child: SizedBox( child: SizedBox(
width: _cardWidth, width: cardWidth,
height: _cardHeight, height: _cardHeight,
child: spaceCard, child: spaceCard,
), ),
@ -330,7 +365,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
); );
final targetPos = Offset( final targetPos = Offset(
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20, position.dx + cardWidth + (_horizontalSpacing / 4) - 20,
position.dy, position.dy,
); );
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
@ -418,17 +453,17 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets(); final treeWidgets = _buildTreeWidgets();
return InteractiveViewer( return GestureDetector(
transformationController: _transformationController, onTap: _resetSelectionAndZoom,
boundaryMargin: EdgeInsets.symmetric( child: InteractiveViewer(
horizontal: context.screenWidth * 0.3, transformationController: _transformationController,
vertical: context.screenHeight * 0.3, boundaryMargin: EdgeInsets.symmetric(
), horizontal: context.screenWidth * 0.3,
minScale: 0.5, vertical: context.screenHeight * 0.3,
maxScale: 3.0, ),
constrained: false, minScale: 0.5,
child: GestureDetector( maxScale: 3.0,
onTap: _resetSelectionAndZoom, constrained: false,
child: SizedBox( child: SizedBox(
width: context.screenWidth * 5, width: context.screenWidth * 5,
height: context.screenHeight * 5, height: context.screenHeight * 5,

View File

@ -2,41 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.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/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/main_module/widgets/community_structure_header_action_buttons_composer.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/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/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureHeader extends StatelessWidget { class CommunityStructureHeader extends StatelessWidget {
const CommunityStructureHeader({super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
@ -57,7 +33,7 @@ class CommunityStructureHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: _buildCommunityInfo(context, theme, screenWidth), child: _buildCommunityInfo(context, screenWidth),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
], ],
@ -67,8 +43,7 @@ class CommunityStructureHeader extends StatelessWidget {
); );
} }
Widget _buildCommunityInfo( Widget _buildCommunityInfo(BuildContext context, double screenWidth) {
BuildContext context, ThemeData theme, double screenWidth) {
final selectedCommunity = final selectedCommunity =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity; context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
final selectedSpace = final selectedSpace =
@ -78,7 +53,7 @@ class CommunityStructureHeader extends StatelessWidget {
children: [ children: [
Text( Text(
'Community Structure', 'Community Structure',
style: theme.textTheme.headlineLarge?.copyWith( style: context.textTheme.headlineLarge?.copyWith(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
), ),
), ),
@ -91,7 +66,7 @@ class CommunityStructureHeader extends StatelessWidget {
Flexible( Flexible(
child: SelectableText( child: SelectableText(
selectedCommunity.name, selectedCommunity.name,
style: theme.textTheme.bodyLarge?.copyWith( style: context.textTheme.bodyLarge?.copyWith(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
), ),
maxLines: 1, maxLines: 1,
@ -115,27 +90,8 @@ class CommunityStructureHeader extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
CommunityStructureHeaderActionButtons( CommunityStructureHeaderActionButtonsComposer(
onDelete: (space) {}, selectedCommunity: selectedCommunity,
onDuplicate: (space) {},
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, selectedSpace: selectedSpace,
), ),
], ],

View File

@ -19,27 +19,27 @@ class CommunityStructureHeaderActionButtons extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (selectedSpace == null) return const SizedBox.shrink();
return Wrap( return Wrap(
alignment: WrapAlignment.end, alignment: WrapAlignment.end,
spacing: 10, spacing: 10,
children: [ children: [
if (selectedSpace != null) ...[ CommunityStructureHeaderButton(
CommunityStructureHeaderButton( label: 'Edit',
label: 'Edit', svgAsset: Assets.editSpace,
svgAsset: Assets.editSpace, onPressed: () => onEdit(selectedSpace!),
onPressed: () => onEdit(selectedSpace!), ),
), CommunityStructureHeaderButton(
CommunityStructureHeaderButton( label: 'Duplicate',
label: 'Duplicate', svgAsset: Assets.duplicate,
svgAsset: Assets.duplicate, onPressed: () => onDuplicate(selectedSpace!),
onPressed: () => onDuplicate(selectedSpace!), ),
), CommunityStructureHeaderButton(
CommunityStructureHeaderButton( label: 'Delete',
label: 'Delete', svgAsset: Assets.spaceDelete,
svgAsset: Assets.spaceDelete, onPressed: () => onDelete(selectedSpace!),
onPressed: () => onDelete(selectedSpace!), ),
),
],
], ],
); );
} }

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_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/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/delete_space/presentation/widgets/delete_space_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
const CommunityStructureHeaderActionButtonsComposer({
required this.selectedCommunity,
required this.selectedSpace,
super.key,
});
final CommunityModel selectedCommunity;
final SpaceModel? selectedSpace;
@override
Widget build(BuildContext context) {
return CommunityStructureHeaderActionButtons(
onDelete: (space) => showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => DeleteSpaceDialog(
space: space,
community: selectedCommunity,
onSuccess: () {
final updatedSpaces = SpacesRecursiveHelper.recusrivelyDelete(
selectedCommunity.spaces,
space.uuid,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: selectedCommunity),
);
},
),
),
onDuplicate: (space) {},
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
communityUuid: selectedCommunity.uuid,
onSuccess: (updatedSpaceDetails) {
final communitiesBloc = context.read<CommunitiesBloc>();
final updatedSpaces = SpacesRecursiveHelper.recusrivelyUpdate(
selectedCommunity.spaces,
updatedSpaceDetails,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
communitiesBloc.add(CommunitiesUpdateCommunity(community));
},
),
selectedSpace: selectedSpace,
);
}
}

View File

@ -2,31 +2,22 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class PlusButtonWidget extends StatelessWidget { class PlusButtonWidget extends StatelessWidget {
final Offset offset; final void Function() onTap;
final void Function() onButtonTap;
const PlusButtonWidget({ const PlusButtonWidget({
required this.onTap,
super.key, super.key,
required this.offset,
required this.onButtonTap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return IconButton.filled(
onTap: onButtonTap, onPressed: onTap,
child: Container( style: IconButton.styleFrom(backgroundColor: ColorsManager.spaceColor),
width: 30, icon: const Icon(
height: 30, Icons.add,
decoration: const BoxDecoration( color: ColorsManager.whiteColors,
color: ColorsManager.spaceColor, size: 20,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
),
), ),
); );
} }

View File

@ -29,10 +29,9 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
widget.buildSpaceContainer(), widget.buildSpaceContainer(),
if (isHovered) if (isHovered)
Positioned( Positioned(
bottom: 0, bottom: -5,
child: PlusButtonWidget( child: PlusButtonWidget(
offset: Offset.zero, onTap: widget.onTap,
onButtonTap: widget.onTap,
), ),
), ),
], ],

View File

@ -20,21 +20,19 @@ class SpaceCell extends StatelessWidget {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
width: 150, padding: const EdgeInsetsDirectional.only(end: 10),
height: 70, height: 70,
decoration: _containerDecoration(), decoration: _containerDecoration(),
child: Row( child: Row(
spacing: 10,
mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildIconContainer(), _buildIconContainer(),
const SizedBox(width: 10), Text(
Expanded( name,
child: Text( style: context.textTheme.bodyLarge?.copyWith(
name, fontWeight: FontWeight.bold,
style: context.textTheme.bodyLarge?.copyWith( color: ColorsManager.blackColor,
fontWeight: FontWeight.bold,
color: ColorsManager.blackColor,
),
overflow: TextOverflow.ellipsis,
), ),
), ),
], ],

View File

@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/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/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
class SpaceManagementCommunityStructure extends StatelessWidget { class SpaceManagementCommunityStructure extends StatelessWidget {
@ -13,28 +15,44 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state; final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity; final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace; final selectedSpace = selectionBloc.selectedSpace;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Visibility(
visible: selectedCommunity!.spaces.isNotEmpty,
replacement: _buildEmptyWidget(selectedCommunity),
child: _buildCanvas(selectedCommunity, selectedSpace),
),
],
);
}
Widget _buildCanvas(
CommunityModel selectedCommunity,
SpaceModel? selectedSpace,
) {
return Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
);
}
Widget _buildEmptyWidget(CommunityModel selectedCommunity) {
const spacer = Spacer(flex: 6); const spacer = Spacer(flex: 6);
return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty, return Expanded(
replacement: Row( child: Row(
children: [ children: [
spacer, spacer,
Expanded( Expanded(
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid), child: CreateSpaceButton(
), communityUuid: selectedCommunity.uuid,
spacer
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
), ),
), ),
spacer,
], ],
), ),
); );

View File

@ -0,0 +1,64 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
final class RemoteDeleteSpaceService implements DeleteSpaceService {
const RemoteDeleteSpaceService(this._httpService);
final HTTPService _httpService;
@override
Future<void> delete(DeleteSpaceParam param) async {
try {
await _httpService.delete(
path: await _makeUrl(param),
expectedResponseModel: (json) {
final response = json as Map<String, dynamic>;
final hasSuccessfullyDeletedSpace = response['success'] as bool? ?? false;
if (!hasSuccessfullyDeletedSpace) {
throw APIException('Failed to delete space');
}
return hasSuccessfullyDeletedSpace;
},
);
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
throw APIException(_getErrorMessageFromBody(message));
} catch (e) {
throw APIException(e.toString());
}
}
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
if (body == null) return 'Failed to delete space';
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['message'] as String? ?? '';
return errorMessage;
}
Future<String> _makeUrl(DeleteSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) {
throw APIException('Project UUID is not set');
}
if (param.communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
if (param.spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
return ApiEndpoints.deleteSpace
.replaceAll('{projectId}', projectUuid)
.replaceAll('{communityId}', param.communityUuid)
.replaceAll('{spaceId}', param.spaceUuid);
}
}

View File

@ -0,0 +1,9 @@
class DeleteSpaceParam {
const DeleteSpaceParam({
required this.spaceUuid,
required this.communityUuid,
});
final String spaceUuid;
final String communityUuid;
}

View File

@ -0,0 +1,5 @@
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
abstract interface class DeleteSpaceService {
Future<void> delete(DeleteSpaceParam param);
}

View File

@ -0,0 +1,31 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'delete_space_event.dart';
part 'delete_space_state.dart';
class DeleteSpaceBloc extends Bloc<DeleteSpaceEvent, DeleteSpaceState> {
DeleteSpaceBloc(this._deleteSpaceService) : super(DeleteSpaceInitial()) {
on<DeleteSpace>(_onDeleteSpace);
}
final DeleteSpaceService _deleteSpaceService;
Future<void> _onDeleteSpace(
DeleteSpace event,
Emitter<DeleteSpaceState> emit,
) async {
emit(DeleteSpaceLoading());
try {
await _deleteSpaceService.delete(event.param);
emit(const DeleteSpaceSuccess('Space deleted successfully'));
} on APIException catch (e) {
emit(DeleteSpaceFailure(e.message));
} catch (e) {
emit(DeleteSpaceFailure(e.toString()));
}
}
}

View File

@ -0,0 +1,17 @@
part of 'delete_space_bloc.dart';
sealed class DeleteSpaceEvent extends Equatable {
const DeleteSpaceEvent();
@override
List<Object> get props => [];
}
final class DeleteSpace extends DeleteSpaceEvent {
const DeleteSpace(this.param);
final DeleteSpaceParam param;
@override
List<Object> get props => [param];
}

View File

@ -0,0 +1,30 @@
part of 'delete_space_bloc.dart';
sealed class DeleteSpaceState extends Equatable {
const DeleteSpaceState();
@override
List<Object> get props => [];
}
final class DeleteSpaceInitial extends DeleteSpaceState {}
final class DeleteSpaceLoading extends DeleteSpaceState {}
final class DeleteSpaceSuccess extends DeleteSpaceState {
const DeleteSpaceSuccess(this.successMessage);
final String successMessage;
@override
List<Object> get props => [successMessage];
}
final class DeleteSpaceFailure extends DeleteSpaceState {
const DeleteSpaceFailure(this.errorMessage);
final String errorMessage;
@override
List<Object> get props => [errorMessage];
}

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DeleteSpaceDialog extends StatelessWidget {
const DeleteSpaceDialog({
required this.space,
required this.community,
required this.onSuccess,
super.key,
});
final SpaceModel space;
final CommunityModel community;
final void Function() onSuccess;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DeleteSpaceBloc(
RemoteDeleteSpaceService(HTTPService()),
),
child: Builder(
builder: (context) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
padding: const EdgeInsetsDirectional.all(32),
constraints: BoxConstraints(
maxWidth: context.screenWidth * 0.2,
),
child: BlocConsumer<DeleteSpaceBloc, DeleteSpaceState>(
listener: (context, state) {
if (state case DeleteSpaceSuccess()) onSuccess();
},
builder: (context, state) => switch (state) {
DeleteSpaceInitial() => DeleteSpaceDialogForm(
space: space,
communityUuid: community.uuid,
),
DeleteSpaceLoading() => const DeleteSpaceLoadingWidget(),
DeleteSpaceSuccess() => DeleteSpaceStatusWidget(
message: state.successMessage,
icon: const Icon(
Icons.check_circle,
size: 92,
color: ColorsManager.goodGreen,
),
),
DeleteSpaceFailure() => DeleteSpaceStatusWidget(
message: state.errorMessage,
icon: const Icon(
Icons.error,
size: 92,
color: ColorsManager.red,
),
),
},
),
),
),
),
);
}
}

View File

@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DeleteSpaceDialogForm extends StatelessWidget {
const DeleteSpaceDialogForm({
required this.space,
required this.communityUuid,
super.key,
});
final SpaceModel space;
final String communityUuid;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(Assets.xDelete, width: 36, height: 36),
const SizedBox(height: 16),
SelectableText(
'Delete Space',
textAlign: TextAlign.center,
style: context.textTheme.titleLarge?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 24,
),
),
const SizedBox(height: 8),
SelectableText(
'Are you sure you want to delete this space? This action is irreversible',
textAlign: TextAlign.center,
style: context.textTheme.bodyLarge?.copyWith(
color: ColorsManager.lightGreyColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: FilledButton(
style: _buildButtonStyle(
context,
color: ColorsManager.grey25,
textColor: ColorsManager.blackColor,
),
onPressed: Navigator.of(context).pop,
child: const Text('Cancel'),
),
),
const SizedBox(width: 16),
Expanded(
child: FilledButton(
style: _buildButtonStyle(
context,
color: ColorsManager.semiTransparentRed,
textColor: ColorsManager.whiteColors,
),
onPressed: () {
context.read<DeleteSpaceBloc>().add(
DeleteSpace(
DeleteSpaceParam(
spaceUuid: space.uuid,
communityUuid: communityUuid,
),
),
);
},
child: const Text('Delete'),
),
),
],
),
],
);
}
ButtonStyle _buildButtonStyle(
BuildContext context, {
required Color color,
required Color textColor,
}) {
return FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
foregroundColor: textColor,
textStyle: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
),
);
}
}

View File

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
class DeleteSpaceLoadingWidget extends StatelessWidget {
const DeleteSpaceLoadingWidget({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox.square(
dimension: 32,
child: Center(child: CircularProgressIndicator()),
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DeleteSpaceStatusWidget extends StatelessWidget {
const DeleteSpaceStatusWidget({
required this.message,
required this.icon,
super.key,
});
final String message;
final Widget icon;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16,
children: [
icon,
SelectableText(
message,
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.blackColor,
fontSize: 22,
),
textAlign: TextAlign.center,
),
FilledButton(
onPressed: Navigator.of(context).pop,
child: const Text('Close'),
),
],
);
}
}

View File

@ -2,23 +2,27 @@ import 'package:syncrow_web/pages/space_management_v2/modules/space_details/doma
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
class UniqueSubspacesDecorator implements SpaceDetailsService { class UniqueSpaceDetailsSpacesDecoratorService implements SpaceDetailsService {
final SpaceDetailsService _decoratee; final SpaceDetailsService _decoratee;
const UniqueSubspacesDecorator(this._decoratee); const UniqueSpaceDetailsSpacesDecoratorService(this._decoratee);
@override @override
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async { Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
final response = await _decoratee.getSpaceDetails(param); final response = await _decoratee.getSpaceDetails(param);
final uniqueSubspaces = <String, Subspace>{}; final uniqueSubspaces = <String, Subspace>{};
final duplicateNames = <String>{};
for (final subspace in response.subspaces) { for (final subspace in response.subspaces) {
final normalizedName = subspace.name.trim().toLowerCase(); final normalizedName = subspace.name.trim().toLowerCase();
if (!uniqueSubspaces.containsKey(normalizedName)) { if (uniqueSubspaces.containsKey(normalizedName)) {
duplicateNames.add(normalizedName);
} else {
uniqueSubspaces[normalizedName] = subspace; uniqueSubspaces[normalizedName] = subspace;
} }
} }
duplicateNames.forEach(uniqueSubspaces.remove);
return response.copyWith( return response.copyWith(
subspaces: uniqueSubspaces.values.toList(), subspaces: uniqueSubspaces.values.toList(),

View File

@ -10,7 +10,12 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AddDeviceTypeWidget extends StatefulWidget { class AddDeviceTypeWidget extends StatefulWidget {
const AddDeviceTypeWidget({super.key}); const AddDeviceTypeWidget({
super.key,
this.initialProducts = const [],
});
final List<Product> initialProducts;
@override @override
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState(); State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
@ -18,6 +23,16 @@ class AddDeviceTypeWidget extends StatefulWidget {
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> { class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
final Map<Product, int> _selectedProducts = {}; final Map<Product, int> _selectedProducts = {};
final Map<Product, int> _initialProductCounts = {};
@override
void initState() {
super.initState();
for (final product in widget.initialProducts) {
_initialProductCounts[product] = (_initialProductCounts[product] ?? 0) + 1;
}
_selectedProducts.addAll(_initialProductCounts);
}
void _onIncrement(Product product) { void _onIncrement(Product product) {
setState(() { setState(() {
@ -27,8 +42,12 @@ class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
void _onDecrement(Product product) { void _onDecrement(Product product) {
setState(() { setState(() {
if ((_selectedProducts[product] ?? 0) > 0) { final initialCount = _initialProductCounts[product] ?? 0;
_selectedProducts[product] = _selectedProducts[product]! - 1; final currentCount = _selectedProducts[product] ?? 0;
if (currentCount > initialCount) {
_selectedProducts[product] = currentCount - 1;
} else if (currentCount > 0 && initialCount == 0) {
_selectedProducts[product] = currentCount - 1;
if (_selectedProducts[product] == 0) { if (_selectedProducts[product] == 0) {
_selectedProducts.remove(product); _selectedProducts.remove(product);
} }
@ -63,7 +82,22 @@ class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
actions: [ actions: [
SpaceDetailsActionButtons( SpaceDetailsActionButtons(
onSave: () { onSave: () {
final result = _selectedProducts.entries final resultMap = <Product, int>{};
resultMap.addAll(_selectedProducts);
for (final entry in _initialProductCounts.entries) {
final product = entry.key;
final initialCount = entry.value;
final currentCount = resultMap[product] ?? 0;
if (currentCount > initialCount) {
resultMap[product] = currentCount - initialCount;
} else {
resultMap.remove(product);
}
}
final result = resultMap.entries
.expand((entry) => List.generate(entry.value, (_) => entry.key)) .expand((entry) => List.generate(entry.value, (_) => entry.key))
.toList(); .toList();
Navigator.of(context).pop(result); Navigator.of(context).pop(result);

View File

@ -205,7 +205,14 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
onCancel: () async { onCancel: () async {
final newProducts = await showDialog<List<Product>>( final newProducts = await showDialog<List<Product>>(
context: context, context: context,
builder: (context) => const AddDeviceTypeWidget(), builder: (context) => AddDeviceTypeWidget(
initialProducts: [
..._space.productAllocations.map((e) => e.product),
..._space.subspaces
.expand((s) => s.productAllocations)
.map((e) => e.product),
],
),
); );
if (newProducts == null || newProducts.isEmpty) return; if (newProducts == null || newProducts.isEmpty) return;

View File

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

View File

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

View File

@ -84,6 +84,5 @@ 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,7 +17,8 @@ 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 = '/projects/{projectId}/devices'; static const String getSpaceDevices =
'/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';

View File

@ -517,4 +517,5 @@ class Assets {
static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg'; static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg';
static const String homeIcon = 'assets/icons/home_icon.svg'; static const String homeIcon = 'assets/icons/home_icon.svg';
static const String groupIcon = 'assets/icons/group_icon.svg'; static const String groupIcon = 'assets/icons/group_icon.svg';
static const String xDelete = 'assets/icons/x_delete.svg';
} }