Compare commits

..

15 Commits

Author SHA1 Message Date
388391eec4 stop stacking snackbars 2025-06-29 11:29:43 +03:00
ad00cf35ba added the PR notes 2025-06-23 16:05:16 +03:00
1200a809c2 now cant use offline device to controll 2025-06-23 14:33:56 +03:00
03a6c5474b SP-1768-FE-The-white-are-in-empty-devices-table-should-take-the-whole-table-size-not-just-the-top (#282)
<!--
  Thanks for contributing!

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

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

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

## Description

<!--- Describe your changes in detail -->
fix white container take only a small part of the device table

## Type of Change

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

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


[SP-1768]:
https://syncrow.atlassian.net/browse/SP-1768?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 11:14:32 +03:00
5f30a5a61b Refactor empty state widget to use a container for better layout control 2025-06-23 10:01:01 +03:00
0712e6d64b Sp 1593 reworks (#277)
<!--
  Thanks for contributing!

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

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

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

## Description

1. AQI Distribution chart bars when all values are empty.
2. Min element in Y axis of Range of AQI chart is visible.
3. Matched AQI chart titles to have the same size for consistency.
4. Allowed `RangeOfAqiValue` model's values to be nullable, and they
fallback to `0` when null.
5. Implemented AQI Legend.
6. Increased the size of AQI Distribution chart's tooltip.
7. Improved alignment of location cell.
8. Doesn't fetch devices on date change.

## Type of Change

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

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


[SP-1593]:
https://syncrow.atlassian.net/browse/SP-1593?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 09:48:17 +03:00
1f82e84115 doesnt fetch devices on date change. 2025-06-22 10:55:41 +03:00
23c3bf11f9 Improved alignment of AqiLocationInfoCell. 2025-06-19 15:38:28 +03:00
5201a65a97 matched sizes of bottom titles in aqi charts. 2025-06-19 15:19:58 +03:00
e4cc5fce50 Increased the size of AqiDistributionChart tooltip. 2025-06-19 15:18:18 +03:00
8dea89db0e Implemented AQI legend. 2025-06-19 15:12:54 +03:00
ad5ada9d55 allowed RangeOfAqiValue values to be nullable, and if they were null they fallback to zero. 2025-06-19 14:24:49 +03:00
7172a0e3fb Matched aqi charts title's to have the same size no matter what the window size is. 2025-06-19 14:23:39 +03:00
78898968e8 include min in RangeOfAqiChartsHelper.titlesData.leftTitles. 2025-06-19 14:23:04 +03:00
666c64231f hides bars in AqiDistributionChart where all values are zero. 2025-06-19 14:22:37 +03:00
29 changed files with 215 additions and 811 deletions

View File

@ -38,9 +38,9 @@ class RangeOfAqiValue extends Equatable {
factory RangeOfAqiValue.fromJson(Map<String, dynamic> json) {
return RangeOfAqiValue(
type: json['type'] as String,
min: (json['min'] as num).toDouble(),
average: (json['average'] as num).toDouble(),
max: (json['max'] as num).toDouble(),
min: (json['min'] as num? ?? 0).toDouble(),
average: (json['average'] as num? ?? 0).toDouble(),
max: (json['max'] as num? ?? 0).toDouble(),
);
}

View File

@ -24,11 +24,13 @@ abstract final class FetchAirQualityDataHelper {
}) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
final aqiType = context.read<AirQualityDistributionBloc>().state.selectedAqiType;
if (shouldFetchAnalyticsDevices) {
loadAnalyticsDevices(
context,
communityUuid: communityUuid,
spaceUuid: spaceUuid,
);
}
loadRangeOfAqi(
context,
spaceUuid: spaceUuid,

View File

@ -23,6 +23,7 @@ abstract final class RangeOfAqiChartsHelper {
return titlesData.copyWith(
bottomTitles: titlesData.bottomTitles.copyWith(
sideTitles: titlesData.bottomTitles.sideTitles.copyWith(
reservedSize: 36,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text(
@ -40,6 +41,7 @@ abstract final class RangeOfAqiChartsHelper {
reservedSize: 70,
interval: 50,
maxIncluded: false,
minIncluded: true,
getTitlesWidget: (value, meta) {
final text = value >= 300 ? '301+' : value.toInt().toString();
return Padding(

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_legend.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
class AirQualityView extends StatelessWidget {
@ -20,6 +21,10 @@ class AirQualityView extends StatelessWidget {
child: Column(
spacing: 32,
children: [
SizedBox(
height: height * 0.1,
child: const AqiLegend(),
),
SizedBox(
height: height * 1.2,
child: const AirQualityEndSideWidget(),
@ -40,7 +45,7 @@ class AirQualityView extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height * 1.1,
height: height * 1.2,
child: const Column(
children: [
Expanded(
@ -52,8 +57,9 @@ class AirQualityView extends StatelessWidget {
child: Column(
spacing: 20,
children: [
Expanded(child: RangeOfAqiChartBox()),
Expanded(child: AqiDistributionChartBox()),
Expanded(flex: 2, child: AqiLegend()),
Expanded(flex: 12, child: RangeOfAqiChartBox()),
Expanded(flex: 12, child: AqiDistributionChartBox()),
],
),
),

View File

@ -32,8 +32,13 @@ class AqiDistributionChart extends StatelessWidget {
}
List<BarChartGroupData> _buildBarGroups() {
return List.generate(chartData.length, (index) {
final data = chartData[index];
final groups = <BarChartGroupData>[];
for (var i = 0; i < chartData.length; i++) {
final data = chartData[i];
final isAllZero = data.data.every((d) => d.percentage == 0);
if (isAllZero) {
continue;
}
final stackItems = <BarChartRodData>[];
double currentY = 0;
var isFirstElement = true;
@ -56,13 +61,15 @@ class AqiDistributionChart extends StatelessWidget {
currentY += percentageData.percentage + _rodStackItemsSpacing;
isFirstElement = false;
}
return BarChartGroupData(
x: index,
groups.add(
BarChartGroupData(
x: i,
barRods: stackItems,
groupVertically: true,
),
);
});
}
return groups;
}
BarTouchData _barTouchData(BuildContext context) {
@ -73,6 +80,7 @@ class AqiDistributionChart extends StatelessWidget {
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
maxContentWidth: 500,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final data = chartData[group.x];
@ -81,10 +89,13 @@ class AqiDistributionChart extends StatelessWidget {
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 8,
fontSize: 11,
);
for (final percentageData in data.data) {
if (percentageData.percentage == 0) {
continue;
}
final percentage = percentageData.percentage.toStringAsFixed(1);
final type = percentageData.type[0].toUpperCase() +
percentageData.type.substring(1).replaceAll('_', ' ');
@ -98,7 +109,7 @@ class AqiDistributionChart extends StatelessWidget {
DateFormat('dd/MM/yyyy').format(data.date),
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 9,
fontSize: 12,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.start,
@ -118,7 +129,6 @@ class AqiDistributionChart extends StatelessWidget {
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 20,
maxIncluded: false,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding(
@ -140,7 +150,7 @@ class AqiDistributionChart extends StatelessWidget {
final bottomTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
showTitles: chartData.isNotEmpty,
getTitlesWidget: (value, _) => FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
@ -148,7 +158,7 @@ class AqiDistributionChart extends StatelessWidget {
chartData[value.toInt()].date.day.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGreyColor,
fontSize: 8,
fontSize: 12,
),
),
),

View File

@ -19,7 +19,7 @@ class AqiDistributionChartTitle extends StatelessWidget {
children: [
ChartsLoadingWidget(isLoading: isLoading),
const Expanded(
flex: 3,
flex: 4,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
@ -28,7 +28,9 @@ class AqiDistributionChartTitle extends StatelessWidget {
),
),
),
FittedBox(
Expanded(
flex: 2,
child: FittedBox(
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AqiTypeDropdown(
@ -47,6 +49,7 @@ class AqiDistributionChartTitle extends StatelessWidget {
},
),
),
),
],
);
}

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
import 'package:syncrow_web/utils/style.dart';
class AqiLegend extends StatelessWidget {
const AqiLegend({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsetsDirectional.all(20),
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16,
children: RangeOfAqiChartsHelper.gradientData.map((e) {
return Flexible(
flex: 4,
child: FittedBox(
fit: BoxFit.fill,
child: ChartInformativeCell(
color: e.$1,
title: FittedBox(
fit: BoxFit.fill,
child: Text(e.$2),
),
height: null,
),
),
);
}).toList(),
),
);
}
}

View File

@ -47,19 +47,29 @@ class AqiLocationInfoCell extends StatelessWidget {
),
),
Align(
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsetsDirectional.all(10),
child: SizedBox(
height: 40,
width: 120,
alignment: AlignmentDirectional.bottomCenter,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: SvgPicture.asset(
svgPath,
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.bottomStart,
),
),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsetsDirectional.all(10),
child: Text(
value,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.vividBlue.withValues(alpha: 0.7),
color: ColorsManager.vividBlue.withValues(
alpha: 0.7,
),
fontWeight: FontWeight.w700,
fontSize: 24,
),
@ -67,16 +77,7 @@ class AqiLocationInfoCell extends StatelessWidget {
),
),
),
),
Align(
alignment: AlignmentDirectional.bottomStart,
child: SizedBox.square(
dimension: MediaQuery.sizeOf(context).width * 0.45,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.bottomStart,
child: SvgPicture.asset(svgPath),
),
],
),
),
],

View File

@ -7,16 +7,18 @@ class ChartInformativeCell extends StatelessWidget {
required this.title,
required this.color,
this.hasBorder = false,
this.height,
});
final Widget title;
final Color color;
final bool hasBorder;
final double? height;
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.sizeOf(context).height * 0.0385,
height: height ?? MediaQuery.sizeOf(context).height * 0.0385,
padding: const EdgeInsetsDirectional.symmetric(
vertical: 8,
horizontal: 12,

View File

@ -179,7 +179,10 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildEmptyState() => Column(
Widget _buildEmptyState() => Container(
height: widget.size.height,
color: ColorsManager.whiteColors,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
@ -203,7 +206,9 @@ class _DynamicTableState extends State<DynamicTable> {
),
],
),
SizedBox(height: widget.size.height * 0.5),
],
),
);
Widget _buildSelectAllCheckbox() {
return Container(

View File

@ -62,7 +62,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
final buttonLabel =
(selectedDevices.length > 1) ? 'Batch Control' : 'Control';
final isAnyDeviceOffline =
selectedDevices.any((element) => !(element.online ?? false));
return Row(
children: [
Expanded(child: SpaceTreeView(
@ -103,8 +104,28 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
decoration: containerDecoration,
child: Center(
child: DefaultButton(
backgroundColor: isAnyDeviceOffline
? ColorsManager.primaryColor
.withValues(alpha: 0.1)
: null,
onPressed: isControlButtonEnabled
? () {
if (isAnyDeviceOffline) {
ScaffoldMessenger.of(context)
.clearSnackBars();
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'This Device is Offline',
),
duration:
Duration(seconds: 2),
),
);
return;
}
if (selectedDevices.length == 1) {
showDialog(
context: context,

View File

@ -1,6 +0,0 @@
class SpaceConnectionModel {
final String from;
final String to;
const SpaceConnectionModel({required this.from, required this.to});
}

View File

@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpacesConnectionsArrowPainter extends CustomPainter {
final List<SpaceConnectionModel> connections;
final Map<String, Offset> positions;
final double cardWidth = 150.0;
final double cardHeight = 90.0;
final String? selectedSpaceUuid;
SpacesConnectionsArrowPainter({
required this.connections,
required this.positions,
this.selectedSpaceUuid,
});
@override
void paint(Canvas canvas, Size size) {
for (final connection in connections) {
final isSelected = connection.to == selectedSpaceUuid;
final paint = Paint()
..color = isSelected
? ColorsManager.primaryColor
: ColorsManager.blackColor.withValues(alpha: 0.5)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
final from = positions[connection.from];
final to = positions[connection.to];
if (from != null && to != null) {
final startPoint =
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10);
final endPoint = Offset(to.dx + cardWidth / 2, to.dy);
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 60);
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
controlPoint2.dy, endPoint.dx, endPoint.dy);
canvas.drawPath(path, paint);
final circlePaint = Paint()
..color = isSelected
? ColorsManager.primaryColor
: ColorsManager.blackColor.withValues(alpha: 0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn;
canvas.drawCircle(endPoint, 4, circlePaint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
abstract final class SpaceManagementCommunityDialogHelper {
static void showCreateDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const SelectableText('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}
}

View File

@ -1,236 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
class CommunityStructureCanvas extends StatefulWidget {
const CommunityStructureCanvas({
required this.community,
super.key,
});
final CommunityModel community;
@override
State<CommunityStructureCanvas> createState() =>_CommunityStructureCanvasState();
}
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
with SingleTickerProviderStateMixin {
final Map<String, Offset> _positions = {};
final double _cardWidth = 150.0;
final double _cardHeight = 90.0;
final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0;
String? _selectedSpaceUuid;
late TransformationController _transformationController;
late AnimationController _animationController;
@override
void initState() {
super.initState();
_transformationController = TransformationController();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
);
}
@override
void dispose() {
_transformationController.dispose();
_animationController.dispose();
super.dispose();
}
void _runAnimation(Matrix4 target) {
final animation = Matrix4Tween(
begin: _transformationController.value,
end: target,
).animate(_animationController);
void listener() {
_transformationController.value = animation.value;
}
animation.addListener(listener);
_animationController.forward(from: 0).whenCompleteOrCancel(() {
animation.removeListener(listener);
});
}
void _onSpaceTapped(String spaceUuid) {
setState(() {
_selectedSpaceUuid = spaceUuid;
});
final position = _positions[spaceUuid];
if (position == null) return;
const scale = 2.0;
final viewSize = context.size;
if (viewSize == null) return;
final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2);
final y =
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
final matrix = Matrix4.identity()
..translate(x, y)
..scale(scale);
_runAnimation(matrix);
}
void _resetSelectionAndZoom() {
setState(() {
_selectedSpaceUuid = null;
});
_runAnimation(Matrix4.identity());
}
void _calculateLayout(
List<SpaceModel> spaces,
int depth,
Map<int, double> levelXOffset,
) {
for (final space in spaces) {
double childSubtreeWidth = 0;
if (space.children.isNotEmpty) {
_calculateLayout(space.children, depth + 1, levelXOffset);
final firstChildPos = _positions[space.children.first.uuid];
final lastChildPos = _positions[space.children.last.uuid];
if (firstChildPos != null && lastChildPos != null) {
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx;
}
}
final currentX = levelXOffset.putIfAbsent(depth, () => 0.0);
double? x;
if (space.children.isNotEmpty) {
final firstChildPos = _positions[space.children.first.uuid]!;
x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2;
} else {
x = currentX;
}
if (x < currentX) {
final shiftX = currentX - x;
_shiftSubtree(space, shiftX);
final keysToShift = levelXOffset.keys.where((d) => d > depth).toList();
for (final key in keysToShift) {
levelXOffset[key] = levelXOffset[key]! + shiftX;
}
x += shiftX;
}
final y = depth * (_verticalSpacing + _cardHeight);
_positions[space.uuid] = Offset(x, y);
levelXOffset[depth] = x + _cardWidth + _horizontalSpacing;
}
}
void _shiftSubtree(SpaceModel space, double shiftX) {
if (_positions.containsKey(space.uuid)) {
_positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0);
}
for (final child in space.children) {
_shiftSubtree(child, shiftX);
}
}
List<Widget> _buildTreeWidgets() {
_positions.clear();
final community = widget.community;
_calculateLayout(community.spaces, 0, {});
final widgets = <Widget>[];
final connections = <SpaceConnectionModel>[];
_generateWidgets(community.spaces, widgets, connections);
return [
CustomPaint(
painter: SpacesConnectionsArrowPainter(
connections: connections,
positions: _positions,
selectedSpaceUuid: _selectedSpaceUuid,
),
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
),
];
}
void _generateWidgets(
List<SpaceModel> spaces,
List<Widget> widgets,
List<SpaceConnectionModel> connections,
) {
for (final space in spaces) {
final position = _positions[space.uuid];
if (position == null) continue;
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: SpaceCardWidget(
index: spaces.indexOf(space),
onPositionChanged: (newPosition) {},
buildSpaceContainer: (index) {
return Opacity(
opacity: 1.0,
child: SpaceCell(
index: index,
onTap: () => _onSpaceTapped(space.uuid),
icon: space.icon,
name: space.spaceName,
),
);
},
screenSize: MediaQuery.sizeOf(context),
position: position,
isHovered: false,
onHoverChanged: (int index, bool isHovered) {},
onButtonTap: (int index, Offset newPosition) {},
),
),
);
for (final child in space.children) {
connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
}
_generateWidgets(space.children, widgets, connections);
}
}
@override
Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets();
return InteractiveViewer(
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: MediaQuery.sizeOf(context).width * 0.3,
vertical: MediaQuery.sizeOf(context).height * 0.2,
),
minScale: 0.5,
maxScale: 3.0,
constrained: false,
child: GestureDetector(
onTap: _resetSelectionAndZoom,
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 2,
height: MediaQuery.sizeOf(context).height * 2,
child: Stack(children: treeWidgets),
),
),
);
}
}

View File

@ -1,50 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityTemplateCell extends StatelessWidget {
const CommunityTemplateCell({
super.key,
required this.onTap,
required this.title,
});
final void Function() onTap;
final Widget title;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: AspectRatio(
aspectRatio: 2.0,
child: Container(
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 4,
strokeAlign: BorderSide.strokeAlignOutside,
color: ColorsManager.borderColor,
),
borderRadius: BorderRadius.circular(5),
),
),
),
),
),
DefaultTextStyle(
style: context.textTheme.bodyLarge!.copyWith(
color: ColorsManager.blackColor,
),
child: title,
),
],
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateSpaceButton extends StatelessWidget {
const CreateSpaceButton({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {},
child: Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
),
child: Center(
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: Colors.blue,
),
),
),
),
);
}
}

View File

@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class PlusButtonWidget extends StatelessWidget {
final int index;
final String direction;
final Offset offset;
final void Function(int index, Offset newPosition) onButtonTap;
const PlusButtonWidget({
super.key,
required this.index,
required this.direction,
required this.offset,
required this.onButtonTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (direction == 'down') {
onButtonTap(index, const Offset(0, 150));
} else {
onButtonTap(index, const Offset(150, 0));
}
},
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
),
),
);
}
}

View File

@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart';
class SpaceCardWidget extends StatelessWidget {
final int index;
final Size screenSize;
final Offset position;
final bool isHovered;
final void Function(int index, bool isHovered) onHoverChanged;
final void Function(int index, Offset newPosition) onButtonTap;
final Widget Function(int index) buildSpaceContainer;
final ValueChanged<Offset> onPositionChanged;
const SpaceCardWidget({
super.key,
required this.index,
required this.onPositionChanged,
required this.screenSize,
required this.position,
required this.isHovered,
required this.onHoverChanged,
required this.onButtonTap,
required this.buildSpaceContainer,
});
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => onHoverChanged(index, true),
onExit: (_) => onHoverChanged(index, false),
child: SizedBox(
width: 150,
height: 90,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
buildSpaceContainer(index),
if (isHovered)
Positioned(
bottom: 0,
child: PlusButtonWidget(
index: index,
direction: 'down',
offset: Offset.zero,
onButtonTap: onButtonTap,
),
),
if (isHovered)
Positioned(
right: -15,
child: PlusButtonWidget(
index: index,
direction: 'right',
offset: Offset.zero,
onButtonTap: onButtonTap,
),
),
],
),
),
);
}
}

View File

@ -1,88 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpaceCell extends StatelessWidget {
final int index;
final String icon;
final String name;
final VoidCallback? onDoubleTap;
final VoidCallback? onTap;
const SpaceCell({
super.key,
required this.index,
required this.icon,
required this.name,
this.onTap,
this.onDoubleTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
onDoubleTap: onDoubleTap,
onTap: onTap,
child: Container(
width: 150,
height: 70,
decoration: _containerDecoration(),
child: Row(
children: [
_buildIconContainer(),
const SizedBox(width: 10),
Expanded(
child: Text(
name,
style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.blackColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
Widget _buildIconContainer() {
return Container(
width: 40,
height: double.infinity,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
bottomLeft: Radius.circular(15),
),
),
child: Center(
child: SvgPicture.asset(
icon,
color: ColorsManager.whiteColors,
width: 24,
height: 24,
),
),
);
}
BoxDecoration _containerDecoration() {
return BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: ColorsManager.lightGrayColor.withValues(alpha: 0.5),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
);
}
}

View File

@ -1,8 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart';
class SpaceManagementBody extends StatelessWidget {
@ -10,21 +6,9 @@ class SpaceManagementBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
return const Row(
children: [
const SpaceManagementCommunitiesTree(),
Expanded(
child: BlocBuilder<CommunitiesTreeSelectionBloc,
CommunitiesTreeSelectionState>(
buildWhen: (previous, current) =>
previous.selectedCommunity != current.selectedCommunity,
builder: (context, state) => Visibility(
visible: state.selectedCommunity == null,
replacement: const SpaceManagementCommunityStructure(),
child: const SpaceManagementTemplatesView(),
),
),
),
SpaceManagementCommunitiesTree(),
],
);
}

View File

@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
class SpaceManagementCommunityStructure extends StatelessWidget {
const SpaceManagementCommunityStructure({super.key});
@override
Widget build(BuildContext context) {
final selectedCommunity =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity!;
const spacer = Spacer(flex: 10);
return Visibility(
visible: selectedCommunity.spaces.isNotEmpty,
replacement: const Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer]),
child: CommunityStructureCanvas(community: selectedCommunity),
);
}
}

View File

@ -1,52 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpaceManagementTemplatesView extends StatelessWidget {
const SpaceManagementTemplatesView({super.key});
@override
Widget build(BuildContext context) {
return Expanded(
child: ColoredBox(
color: ColorsManager.whiteColors,
child: GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 2.0,
),
itemCount: _gridItems(context).length,
itemBuilder: (context, index) {
final model = _gridItems(context)[index];
return CommunityTemplateCell(
onTap: model.onTap,
title: model.title,
);
},
),
),
);
}
List<_CommunityTemplateModel> _gridItems(BuildContext context) {
return [
_CommunityTemplateModel(
title: const Text('Blank'),
onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context),
),
];
}
}
class _CommunityTemplateModel {
final Widget title;
final void Function() onTap;
_CommunityTemplateModel({
required this.title,
required this.onTap,
});
}

View File

@ -32,7 +32,7 @@ class CommunitiesTreeSelectionBloc
) {
emit(
CommunitiesTreeSelectionState(
selectedCommunity: event.community,
selectedCommunity: null,
selectedSpace: event.space,
),
);

View File

@ -8,7 +8,7 @@ sealed class CommunitiesTreeSelectionEvent extends Equatable {
}
final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
final CommunityModel community;
final CommunityModel? community;
const SelectCommunityEvent({required this.community});
@override
@ -16,10 +16,9 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
}
final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent {
final SpaceModel space;
final CommunityModel community;
final SpaceModel? space;
const SelectSpaceEvent({required this.space, required this.community});
const SelectSpaceEvent({required this.space});
@override
List<Object?> get props => [space];

View File

@ -30,7 +30,7 @@ class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget {
initiallyExpanded: spaceIsExpanded,
onExpansionChanged: (expanded) {},
onItemSelected: () => context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(community: community, space: space),
SelectSpaceEvent(space: space),
),
children: space.children
.map(

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
@ -40,7 +41,7 @@ class SpaceManagementSidebarHeader extends StatelessWidget {
if (isSelected) {
_clearSelection(context);
} else {
SpaceManagementCommunityDialogHelper.showCreateDialog(context);
_showCreateCommunityDialog(context);
}
}
@ -49,4 +50,19 @@ class SpaceManagementSidebarHeader extends StatelessWidget {
const ClearCommunitiesTreeSelectionEvent(),
);
}
void _showCreateCommunityDialog(BuildContext context) => showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const Text('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}

View File

@ -53,7 +53,7 @@ class RemoteCreateCommunityService implements CreateCommunityService {
return _defaultErrorMessage;
}
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['message'] as String? ?? '';
final errorMessage = error?['error'] as String? ?? '';
return errorMessage;
}

View File

@ -41,8 +41,11 @@ class CreateCommunityDialog extends StatelessWidget {
);
onCreateCommunity.call(community);
break;
case CreateCommunityFailure():
case CreateCommunityFailure(:final message):
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
break;
default:
break;