Compare commits

..

37 Commits

Author SHA1 Message Date
0e912207e5 flow 2025-01-21 11:41:53 +04:00
eb53671e3a edit flow 2025-01-20 21:47:22 +04:00
2f6bd31aa2 helper class 2025-01-20 10:52:23 +04:00
fe680d15f2 edit and create 2025-01-20 10:28:46 +04:00
440263e2f9 added initial flow 2025-01-18 00:32:48 +04:00
ec5b7d4395 changed button name 2025-01-17 23:53:43 +04:00
7109421358 fixed first time flow 2025-01-17 23:51:56 +04:00
145086b9de updated grid view 2025-01-17 12:40:17 +04:00
bae5ae17a7 updated the condition 2025-01-16 10:17:57 +04:00
9706c2655c added color 2025-01-16 02:15:24 +04:00
8a95f93556 fixed list view 2025-01-16 02:14:04 +04:00
60028cdf78 only appears the separator if there is both space model and product 2025-01-16 02:03:08 +04:00
c12c73f20a add close button color 2025-01-16 00:10:45 +04:00
a7256c8d5d updated duplication ui 2025-01-15 11:29:19 +04:00
5975adb5e2 cleaned subspace model create 2025-01-15 10:22:42 +04:00
0bb24604bc fixed subspace create 2025-01-15 09:39:26 +04:00
cf2690123e revert back 2025-01-14 12:22:59 +04:00
a220483310 revert back 2025-01-14 12:22:51 +04:00
12df07e681 changed chip to list 2025-01-14 12:14:48 +04:00
a4e7f30411 moved pagination to bloc 2025-01-13 14:14:09 +04:00
210fbf7497 initialize with const 2025-01-13 12:16:49 +04:00
acbb6ca7c0 removed try catch 2025-01-13 12:12:22 +04:00
408c40aa60 revert 2025-01-12 12:12:14 +04:00
2abe7a6feb revert 2025-01-12 12:11:32 +04:00
a381fd317d added space delete 2025-01-12 12:10:54 +04:00
a588351482 fixed space creation api 2025-01-12 11:37:10 +04:00
1be52adcc8 added assign tag 2025-01-12 10:43:47 +04:00
3c5e0a7778 device type select 2025-01-12 10:04:44 +04:00
6591ef1664 load space models 2025-01-12 09:25:29 +04:00
cfc1b544b7 added subspaces 2025-01-12 09:10:33 +04:00
15640ff0df added space model select 2025-01-12 01:15:44 +04:00
bfbc32d51b moved select space a bit down 2025-01-11 18:37:34 +04:00
8aa493a15e added space model link to space dialog 2025-01-11 15:42:35 +04:00
e70df16de3 added asset 2025-01-09 16:39:54 +04:00
a7e7554813 added buttons 2025-01-09 16:39:46 +04:00
1ab8c8341d updated widget 2025-01-09 16:31:20 +04:00
67516817ec add asset validation 2025-01-09 16:18:19 +04:00
66 changed files with 4161 additions and 1101 deletions

12
assets/icons/link.svg Normal file
View File

@ -0,0 +1,12 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 440">
<path id="Vector" d="M6.7986 0.892983L4.84001 2.85148C4.48185 3.20973 4.23182 3.63572 4.08964 4.08854C3.62464 4.23503 3.19865 4.49284 2.85148 4.84001L0.892983 6.7986C-0.29757 7.98915 -0.297753 9.91625 0.892983 11.107C2.08354 12.2976 4.01072 12.2977 5.20146 11.107L7.15996 9.14848C7.5182 8.79033 7.76814 8.36433 7.91032 7.91142C8.37541 7.76503 8.80132 7.50713 9.14848 7.15996L11.107 5.20146C12.2976 4.01081 12.2978 2.08372 11.107 0.892983C9.91643 -0.29757 7.98933 -0.297753 6.7986 0.892983ZM4.17735 6.1656C4.32576 6.5276 4.54658 6.86653 4.84001 7.15996C5.1251 7.44496 5.46431 7.67027 5.83418 7.82289L3.87577 9.78139C3.41893 10.2381 2.67552 10.2382 2.21867 9.78139C1.76182 9.32445 1.76182 8.58113 2.21867 8.12428L4.17717 6.16569C4.17726 6.16569 4.17726 6.16569 4.17735 6.1656ZM6.82854 8.81706L4.86995 10.7756C3.86259 11.783 2.23194 11.7831 1.2244 10.7756C0.216957 9.76821 0.216866 8.13747 1.2244 7.13002L3.1829 5.17143C3.41105 4.94328 3.67939 4.76082 3.9719 4.63255C3.92823 4.9897 3.94819 5.34263 4.02427 5.67991C3.96128 5.72697 3.90159 5.77843 3.84574 5.83427L1.88725 7.79277C1.24766 8.43245 1.24766 9.47313 1.88725 10.1127C2.52683 10.7523 3.56752 10.7523 4.2072 10.1127L6.16569 8.15422C6.80592 7.5139 6.80601 6.47459 6.16569 5.83427C5.82365 5.49223 5.74034 4.99492 5.90358 4.57753C6.24891 4.70598 6.56587 4.90876 6.82854 5.17143C7.8336 6.1765 7.83369 7.8119 6.82854 8.81706ZM10.7756 4.86995L8.81706 6.82854C8.58891 7.05669 8.32057 7.23915 8.02806 7.36742C8.07173 7.01027 8.05177 6.65733 7.97578 6.32005C8.03868 6.27299 8.09846 6.22154 8.15422 6.16569L10.1128 4.2072C10.7524 3.56761 10.7524 2.52683 10.1128 1.88725C9.5385 1.31303 8.64028 1.25379 7.99913 1.71229C7.89384 1.78755 7.86958 1.93394 7.94484 2.03922C8.0201 2.14451 8.16649 2.16886 8.27177 2.09352C8.73952 1.75907 9.37434 1.81162 9.78139 2.21867C10.2382 2.67552 10.2382 3.41883 9.78139 3.87577L7.8228 5.83427C7.8228 5.83427 7.8228 5.83427 7.82271 5.83436C7.67421 5.47236 7.45338 5.13344 7.15996 4.84001C6.87495 4.555 6.53566 4.32969 6.16578 4.17707L6.96431 3.37855C7.05577 3.28709 7.05577 3.13868 6.96431 3.04713C6.87276 2.95567 6.72444 2.95567 6.63289 3.04713L5.83427 3.84574C5.19404 4.48597 5.19395 5.52528 5.83427 6.16569C6.17631 6.50764 6.25963 7.00505 6.09639 7.42244C5.75105 7.29399 5.43409 7.0912 5.17143 6.82844C4.16645 5.82347 4.16636 4.18797 5.17143 3.1829L7.13002 1.2244C8.13737 0.216957 9.76811 0.216774 10.7756 1.2244C11.783 2.23176 11.7831 3.8625 10.7756 4.86995Z" fill="#023DFE" fill-opacity="0.7"/>
<path id="Vector_2" d="M7.69571 2.55103C7.69571 2.68048 7.59079 2.7854 7.46143 2.7854C7.33197 2.7854 7.22705 2.68048 7.22705 2.55103C7.22705 2.42157 7.33197 2.31665 7.46143 2.31665C7.59079 2.31665 7.69571 2.42157 7.69571 2.55103Z" fill="#023DFE" fill-opacity="0.7"/>
<path id="Vector_3" d="M3.18286 3.1831C3.27441 3.09164 3.27441 2.94323 3.18286 2.85168L2.18859 1.85741C2.09704 1.76595 1.94872 1.76595 1.85717 1.85741C1.76571 1.94897 1.76571 2.09737 1.85717 2.18893L2.85143 3.18319C2.94308 3.27465 3.09139 3.27465 3.18286 3.1831Z" fill="#023DFE" fill-opacity="0.7"/>
<path id="Vector_4" d="M0.891022 3.9375C0.761658 3.9375 0.656738 4.04242 0.656738 4.17178C0.656738 4.30124 0.761658 4.40616 0.891022 4.40616H2.29718C2.42655 4.40616 2.53147 4.30124 2.53147 4.17178C2.53147 4.04242 2.42655 3.9375 2.29718 3.9375H0.891022Z" fill="#023DFE" fill-opacity="0.7"/>
<path id="Vector_5" d="M3.93774 0.821289V2.22736C3.93774 2.35672 4.04266 2.46173 4.17203 2.46173C4.30148 2.46173 4.4064 2.35672 4.4064 2.22736V0.821289C4.4064 0.691834 4.30148 0.586914 4.17203 0.586914C4.04266 0.586914 3.93774 0.691834 3.93774 0.821289Z" fill="#023DFE" fill-opacity="0.7"/>
<path id="Vector_6" d="M8.8172 8.81738C8.72565 8.90884 8.72565 9.05724 8.8172 9.1488L9.81146 10.1431C9.85724 10.1888 9.91721 10.2117 9.97717 10.2117C10.184 10.2117 10.291 9.95986 10.1429 9.81164L9.14862 8.81738C9.05707 8.72591 8.90875 8.72591 8.8172 8.81738Z" fill="#023DFE" fill-opacity="0.7"/>
<path id="Vector_7" d="M8.03882 11.1785V9.77237C8.03882 9.64301 7.93381 9.53809 7.80444 9.53809C7.67499 9.53809 7.57007 9.64301 7.57007 9.77237V11.1785C7.57007 11.3079 7.67499 11.4128 7.80444 11.4128C7.93381 11.4128 8.03882 11.3079 8.03882 11.1785Z" fill="#023DFE" fill-opacity="0.7"/>
<path id="Vector_8" d="M11.1793 8.03906C11.3086 8.03906 11.4135 7.93405 11.4135 7.80469C11.4135 7.67523 11.3086 7.57031 11.1793 7.57031H9.7731C9.64374 7.57031 9.53882 7.67523 9.53882 7.80469C9.53882 7.93405 9.64374 8.03906 9.7731 8.03906H11.1793Z" fill="#023DFE" fill-opacity="0.7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DialogDropdown extends StatefulWidget {
final List<String> items;
final ValueChanged<String> onSelected;
final String? selectedValue;
const DialogDropdown({
Key? key,
required this.items,
required this.onSelected,
this.selectedValue,
}) : super(key: key);
@override
_DialogDropdownState createState() => _DialogDropdownState();
}
class _DialogDropdownState extends State<DialogDropdown> {
bool _isOpen = false;
late OverlayEntry _overlayEntry;
@override
void initState() {
super.initState();
}
void _toggleDropdown() {
if (_isOpen) {
_closeDropdown();
} else {
_openDropdown();
}
}
void _openDropdown() {
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry);
_isOpen = true;
}
void _closeDropdown() {
_overlayEntry.remove();
_isOpen = false;
}
OverlayEntry _createOverlayEntry() {
final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) {
return GestureDetector(
onTap: () {
_closeDropdown();
},
behavior: HitTestBehavior.translucent,
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy + size.height,
width: size.width,
child: Material(
elevation: 4.0,
child: Container(
color: ColorsManager.whiteColors,
constraints: const BoxConstraints(
maxHeight: 200.0, // Set max height for dropdown
),
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.items.length,
itemBuilder: (context, index) {
final item = widget.items[index];
return Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.lightGrayBorderColor,
width: 1.0,
),
),
),
child: ListTile(
title: Text(
item,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager.textPrimaryColor,
),
),
onTap: () {
widget.onSelected(item);
_closeDropdown();
},
),
);
},
),
),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _toggleDropdown,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.transparentColor),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.selectedValue ?? 'Select an item',
style: Theme.of(context).textTheme.bodyMedium,
),
const Icon(Icons.arrow_drop_down),
],
),
),
);
}
}

View File

@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DialogTextfieldDropdown extends StatefulWidget {
final List<String> items;
final ValueChanged<String> onSelected;
final String? initialValue;
const DialogTextfieldDropdown({
Key? key,
required this.items,
required this.onSelected,
this.initialValue,
}) : super(key: key);
@override
_DialogTextfieldDropdownState createState() =>
_DialogTextfieldDropdownState();
}
class _DialogTextfieldDropdownState extends State<DialogTextfieldDropdown> {
bool _isOpen = false;
late OverlayEntry _overlayEntry;
final TextEditingController _controller = TextEditingController();
late List<String> _filteredItems; // Filtered items list
@override
void initState() {
super.initState();
_controller.text = widget.initialValue ?? 'Select Tag';
_filteredItems = List.from(widget.items); // Initialize filtered items
}
void _toggleDropdown() {
if (_isOpen) {
_closeDropdown();
} else {
_openDropdown();
}
}
void _openDropdown() {
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry);
_isOpen = true;
}
void _closeDropdown() {
_overlayEntry.remove();
_isOpen = false;
}
OverlayEntry _createOverlayEntry() {
final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) {
return GestureDetector(
onTap: () {
_closeDropdown();
},
behavior: HitTestBehavior.translucent,
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy + size.height,
width: size.width,
child: Material(
elevation: 4.0,
child: Container(
color: ColorsManager.whiteColors,
constraints: const BoxConstraints(
maxHeight: 200.0,
),
child: ListView.builder(
shrinkWrap: true,
itemCount: _filteredItems.length,
itemBuilder: (context, index) {
final item = _filteredItems[index];
return Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.lightGrayBorderColor,
width: 1.0,
),
),
),
child: ListTile(
title: Text(item,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager.textPrimaryColor)),
onTap: () {
_controller.text = item;
widget.onSelected(item);
setState(() {
_filteredItems
.remove(item); // Remove selected item
});
_closeDropdown();
},
),
);
},
),
),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _toggleDropdown,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.transparentColor),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextFormField(
controller: _controller,
onChanged: (value) {
setState(() {
_filteredItems = widget.items
.where((item) =>
item.toLowerCase().contains(value.toLowerCase()))
.toList(); // Filter items dynamically
});
widget.onSelected(value);
},
style: Theme.of(context).textTheme.bodyMedium,
decoration: const InputDecoration(
hintText: 'Enter or Select tag',
border: InputBorder.none,
),
),
),
const Icon(Icons.arrow_drop_down),
],
),
),
);
}
}

View File

@ -0,0 +1,38 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart';
class AddDeviceTypeBloc
extends Bloc<AddDeviceTypeEvent, List<SelectedProduct>> {
AddDeviceTypeBloc(List<SelectedProduct> initialProducts)
: super(initialProducts) {
on<UpdateProductCountEvent>(_onUpdateProductCount);
}
void _onUpdateProductCount(
UpdateProductCountEvent event, Emitter<List<SelectedProduct>> emit) {
final existingProduct = state.firstWhere(
(p) => p.productId == event.productId,
orElse: () => SelectedProduct(productId: event.productId, count: 0,productName: event.productName,product: event.product ),
);
if (event.count > 0) {
if (!state.contains(existingProduct)) {
emit([
...state,
SelectedProduct(productId: event.productId, count: event.count, productName: event.productName, product: event.product)
]);
} else {
final updatedList = state.map((p) {
if (p.productId == event.productId) {
return SelectedProduct(productId: p.productId, count: event.count, productName: p.productName,product: p.product);
}
return p;
}).toList();
emit(updatedList);
}
} else {
emit(state.where((p) => p.productId != event.productId).toList());
}
}
}

View File

@ -0,0 +1,19 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
abstract class AddDeviceTypeEvent extends Equatable {
@override
List<Object> get props => [];
}
class UpdateProductCountEvent extends AddDeviceTypeEvent {
final String productId;
final int count;
final String productName;
final ProductModel product;
UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product});
@override
List<Object> get props => [productId, count];
}

View File

@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/action_button_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AddDeviceTypeWidget extends StatelessWidget {
final List<ProductModel>? products;
final ValueChanged<List<SelectedProduct>>? onProductsSelected;
final List<SelectedProduct>? initialSelectedProducts;
final List<SubspaceModel>? subspaces;
final List<Tag>? spaceTags;
final List<String>? allTags;
final String spaceName;
final Function(List<Tag>,List<SubspaceModel>?)? onSave;
const AddDeviceTypeWidget(
{super.key,
this.products,
this.initialSelectedProducts,
this.onProductsSelected,
this.subspaces,
this.allTags,
this.spaceTags,
this.onSave,
required this.spaceName});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final crossAxisCount = size.width > 1200
? 8
: size.width > 800
? 5
: 3;
return BlocProvider(
create: (_) => AddDeviceTypeBloc(initialSelectedProducts ?? []),
child: Builder(
builder: (context) => AlertDialog(
title: const Text('Add Devices'),
backgroundColor: ColorsManager.whiteColors,
content: SingleChildScrollView(
child: Container(
width: size.width * 0.9,
height: size.height * 0.65,
color: ColorsManager.textFieldGreyColor,
child: Column(
children: [
const SizedBox(height: 16),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: ScrollableGridViewWidget(
products: products, crossAxisCount: crossAxisCount),
),
),
],
),
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CancelButton(
label: 'Cancel',
onPressed: () async {
Navigator.of(context).pop();
},
),
ActionButton(
label: 'Continue',
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: ColorsManager.whiteColors,
onPressed: () async {
final currentState =
context.read<AddDeviceTypeBloc>().state;
Navigator.of(context).pop();
if (currentState.isNotEmpty) {
final initialTags = generateInitialTags(
spaceTags: spaceTags,
subspaces: subspaces,
);
final dialogTitle = initialTags.isNotEmpty
? 'Edit Device'
: 'Assign Tags';
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (context) => AssignTagDialog(
products: products,
subspaces: subspaces,
addedProducts: currentState,
allTags: allTags,
spaceName: spaceName,
initialTags: initialTags,
title: dialogTitle,
onSave: (tags,subspaces){
onSave!(tags,subspaces);
},
),
);
}
},
),
],
),
],
),
));
}
List<Tag> generateInitialTags({
List<Tag>? spaceTags,
List<SubspaceModel>? subspaces,
}) {
final List<Tag> initialTags = [];
if (spaceTags != null) {
initialTags.addAll(spaceTags);
}
if (subspaces != null) {
for (var subspace in subspaces) {
if (subspace.tags != null) {
initialTags.addAll(
subspace.tags!.map(
(tag) => tag.copyWith(location: subspace.subspaceName),
),
);
}
}
}
return initialTags;
}
}

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_name_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class DeviceTypeTileWidget extends StatelessWidget {
final ProductModel product;
final List<SelectedProduct> productCounts;
const DeviceTypeTileWidget({
super.key,
required this.product,
required this.productCounts,
});
@override
Widget build(BuildContext context) {
final selectedProduct = productCounts.firstWhere(
(p) => p.productId == product.uuid,
orElse: () => SelectedProduct(
productId: product.uuid,
count: 0,
productName: product.catName,
product: product),
);
return Card(
elevation: 2,
color: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
DeviceIconWidget(icon: product.icon ?? Assets.doorSensor),
const SizedBox(height: 4),
DeviceNameWidget(name: product.name),
const SizedBox(height: 4),
CounterWidget(
initialCount: selectedProduct.count,
onCountChanged: (newCount) {
context.read<AddDeviceTypeBloc>().add(
UpdateProductCountEvent(
productId: product.uuid,
count: newCount,
productName: product.catName,
product: product),
);
},
),
],
),
),
);
}
}

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
class ScrollableGridViewWidget extends StatelessWidget {
final List<ProductModel>? products;
final int crossAxisCount;
final List<SelectedProduct>? initialProductCounts;
const ScrollableGridViewWidget({
super.key,
required this.products,
required this.crossAxisCount,
this.initialProductCounts,
});
@override
Widget build(BuildContext context) {
final scrollController = ScrollController();
return Scrollbar(
controller: scrollController,
thumbVisibility: true,
child: BlocBuilder<AddDeviceTypeBloc, List<SelectedProduct>>(
builder: (context, productCounts) {
return GridView.builder(
controller: scrollController,
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 6,
crossAxisSpacing: 4,
childAspectRatio: .8,
),
itemCount: products?.length ?? 0,
itemBuilder: (context, index) {
final product = products![index];
final initialProductCount = _findInitialProductCount(product);
return DeviceTypeTileWidget(
product: product,
productCounts: initialProductCount != null
? [...productCounts, initialProductCount]
: productCounts,
);
},
);
},
),
);
}
SelectedProduct? _findInitialProductCount(ProductModel product) {
if (initialProductCounts == null) return null;
final matchingProduct = initialProductCounts!.firstWhere(
(selectedProduct) => selectedProduct.productId == product.uuid,
orElse: () => SelectedProduct(
productId: '',
count: 0,
productName: '',
product: null,
),
);
return matchingProduct.productId.isNotEmpty ? matchingProduct : null;
}
}

View File

@ -1,10 +1,13 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_state.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
import 'package:syncrow_web/services/product_api.dart';
import 'package:syncrow_web/services/space_mana_api.dart';
import 'package:syncrow_web/services/space_model_mang_api.dart';
@ -52,10 +55,14 @@ class SpaceManagementBloc
break;
}
}
var prevSpaceModels = await fetchSpaceModels(previousState);
emit(SpaceManagementLoaded(
communities: updatedCommunities,
products: previousState.products,
selectedCommunity: previousState.selectedCommunity,
spaceModels: prevSpaceModels,
));
}
} else {
@ -66,6 +73,41 @@ class SpaceManagementBloc
}
}
Future<List<SpaceTemplateModel>> fetchSpaceModels(
SpaceManagementState previousState) async {
try {
List<SpaceTemplateModel> allSpaces = [];
List<SpaceTemplateModel> prevSpaceModels = [];
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
prevSpaceModels = List<SpaceTemplateModel>.from(
(previousState as dynamic).spaceModels ?? [],
);
}
if (prevSpaceModels.isEmpty) {
bool hasNext = true;
int page = 1;
while (hasNext) {
final spaces = await _spaceModelApi.listSpaceModels(page: page);
if (spaces.isNotEmpty) {
allSpaces.addAll(spaces);
page++;
} else {
hasNext = false;
}
}
prevSpaceModels = await _spaceModelApi.listSpaceModels(page: 1);
}
return allSpaces;
} catch (e) {
return [];
}
}
void _onloadProducts() async {
if (_cachedProducts == null) {
final products = await _productApi.fetchProducts();
@ -89,19 +131,24 @@ class SpaceManagementBloc
return await _api.getSpaceHierarchy(communityUuid);
}
void _onNewCommunity(
Future<void> _onNewCommunity(
NewCommunityEvent event,
Emitter<SpaceManagementState> emit,
) {
) async {
try {
final previousState = state;
if (event.communities.isEmpty) {
emit(const SpaceManagementError('No communities provided.'));
return;
}
var prevSpaceModels = await fetchSpaceModels(previousState);
emit(BlankState(
communities: event.communities,
products: _cachedProducts ?? [],
spaceModels: prevSpaceModels,
));
} catch (error) {
emit(SpaceManagementError('Error loading communities: $error'));
@ -112,6 +159,7 @@ class SpaceManagementBloc
BlankStateEvent event, Emitter<SpaceManagementState> emit) async {
try {
final previousState = state;
var prevSpaceModels = await fetchSpaceModels(previousState);
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
@ -119,6 +167,7 @@ class SpaceManagementBloc
emit(BlankState(
communities: List<CommunityModel>.from(prevCommunities),
products: _cachedProducts ?? [],
spaceModels: prevSpaceModels,
));
return;
}
@ -139,6 +188,7 @@ class SpaceManagementBloc
}));
emit(BlankState(
spaceModels: prevSpaceModels,
communities: updatedCommunities,
products: _cachedProducts ?? [],
));
@ -151,6 +201,7 @@ class SpaceManagementBloc
LoadCommunityAndSpacesEvent event,
Emitter<SpaceManagementState> emit,
) async {
var prevState = state;
emit(SpaceManagementLoading());
try {
_onloadProducts();
@ -172,8 +223,11 @@ class SpaceManagementBloc
}).toList(),
);
final prevSpaceModels = await fetchSpaceModels(prevState);
emit(SpaceManagementLoaded(
communities: updatedCommunities, products: _cachedProducts ?? []));
communities: updatedCommunities,
products: _cachedProducts ?? [],
spaceModels: prevSpaceModels));
} catch (e) {
emit(SpaceManagementError('Error loading communities and spaces: $e'));
}
@ -213,6 +267,7 @@ class SpaceManagementBloc
try {
CommunityModel? newCommunity =
await _api.createCommunity(event.name, event.description);
var prevSpaceModels = await fetchSpaceModels(previousState);
if (newCommunity != null) {
if (previousState is SpaceManagementLoaded ||
@ -222,6 +277,7 @@ class SpaceManagementBloc
);
final updatedCommunities = prevCommunities..add(newCommunity);
emit(SpaceManagementLoaded(
spaceModels: prevSpaceModels,
communities: updatedCommunities,
products: _cachedProducts ?? [],
selectedCommunity: newCommunity,
@ -276,12 +332,16 @@ class SpaceManagementBloc
final communities = List<CommunityModel>.from(
(previousState as dynamic).communities,
);
final spaceModels = List<SpaceTemplateModel>.from(
(previousState as dynamic).spaceModels,
);
emit(SpaceManagementLoaded(
communities: communities,
products: _cachedProducts ?? [],
selectedCommunity: selectedCommunity,
selectedSpace: selectedSpace,
));
communities: communities,
products: _cachedProducts ?? [],
selectedCommunity: selectedCommunity,
selectedSpace: selectedSpace,
spaceModels: spaceModels ?? []));
}
} catch (e) {
emit(SpaceManagementError('Error updating state: $e'));
@ -304,7 +364,7 @@ class SpaceManagementBloc
emit(SpaceCreationSuccess(spaces: updatedSpaces));
if (previousState is SpaceManagementLoaded) {
_updateLoadedState(
await _updateLoadedState(
previousState,
allSpaces,
event.communityUuid,
@ -322,23 +382,25 @@ class SpaceManagementBloc
}
}
void _updateLoadedState(
Future<void> _updateLoadedState(
SpaceManagementLoaded previousState,
List<SpaceModel> allSpaces,
String communityUuid,
Emitter<SpaceManagementState> emit,
) {
) async {
var prevSpaceModels = await fetchSpaceModels(previousState);
final communities = List<CommunityModel>.from(previousState.communities);
for (var community in communities) {
if (community.uuid == communityUuid) {
community.spaces = allSpaces;
emit(SpaceManagementLoaded(
communities: communities,
products: _cachedProducts ?? [],
selectedCommunity: community,
selectedSpace: null,
));
communities: communities,
products: _cachedProducts ?? [],
selectedCommunity: community,
selectedSpace: null,
spaceModels: prevSpaceModels));
return;
}
}
@ -377,6 +439,21 @@ class SpaceManagementBloc
);
} else {
// Call create if the space does not have a UUID
final List<CreateTagBodyModel> tagBodyModels = space.tags != null
? space.tags!.map((tag) => tag.toCreateTagBodyModel()).toList()
: [];
final createSubspaceBodyModels = space.subspaces?.map((subspace) {
final tagBodyModels = subspace.tags
?.map((tag) => tag.toCreateTagBodyModel())
.toList() ??
[];
return CreateSubspaceModel()
..subspaceName = subspace.subspaceName
..tags = tagBodyModels;
}).toList() ??
[];
final response = await _api.createSpace(
communityId: communityUuid,
name: space.name,
@ -385,6 +462,9 @@ class SpaceManagementBloc
position: space.position,
icon: space.icon,
direction: space.incomingConnection?.direction,
spaceModelUuid: space.spaceModel?.uuid,
tags: tagBodyModels,
subspaces: createSubspaceBodyModels,
);
space.uuid = response?.uuid;
}
@ -424,6 +504,8 @@ class SpaceManagementBloc
SpaceModelLoadEvent event, Emitter<SpaceManagementState> emit) async {
emit(SpaceManagementLoading());
try {
var prevState = state;
List<CommunityModel> communities = await _api.fetchCommunities();
List<CommunityModel> updatedCommunities = await Future.wait(
@ -442,12 +524,12 @@ class SpaceManagementBloc
}).toList(),
);
List<SpaceTemplateModel> spaceModels =
await _spaceModelApi.listSpaceModels(page: 1);
var prevSpaceModels = await fetchSpaceModels(prevState);
emit(SpaceModelLoaded(
communities: updatedCommunities,
products: _cachedProducts ?? [],
spaceModels: spaceModels));
spaceModels: prevSpaceModels));
} catch (e) {
emit(SpaceManagementError('Error loading communities and spaces: $e'));
}

View File

@ -20,12 +20,15 @@ class SpaceManagementLoaded extends SpaceManagementState {
final List<ProductModel> products;
CommunityModel? selectedCommunity;
SpaceModel? selectedSpace;
List<SpaceTemplateModel>? spaceModels;
SpaceManagementLoaded(
{required this.communities,
required this.products,
this.selectedCommunity,
this.selectedSpace});
this.selectedSpace,
this.spaceModels
});
}
class SpaceModelManagenetLoaded extends SpaceManagementState {
@ -35,10 +38,13 @@ class SpaceModelManagenetLoaded extends SpaceManagementState {
class BlankState extends SpaceManagementState {
final List<CommunityModel> communities;
final List<ProductModel> products;
List<SpaceTemplateModel>? spaceModels;
BlankState({
required this.communities,
required this.products,
this.spaceModels
});
}

View File

@ -0,0 +1,13 @@
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
class CreateSubspaceModel {
late String subspaceName;
late List<CreateTagBodyModel>? tags;
Map<String, dynamic> toJson() {
return {
'subspaceName': subspaceName,
'tags': tags?.map((tag) => tag.toJson()).toList(),
};
}
}

View File

@ -1,11 +1,13 @@
import 'dart:ui';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:uuid/uuid.dart';
enum SpaceStatus { newSpace, modified, unchanged, deleted }
enum SpaceStatus { newSpace, modified, unchanged, deleted, parentDeleted }
class SpaceModel {
String? uuid;
@ -21,6 +23,9 @@ class SpaceModel {
bool isHovered;
SpaceStatus status;
String internalId;
SpaceTemplateModel? spaceModel;
List<Tag>? tags;
List<SubspaceModel>? subspaces;
List<Connection> outgoingConnections = []; // Connections from this space
Connection? incomingConnection; // Connections to this space
@ -40,6 +45,9 @@ class SpaceModel {
this.isHovered = false,
this.incomingConnection,
this.status = SpaceStatus.unchanged,
this.spaceModel,
this.tags,
this.subspaces,
}) : internalId = internalId ?? const Uuid().v4();
factory SpaceModel.fromJson(Map<String, dynamic> json,
@ -62,6 +70,11 @@ class SpaceModel {
name: json['spaceName'],
isPrivate: json['isPrivate'] ?? false,
invitationCode: json['invitationCode'],
subspaces: (json['subspaces'] as List<dynamic>?)
?.where((e) => e is Map<String, dynamic>) // Validate type
.map((e) => SubspaceModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
parent: parentInternalId != null
? SpaceModel(
internalId: parentInternalId,
@ -83,6 +96,11 @@ class SpaceModel {
icon: json['icon'] ?? Assets.location,
position: Offset(json['x'] ?? 0, json['y'] ?? 0),
isHovered: false,
tags: (json['tags'] as List<dynamic>?)
?.where((item) => item is Map<String, dynamic>) // Validate type
.map((item) => Tag.fromJson(item as Map<String, dynamic>))
.toList() ??
[],
);
if (json['incomingConnections'] != null &&
@ -108,6 +126,7 @@ class SpaceModel {
'isPrivate': isPrivate,
'invitationCode': invitationCode,
'parent': parent?.uuid,
'subspaces': subspaces?.map((e) => e.toJson()).toList(),
'community': community?.toMap(),
'children': children.map((child) => child.toMap()).toList(),
'icon': icon,
@ -115,6 +134,7 @@ class SpaceModel {
'isHovered': isHovered,
'outgoingConnections': outgoingConnections.map((c) => c.toMap()).toList(),
'incomingConnection': incomingConnection?.toMap(),
'tags': tags?.map((e) => e.toJson()).toList(),
};
}
@ -122,3 +142,28 @@ class SpaceModel {
outgoingConnections.add(connection);
}
}
extension SpaceExtensions on SpaceModel {
List<String> listAllTagValues() {
final List<String> tagValues = [];
if (tags != null) {
tagValues.addAll(
tags!.map((tag) => tag.tag ?? '').where((tag) => tag.isNotEmpty));
}
if (subspaces != null) {
for (final subspace in subspaces!) {
if (subspace.tags != null) {
tagValues.addAll(
subspace.tags!
.map((tag) => tag.tag ?? '')
.where((tag) => tag.isNotEmpty),
);
}
}
}
return tagValues;
}
}

View File

@ -0,0 +1,110 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/utils/constants/action_enum.dart';
import 'tag.dart';
class SubspaceModel {
final String? uuid;
String subspaceName;
final bool disabled;
List<Tag>? tags;
SubspaceModel({
this.uuid,
required this.subspaceName,
required this.disabled,
this.tags,
});
factory SubspaceModel.fromJson(Map<String, dynamic> json) {
return SubspaceModel(
uuid: json['uuid'] ?? '',
subspaceName: json['subspaceName'] ?? '',
disabled: json['disabled'] ?? false,
tags: (json['tags'] as List<dynamic>?)
?.map((item) => Tag.fromJson(item))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'subspaceName': subspaceName,
'disabled': disabled,
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
};
}
}
class UpdateSubspaceModel {
final String uuid;
final Action action;
final String? subspaceName;
final List<UpdateTag>? tags;
UpdateSubspaceModel({
required this.action,
required this.uuid,
this.subspaceName,
this.tags,
});
factory UpdateSubspaceModel.fromJson(Map<String, dynamic> json) {
return UpdateSubspaceModel(
action: ActionExtension.fromValue(json['action']),
uuid: json['uuid'] ?? '',
subspaceName: json['subspaceName'] ?? '',
tags: (json['tags'] as List)
.map((item) => UpdateTag.fromJson(item))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'action': action.value,
'uuid': uuid,
'subspaceName': subspaceName,
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
};
}
}
class UpdateTag {
final Action action;
final String? uuid;
final String tag;
final bool disabled;
final ProductModel? product;
UpdateTag({
required this.action,
this.uuid,
required this.tag,
required this.disabled,
this.product,
});
factory UpdateTag.fromJson(Map<String, dynamic> json) {
return UpdateTag(
action: ActionExtension.fromValue(json['action']),
uuid: json['uuid'] ?? '',
tag: json['tag'] ?? '',
disabled: json['disabled'] ?? false,
product: json['product'] != null
? ProductModel.fromMap(json['product'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'action': action.value,
'uuid': uuid,
'tag': tag,
'disabled': disabled,
'product': product?.toMap(),
};
}
}

View File

@ -0,0 +1,68 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
import 'package:uuid/uuid.dart';
class Tag {
String? uuid;
String? tag;
final ProductModel? product;
String internalId;
String? location;
Tag(
{this.uuid,
required this.tag,
this.product,
String? internalId,
this.location})
: internalId = internalId ?? const Uuid().v4();
factory Tag.fromJson(Map<String, dynamic> json) {
final String internalId = json['internalId'] ?? const Uuid().v4();
return Tag(
uuid: json['uuid'] ?? '',
internalId: internalId,
tag: json['tag'] ?? '',
product: json['product'] != null
? ProductModel.fromMap(json['product'])
: null,
);
}
Tag copyWith({
String? tag,
ProductModel? product,
String? location,
}) {
return Tag(
tag: tag ?? this.tag,
product: product ?? this.product,
location: location ?? this.location,
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'tag': tag,
'product': product?.toMap(),
};
}
}
extension TagModelExtensions on Tag {
TagBodyModel toTagBodyModel() {
return TagBodyModel()
..uuid = uuid ?? ''
..tag = tag ?? ''
..productUuid = product?.uuid;
}
CreateTagBodyModel toCreateTagBodyModel() {
return CreateTagBodyModel()
..tag = tag ?? ''
..productUuid = product?.uuid;
}
}

View File

@ -51,6 +51,7 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
selectedCommunity: null,
selectedSpace: null,
products: state.products,
shouldNavigateToSpaceModelPage: false,
);
} else if (state is SpaceManagementLoaded) {
return LoadedSpaceView(
@ -58,12 +59,16 @@ class SpaceManagementPageState extends State<SpaceManagementPage> {
selectedCommunity: state.selectedCommunity,
selectedSpace: state.selectedSpace,
products: state.products,
spaceModels: state.spaceModels,
shouldNavigateToSpaceModelPage: false,
);
} else if (state is SpaceModelLoaded) {
return LoadedSpaceView(
communities: state.communities,
products: state.products,
spaceModels: state.spaceModels);
spaceModels: state.spaceModels,
shouldNavigateToSpaceModelPage: true,
);
} else if (state is SpaceManagementError) {
return Center(child: Text('Error: ${state.errorMessage}'));
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -17,6 +18,7 @@ class CommunityStructureHeader extends StatefulWidget {
final ValueChanged<String> onNameSubmitted;
final List<CommunityModel> communities;
final CommunityModel? community;
final SpaceModel? selectedSpace;
const CommunityStructureHeader(
{super.key,
@ -29,7 +31,8 @@ class CommunityStructureHeader extends StatefulWidget {
required this.onEditName,
required this.onNameSubmitted,
this.community,
required this.communities});
required this.communities,
this.selectedSpace});
@override
State<CommunityStructureHeader> createState() =>
@ -137,10 +140,8 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
],
),
),
if (widget.isSave) ...[
const SizedBox(width: 8),
_buildActionButtons(theme),
],
const SizedBox(width: 8),
_buildActionButtons(theme),
],
),
],
@ -152,11 +153,19 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
alignment: WrapAlignment.end,
spacing: 10,
children: [
if (widget.isSave)
_buildButton(
label: "Save",
icon: const Icon(Icons.save,
size: 18, color: ColorsManager.spaceColor),
onPressed: widget.onSave,
theme: theme),
if(widget.selectedSpace!= null)
_buildButton(
label: "Save",
icon: const Icon(Icons.save,
size: 18, color: ColorsManager.spaceColor),
onPressed: widget.onSave,
label: "Delete",
icon: const Icon(Icons.delete,
size: 18, color: ColorsManager.warningRed),
onPressed: widget.onDelete,
theme: theme),
],
);

View File

@ -11,12 +11,15 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_pr
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_community_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_container_widget.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CommunityStructureArea extends StatefulWidget {
@ -26,6 +29,7 @@ class CommunityStructureArea extends StatefulWidget {
final ValueChanged<SpaceModel?>? onSpaceSelected;
final List<CommunityModel> communities;
final List<SpaceModel> spaces;
final List<SpaceTemplateModel>? spaceModels;
CommunityStructureArea({
this.selectedCommunity,
@ -34,6 +38,7 @@ class CommunityStructureArea extends StatefulWidget {
this.products,
required this.spaces,
this.onSpaceSelected,
this.spaceModels,
});
@override
@ -126,6 +131,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
isEditingName: isEditingName,
nameController: _nameController,
onSave: _saveSpaces,
selectedSpace: widget.selectedSpace,
onDelete: _onDelete,
onEditName: () {
setState(() {
@ -171,7 +177,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
painter: CurvedLinePainter([connection])),
),
for (var entry in spaces.asMap().entries)
if (entry.value.status != SpaceStatus.deleted)
if (entry.value.status != SpaceStatus.deleted ||
entry.value.status != SpaceStatus.parentDeleted)
Positioned(
left: entry.value.position.dx,
top: entry.value.position.dy,
@ -284,9 +291,14 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
builder: (BuildContext context) {
return CreateSpaceDialog(
products: widget.products,
spaceModels: widget.spaceModels,
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
onCreateSpace: (String name, String icon,
List<SelectedProduct> selectedProducts) {
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
setState(() {
// Set the first space in the center or use passed position
@ -299,7 +311,9 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
isPrivate: false,
children: [],
status: SpaceStatus.newSpace,
);
spaceModel: spaceModel,
subspaces: subspaces,
tags: tags);
if (parentIndex != null && direction != null) {
SpaceModel parentSpace = spaces[parentIndex];
@ -335,12 +349,19 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
icon: space.icon,
editSpace: space,
isEdit: true,
onCreateSpace: (String name, String icon,
List<SelectedProduct> selectedProducts) {
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
setState(() {
// Update the space's properties
space.name = name;
space.icon = icon;
space.spaceModel = spaceModel;
space.subspaces = subspaces;
space.tags = tags;
if (space.status != SpaceStatus.newSpace) {
space.status = SpaceStatus.modified; // Mark as modified
@ -363,7 +384,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
List<SpaceModel> result = [];
void flatten(SpaceModel space) {
if (space.status == SpaceStatus.deleted) return;
if (space.status == SpaceStatus.deleted ||
space.status == SpaceStatus.parentDeleted) return;
result.add(space);
@ -456,7 +478,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _markChildrenAsDeleted(SpaceModel parent) {
for (var child in parent.children) {
child.status = SpaceStatus.deleted;
child.status = SpaceStatus.parentDeleted;
_markChildrenAsDeleted(child);
}
}
@ -464,7 +487,9 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _removeConnectionsForDeletedSpaces() {
connections.removeWhere((connection) {
return connection.startSpace.status == SpaceStatus.deleted ||
connection.endSpace.status == SpaceStatus.deleted;
connection.endSpace.status == SpaceStatus.deleted ||
connection.startSpace.status == SpaceStatus.parentDeleted ||
connection.endSpace.status == SpaceStatus.parentDeleted;
});
}

View File

@ -2,19 +2,23 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/constants/space_icon_const.dart';
class CreateSpaceDialog extends StatefulWidget {
final Function(String, String, List<SelectedProduct> selectedProducts)
onCreateSpace;
final Function(String, String, List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) onCreateSpace;
final List<ProductModel>? products;
final String? name;
final String? icon;
@ -22,6 +26,9 @@ class CreateSpaceDialog extends StatefulWidget {
final List<SelectedProduct> selectedProducts;
final SpaceModel? parentSpace;
final SpaceModel? editSpace;
final List<SpaceTemplateModel>? spaceModels;
final List<SubspaceModel>? subspaces;
final List<Tag>? tags;
const CreateSpaceDialog(
{super.key,
@ -32,7 +39,10 @@ class CreateSpaceDialog extends StatefulWidget {
this.icon,
this.isEdit = false,
this.editSpace,
this.selectedProducts = const []});
this.selectedProducts = const [],
this.spaceModels,
this.subspaces,
this.tags});
@override
CreateSpaceDialogState createState() => CreateSpaceDialogState();
@ -40,12 +50,15 @@ class CreateSpaceDialog extends StatefulWidget {
class CreateSpaceDialogState extends State<CreateSpaceDialog> {
String selectedIcon = Assets.location;
SpaceTemplateModel? selectedSpaceModel;
String enteredName = '';
List<SelectedProduct> selectedProducts = [];
late TextEditingController nameController;
bool isOkButtonEnabled = false;
bool isNameFieldInvalid = false;
bool isNameFieldExist = false;
List<SubspaceModel>? subspaces;
List<Tag>? tags;
@override
void initState() {
@ -58,196 +71,485 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
enteredName.isNotEmpty || nameController.text.isNotEmpty;
}
@override
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return AlertDialog(
title: widget.isEdit
? const Text('Edit Space')
: const Text('Create New Space'),
backgroundColor: ColorsManager.whiteColors,
content: SizedBox(
width: screenWidth * 0.5, // Limit dialog width
width: screenWidth * 0.5,
child: SingleChildScrollView(
// Scrollable content to prevent overflow
child: Column(
mainAxisSize: MainAxisSize.min,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
alignment: Alignment.center,
children: [
Container(
width: screenWidth * 0.1, // Adjusted width
height: screenWidth * 0.1, // Adjusted height
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
),
SvgPicture.asset(
selectedIcon,
width: screenWidth * 0.04,
height: screenWidth * 0.04,
),
Positioned(
top: 6,
right: 6,
child: InkWell(
onTap: _showIconSelectionDialog,
child: Container(
width: screenWidth * 0.020,
height: screenWidth * 0.020,
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
shape: BoxShape.circle,
),
child: SvgPicture.asset(
Assets.iconEdit,
width: screenWidth * 0.06,
height: screenWidth * 0.06,
),
),
),
),
],
),
const SizedBox(width: 16),
Expanded(
// Ensure the text field expands responsively
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 50),
Stack(
alignment: Alignment.center,
children: [
TextField(
controller: nameController,
onChanged: (value) {
enteredName = value.trim();
setState(() {
isNameFieldExist = false;
isOkButtonEnabled = false;
isNameFieldInvalid = value.isEmpty;
if (!isNameFieldInvalid) {
if (_isNameConflict(value)) {
isNameFieldExist = true;
isOkButtonEnabled = false;
} else {
isNameFieldExist = false;
isOkButtonEnabled = true;
}
}
});
},
style: const TextStyle(color: ColorsManager.blackColor),
decoration: InputDecoration(
hintText: 'Please enter the name',
hintStyle: const TextStyle(
fontSize: 14,
color: ColorsManager.lightGrayColor,
fontWeight: FontWeight.w400,
),
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: isNameFieldInvalid || isNameFieldExist
? ColorsManager.red
: ColorsManager.boxColor,
width: 1.5,
Container(
width: screenWidth * 0.1,
height: screenWidth * 0.1,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
),
SvgPicture.asset(
selectedIcon,
width: screenWidth * 0.04,
height: screenWidth * 0.04,
),
Positioned(
top: 20,
right: 20,
child: InkWell(
onTap: _showIconSelectionDialog,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: ColorsManager.boxColor,
width: 1.5,
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
),
),
if (isNameFieldInvalid)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Space name should not be empty.',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
if (isNameFieldExist)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Name already exist',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
const SizedBox(height: 16),
if (selectedProducts.isNotEmpty)
_buildSelectedProductsButtons(widget.products ?? [])
else
DefaultButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AddDeviceWidget(
products: widget.products,
onProductsSelected: (selectedProductsMap) {
setState(() {
selectedProducts = selectedProductsMap;
});
},
),
);
},
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: ColorsManager.blackColor,
borderColor: ColorsManager.neutralGray,
borderRadius: 16.0,
padding: 10.0, // Reduced padding for smaller size
child: Align(
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 6.0),
child: SvgPicture.asset(
Assets.addIcon,
width: screenWidth *
0.015, // Adjust icon size
height: screenWidth * 0.015,
),
),
const SizedBox(width: 3),
Flexible(
child: Text(
'Add devices / Assign a space model',
overflow: TextOverflow
.ellipsis, // Prevent overflow
style: Theme.of(context)
.textTheme
.bodyMedium,
),
),
],
),
)),
],
),
),
],
],
),
),
const SizedBox(width: 20),
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: nameController,
onChanged: (value) {
enteredName = value.trim();
setState(() {
isNameFieldExist = false;
isOkButtonEnabled = false;
isNameFieldInvalid = value.isEmpty;
if (!isNameFieldInvalid) {
if (_isNameConflict(value)) {
isNameFieldExist = true;
isOkButtonEnabled = false;
} else {
isNameFieldExist = false;
isOkButtonEnabled = true;
}
}
});
},
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: 'Please enter the name',
hintStyle: const TextStyle(
fontSize: 14,
color: ColorsManager.lightGrayColor,
fontWeight: FontWeight.w400,
),
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: isNameFieldInvalid || isNameFieldExist
? ColorsManager.red
: ColorsManager.boxColor,
width: 1.5,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: ColorsManager.boxColor,
width: 1.5,
),
),
),
),
if (isNameFieldInvalid)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Space name should not be empty.',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
if (isNameFieldExist)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Name already exist',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
const SizedBox(height: 10),
selectedSpaceModel == null
? DefaultButton(
onPressed: () {
_showLinkSpaceModelDialog(context);
},
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: Colors.black,
borderColor: ColorsManager.neutralGray,
borderRadius: 16.0,
padding: 10.0, // Reduced padding for smaller size
child: Align(
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 6.0),
child: SvgPicture.asset(
Assets.link,
width: screenWidth *
0.015, // Adjust icon size
height: screenWidth * 0.015,
),
),
const SizedBox(width: 3),
Flexible(
child: Text(
'Link a space model',
overflow: TextOverflow
.ellipsis, // Prevent overflow
style: Theme.of(context)
.textTheme
.bodyMedium,
),
),
],
),
),
)
: Container(
width: screenWidth * 0.35,
padding: const EdgeInsets.symmetric(
vertical: 10.0, horizontal: 16.0),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
Chip(
label: Text(
selectedSpaceModel?.modelName ?? '',
style: const TextStyle(
color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
color: ColorsManager.transparentColor,
width: 0,
),
),
deleteIcon: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const Icon(
Icons.close,
size: 16,
color: ColorsManager.lightGrayColor,
),
),
onDeleted: () => setState(() {
this.selectedSpaceModel = null;
})),
],
),
),
const SizedBox(height: 25),
const Row(
children: [
Expanded(
child: Divider(
color: ColorsManager.neutralGray,
thickness: 1.0,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'OR',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: Divider(
color: ColorsManager.neutralGray,
thickness: 1.0,
),
),
],
),
const SizedBox(height: 25),
subspaces == null
? DefaultButton(
onPressed: () {
_showSubSpaceDialog(context, enteredName, [],
false, widget.products, subspaces);
},
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: Colors.black,
borderColor: ColorsManager.neutralGray,
borderRadius: 16.0,
padding: 10.0, // Reduced padding for smaller size
child: Align(
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 6.0),
child: SvgPicture.asset(
Assets.addIcon,
width: screenWidth *
0.015, // Adjust icon size
height: screenWidth * 0.015,
),
),
const SizedBox(width: 3),
Flexible(
child: Text(
'Create sub space',
overflow: TextOverflow
.ellipsis, // Prevent overflow
style: Theme.of(context)
.textTheme
.bodyMedium,
),
),
],
),
),
)
: SizedBox(
width: screenWidth * 0.35,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
if (subspaces != null)
...subspaces!.map(
(subspace) => Chip(
label: Text(
subspace.subspaceName,
style: const TextStyle(
color: ColorsManager
.spaceColor), // Text color
),
backgroundColor: ColorsManager
.whiteColors, // Chip background color
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
16), // Rounded chip
side: const BorderSide(
color: ColorsManager
.spaceColor), // Border color
),
),
),
GestureDetector(
onTap: () async {
_showSubSpaceDialog(
context,
enteredName,
[],
false,
widget.products,
subspaces);
},
child: Chip(
label: const Text(
'Edit',
style: TextStyle(
color: ColorsManager.spaceColor),
),
backgroundColor:
ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor),
),
),
),
],
),
),
),
const SizedBox(height: 10),
(tags?.isNotEmpty == true ||
subspaces?.any((subspace) =>
subspace.tags?.isNotEmpty == true) ==
true)
? SizedBox(
width: screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
// Combine tags from spaceModel and subspaces
..._groupTags([
...?tags,
...?subspaces?.expand(
(subspace) => subspace.tags ?? [])
]).entries.map(
(entry) => Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
entry.key.icon ??
'assets/icons/gateway.svg',
fit: BoxFit.contain,
),
),
label: Text(
'x${entry.value}', // Show count
style: const TextStyle(
color: ColorsManager.spaceColor,
),
),
backgroundColor:
ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor,
),
),
),
),
GestureDetector(
onTap: () async {
_showTagCreateDialog(context, enteredName,
widget.products);
// Edit action
},
child: Chip(
label: const Text(
'Edit',
style: TextStyle(
color: ColorsManager.spaceColor),
),
backgroundColor:
ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor),
),
),
),
],
),
),
)
: DefaultButton(
onPressed: () {
_showTagCreateDialog(
context, enteredName, widget.products);
},
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: Colors.black,
borderColor: ColorsManager.neutralGray,
borderRadius: 16.0,
padding: 10.0, // Reduced padding for smaller size
child: Align(
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(left: 6.0),
child: SvgPicture.asset(
Assets.addIcon,
width: screenWidth *
0.015, // Adjust icon size
height: screenWidth * 0.015,
),
),
const SizedBox(width: 3),
Flexible(
child: Text(
'Add devices',
overflow: TextOverflow
.ellipsis, // Prevent overflow
style: Theme.of(context)
.textTheme
.bodyMedium,
),
),
],
),
),
)
],
),
),
],
),
@ -277,8 +579,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
? enteredName
: (widget.name ?? '');
if (newName.isNotEmpty) {
widget.onCreateSpace(
newName, selectedIcon, selectedProducts);
widget.onCreateSpace(newName, selectedIcon,
selectedProducts, selectedSpaceModel,subspaces,tags);
Navigator.of(context).pop();
}
}
@ -313,74 +615,6 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
Widget _buildSelectedProductsButtons(List<ProductModel> products) {
final screenWidth = MediaQuery.of(context).size.width;
return Container(
width: screenWidth * 0.6,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: ColorsManager.neutralGray,
width: 2,
),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0; i < selectedProducts.length; i++) ...[
HoverableButton(
iconPath:
_mapIconToProduct(selectedProducts[i].productId, products),
text: 'x${selectedProducts[i].count}',
onTap: () {
setState(() {
selectedProducts.remove(selectedProducts[i]);
});
// Handle button tap
},
),
if (i < selectedProducts.length - 1)
const SizedBox(
width: 2), // Add space except after the last button
],
const SizedBox(width: 2),
GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => AddDeviceWidget(
products: widget.products,
initialSelectedProducts: selectedProducts,
onProductsSelected: (selectedProductsMap) {
setState(() {
selectedProducts = selectedProductsMap;
});
},
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.add,
color: ColorsManager.spaceColor,
size: 24,
),
),
),
],
),
);
}
bool _isNameConflict(String value) {
return (widget.parentSpace?.children.any((child) => child.name == value) ??
false) ||
@ -390,20 +624,133 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
false);
}
String _mapIconToProduct(String uuid, List<ProductModel> products) {
// Find the product with the matching UUID
final product = products.firstWhere(
(product) => product.uuid == uuid,
orElse: () => ProductModel(
uuid: '',
catName: '',
prodId: '',
prodType: '',
name: '',
icon: Assets.presenceSensor,
),
void _showLinkSpaceModelDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return LinkSpaceModelDialog(
spaceModels: widget.spaceModels ?? [],
onSave: (selectedModel) {
if (selectedModel != null) {
setState(() {
selectedSpaceModel = selectedModel;
subspaces = null;
});
}
},
);
},
);
}
return product.icon ?? Assets.presenceSensor;
void _showSubSpaceDialog(
BuildContext context,
String name,
final List<Tag>? spaceTags,
bool isEdit,
List<ProductModel>? products,
final List<SubspaceModel>? existingSubSpaces) {
showDialog(
context: context,
builder: (BuildContext context) {
return CreateSubSpaceDialog(
spaceName: name,
dialogTitle: 'Create Sub-space',
spaceTags: spaceTags,
isEdit: isEdit,
products: products,
existingSubSpaces: existingSubSpaces,
onSave: (slectedSubspaces) {
if (slectedSubspaces != null) {
setState(() {
subspaces = slectedSubspaces;
selectedSpaceModel = null;
});
}
});
},
);
}
void _showTagCreateDialog(
BuildContext context, String name, List<ProductModel>? products) {
showDialog(
context: context,
builder: (BuildContext context) {
return AddDeviceTypeWidget(
spaceName: name,
products: products,
subspaces: subspaces,
spaceTags: tags,
allTags: [],
initialSelectedProducts:
createInitialSelectedProducts(tags, subspaces),
onSave: (selectedSpaceTags, selectedSubspaces) {
setState(() {
tags = selectedSpaceTags;
selectedSpaceModel = null;
if (selectedSubspaces != null) {
if (subspaces != null) {
for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags;
}
}
}
}
}
});
},
);
},
);
}
List<SelectedProduct> createInitialSelectedProducts(
List<Tag>? tags, List<SubspaceModel>? subspaces) {
final Map<ProductModel, int> productCounts = {};
if (tags != null) {
for (var tag in tags) {
if (tag.product != null) {
productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1;
}
}
}
if (subspaces != null) {
for (var subspace in subspaces) {
if (subspace.tags != null) {
for (var tag in subspace.tags!) {
if (tag.product != null) {
productCounts[tag.product!] =
(productCounts[tag.product!] ?? 0) + 1;
}
}
}
}
}
return productCounts.entries
.map((entry) => SelectedProduct(
productId: entry.key.uuid,
count: entry.value,
productName: entry.key.name ?? 'Unnamed',
product: entry.key,
))
.toList();
}
Map<ProductModel, int> _groupTags(List<Tag> tags) {
final Map<ProductModel, int> groupedTags = {};
for (var tag in tags) {
if (tag.product != null) {
groupedTags[tag.product!] = (groupedTags[tag.product!] ?? 0) + 1;
}
}
return groupedTags;
}
}

View File

@ -11,12 +11,13 @@ import 'package:syncrow_web/pages/spaces_management/space_model/models/space_tem
import 'package:syncrow_web/pages/spaces_management/space_model/view/space_model_page.dart';
import 'package:syncrow_web/services/space_model_mang_api.dart';
class LoadedSpaceView extends StatefulWidget {
class LoadedSpaceView extends StatelessWidget {
final List<CommunityModel> communities;
final CommunityModel? selectedCommunity;
final SpaceModel? selectedSpace;
final List<ProductModel>? products;
final List<SpaceTemplateModel>? spaceModels;
final bool shouldNavigateToSpaceModelPage;
const LoadedSpaceView({
super.key,
@ -25,17 +26,11 @@ class LoadedSpaceView extends StatefulWidget {
this.selectedSpace,
this.products,
this.spaceModels,
required this.shouldNavigateToSpaceModelPage
});
@override
_LoadedStateViewState createState() => _LoadedStateViewState();
}
class _LoadedStateViewState extends State<LoadedSpaceView> {
@override
Widget build(BuildContext context) {
final bool hasSpaceModels =
widget.spaceModels != null && widget.spaceModels!.isNotEmpty;
return Stack(
clipBehavior: Clip.none,
@ -43,29 +38,29 @@ class _LoadedStateViewState extends State<LoadedSpaceView> {
Row(
children: [
SidebarWidget(
communities: widget.communities,
selectedSpaceUuid: widget.selectedSpace?.uuid ??
widget.selectedCommunity?.uuid ??
'',
communities: communities,
selectedSpaceUuid:
selectedSpace?.uuid ?? selectedCommunity?.uuid ?? '',
),
hasSpaceModels
shouldNavigateToSpaceModelPage
? Expanded(
child: BlocProvider(
create: (context) => SpaceModelBloc(
api: SpaceModelManagementApi(),
initialSpaceModels: widget.spaceModels ?? [],
initialSpaceModels: spaceModels ?? [],
),
child: SpaceModelPage(
products: widget.products,
products: products,
),
),
)
: CommunityStructureArea(
selectedCommunity: widget.selectedCommunity,
selectedSpace: widget.selectedSpace,
spaces: widget.selectedCommunity?.spaces ?? [],
products: widget.products,
communities: widget.communities,
selectedCommunity: selectedCommunity,
selectedSpace: selectedSpace,
spaces: selectedCommunity?.spaces ?? [],
products: products,
communities: communities,
spaceModels: spaceModels,
),
],
),

View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class SelectedProductsButtons extends StatelessWidget {
final List<ProductModel> products;
final List<SelectedProduct> selectedProducts;
final Function(List<SelectedProduct>) onProductsUpdated;
final BuildContext context;
const SelectedProductsButtons({
Key? key,
required this.products,
required this.selectedProducts,
required this.onProductsUpdated,
required this.context,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Container(
width: screenWidth * 0.6,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: ColorsManager.neutralGray,
width: 2,
),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
..._buildSelectedProductButtons(),
_buildAddButton(),
],
),
);
}
List<Widget> _buildSelectedProductButtons() {
return [
for (var i = 0; i < selectedProducts.length; i++) ...[
HoverableButton(
iconPath: _mapIconToProduct(selectedProducts[i].productId, products),
text: 'x${selectedProducts[i].count}',
onTap: () {
_removeProduct(i);
},
),
if (i < selectedProducts.length - 1)
const SizedBox(width: 2), // Add space except after the last button
],
];
}
Widget _buildAddButton() {
return GestureDetector(
onTap: _showAddDeviceDialog,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.add,
color: ColorsManager.spaceColor,
size: 24,
),
),
);
}
void _showAddDeviceDialog() {
showDialog(
context: context,
builder: (context) => AddDeviceWidget(
products: products,
initialSelectedProducts: selectedProducts,
onProductsSelected: (selectedProductsMap) {
onProductsUpdated(selectedProductsMap);
},
),
);
}
void _removeProduct(int index) {
final updatedProducts = [...selectedProducts];
updatedProducts.removeAt(index);
onProductsUpdated(updatedProducts);
}
String _mapIconToProduct(String uuid, List<ProductModel> products) {
// Find the product with the matching UUID
final product = products.firstWhere(
(product) => product.uuid == uuid,
orElse: () => ProductModel(
uuid: '',
catName: '',
prodId: '',
prodType: '',
name: '',
icon: Assets.presenceSensor,
),
);
return product.icon ?? Assets.presenceSensor;
}
}

View File

@ -0,0 +1,148 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
AssignTagBloc() : super(AssignTagInitial()) {
on<InitializeTags>((event, emit) {
final initialTags = event.initialTags ?? [];
final existingTagCounts = <String, int>{};
for (var tag in initialTags) {
if (tag.product != null) {
existingTagCounts[tag.product!.uuid] =
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
}
}
final allTags = <Tag>[];
for (var selectedProduct in event.addedProducts) {
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
if (selectedProduct.count == 0 ||
selectedProduct.count <= existingCount) {
allTags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId));
continue;
}
final missingCount = selectedProduct.count - existingCount;
allTags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId));
if (missingCount > 0) {
allTags.addAll(List.generate(
missingCount,
(index) => Tag(
tag: '',
product: selectedProduct.product,
location: 'Main Space',
),
));
}
}
emit(AssignTagLoaded(
tags: allTags,
isSaveEnabled: _validateTags(allTags),
));
});
on<UpdateTagEvent>((event, emit) {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags);
tags[event.index].tag = event.tag;
emit(AssignTagLoaded(
tags: tags,
isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
));
}
});
on<UpdateLocation>((event, emit) {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags);
// Use copyWith for immutability
tags[event.index] =
tags[event.index].copyWith(location: event.location);
emit(AssignTagLoaded(
tags: tags,
isSaveEnabled: _validateTags(tags),
));
}
});
on<ValidateTags>((event, emit) {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags);
emit(AssignTagLoaded(
tags: tags,
isSaveEnabled: _validateTags(tags),
errorMessage: _getValidationError(tags),
));
}
});
on<DeleteTag>((event, emit) {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final updatedTags = List<Tag>.from(currentState.tags)
..remove(event.tagToDelete);
emit(AssignTagLoaded(
tags: updatedTags,
isSaveEnabled: _validateTags(updatedTags),
));
} else {
emit(const AssignTagLoaded(
tags: [],
isSaveEnabled: false,
));
}
});
}
bool _validateTags(List<Tag> tags) {
if (tags.isEmpty) {
return false;
}
final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet();
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
return uniqueTags.length == tags.length && !hasEmptyTag;
}
String? _getValidationError(List<Tag> tags) {
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
if (hasEmptyTag) return 'Tags cannot be empty.';
final duplicateTags = tags
.map((tag) => tag.tag?.trim() ?? '')
.fold<Map<String, int>>({}, (map, tag) {
map[tag] = (map[tag] ?? 0) + 1;
return map;
})
.entries
.where((entry) => entry.value > 1)
.map((entry) => entry.key)
.toList();
if (duplicateTags.isNotEmpty) {
return 'Duplicate tags found: ${duplicateTags.join(', ')}';
}
return null;
}
}

View File

@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
abstract class AssignTagEvent extends Equatable {
const AssignTagEvent();
@override
List<Object> get props => [];
}
class InitializeTags extends AssignTagEvent {
final List<Tag>? initialTags;
final List<SelectedProduct> addedProducts;
const InitializeTags({
required this.initialTags,
required this.addedProducts,
});
@override
List<Object> get props => [initialTags ?? [], addedProducts];
}
class UpdateTagEvent extends AssignTagEvent {
final int index;
final String tag;
const UpdateTagEvent({required this.index, required this.tag});
@override
List<Object> get props => [index, tag];
}
class UpdateLocation extends AssignTagEvent {
final int index;
final String location;
const UpdateLocation({required this.index, required this.location});
@override
List<Object> get props => [index, location];
}
class ValidateTags extends AssignTagEvent {}
class DeleteTag extends AssignTagEvent {
final Tag tagToDelete;
final List<Tag> tags;
const DeleteTag({required this.tagToDelete, required this.tags});
@override
List<Object> get props => [tagToDelete, tags];
}

View File

@ -0,0 +1,38 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
abstract class AssignTagState extends Equatable {
const AssignTagState();
@override
List<Object> get props => [];
}
class AssignTagInitial extends AssignTagState {}
class AssignTagLoading extends AssignTagState {}
class AssignTagLoaded extends AssignTagState {
final List<Tag> tags;
final bool isSaveEnabled;
final String? errorMessage;
const AssignTagLoaded({
required this.tags,
required this.isSaveEnabled,
this.errorMessage,
});
@override
List<Object> get props => [tags, isSaveEnabled];
}
class AssignTagError extends AssignTagState {
final String errorMessage;
const AssignTagError(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}

View File

@ -0,0 +1,340 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AssignTagDialog extends StatelessWidget {
final List<ProductModel>? products;
final List<SubspaceModel>? subspaces;
final List<Tag>? initialTags;
final ValueChanged<List<Tag>>? onTagsAssigned;
final List<SelectedProduct> addedProducts;
final List<String>? allTags;
final String spaceName;
final String title;
final Function(List<Tag>, List<SubspaceModel>?)? onSave;
const AssignTagDialog(
{Key? key,
required this.products,
required this.subspaces,
required this.addedProducts,
this.initialTags,
this.onTagsAssigned,
this.allTags,
required this.spaceName,
required this.title,
this.onSave})
: super(key: key);
@override
Widget build(BuildContext context) {
final List<String> locations =
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList();
return BlocProvider(
create: (_) => AssignTagBloc()
..add(InitializeTags(
initialTags: initialTags,
addedProducts: addedProducts,
)),
child: BlocBuilder<AssignTagBloc, AssignTagState>(
builder: (context, state) {
if (state is AssignTagLoaded) {
final controllers = List.generate(
state.tags.length,
(index) => TextEditingController(text: state.tags[index].tag),
);
return AlertDialog(
title: Text(title),
backgroundColor: ColorsManager.whiteColors,
content: SingleChildScrollView(
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(
ColorsManager.dataHeaderGrey),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
DataColumn(
label: Text('#',
style:
Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Device',
style:
Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Tag',
style:
Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Location',
style:
Theme.of(context).textTheme.bodyMedium)),
],
rows: state.tags.isEmpty
? [
const DataRow(cells: [
DataCell(
Center(
child: Text(
'No Data Available',
style: TextStyle(
fontSize: 14,
color: ColorsManager.lightGrayColor,
),
),
),
),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
])
]
: List.generate(state.tags.length, (index) {
final tag = state.tags[index];
final controller = controllers[index];
return DataRow(
cells: [
DataCell(Text(index.toString())),
DataCell(
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
tag.product?.name ?? 'Unknown',
overflow: TextOverflow.ellipsis,
)),
IconButton(
icon: const Icon(Icons.close,
color: ColorsManager.warningRed,
size: 16),
onPressed: () {
context.read<AssignTagBloc>().add(
DeleteTag(
tagToDelete: tag,
tags: state.tags));
},
tooltip: 'Delete Tag',
)
],
),
),
DataCell(
Row(
children: [
Expanded(
child: TextFormField(
controller: controller,
onChanged: (value) {
context
.read<AssignTagBloc>()
.add(UpdateTagEvent(
index: index,
tag: value.trim(),
));
},
decoration: const InputDecoration(
hintText: 'Enter Tag',
border: InputBorder.none,
),
style: const TextStyle(
fontSize: 14,
color: ColorsManager.blackColor,
),
),
),
SizedBox(
width: MediaQuery.of(context)
.size
.width *
0.15,
child: PopupMenuButton<String>(
color: ColorsManager.whiteColors,
icon: const Icon(
Icons.arrow_drop_down,
color:
ColorsManager.blackColor),
onSelected: (value) {
controller.text = value;
context
.read<AssignTagBloc>()
.add(UpdateTagEvent(
index: index,
tag: value,
));
},
itemBuilder: (context) {
return (allTags ?? [])
.where((tagValue) => !state
.tags
.map((e) => e.tag)
.contains(tagValue))
.map((tagValue) {
return PopupMenuItem<String>(
textStyle: const TextStyle(
color: ColorsManager
.textPrimaryColor),
value: tagValue,
child: ConstrainedBox(
constraints:
BoxConstraints(
minWidth: MediaQuery.of(
context)
.size
.width *
0.15,
maxWidth: MediaQuery.of(
context)
.size
.width *
0.15,
),
child: Text(
tagValue,
overflow: TextOverflow
.ellipsis,
),
));
}).toList();
},
),
),
],
),
),
DataCell(
DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: tag.location ?? 'Main',
dropdownColor: ColorsManager
.whiteColors, // Dropdown background
style: const TextStyle(
color: Colors
.black), // Style for selected text
items: [
const DropdownMenuItem<String>(
value: 'Main Space',
child: Text(
'Main Space',
style: TextStyle(
color: ColorsManager
.textPrimaryColor),
),
),
...locations.map((location) {
return DropdownMenuItem<String>(
value: location,
child: Text(
location,
style: const TextStyle(
color: ColorsManager
.textPrimaryColor),
),
);
}).toList(),
],
onChanged: (value) {
if (value != null) {
context
.read<AssignTagBloc>()
.add(UpdateLocation(
index: index,
location: value,
));
}
},
),
),
),
],
);
}),
),
),
if (state.errorMessage != null)
Text(
state.errorMessage!,
style: const TextStyle(color: ColorsManager.warningRed),
),
],
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 10),
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () async {
Navigator.of(context).pop();
},
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
borderRadius: 10,
backgroundColor: state.isSaveEnabled
? ColorsManager.secondaryColor
: ColorsManager.grayColor,
foregroundColor: ColorsManager.whiteColors,
onPressed: state.isSaveEnabled
? () async {
Navigator.of(context).pop();
final assignedTags = <Tag>{};
for (var tag in state.tags) {
if (tag.location == null ||
subspaces == null) {
continue;
}
for (var subspace in subspaces!) {
if (tag.location == subspace.subspaceName) {
subspace.tags ??= [];
subspace.tags!.add(tag);
assignedTags.add(tag);
break;
}
}
}
onSave!(state.tags,subspaces);
}
: null,
child: const Text('Save'),
),
),
const SizedBox(width: 10),
],
),
],
);
} else if (state is AssignTagLoading) {
return const Center(child: CircularProgressIndicator());
} else {
return const Center(child: Text('Something went wrong.'));
}
},
),
);
}
}

View File

@ -40,7 +40,7 @@ class AssignTagModelBloc
(index) => TagModel(
tag: '',
product: selectedProduct.product,
location: 'None',
location: 'Main Space',
),
));
}
@ -127,7 +127,8 @@ class AssignTagModelBloc
}
final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet();
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
return uniqueTags.length == tags.length && !hasEmptyTag;
final isValid = uniqueTags.length == tags.length && !hasEmptyTag;
return isValid;
}
String? _getValidationError(List<TagModel> tags) {

View File

@ -10,16 +10,16 @@ abstract class AssignTagModelEvent extends Equatable {
}
class InitializeTagModels extends AssignTagModelEvent {
final List<TagModel>? initialTags;
final List<TagModel> initialTags;
final List<SelectedProduct> addedProducts;
const InitializeTagModels({
required this.initialTags,
this.initialTags = const [],
required this.addedProducts,
});
@override
List<Object> get props => [initialTags ?? [], addedProducts];
List<Object> get props => [initialTags, addedProducts];
}
class UpdateTag extends AssignTagModelEvent {

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/dialog_dropdown.dart';
import 'package:syncrow_web/common/dialog_textfield_dropdown.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
@ -7,38 +9,43 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_pr
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AssignTagModelsDialog extends StatelessWidget {
final List<ProductModel>? products;
final List<SubspaceTemplateModel>? subspaces;
final List<TagModel>? initialTags;
final List<TagModel> initialTags;
final ValueChanged<List<TagModel>>? onTagsAssigned;
final List<SelectedProduct> addedProducts;
final List<String>? allTags;
final String spaceName;
final String title;
final void Function(
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces)? onUpdate;
const AssignTagModelsDialog({
Key? key,
required this.products,
required this.subspaces,
required this.addedProducts,
this.initialTags,
this.onTagsAssigned,
this.allTags,
required this.spaceName,
required this.title
}) : super(key: key);
const AssignTagModelsDialog(
{Key? key,
required this.products,
required this.subspaces,
required this.addedProducts,
required this.initialTags,
this.onTagsAssigned,
this.allTags,
required this.spaceName,
required this.title,
this.onUpdate})
: super(key: key);
@override
Widget build(BuildContext context) {
final List<String> locations =
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList();
final List<String> locations = (subspaces ?? [])
.map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
return BlocProvider(
create: (_) => AssignTagModelBloc()
..add(InitializeTagModels(
@ -79,6 +86,8 @@ class AssignTagModelsDialog extends StatelessWidget {
style:
Theme.of(context).textTheme.bodyMedium)),
DataColumn(
numeric: false,
headingRowAlignment: MainAxisAlignment.start,
label: Text('Tag',
style:
Theme.of(context).textTheme.bodyMedium)),
@ -109,6 +118,8 @@ class AssignTagModelsDialog extends StatelessWidget {
: List.generate(state.tags.length, (index) {
final tag = state.tags[index];
final controller = controllers[index];
final availableTags = getAvailableTags(
allTags ?? [], state.tags, tag);
return DataRow(
cells: [
@ -123,147 +134,80 @@ class AssignTagModelsDialog extends StatelessWidget {
tag.product?.name ?? 'Unknown',
overflow: TextOverflow.ellipsis,
)),
IconButton(
icon: const Icon(Icons.close,
color: ColorsManager.warningRed,
size: 16),
onPressed: () {
const SizedBox(width: 10),
Container(
width: 20.0,
height: 20.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager
.lightGrayColor,
width: 1.0,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager
.lightGreyColor,
size: 16,
),
onPressed: () {
context
.read<AssignTagModelBloc>()
.add(DeleteTagModel(
tagToDelete: tag,
tags: state.tags));
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment: Alignment
.centerLeft, // Align cell content to the left
child: SizedBox(
width: double
.infinity, // Ensure full width for dropdown
child: DialogTextfieldDropdown(
items: availableTags,
initialValue: tag.tag,
onSelected: (value) {
controller.text = value;
context
.read<AssignTagModelBloc>()
.add(DeleteTagModel(
tagToDelete: tag,
tags: state.tags));
.add(UpdateTag(
index: index,
tag: value,
));
},
tooltip: 'Delete Tag',
)
],
),
),
),
),
DataCell(
Row(
children: [
Expanded(
child: TextFormField(
controller: controller,
onChanged: (value) {
context
.read<AssignTagModelBloc>()
.add(UpdateTag(
index: index,
tag: value.trim(),
));
},
decoration: const InputDecoration(
hintText: 'Enter Tag',
border: InputBorder.none,
),
style: const TextStyle(
fontSize: 14,
color: ColorsManager.blackColor,
),
),
),
SizedBox(
width: MediaQuery.of(context)
.size
.width *
0.15,
child: PopupMenuButton<String>(
color: ColorsManager.whiteColors,
icon: const Icon(
Icons.arrow_drop_down,
color: ColorsManager.blackColor),
onSelected: (value) {
controller.text = value;
context
.read<AssignTagModelBloc>()
.add(UpdateTag(
index: index,
tag: value,
));
},
itemBuilder: (context) {
return (allTags ?? [])
.where((tagValue) => !state
.tags
.map((e) => e.tag)
.contains(tagValue))
.map((tagValue) {
return PopupMenuItem<String>(
textStyle: const TextStyle(
color: ColorsManager
.textPrimaryColor),
value: tagValue,
child: ConstrainedBox(
constraints:
BoxConstraints(
minWidth: MediaQuery.of(
context)
.size
.width *
0.15,
maxWidth: MediaQuery.of(
context)
.size
.width *
0.15,
),
child: Text(
tagValue,
overflow: TextOverflow
.ellipsis,
),
));
}).toList();
},
),
),
],
),
),
DataCell(
DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: tag.location ?? 'None',
dropdownColor: ColorsManager
.whiteColors, // Dropdown background
style: const TextStyle(
color: Colors
.black), // Style for selected text
items: [
const DropdownMenuItem<String>(
value: 'None',
child: Text(
'None',
style: TextStyle(
color: ColorsManager
.textPrimaryColor),
),
),
...locations.map((location) {
return DropdownMenuItem<String>(
value: location,
child: Text(
location,
style: const TextStyle(
color: ColorsManager
.textPrimaryColor),
),
);
}).toList(),
],
onChanged: (value) {
if (value != null) {
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: locations,
selectedValue:
tag.location ?? 'None',
onSelected: (value) {
context
.read<AssignTagModelBloc>()
.add(UpdateLocation(
index: index,
location: value,
));
}
},
),
),
},
)),
),
],
);
@ -284,23 +228,54 @@ class AssignTagModelsDialog extends StatelessWidget {
children: [
const SizedBox(width: 10),
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () async {
Navigator.of(context).pop();
await showDialog(
barrierDismissible: false,
context: context,
builder: (context) => CreateSpaceModelDialog(
products: products,
allTags: allTags,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
subspaceModels: subspaces,
tags: initialTags),
),
);
},
child: Builder(
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
for (var tag in state.tags) {
if (tag.location == null || subspaces == null) {
continue;
}
final previousTagSubspace =
checkTagExistInSubspace(tag, subspaces ?? []);
if (tag.location == 'Main Space') {
removeTagFromSubspace(tag, previousTagSubspace);
} else if (tag.location !=
previousTagSubspace?.subspaceName) {
removeTagFromSubspace(tag, previousTagSubspace);
moveToNewSubspace(tag, subspaces ?? []);
state.tags.removeWhere(
(t) => t.internalId == tag.internalId);
} else {
updateTagInSubspace(tag, previousTagSubspace);
state.tags.removeWhere(
(t) => t.internalId == tag.internalId);
}
}
if (context.mounted) {
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (dialogContext) =>
AddDeviceTypeModelWidget(
products: products,
subspaces: subspaces,
isCreate: false,
initialSelectedProducts: addedProducts,
allTags: allTags,
spaceName: spaceName,
spaceTagModels: state.tags,
onUpdate: (tags, subspaces) {
onUpdate?.call(state.tags, subspaces);
Navigator.of(context).pop();
},
),
);
}
},
),
),
),
const SizedBox(width: 10),
@ -314,34 +289,36 @@ class AssignTagModelsDialog extends StatelessWidget {
onPressed: state.isSaveEnabled
? () async {
Navigator.of(context).pop();
final assignedTags = <TagModel>{};
for (var tag in state.tags) {
if (tag.location == null ||
subspaces == null) {
continue;
}
for (var subspace in subspaces!) {
if (tag.location == subspace.subspaceName) {
subspace.tags ??= [];
subspace.tags!.add(tag);
assignedTags.add(tag);
break;
}
final previousTagSubspace =
checkTagExistInSubspace(
tag, subspaces ?? []);
if (tag.location == 'Main Space') {
removeTagFromSubspace(
tag, previousTagSubspace);
} else if (tag.location !=
previousTagSubspace?.subspaceName) {
removeTagFromSubspace(
tag, previousTagSubspace);
moveToNewSubspace(tag, subspaces ?? []);
state.tags.removeWhere(
(t) => t.internalId == tag.internalId);
} else {
updateTagInSubspace(
tag, previousTagSubspace);
state.tags.removeWhere(
(t) => t.internalId == tag.internalId);
}
}
state.tags.removeWhere(assignedTags.contains);
await showDialog(
barrierDismissible: false,
context: context,
builder: (context) => CreateSpaceModelDialog(
products: products,
allTags: allTags,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
subspaceModels: subspaces,
tags: state.tags),
),
);
onUpdate?.call(state.tags, subspaces);
}
: null,
child: const Text('Save'),
@ -361,4 +338,50 @@ class AssignTagModelsDialog extends StatelessWidget {
),
);
}
List<String> getAvailableTags(
List<String> allTags, List<TagModel> currentTags, TagModel currentTag) {
return allTags
.where((tagValue) => !currentTags
.where((e) => e != currentTag) // Exclude the current row
.map((e) => e.tag)
.contains(tagValue))
.toList();
}
void removeTagFromSubspace(TagModel tag, SubspaceTemplateModel? subspace) {
subspace?.tags?.removeWhere((t) => t.internalId == tag.internalId);
}
SubspaceTemplateModel? checkTagExistInSubspace(
TagModel tag, List<SubspaceTemplateModel>? subspaces) {
if (subspaces == null) return null;
for (var subspace in subspaces) {
if (subspace.tags == null) return null;
for (var t in subspace.tags!) {
if (tag.internalId == t.internalId) return subspace;
}
}
return null;
}
void moveToNewSubspace(TagModel tag, List<SubspaceTemplateModel> subspaces) {
final targetSubspace = subspaces
.firstWhere((subspace) => subspace.subspaceName == tag.location);
targetSubspace.tags ??= [];
if (targetSubspace.tags?.any((t) => t.internalId == tag.internalId) !=
true) {
targetSubspace.tags?.add(tag);
}
}
void updateTagInSubspace(TagModel tag, SubspaceTemplateModel? subspace) {
final currentTag = subspace?.tags?.firstWhere(
(t) => t.internalId == tag.internalId,
);
if (currentTag != null) {
currentTag.tag = tag.tag;
}
}
}

View File

@ -0,0 +1,57 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/utils/constants/action_enum.dart';
import 'subspace_event.dart';
import 'subspace_state.dart';
class SubSpaceBloc extends Bloc<SubSpaceEvent, SubSpaceState> {
SubSpaceBloc() : super(SubSpaceState([], [], '')) {
on<AddSubSpace>((event, emit) {
final existingNames =
state.subSpaces.map((e) => e.subspaceName).toSet();
if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) {
emit(SubSpaceState(
state.subSpaces,
state.updatedSubSpaceModels,
'Subspace name already exists.',
));
} else {
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
..add(event.subSpace);
emit(SubSpaceState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'',
));
}
});
// Handle RemoveSubSpace Event
on<RemoveSubSpace>((event, emit) {
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
..remove(event.subSpace);
final updatedSubspaceModels = List<UpdateSubspaceModel>.from(
state.updatedSubSpaceModels,
);
if (event.subSpace.uuid?.isNotEmpty ?? false) {
updatedSubspaceModels.add(UpdateSubspaceModel(
action: Action.delete,
uuid: event.subSpace.uuid!,
));
}
emit(SubSpaceState(
updatedSubSpaces,
updatedSubspaceModels,
'', // Clear error message
));
});
// Handle UpdateSubSpace Event
}
}

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
abstract class SubSpaceEvent {}
class AddSubSpace extends SubSpaceEvent {
final SubspaceModel subSpace;
AddSubSpace(this.subSpace);
}
class RemoveSubSpace extends SubSpaceEvent {
final SubspaceModel subSpace;
RemoveSubSpace(this.subSpace);
}
class UpdateSubSpace extends SubSpaceEvent {
final SubspaceModel updatedSubSpace;
UpdateSubSpace(this.updatedSubSpace);
}

View File

@ -0,0 +1,26 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
class SubSpaceState {
final List<SubspaceModel> subSpaces;
final List<UpdateSubspaceModel> updatedSubSpaceModels;
final String errorMessage;
SubSpaceState(
this.subSpaces,
this.updatedSubSpaceModels,
this.errorMessage,
);
SubSpaceState copyWith({
List<SubspaceModel>? subSpaces,
List<UpdateSubspaceModel>? updatedSubSpaceModels,
String? errorMessage,
}) {
return SubSpaceState(
subSpaces ?? this.subSpaces,
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
errorMessage ?? this.errorMessage,
);
}
}

View File

@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_event.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_state.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateSubSpaceDialog extends StatelessWidget {
final bool isEdit;
final String dialogTitle;
final List<SubspaceModel>? existingSubSpaces;
final String? spaceName;
final List<Tag>? spaceTags;
final List<ProductModel>? products;
final Function(List<SubspaceModel>?)? onSave;
const CreateSubSpaceDialog(
{Key? key,
required this.isEdit,
required this.dialogTitle,
this.existingSubSpaces,
required this.spaceName,
required this.spaceTags,
required this.products,
required this.onSave})
: super(key: key);
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final textController = TextEditingController();
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: BlocProvider(
create: (_) {
final bloc = SubSpaceBloc();
if (existingSubSpaces != null) {
for (var subSpace in existingSubSpaces!) {
bloc.add(AddSubSpace(subSpace));
}
}
return bloc;
},
child: BlocBuilder<SubSpaceBloc, SubSpaceState>(
builder: (context, state) {
return Container(
color: ColorsManager.whiteColors,
child: SizedBox(
width: screenWidth * 0.35,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
dialogTitle,
style: Theme.of(context)
.textTheme
.headlineLarge
?.copyWith(color: ColorsManager.blackColor),
),
const SizedBox(height: 16),
Container(
width: screenWidth * 0.35,
padding: const EdgeInsets.symmetric(
vertical: 10.0, horizontal: 16.0),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
...state.subSpaces.map(
(subSpace) => Chip(
label: Text(
subSpace.subspaceName,
style: const TextStyle(
color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
color: ColorsManager.transparentColor,
width: 0,
),
),
deleteIcon: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const Icon(
Icons.close,
size: 16,
color: ColorsManager.lightGrayColor,
),
),
onDeleted: () => context
.read<SubSpaceBloc>()
.add(RemoveSubSpace(subSpace)),
),
),
SizedBox(
width: 200,
child: TextField(
controller: textController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: state.subSpaces.isEmpty
? 'Please enter the name'
: null,
hintStyle: const TextStyle(
color: ColorsManager.lightGrayColor),
),
onSubmitted: (value) {
if (value.trim().isNotEmpty) {
context.read<SubSpaceBloc>().add(
AddSubSpace(SubspaceModel(
subspaceName: value.trim(),
disabled: false)));
textController.clear();
}
},
style: const TextStyle(
color: ColorsManager.blackColor),
),
),
if (state.errorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
state.errorMessage,
style: const TextStyle(
color: ColorsManager.warningRed,
fontSize: 12,
),
),
),
],
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () async {},
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
onPressed: () async {
final subSpaces = context
.read<SubSpaceBloc>()
.state
.subSpaces;
onSave!(subSpaces);
Navigator.of(context).pop();
},
backgroundColor: ColorsManager.secondaryColor,
borderRadius: 10,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
),
],
),
],
),
),
));
},
),
),
);
}
}

View File

@ -6,19 +6,23 @@ import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_
import 'package:syncrow_web/utils/constants/action_enum.dart';
class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
SubSpaceModelBloc() : super(SubSpaceModelState([], [], '')) {
SubSpaceModelBloc() : super(SubSpaceModelState([], [], '', {})) {
// Handle AddSubSpaceModel Event
on<AddSubSpaceModel>((event, emit) {
// Check for duplicate names (case-insensitive)
final existingNames =
state.subSpaces.map((e) => e.subspaceName.toLowerCase()).toSet();
if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) {
// Emit state with an error message if duplicate name exists
final updatedDuplicates = Set<String>.from(state.duplicates)
..add(event.subSpace.subspaceName.toLowerCase());
final updatedSubSpaces =
List<SubspaceTemplateModel>.from(state.subSpaces)
..add(event.subSpace);
emit(SubSpaceModelState(
state.subSpaces,
updatedSubSpaces,
state.updatedSubSpaceModels,
'Subspace name already exists.',
'*Duplicated sub-space name',
updatedDuplicates,
));
} else {
// Add subspace if no duplicate exists
@ -29,7 +33,9 @@ class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
emit(SubSpaceModelState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'', // Clear error message
'',
state.duplicates,
// Clear error message
));
}
});
@ -42,6 +48,16 @@ class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
final updatedSubspaceModels = List<UpdateSubspaceTemplateModel>.from(
state.updatedSubSpaceModels,
);
final nameOccurrences = <String, int>{};
for (final subSpace in updatedSubSpaces) {
final lowerName = subSpace.subspaceName.toLowerCase();
nameOccurrences[lowerName] = (nameOccurrences[lowerName] ?? 0) + 1;
}
final updatedDuplicates = nameOccurrences.entries
.where((entry) => entry.value > 1)
.map((entry) => entry.key)
.toSet();
if (event.subSpace.uuid?.isNotEmpty ?? false) {
updatedSubspaceModels.add(UpdateSubspaceTemplateModel(
@ -53,7 +69,9 @@ class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
emit(SubSpaceModelState(
updatedSubSpaces,
updatedSubspaceModels,
'', // Clear error message
'',
updatedDuplicates,
// Clear error message
));
});
@ -78,7 +96,9 @@ class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
emit(SubSpaceModelState(
updatedSubSpaces,
updatedSubspaceModels,
'', // Clear error message
'',
state.duplicates,
// Clear error message
));
});
}

View File

@ -5,22 +5,26 @@ class SubSpaceModelState {
final List<SubspaceTemplateModel> subSpaces;
final List<UpdateSubspaceTemplateModel> updatedSubSpaceModels;
final String errorMessage;
final Set<String> duplicates;
SubSpaceModelState(
this.subSpaces,
this.updatedSubSpaceModels,
this.errorMessage,
this.duplicates,
);
SubSpaceModelState copyWith({
List<SubspaceTemplateModel>? subSpaces,
List<UpdateSubspaceTemplateModel>? updatedSubSpaceModels,
String? errorMessage,
Set<String>? duplicates,
}) {
return SubSpaceModelState(
subSpaces ?? this.subSpaces,
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
errorMessage ?? this.errorMessage,
duplicates ?? this.duplicates,
);
}
}

View File

@ -2,37 +2,25 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateSubSpaceModelDialog extends StatelessWidget {
final bool isEdit;
final String dialogTitle;
final List<SubspaceTemplateModel>? existingSubSpaces;
final String? spaceName;
final List<TagModel>? spaceTagModels;
final List<String>? allTags;
final List<ProductModel>? products;
final SpaceTemplateModel? spaceModel;
final void Function(List<SubspaceTemplateModel> newSubspaces)? onUpdate;
const CreateSubSpaceModelDialog({
Key? key,
required this.isEdit,
required this.dialogTitle,
this.existingSubSpaces,
required this.allTags,
required this.spaceName,
required this.spaceTagModels,
required this.products,
required this.spaceModel,
}) : super(key: key);
const CreateSubSpaceModelDialog(
{Key? key,
required this.isEdit,
required this.dialogTitle,
this.existingSubSpaces,
this.onUpdate})
: super(key: key);
@override
Widget build(BuildContext context) {
@ -58,7 +46,7 @@ class CreateSubSpaceModelDialog extends StatelessWidget {
return Container(
color: ColorsManager.whiteColors,
child: SizedBox(
width: screenWidth * 0.35,
width: screenWidth * 0.3,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@ -85,41 +73,64 @@ class CreateSubSpaceModelDialog extends StatelessWidget {
spacing: 8.0,
runSpacing: 8.0,
children: [
...state.subSpaces.map(
(subSpace) => Chip(
label: Text(
subSpace.subspaceName,
style: const TextStyle(
color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
color: ColorsManager.transparentColor,
width: 0,
),
),
deleteIcon: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
...state.subSpaces.asMap().entries.map(
(entry) {
final index = entry.key;
final subSpace = entry.value;
final lowerName =
subSpace.subspaceName.toLowerCase();
final duplicateIndices = state.subSpaces
.asMap()
.entries
.where((e) =>
e.value.subspaceName.toLowerCase() ==
lowerName)
.map((e) => e.key)
.toList();
final isDuplicate =
duplicateIndices.length > 1 &&
duplicateIndices.indexOf(index) != 0;
return Chip(
label: Text(
subSpace.subspaceName,
style: const TextStyle(
color: ColorsManager.spaceColor,
),
),
child: const Icon(
Icons.close,
size: 16,
color: ColorsManager.lightGrayColor,
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(
color: isDuplicate
? ColorsManager.red
: ColorsManager.transparentColor,
width: 0,
),
),
),
onDeleted: () => context
.read<SubSpaceModelBloc>()
.add(RemoveSubSpaceModel(subSpace)),
),
deleteIcon: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const Icon(
Icons.close,
size: 16,
color: ColorsManager.lightGrayColor,
),
),
onDeleted: () => context
.read<SubSpaceModelBloc>()
.add(RemoveSubSpaceModel(subSpace)),
);
},
),
SizedBox(
width: 200,
@ -147,20 +158,20 @@ class CreateSubSpaceModelDialog extends StatelessWidget {
color: ColorsManager.blackColor),
),
),
if (state.errorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
state.errorMessage,
style: const TextStyle(
color: ColorsManager.warningRed,
fontSize: 12,
),
),
),
],
),
),
if (state.errorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(
state.errorMessage,
style: const TextStyle(
color: ColorsManager.red,
fontSize: 12,
),
),
),
const SizedBox(height: 16),
Row(
children: [
@ -169,51 +180,31 @@ class CreateSubSpaceModelDialog extends StatelessWidget {
label: 'Cancel',
onPressed: () async {
Navigator.of(context).pop();
await showDialog(
barrierDismissible: false,
context: context,
builder: (context) =>
CreateSpaceModelDialog(
products: products,
allTags: allTags,
spaceModel: SpaceTemplateModel(
modelName: spaceName ?? '',
subspaceModels: existingSubSpaces,
tags: spaceTagModels,
),
),
);
},
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
onPressed: () async {
final subSpaces = context
.read<SubSpaceModelBloc>()
.state
.subSpaces;
Navigator.of(context).pop();
await showDialog(
barrierDismissible: false,
context: context,
builder: (context) =>
CreateSpaceModelDialog(
products: products,
allTags: allTags,
spaceModel: SpaceTemplateModel(
modelName: spaceName ?? '',
subspaceModels: subSpaces,
tags: spaceTagModels,
),
),
);
},
onPressed: (state.subSpaces.isEmpty ||
state.errorMessage.isNotEmpty)
? null
: () async {
final subSpaces = context
.read<SubSpaceModelBloc>()
.state
.subSpaces;
Navigator.of(context).pop();
if (onUpdate != null) {
onUpdate!(subSpaces);
}
},
backgroundColor: ColorsManager.secondaryColor,
borderRadius: 10,
foregroundColor: ColorsManager.whiteColors,
foregroundColor: state.subSpaces.isEmpty ||
state.errorMessage.isNotEmpty
? ColorsManager.whiteColorsWithOpacity
: ColorsManager.whiteColors,
child: const Text('OK'),
),
),

View File

@ -0,0 +1,80 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
class TagHelper {
static List<TagModel> generateInitialTags({
List<TagModel>? spaceTagModels,
List<SubspaceTemplateModel>? subspaces,
}) {
final List<TagModel> initialTags = [];
if (spaceTagModels != null) {
initialTags.addAll(spaceTagModels);
}
if (subspaces != null) {
for (var subspace in subspaces) {
if (subspace.tags != null) {
for (var existingTag in subspace.tags!) {
initialTags.addAll(
subspace.tags!.map(
(tag) => tag.copyWith(
location: subspace.subspaceName,
internalId: existingTag.internalId,
tag: existingTag.tag),
),
);
}
}
}
}
return initialTags;
}
static Map<ProductModel, int> groupTags(List<TagModel> tags) {
final Map<ProductModel, int> groupedTags = {};
for (var tag in tags) {
if (tag.product != null) {
groupedTags[tag.product!] = (groupedTags[tag.product!] ?? 0) + 1;
}
}
return groupedTags;
}
static List<SelectedProduct> createInitialSelectedProducts(
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces) {
final Map<ProductModel, int> productCounts = {};
if (tags != null) {
for (var tag in tags) {
if (tag.product != null) {
productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1;
}
}
}
if (subspaces != null) {
for (var subspace in subspaces) {
if (subspace.tags != null) {
for (var tag in subspace.tags!) {
if (tag.product != null) {
productCounts[tag.product!] =
(productCounts[tag.product!] ?? 0) + 1;
}
}
}
}
}
return productCounts.entries
.map((entry) => SelectedProduct(
productId: entry.key.uuid,
count: entry.value,
productName: entry.key.name ?? 'Unnamed',
product: entry.key,
))
.toList();
}
}

View File

@ -0,0 +1,12 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart';
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart';
class SpaceModelBloc extends Bloc<SpaceModelEvent, SpaceModelState> {
SpaceModelBloc() : super(SpaceModelInitial()) {
on<SpaceModelSelectedEvent>((event, emit) {
emit(SpaceModelSelectedState(event.selectedIndex));
});
}
}

View File

@ -0,0 +1,7 @@
abstract class SpaceModelEvent {}
class SpaceModelSelectedEvent extends SpaceModelEvent {
final int selectedIndex;
SpaceModelSelectedEvent(this.selectedIndex);
}

View File

@ -0,0 +1,9 @@
abstract class SpaceModelState {}
class SpaceModelInitial extends SpaceModelState {}
class SpaceModelSelectedState extends SpaceModelState {
final int selectedIndex;
SpaceModelSelectedState(this.selectedIndex);
}

View File

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart';
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/space_model_card_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class LinkSpaceModelDialog extends StatelessWidget {
final Function(SpaceTemplateModel?)? onSave;
final int? initialSelectedIndex;
final List<SpaceTemplateModel> spaceModels;
const LinkSpaceModelDialog({
Key? key,
this.onSave,
this.initialSelectedIndex,
required this.spaceModels,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SpaceModelBloc()
..add(
SpaceModelSelectedEvent(initialSelectedIndex ?? -1),
),
child: Builder(
builder: (context) {
final bloc = context.read<SpaceModelBloc>();
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
title: const Text('Link a space model'),
content: spaceModels.isNotEmpty
? Container(
color: ColorsManager.textFieldGreyColor,
width: MediaQuery.of(context).size.width * 0.7,
height: MediaQuery.of(context).size.height * 0.6,
child: BlocBuilder<SpaceModelBloc, SpaceModelState>(
builder: (context, state) {
int selectedIndex = -1;
if (state is SpaceModelSelectedState) {
selectedIndex = state.selectedIndex;
}
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
childAspectRatio: 3,
),
itemCount: spaceModels.length,
itemBuilder: (BuildContext context, int index) {
final model = spaceModels[index];
final isSelected = selectedIndex == index;
return GestureDetector(
onTap: () {
bloc.add(SpaceModelSelectedEvent(index));
},
child: Container(
margin: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
border: Border.all(
color: isSelected
? ColorsManager.spaceColor
: Colors.transparent,
width: 2.0,
),
borderRadius: BorderRadius.circular(8.0),
),
child: SpaceModelCardWidget(model: model,),
),
);
},
);
},
),
)
: const Text('No space models available.'),
actions: [
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CancelButton(
onPressed: () {
Navigator.of(context).pop();
},
label: 'Cancel',
),
const SizedBox(width: 10),
BlocBuilder<SpaceModelBloc, SpaceModelState>(
builder: (context, state) {
final isEnabled = state is SpaceModelSelectedState &&
state.selectedIndex >= 0;
return SizedBox(
width: 140,
child: DefaultButton(
height: 40,
borderRadius: 10,
onPressed: isEnabled
? () {
if (onSave != null) {
final selectedModel =
spaceModels[state.selectedIndex];
onSave!(selectedModel);
}
Navigator.of(context).pop();
}
: null,
child: const Text('Save'),
),
);
},
),
],
),
),
],
);
},
),
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_spac
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
import 'package:syncrow_web/services/space_model_mang_api.dart';
@ -67,28 +68,111 @@ class CreateSpaceModelBloc
_space = event.spaceTemplate;
emit(CreateSpaceModelLoaded(_space!));
});
on<AddSubspacesToSpaceTemplate>((event, emit) {
final currentState = state;
if (currentState is CreateSpaceModelLoaded) {
final updatedSpace = currentState.space.copyWith(
subspaceModels: [
...(_space!.subspaceModels ?? []),
...event.subspaces,
],
);
final eventSubspaceIds =
event.subspaces.map((e) => e.internalId).toSet();
// Update or retain subspaces
final updatedSubspaces = currentState.space.subspaceModels
?.where((subspace) =>
eventSubspaceIds.contains(subspace.internalId))
.map((subspace) {
final matchingEventSubspace = event.subspaces.firstWhere(
(e) => e.internalId == subspace.internalId,
orElse: () => subspace,
);
// Update the subspace's tags
final eventTagIds = matchingEventSubspace.tags
?.map((e) => e.internalId)
.toSet() ??
{};
final updatedTags = [
...?subspace.tags?.map<TagModel>((tag) {
final matchingTag =
matchingEventSubspace.tags?.firstWhere(
(e) => e.internalId == tag.internalId,
orElse: () => tag,
);
final isUpdated = matchingTag != tag;
return isUpdated
? tag.copyWith(tag: matchingTag?.tag)
: tag;
}) ??
<TagModel>[],
...?matchingEventSubspace.tags?.where(
(e) =>
subspace.tags
?.every((t) => t.internalId != e.internalId) ??
true,
) ??
<TagModel>[],
];
return subspace.copyWith(
subspaceName: matchingEventSubspace.subspaceName,
tags: updatedTags,
);
}).toList() ??
[];
// Add new subspaces
event.subspaces
.where((e) =>
updatedSubspaces.every((s) => s.internalId != e.internalId))
.forEach((newSubspace) {
updatedSubspaces.add(newSubspace);
});
final updatedSpace =
currentState.space.copyWith(subspaceModels: updatedSubspaces);
emit(CreateSpaceModelLoaded(updatedSpace));
} else {
emit(CreateSpaceModelError("Space template not initialized"));
}
});
on<AddTagsToSpaceTemplate>((event, emit) {
final currentState = state;
if (currentState is CreateSpaceModelLoaded) {
final eventTagIds = event.tags.map((e) => e.internalId).toSet();
final updatedTags = currentState.space.tags
?.where((tag) => eventTagIds.contains(tag.internalId))
.map((tag) {
final matchingEventTag = event.tags.firstWhere(
(e) => e.internalId == tag.internalId,
orElse: () => tag,
);
return matchingEventTag != tag
? tag.copyWith(tag: matchingEventTag.tag)
: tag;
}).toList() ??
[];
event.tags
.where(
(e) => updatedTags.every((t) => t.internalId != e.internalId))
.forEach((e) {
updatedTags.add(e);
});
emit(CreateSpaceModelLoaded(
currentState.space.copyWith(tags: updatedTags)));
} else {
emit(CreateSpaceModelError("Space template not initialized"));
}
});
on<UpdateSpaceTemplateName>((event, emit) {
final currentState = state;
if (currentState is CreateSpaceModelLoaded) {
if (event.name.trim().isEmpty) {
emit(CreateSpaceModelLoaded(
currentState.space,
errorMessage: "Model name cannot be empty",
@ -96,7 +180,7 @@ class CreateSpaceModelBloc
} else {
final updatedSpaceModel =
currentState.space.copyWith(modelName: event.name);
emit(CreateSpaceModelLoaded(updatedSpaceModel));
}
} else {

View File

@ -1,6 +1,7 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
abstract class CreateSpaceModelEvent extends Equatable {
const CreateSpaceModelEvent();
@ -21,6 +22,7 @@ class CreateSpaceTemplate extends CreateSpaceModelEvent {
final SpaceTemplateModel spaceTemplate;
final Function(SpaceTemplateModel)? onCreate;
const CreateSpaceTemplate({
required this.spaceTemplate,
this.onCreate,
@ -45,6 +47,13 @@ class AddSubspacesToSpaceTemplate extends CreateSpaceModelEvent {
AddSubspacesToSpaceTemplate(this.subspaces);
}
class AddTagsToSpaceTemplate extends CreateSpaceModelEvent {
final List<TagModel> tags;
AddTagsToSpaceTemplate(this.tags);
}
class ValidateSpaceTemplateName extends CreateSpaceModelEvent {
final String name;

View File

@ -13,7 +13,7 @@ class SpaceTemplateModel extends Equatable {
String internalId;
@override
List<Object?> get props => [modelName, subspaceModels];
List<Object?> get props => [modelName, subspaceModels, tags];
SpaceTemplateModel({
this.uuid,

View File

@ -1,22 +1,28 @@
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
import 'package:uuid/uuid.dart';
class SubspaceTemplateModel {
final String? uuid;
String subspaceName;
final bool disabled;
List<TagModel>? tags;
String internalId;
SubspaceTemplateModel({
this.uuid,
required this.subspaceName,
required this.disabled,
this.tags,
});
String? internalId,
}) : internalId = internalId ?? const Uuid().v4();
factory SubspaceTemplateModel.fromJson(Map<String, dynamic> json) {
final String internalId = json['internalId'] ?? const Uuid().v4();
return SubspaceTemplateModel(
uuid: json['uuid'] ?? '',
subspaceName: json['subspaceName'] ?? '',
internalId: internalId,
disabled: json['disabled'] ?? false,
tags: (json['tags'] as List<dynamic>?)
?.map((item) => TagModel.fromJson(item))
@ -33,4 +39,20 @@ class SubspaceTemplateModel {
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
};
}
SubspaceTemplateModel copyWith({
String? uuid,
String? subspaceName,
bool? disabled,
List<TagModel>? tags,
String? internalId,
}) {
return SubspaceTemplateModel(
uuid: uuid ?? this.uuid,
subspaceName: subspaceName ?? this.subspaceName,
disabled: disabled ?? this.disabled,
tags: tags ?? this.tags,
internalId: internalId ?? this.internalId,
);
}
}

View File

@ -0,0 +1,16 @@
class CreateTagBodyModel {
late String tag;
late final String? productUuid;
Map<String, dynamic> toJson() {
return {
'tag': tag,
'productUuid': productUuid,
};
}
@override
String toString() {
return toJson().toString();
}
}

View File

@ -30,15 +30,16 @@ class TagModel {
);
}
TagModel copyWith({
String? tag,
ProductModel? product,
String? location,
}) {
TagModel copyWith(
{String? tag,
ProductModel? product,
String? location,
String? internalId}) {
return TagModel(
tag: tag ?? this.tag,
product: product ?? this.product,
location: location ?? this.location,
internalId: internalId ?? this.internalId,
);
}
@ -58,4 +59,4 @@ extension TagModelExtensions on TagModel {
..tag = tag ?? ''
..productUuid = product?.uuid;
}
}
}

View File

@ -67,10 +67,24 @@ class SpaceModelPage extends StatelessWidget {
}
// Render existing space model
final model = spaceModels[index];
return Container(
margin: const EdgeInsets.all(8.0),
child: SpaceModelCardWidget(model: model),
);
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return CreateSpaceModelDialog(
products: products,
allTags: allTagValues,
spaceModel: model,
onLoad: (newModel) {},
);
},
);
},
child: Container(
margin: const EdgeInsets.all(8.0),
child: SpaceModelCardWidget(model: model),
));
},
),
),
@ -94,14 +108,14 @@ class SpaceModelPage extends StatelessWidget {
double _calculateChildAspectRatio(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
if (screenWidth > 1600) {
return 2; // Taller cards for larger screens
return 2;
}
if (screenWidth > 1200) {
return 3; // Adjusted height for medium screens
return 3;
} else if (screenWidth > 800) {
return 3.5; // Adjusted height for smaller screens
return 3.5;
} else {
return 4.0; // Default ratio for smallest screens
return 4.0;
}
}

View File

@ -44,15 +44,15 @@ class CreateSpaceModelDialog extends StatelessWidget {
child: BlocProvider(
create: (_) {
final bloc = CreateSpaceModelBloc(_spaceModelApi);
if (spaceModel != null) {
bloc.add(UpdateSpaceTemplate(spaceModel!));
} else {
bloc.add(UpdateSpaceTemplate(SpaceTemplateModel(
modelName: '',
subspaceModels: const [],
)));
}
if (spaceModel != null) {
bloc.add(UpdateSpaceTemplate(spaceModel!));
} else {
bloc.add(UpdateSpaceTemplate(SpaceTemplateModel(
modelName: '',
subspaceModels: const [],
)));
}
spaceNameController.addListener(() {
bloc.add(UpdateSpaceTemplateName(name: spaceNameController.text));
});
@ -72,7 +72,9 @@ class CreateSpaceModelDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Create New Space Model',
spaceModel?.uuid == null
? 'Create New Space Model'
: 'Edit Space Model',
style: Theme.of(context)
.textTheme
.headlineLarge
@ -108,21 +110,40 @@ class CreateSpaceModelDialog extends StatelessWidget {
),
),
const SizedBox(height: 16),
SubspaceModelCreate(context,
subspaces: state.space.subspaceModels ?? [],
allTags: allTags,
products: products,
spaceModel: spaceModel,
spaceTagModels: spaceModel?.tags ?? [],
spaceNameController: spaceNameController),
SubspaceModelCreate(
context,
subspaces: state.space.subspaceModels ?? [],
onSpaceModelUpdate: (updatedSubspaces) {
context
.read<CreateSpaceModelBloc>()
.add(AddSubspacesToSpaceTemplate(updatedSubspaces));
},
),
const SizedBox(height: 10),
TagChipDisplay(context,
screenWidth: screenWidth,
spaceModel: updatedSpaceModel,
products: products,
subspaces: subspaces,
allTags: allTags,
spaceNameController: spaceNameController),
TagChipDisplay(
context,
screenWidth: screenWidth,
spaceModel: updatedSpaceModel,
products: products,
subspaces: subspaces,
allTags: allTags,
spaceNameController: spaceNameController,
onLoad: (tags, subspaces) {
if (context.read<CreateSpaceModelBloc>().state
is CreateSpaceModelLoaded) {
if (subspaces != null) {
context
.read<CreateSpaceModelBloc>()
.add(AddSubspacesToSpaceTemplate(subspaces));
}
if (tags != null) {
context
.read<CreateSpaceModelBloc>()
.add(AddTagsToSpaceTemplate(tags));
}
}
},
),
const SizedBox(height: 20),
SizedBox(
width: screenWidth * 0.25,

View File

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/ellipsis_item_widget.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/flexible_item_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DynamicProductWidget extends StatelessWidget {
final Map<String, int> productTagCount;
final double maxWidth;
final double maxHeight;
const DynamicProductWidget({
Key? key,
required this.productTagCount,
required this.maxWidth,
required this.maxHeight,
}) : super(key: key);
@override
Widget build(BuildContext context) {
const double itemSpacing = 8.0;
const double lineSpacing = 8.0;
const double textPadding = 16.0;
const double itemHeight = 40.0;
List<Widget> productWidgets = [];
double currentLineWidth = 0.0;
double currentHeight = itemHeight;
for (final product in productTagCount.entries) {
final String prodType = product.key;
final int count = product.value;
final TextPainter textPainter = TextPainter(
text: TextSpan(
text: 'x$count',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
textDirection: TextDirection.ltr,
)..layout();
final double itemWidth = textPainter.width + textPadding + 20;
if (currentLineWidth + itemWidth + itemSpacing > maxWidth) {
currentHeight += itemHeight + lineSpacing;
if (currentHeight > maxHeight) {
productWidgets.add(const EllipsisItemWidget());
break;
}
currentLineWidth = 0.0;
}
productWidgets.add(FlexibleItemWidget(prodType: prodType, count: count));
currentLineWidth += itemWidth + itemSpacing;
}
return Wrap(
spacing: itemSpacing,
runSpacing: lineSpacing,
children: productWidgets,
);
}
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/room_name_widget.dart';
class DynamicRoomWidget extends StatelessWidget {
final List<SubspaceTemplateModel>? subspaceModels;
final double maxWidth;
final double maxHeight;
const DynamicRoomWidget({
Key? key,
required this.subspaceModels,
required this.maxWidth,
required this.maxHeight,
}) : super(key: key);
@override
Widget build(BuildContext context) {
const double itemSpacing = 8.0;
const double lineSpacing = 8.0;
const double textPadding = 16.0;
const double itemHeight = 30.0;
List<Widget> roomWidgets = [];
double currentLineWidth = 0.0;
double currentHeight = itemHeight;
for (final subspace in subspaceModels!) {
final TextPainter textPainter = TextPainter(
text: TextSpan(
text: subspace.subspaceName,
style: const TextStyle(fontSize: 16),
),
textDirection: TextDirection.ltr,
)..layout();
final double itemWidth = textPainter.width + textPadding;
if (currentLineWidth + itemWidth + itemSpacing > maxWidth) {
currentHeight += itemHeight + lineSpacing;
if (currentHeight > maxHeight) {
roomWidgets.add(const RoomNameWidget(name: "..."));
break;
}
currentLineWidth = 0.0;
}
roomWidgets.add(RoomNameWidget(name: subspace.subspaceName));
currentLineWidth += itemWidth + itemSpacing;
}
return Wrap(
spacing: itemSpacing,
runSpacing: lineSpacing,
children: roomWidgets,
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EllipsisItemWidget extends StatelessWidget {
const EllipsisItemWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ColorsManager.transparentColor),
),
child: Text(
"...",
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class FlexibleItemWidget extends StatelessWidget {
final String prodType;
final int count;
const FlexibleItemWidget({
Key? key,
required this.prodType,
required this.count,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ColorsManager.transparentColor),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
prodType,
width: 15,
height: 16,
),
const SizedBox(width: 4),
Text(
'x$count',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
],
),
);
}
}

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class RoomNameWidget extends StatelessWidget {
final String name;
const RoomNameWidget({Key? key, required this.name}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: ColorsManager.transparentColor),
),
child: Text(
name,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_chip_widget.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dynamic_product_widget.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpaceModelCardWidget extends StatelessWidget {
@ -32,111 +32,81 @@ class SpaceModelCardWidget extends StatelessWidget {
}
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: ColorsManager.lightGrayColor.withOpacity(0.5),
color: Colors.grey.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
padding: const EdgeInsets.fromLTRB(16.0, 14.0, 8.0, 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
model.modelName,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
model.modelName,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.black,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Container
Expanded(
child: Wrap(
spacing: 3.0,
runSpacing: 3.0,
children: [
for (var subspace in model.subspaceModels!
.take(calculateTakeCount(context)))
SubspaceChipWidget(subspace: subspace.subspaceName),
],
flex: 1, // Distribute space proportionally
child: Container(
padding: const EdgeInsets.all(8.0),
child: LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment.topLeft,
child: DynamicRoomWidget(
subspaceModels: model.subspaceModels,
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
),
);
},
),
),
),
if (productTagCount.isNotEmpty)
if (productTagCount.isNotEmpty && model.subspaceModels != null)
Container(
width: 1,
height: double.infinity,
width: 1.0,
color: ColorsManager.softGray,
margin: const EdgeInsets.symmetric(horizontal: 4.0),
margin: const EdgeInsets.symmetric(vertical: 6.0),
),
const SizedBox(width: 7),
Expanded(
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: productTagCount.entries.map((entry) {
final prodType = entry.key;
final count = entry.value;
return Chip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
prodType,
width: 15,
height: 16,
),
const SizedBox(width: 4),
Text(
'x$count', // Product count
style: const TextStyle(fontSize: 12),
),
],
),
backgroundColor: ColorsManager.textFieldGreyColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
color: ColorsManager.transparentColor,
width: 0,
),
),
);
}).toList(),
flex: 1, // Distribute space proportionally
child: Container(
padding: const EdgeInsets.all(8.0),
child: LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment.topLeft,
child: DynamicProductWidget(
productTagCount: productTagCount,
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight));
},
),
),
),
],
),
),
const SizedBox(height: 5),
],
),
);
}
int calculateTakeCount(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
// Adjust the count based on the screen width
if (screenWidth > 1500) {
return 3; // For large screens
} else if (screenWidth > 1200) {
return 2;
} else {
return 1; // For smaller screens
}
}
}

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SubspaceChipWidget extends StatelessWidget {
final String subspace;
@ -11,24 +14,25 @@ class SubspaceChipWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Chip(
label: Text(
subspace,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
backgroundColor: ColorsManager.textFieldGreyColor,
labelStyle: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
shape: RoundedRectangleBorder(
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
border: Border.all(
color: ColorsManager.transparentColor,
width: 0,
),
),
child: Text(
subspace,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
);
}
}

View File

@ -1,30 +1,17 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SubspaceModelCreate extends StatelessWidget {
final List<SubspaceTemplateModel> subspaces;
final TextEditingController spaceNameController;
final List<TagModel>? spaceTagModels;
final List<String>? allTags;
final List<ProductModel>? products;
final SpaceTemplateModel? spaceModel;
final void Function(List<SubspaceTemplateModel> newSubspaces)?
onSpaceModelUpdate;
const SubspaceModelCreate(
BuildContext context, {
Key? key,
required this.subspaces,
this.spaceTagModels,
required this.allTags,
required this.products,
required this.spaceModel,
required this.spaceNameController,
}) : super(key: key);
const SubspaceModelCreate(BuildContext context,
{Key? key, required this.subspaces, this.onSpaceModelUpdate})
: super(key: key);
@override
Widget build(BuildContext context) {
@ -36,26 +23,7 @@ class SubspaceModelCreate extends StatelessWidget {
overlayColor: ColorsManager.transparentColor,
),
onPressed: () async {
Navigator.of(context).pop();
await showDialog<List<SubspaceTemplateModel>>(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return CreateSubSpaceModelDialog(
allTags: allTags,
spaceName: spaceNameController.text,
spaceModel: spaceModel,
spaceTagModels: spaceTagModels,
products: products,
isEdit: true,
dialogTitle: subspaces.isEmpty
? 'Create Sub-space'
: 'Edit Sub-space',
existingSubSpaces: subspaces,
);
},
);
await _openDialog(context, 'Create Sub-space');
},
child: const ButtonContentWidget(
icon: Icons.add,
@ -85,7 +53,8 @@ class SubspaceModelCreate extends StatelessWidget {
style: const TextStyle(
color: ColorsManager.spaceColor), // Text color
),
backgroundColor: ColorsManager.whiteColors, // Chip background color
backgroundColor:
ColorsManager.whiteColors, // Chip background color
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(16), // Rounded chip
@ -96,37 +65,18 @@ class SubspaceModelCreate extends StatelessWidget {
),
GestureDetector(
onTap: () async {
Navigator.of(context).pop();
await showDialog<List<SubspaceTemplateModel>>(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return CreateSubSpaceModelDialog(
isEdit: true,
dialogTitle: 'Edit Sub-space',
existingSubSpaces: subspaces,
allTags: allTags,
spaceName: spaceNameController.text,
spaceTagModels: spaceTagModels,
products: products,
spaceModel: spaceModel,
);
},
);
await _openDialog(context, 'Edit Sub-space');
},
child: Chip(
label: const Text(
'Edit',
style: TextStyle(
color: ColorsManager.spaceColor),
style: TextStyle(color: ColorsManager.spaceColor),
),
backgroundColor:
ColorsManager.whiteColors,
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor),
borderRadius: BorderRadius.circular(16),
side:
const BorderSide(color: ColorsManager.spaceColor),
),
),
),
@ -136,4 +86,21 @@ class SubspaceModelCreate extends StatelessWidget {
),
);
}
Future<void> _openDialog(BuildContext context, String dialogTitle) async {
await showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return CreateSubSpaceModelDialog(
isEdit: true,
dialogTitle: dialogTitle,
existingSubSpaces: subspaces,
onUpdate: (subspaceModels) {
onSpaceModelUpdate!(subspaceModels);
},
);
},
);
}
}

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
@ -16,17 +17,19 @@ class TagChipDisplay extends StatelessWidget {
final List<SubspaceTemplateModel>? subspaces;
final List<String>? allTags;
final TextEditingController spaceNameController;
final void Function(
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces)? onLoad;
const TagChipDisplay(
BuildContext context, {
Key? key,
required this.screenWidth,
required this.spaceModel,
required this.products,
required this.subspaces,
required this.allTags,
required this.spaceNameController,
}) : super(key: key);
const TagChipDisplay(BuildContext context,
{Key? key,
required this.screenWidth,
required this.spaceModel,
required this.products,
required this.subspaces,
required this.allTags,
required this.spaceNameController,
this.onLoad})
: super(key: key);
@override
Widget build(BuildContext context) {
@ -51,7 +54,7 @@ class TagChipDisplay extends StatelessWidget {
runSpacing: 8.0,
children: [
// Combine tags from spaceModel and subspaces
..._groupTags([
...TagHelper.groupTags([
...?spaceModel?.tags,
...?spaceModel?.subspaceModels
?.expand((subspace) => subspace.tags ?? [])
@ -82,36 +85,41 @@ class TagChipDisplay extends StatelessWidget {
),
GestureDetector(
onTap: () async {
Navigator.of(context).pop();
// Use the Navigator's context for showDialog
final navigatorContext =
Navigator.of(context).overlay?.context;
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (context) => AddDeviceTypeModelWidget(
products: products,
subspaces: subspaces,
allTags: allTags,
spaceName: spaceNameController.text,
spaceTagModels: spaceModel?.tags,
initialSelectedProducts:
_createInitialSelectedProducts(
spaceModel?.tags, spaceModel?.subspaceModels),
),
);
// Edit action
if (navigatorContext != null) {
await showDialog<bool>(
barrierDismissible: false,
context: navigatorContext,
builder: (context) => AssignTagModelsDialog(
products: products,
subspaces: subspaces,
allTags: allTags,
initialTags: TagHelper.generateInitialTags(
subspaces: subspaces,
spaceTagModels: spaceModel?.tags ?? []),
title: 'Edit Device',
addedProducts:
TagHelper.createInitialSelectedProducts(
spaceModel?.tags ?? [], subspaces),
spaceName: spaceModel?.modelName ?? '',
onUpdate: (tags, subspaces) {
onLoad?.call(tags, subspaces);
}),
);
}
},
child: Chip(
label: const Text(
'Edit',
style: TextStyle(
color: ColorsManager.spaceColor),
style: TextStyle(color: ColorsManager.spaceColor),
),
backgroundColor:
ColorsManager.whiteColors,
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor),
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: ColorsManager.spaceColor),
),
),
),
@ -121,9 +129,7 @@ class TagChipDisplay extends StatelessWidget {
)
: TextButton(
onPressed: () async {
Navigator.of(context).pop();
final result = await showDialog<bool>(
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (context) => AddDeviceTypeModelWidget(
@ -131,9 +137,15 @@ class TagChipDisplay extends StatelessWidget {
subspaces: subspaces,
allTags: allTags,
spaceName: spaceNameController.text,
isCreate: true,
onUpdate: (tags, subspaces) {
onLoad?.call(tags, subspaces);
},
onLoad: (tags, subspaces) {
onLoad?.call(tags, subspaces);
},
),
);
if (result == true) {}
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
@ -144,49 +156,4 @@ class TagChipDisplay extends StatelessWidget {
),
);
}
Map<ProductModel, int> _groupTags(List<TagModel> tags) {
final Map<ProductModel, int> groupedTags = {};
for (var tag in tags) {
if (tag.product != null) {
groupedTags[tag.product!] = (groupedTags[tag.product!] ?? 0) + 1;
}
}
return groupedTags;
}
List<SelectedProduct> _createInitialSelectedProducts(
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces) {
final Map<ProductModel, int> productCounts = {};
if (tags != null) {
for (var tag in tags) {
if (tag.product != null) {
productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1;
}
}
}
if (subspaces != null) {
for (var subspace in subspaces) {
if (subspace.tags != null) {
for (var tag in subspace.tags!) {
if (tag.product != null) {
productCounts[tag.product!] =
(productCounts[tag.product!] ?? 0) + 1;
}
}
}
}
}
return productCounts.entries
.map((entry) => SelectedProduct(
productId: entry.key.uuid,
count: entry.value,
productName: entry.key.name ?? 'Unnamed',
product: entry.key,
))
.toList();
}
}

View File

@ -1,38 +1,75 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart';
class AddDeviceTypeModelBloc
extends Bloc<AddDeviceTypeModelEvent, List<SelectedProduct>> {
AddDeviceTypeModelBloc(List<SelectedProduct> initialProducts)
: super(initialProducts) {
extends Bloc<AddDeviceTypeModelEvent, AddDeviceModelState> {
AddDeviceTypeModelBloc() : super(AddDeviceModelInitial()) {
on<InitializeDeviceTypeModel>(_onInitializeTagModels);
on<UpdateProductCountEvent>(_onUpdateProductCount);
}
void _onUpdateProductCount(
UpdateProductCountEvent event, Emitter<List<SelectedProduct>> emit) {
final existingProduct = state.firstWhere(
(p) => p.productId == event.productId,
orElse: () => SelectedProduct(productId: event.productId, count: 0,productName: event.productName,product: event.product ),
);
void _onInitializeTagModels(
InitializeDeviceTypeModel event, Emitter<AddDeviceModelState> emit) {
emit(AddDeviceModelLoaded(
selectedProducts: event.addedProducts,
initialTag: event.initialTags,
));
}
if (event.count > 0) {
if (!state.contains(existingProduct)) {
emit([
...state,
SelectedProduct(productId: event.productId, count: event.count, productName: event.productName, product: event.product)
]);
void _onUpdateProductCount(
UpdateProductCountEvent event, Emitter<AddDeviceModelState> emit) {
final currentState = state;
if (currentState is AddDeviceModelLoaded) {
final existingProduct = currentState.selectedProducts.firstWhere(
(p) => p.productId == event.productId,
orElse: () => SelectedProduct(
productId: event.productId,
count: 0,
productName: event.productName,
product: event.product,
),
);
List<SelectedProduct> updatedProducts;
if (event.count > 0) {
if (!currentState.selectedProducts.contains(existingProduct)) {
updatedProducts = [
...currentState.selectedProducts,
SelectedProduct(
productId: event.productId,
count: event.count,
productName: event.productName,
product: event.product,
),
];
} else {
updatedProducts = currentState.selectedProducts.map((p) {
if (p.productId == event.productId) {
return SelectedProduct(
productId: p.productId,
count: event.count,
productName: p.productName,
product: p.product,
);
}
return p;
}).toList();
}
} else {
final updatedList = state.map((p) {
if (p.productId == event.productId) {
return SelectedProduct(productId: p.productId, count: event.count, productName: p.productName,product: p.product);
}
return p;
}).toList();
emit(updatedList);
// Remove the product if the count is 0
updatedProducts = currentState.selectedProducts
.where((p) => p.productId != event.productId)
.toList();
}
} else {
emit(state.where((p) => p.productId != event.productId).toList());
// Emit the updated state
emit(AddDeviceModelLoaded(
selectedProducts: updatedProducts,
initialTag: currentState.initialTag));
}
}
}

View File

@ -0,0 +1,36 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
abstract class AddDeviceModelState extends Equatable {
const AddDeviceModelState();
@override
List<Object> get props => [];
}
class AddDeviceModelInitial extends AddDeviceModelState {}
class AddDeviceModelLoading extends AddDeviceModelState {}
class AddDeviceModelLoaded extends AddDeviceModelState {
final List<SelectedProduct> selectedProducts;
final List<TagModel> initialTag;
const AddDeviceModelLoaded({
required this.selectedProducts,
required this.initialTag,
});
@override
List<Object> get props => [selectedProducts, initialTag];
}
class AddDeviceModelError extends AddDeviceModelState {
final String errorMessage;
const AddDeviceModelError(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}

View File

@ -1,11 +1,16 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
abstract class AddDeviceTypeModelEvent extends Equatable {
const AddDeviceTypeModelEvent();
@override
List<Object> get props => [];
}
class UpdateProductCountEvent extends AddDeviceTypeModelEvent {
final String productId;
final int count;
@ -17,3 +22,17 @@ class UpdateProductCountEvent extends AddDeviceTypeModelEvent {
@override
List<Object> get props => [productId, count];
}
class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent {
final List<TagModel> initialTags;
final List<SelectedProduct> addedProducts;
const InitializeDeviceTypeModel({
this.initialTags = const [],
required this.addedProducts,
});
@override
List<Object> get props => [initialTags, addedProducts];
}

View File

@ -1,39 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/action_button_widget.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/scrollable_grid_view_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AddDeviceTypeModelWidget extends StatelessWidget {
final List<ProductModel>? products;
final ValueChanged<List<SelectedProduct>>? onProductsSelected;
final List<SelectedProduct>? initialSelectedProducts;
final List<SubspaceTemplateModel>? subspaces;
final List<TagModel>? spaceTagModels;
final List<String>? allTags;
final String spaceName;
final bool isCreate;
final void Function(
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces)? onLoad;
final void Function(
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces)? onUpdate;
const AddDeviceTypeModelWidget(
{super.key,
this.products,
this.initialSelectedProducts,
this.onProductsSelected,
this.subspaces,
this.allTags,
this.spaceTagModels,
required this.spaceName});
const AddDeviceTypeModelWidget({
super.key,
this.products,
this.initialSelectedProducts,
this.subspaces,
this.allTags,
this.spaceTagModels,
required this.spaceName,
required this.isCreate,
this.onLoad,
this.onUpdate,
});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final crossAxisCount = size.width > 1200
? 8
@ -42,91 +51,116 @@ class AddDeviceTypeModelWidget extends StatelessWidget {
: 3;
return BlocProvider(
create: (_) => AddDeviceTypeModelBloc(initialSelectedProducts ?? []),
child: Builder(
builder: (context) => AlertDialog(
title: const Text('Add Devices'),
backgroundColor: ColorsManager.whiteColors,
content: SingleChildScrollView(
child: Container(
width: size.width * 0.9,
height: size.height * 0.65,
color: ColorsManager.textFieldGreyColor,
child: Column(
children: [
const SizedBox(height: 16),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: ScrollableGridViewWidget(
products: products, crossAxisCount: crossAxisCount),
),
create: (_) => AddDeviceTypeModelBloc()
..add(InitializeDeviceTypeModel(
initialTags: spaceTagModels ?? [],
addedProducts: initialSelectedProducts ?? [],
)),
child: Builder(
builder: (context) => AlertDialog(
title: const Text('Add Devices'),
backgroundColor: ColorsManager.whiteColors,
content: BlocBuilder<AddDeviceTypeModelBloc, AddDeviceModelState>(
builder: (context, state) {
if (state is AddDeviceModelLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is AddDeviceModelLoaded) {
return SingleChildScrollView(
child: Container(
width: size.width * 0.9,
height: size.height * 0.65,
color: ColorsManager.textFieldGreyColor,
child: Column(
children: [
const SizedBox(height: 16),
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20.0),
child: ScrollableGridViewWidget(
products: products,
crossAxisCount: crossAxisCount,
initialProductCounts: state.selectedProducts,
),
),
),
],
),
],
),
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CancelButton(
label: 'Cancel',
onPressed: () async {
),
);
}
return const SizedBox();
},
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CancelButton(
label: 'Cancel',
onPressed: () async {
if (isCreate) {
Navigator.of(context).pop();
await showDialog(
barrierDismissible: false,
context: context,
builder: (context) => CreateSpaceModelDialog(
products: products,
allTags: allTags,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
subspaceModels: subspaces,
tags: spaceTagModels,
)),
}
},
),
SizedBox(
width: 140,
child:
BlocBuilder<AddDeviceTypeModelBloc, AddDeviceModelState>(
builder: (context, state) {
final isDisabled = state is AddDeviceModelLoaded &&
state.selectedProducts.isEmpty;
return DefaultButton(
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: isDisabled
? ColorsManager.whiteColorsWithOpacity
: ColorsManager.whiteColors,
borderRadius: 10,
onPressed: isDisabled
? null // Disable the button
: () async {
if (state is AddDeviceModelLoaded &&
state.selectedProducts.isNotEmpty) {
final initialTags = generateInitialTags(
spaceTagModels: spaceTagModels,
subspaces: subspaces,
);
final dialogTitle = initialTags.isNotEmpty
? 'Edit Device'
: 'Assign Tags';
Navigator.of(context).pop();
await showDialog<bool>(
context: context,
builder: (context) => AssignTagModelsDialog(
products: products,
subspaces: subspaces,
addedProducts: state.selectedProducts,
allTags: allTags,
spaceName: spaceName,
initialTags: state.initialTag,
title: dialogTitle,
onUpdate: (tags, subspaces) {
onUpdate?.call(tags, subspaces);
},
),
);
}
},
child: const Text('Next'),
);
},
),
ActionButton(
label: 'Continue',
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: ColorsManager.whiteColors,
onPressed: () async {
final currentState =
context.read<AddDeviceTypeModelBloc>().state;
Navigator.of(context).pop();
if (currentState.isNotEmpty) {
final initialTags = generateInitialTags(
spaceTagModels: spaceTagModels,
subspaces: subspaces,
);
final dialogTitle = initialTags.isNotEmpty
? 'Edit Device'
: 'Assign Tags';
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (context) => AssignTagModelsDialog(
products: products,
subspaces: subspaces,
addedProducts: currentState,
allTags: allTags,
spaceName: spaceName,
initialTags: initialTags,
title: dialogTitle,
),
);
}
},
),
],
),
],
),
));
),
],
),
],
),
),
);
}
List<TagModel> generateInitialTags({

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart';
class ScrollableGridViewWidget extends StatelessWidget {
@ -24,8 +25,12 @@ class ScrollableGridViewWidget extends StatelessWidget {
return Scrollbar(
controller: scrollController,
thumbVisibility: true,
child: BlocBuilder<AddDeviceTypeModelBloc, List<SelectedProduct>>(
builder: (context, productCounts) {
child: BlocBuilder<AddDeviceTypeModelBloc, AddDeviceModelState>(
builder: (context, state) {
final productCounts = state is AddDeviceModelLoaded
? state.selectedProducts
: <SelectedProduct>[];
return GridView.builder(
controller: scrollController,
shrinkWrap: true,

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_response_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
import 'package:syncrow_web/utils/constants/temp_const.dart';
@ -19,23 +22,26 @@ class CommunitySpaceManagementApi {
.replaceAll('{projectId}', TempConst.projectId),
queryParameters: {'page': page},
expectedResponseModel: (json) {
List<dynamic> jsonData = json['data'];
hasNext = json['hasNext'] ?? false;
int currentPage = json['page'] ?? 1;
List<CommunityModel> communityList = jsonData.map((jsonItem) {
return CommunityModel.fromJson(jsonItem);
}).toList();
allCommunities.addAll(communityList);
page = currentPage + 1;
return communityList;
try {
List<dynamic> jsonData = json['data'] ?? [];
hasNext = json['hasNext'] ?? false;
int currentPage = json['page'] ?? 1;
List<CommunityModel> communityList = jsonData.map((jsonItem) {
return CommunityModel.fromJson(jsonItem);
}).toList();
allCommunities.addAll(communityList);
page = currentPage + 1;
return communityList;
} catch (_) {
hasNext = false;
return [];
}
},
);
}
return allCommunities;
} catch (e) {
debugPrint('Error fetching communities: $e');
return [];
}
}
@ -158,15 +164,17 @@ class CommunitySpaceManagementApi {
}
}
Future<SpaceModel?> createSpace({
required String communityId,
required String name,
String? parentId,
String? direction,
bool isPrivate = false,
required Offset position,
String? icon,
}) async {
Future<SpaceModel?> createSpace(
{required String communityId,
required String name,
String? parentId,
String? direction,
bool isPrivate = false,
required Offset position,
String? spaceModelUuid,
String? icon,
List<CreateTagBodyModel>? tags,
List<CreateSubspaceModel>? subspaces}) async {
try {
final body = {
'spaceName': name,
@ -179,6 +187,13 @@ class CommunitySpaceManagementApi {
if (parentId != null) {
body['parentUuid'] = parentId;
}
if (tags != null) {
body['tags'] = tags;
}
if (spaceModelUuid != null) body['spaceModelUuid'] = spaceModelUuid;
if (subspaces != null) body['subspaces'] = subspaces;
final response = await HTTPService().post(
path: ApiEndpoints.createSpace
.replaceAll('{communityId}', communityId)
@ -260,7 +275,6 @@ class CommunitySpaceManagementApi {
.replaceAll('{communityId}', communityId)
.replaceAll('{projectId}', TempConst.projectId),
expectedResponseModel: (json) {
print(json);
final spaceModels = (json['data'] as List)
.map((spaceJson) => SpaceModel.fromJson(spaceJson))
.toList();

View File

@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -7,71 +6,44 @@ import 'package:syncrow_web/utils/constants/temp_const.dart';
class SpaceModelManagementApi {
Future<List<SpaceTemplateModel>> listSpaceModels({int page = 1}) async {
try {
List<SpaceTemplateModel> spaceModels = [];
bool hasNext = true;
while (hasNext) {
await HTTPService().get(
path: ApiEndpoints.listSpaceModels
.replaceAll('{projectId}', TempConst.projectId),
queryParameters: {'page': page},
expectedResponseModel: (json) {
List<dynamic> jsonData = json['data'];
hasNext = json['hasNext'] ?? false;
int currentPage = json['page'] ?? 1;
List<SpaceTemplateModel> spaceModelList = jsonData.map((jsonItem) {
return SpaceTemplateModel.fromJson(jsonItem);
}).toList();
spaceModels.addAll(spaceModelList);
page = currentPage + 1;
return spaceModelList;
},
);
}
return spaceModels;
} catch (e) {
debugPrint('Error fetching space models: $e');
return [];
}
final response = await HTTPService().get(
path: ApiEndpoints.listSpaceModels
.replaceAll('{projectId}', TempConst.projectId),
queryParameters: {'page': page},
expectedResponseModel: (json) {
List<dynamic> jsonData = json['data'];
return jsonData.map((jsonItem) {
return SpaceTemplateModel.fromJson(jsonItem);
}).toList();
},
);
return response;
}
Future<SpaceTemplateModel?> createSpaceModel(
CreateSpaceTemplateBodyModel spaceModel) async {
try {
final response = await HTTPService().post(
path: ApiEndpoints.createSpaceModel
.replaceAll('{projectId}', TempConst.projectId),
showServerMessage: true,
body: spaceModel.toJson(),
expectedResponseModel: (json) {
return SpaceTemplateModel.fromJson(json['data']);
},
);
return response;
} catch (e) {
debugPrint('Error creating space model: $e');
return null;
}
final response = await HTTPService().post(
path: ApiEndpoints.createSpaceModel
.replaceAll('{projectId}', TempConst.projectId),
showServerMessage: true,
body: spaceModel.toJson(),
expectedResponseModel: (json) {
return SpaceTemplateModel.fromJson(json['data']);
},
);
return response;
}
Future<SpaceTemplateModel?> getSpaceModel(String spaceModelUuid) async {
try {
final response = await HTTPService().get(
path: ApiEndpoints.getSpaceModel
.replaceAll('{projectId}', TempConst.projectId)
.replaceAll('{spaceModelUuid}', spaceModelUuid),
showServerMessage: true,
expectedResponseModel: (json) {
debugPrint('Response JSON: $json');
return SpaceTemplateModel.fromJson(json['data']);
},
);
return response;
} catch (e) {
debugPrint('Error getting space model: $e');
return null;
}
final response = await HTTPService().get(
path: ApiEndpoints.getSpaceModel
.replaceAll('{projectId}', TempConst.projectId)
.replaceAll('{spaceModelUuid}', spaceModelUuid),
showServerMessage: true,
expectedResponseModel: (json) {
return SpaceTemplateModel.fromJson(json['data']);
},
);
return response;
}
}

View File

@ -0,0 +1,15 @@
import 'package:flutter/services.dart';
class AssetValidator {
static Future<bool> isValidAsset(String? assetPath) async {
if (assetPath == null || assetPath.isEmpty) {
return false;
}
try {
await rootBundle.load(assetPath);
return true;
} catch (_) {
return false;
}
}
}

View File

@ -68,6 +68,7 @@ abstract class ColorsManager {
static const Color disabledRedText = Color(0xFF890002);
static const Color invitedOrange = Color(0xFFFFE193);
static const Color invitedOrangeText = Color(0xFFFFBF00);
static const Color lightGrayBorderColor = Color(0xB2D5D5D5);
//background: #F8F8F8;
}

View File

@ -397,5 +397,6 @@ class Assets {
static const String filterTableIcon = 'assets/icons/filter_table_icon.svg';
static const String ZtoAIcon = 'assets/icons/ztoa_icon.png';
static const String AtoZIcon = 'assets/icons/atoz_icon.png';
static const String link = 'assets/icons/link.svg';
}
//user_management.svg