Compare commits

..

9 Commits

11 changed files with 243 additions and 152 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;
loadAnalyticsDevices(
context,
communityUuid: communityUuid,
spaceUuid: spaceUuid,
);
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,
barRods: stackItems,
groupVertically: true,
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,23 +28,26 @@ class AqiDistributionChartTitle extends StatelessWidget {
),
),
),
FittedBox(
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AqiTypeDropdown(
onChanged: (value) {
if (value != null) {
final bloc = context.read<AirQualityDistributionBloc>();
try {
final param = _makeLoadAqiDistributionParam(context, value);
bloc.add(LoadAirQualityDistribution(param));
} catch (_) {
return;
} finally {
bloc.add(UpdateAqiTypeEvent(value));
Expanded(
flex: 2,
child: FittedBox(
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AqiTypeDropdown(
onChanged: (value) {
if (value != null) {
final bloc = context.read<AirQualityDistributionBloc>();
try {
final param = _makeLoadAqiDistributionParam(context, value);
bloc.add(LoadAirQualityDistribution(param));
} catch (_) {
return;
} finally {
bloc.add(UpdateAqiTypeEvent(value));
}
}
}
},
},
),
),
),
],

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,36 +47,37 @@ class AqiLocationInfoCell extends StatelessWidget {
),
),
Align(
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsetsDirectional.all(10),
child: SizedBox(
height: 40,
width: 120,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.bottomEnd,
child: Text(
value,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.vividBlue.withValues(alpha: 0.7),
fontWeight: FontWeight.w700,
fontSize: 24,
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,
),
fontWeight: FontWeight.w700,
fontSize: 24,
),
),
),
),
),
),
),
),
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

@ -50,11 +50,20 @@ class _DynamicTableState extends State<DynamicTable> {
bool _selectAll = false;
final ScrollController _verticalScrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
late ScrollController _horizontalHeaderScrollController;
late ScrollController _horizontalBodyScrollController;
@override
void initState() {
super.initState();
_initializeSelection();
_horizontalHeaderScrollController = ScrollController();
_horizontalBodyScrollController = ScrollController();
// Synchronize horizontal scrolling
_horizontalBodyScrollController.addListener(() {
_horizontalHeaderScrollController
.jumpTo(_horizontalBodyScrollController.offset);
});
}
@override
@ -104,78 +113,108 @@ class _DynamicTableState extends State<DynamicTable> {
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
}
@override
void dispose() {
_horizontalHeaderScrollController.dispose();
_horizontalBodyScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: widget.cellDecoration,
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: Scrollbar(
//fixed the horizontal scrollbar issue
controller: _horizontalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
controller: _verticalScrollController,
child: Column(
children: [
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(color: ColorsManager.boxColor),
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
controller: _horizontalHeaderScrollController,
child: SizedBox(
width: widget.size.width,
child: Column(
child: Row(
children: [
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(
color: ColorsManager.boxColor,
),
child: Row(
children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
})
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
],
),
),
SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children:
List.generate(widget.data.length, (rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(
rowIndex, widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
);
}),
),
),
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
}),
],
),
),
),
),
Expanded(
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
controller: _verticalScrollController,
child: Scrollbar(
controller: _horizontalBodyScrollController,
thumbVisibility: false,
trackVisibility: false,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _horizontalBodyScrollController,
child: Container(
color: ColorsManager.whiteColors,
child: SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children: List.generate(widget.data.length,
(rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(rowIndex,
widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
);
}),
),
),
),
),
),
),
),
),
],
),
);
}
Widget _buildSelectAllCheckbox() {
return Container(
width: 50,
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
),
),
child: Checkbox(
value: _selectAll,
onChanged: widget.withSelectAll && widget.data.isNotEmpty
? _toggleSelectAll
: null,
),
);
}
@ -205,23 +244,6 @@ class _DynamicTableState extends State<DynamicTable> {
),
],
);
Widget _buildSelectAllCheckbox() {
return Container(
width: 50,
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
),
),
child: Checkbox(
value: _selectAll,
onChanged: widget.withSelectAll && widget.data.isNotEmpty
? _toggleSelectAll
: null,
),
);
}
Widget _buildRowCheckbox(int index, double size) {
return Container(
width: 50,
@ -276,8 +298,12 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildTableCell(String content, double size,
{required int rowIndex, required int columnIndex}) {
Widget _buildTableCell(
String content,
double size, {
required int rowIndex,
required int columnIndex,
}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
@ -285,6 +311,7 @@ class _DynamicTableState extends State<DynamicTable> {
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
}
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
if (isSettingsColumn) {
return buildSettingsIcon(
width: 120,
@ -389,10 +416,11 @@ class _DynamicTableState extends State<DynamicTable> {
padding: const EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
Assets.settings,
Assets.settings, // ضع المسار الصحيح هنا
width: 40,
height: 22,
color: ColorsManager.primaryColor,
color: ColorsManager
.primaryColor, // نفس لون الأيقونة في الصورة
),
),
),

View File

@ -289,6 +289,7 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
selectedSpaces: updatedSelectedSpaces,
soldCheck: updatedSoldChecks,
selectedCommunityAndSpaces: communityAndSpaces));
emit(state.copyWith(selectedSpaces: updatedSelectedSpaces));
} catch (e) {
emit(const SpaceTreeErrorState('Something went wrong'));
}
@ -444,12 +445,10 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
List<String> _getThePathToChild(String communityId, String selectedSpaceId) {
List<String> ids = [];
final communityDataSource =
state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList;
for (final community in communityDataSource) {
for (var community in state.communityList) {
if (community.uuid == communityId) {
for (final space in community.spaces) {
final list = <String>[];
for (var space in community.spaces) {
List<String> list = [];
list.add(space.uuid!);
ids = _getAllParentsIds(space, selectedSpaceId, List.from(list));
if (ids.isNotEmpty) {