legacy_gantt_chart 2.0.0 copy "legacy_gantt_chart: ^2.0.0" to clipboard
legacy_gantt_chart: ^2.0.0 copied to clipboard

A flexible and performant Gantt chart widget for Flutter. Supports interactive drag-and-drop, resizing, dynamic data loading, and extensive theming.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:legacy_gantt_chart/legacy_gantt_chart.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:legacy_context_menu/legacy_context_menu.dart';
import 'package:legacy_timeline_scrubber/legacy_timeline_scrubber.dart' as scrubber;
import 'ui/widgets/gantt_grid.dart';
import 'ui/widgets/dashboard_header.dart';
import 'view_models/gantt_view_model.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Legacy Gantt Chart Example',
        theme: ThemeData.from(
          colorScheme: ColorScheme.fromSeed(
            seedColor: Colors.blue,
            brightness: Brightness.light,
          ),
        ),
        darkTheme: ThemeData.from(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
        ),
        themeMode: ThemeMode.system,
        home: const GanttView(),
      );
}

class GanttView extends StatefulWidget {
  const GanttView({super.key});

  @override
  State<GanttView> createState() => _GanttViewState();
}

class _GanttViewState extends State<GanttView> {
  late final GanttViewModel _viewModel;

  @override
  void initState() {
    super.initState();
    _viewModel = GanttViewModel();
  }

  @override
  void dispose() {
    _viewModel.dispose();
    super.dispose();
  }

  LegacyGanttTheme _buildGanttTheme() {
    final baseTheme = LegacyGanttTheme.fromTheme(Theme.of(context));
    final isDarkMode = Theme.of(context).brightness == Brightness.dark;

    switch (_viewModel.selectedTheme) {
      case ThemePreset.forest:
        return baseTheme.copyWith(
          barColorPrimary: Colors.green.shade800,
          barColorSecondary: Colors.green.shade600,
          containedDependencyBackgroundColor: Colors.brown.withValues(alpha: 0.2),
          dependencyLineColor: Colors.brown.shade800,
          timeRangeHighlightColor: Colors.yellow.withValues(alpha: 0.1),
          backgroundColor: isDarkMode ? const Color(0xFF2d2c2a) : const Color(0xFFf5f3f0),
          emptySpaceHighlightColor: Colors.green.withValues(alpha: 0.1),
          emptySpaceAddIconColor: Colors.green.shade600,
          taskTextStyle: baseTheme.taskTextStyle.copyWith(color: Colors.white),
        );
      case ThemePreset.midnight:
        return baseTheme.copyWith(
          barColorPrimary: Colors.indigo.shade700,
          barColorSecondary: Colors.indigo.shade500,
          containedDependencyBackgroundColor: Colors.purple.withValues(alpha: 0.2),
          dependencyLineColor: Colors.purple.shade200,
          timeRangeHighlightColor: Colors.blueGrey.withValues(alpha: 0.2),
          backgroundColor: isDarkMode ? const Color(0xFF1a1a2e) : const Color(0xFFe3e3f3),
          emptySpaceHighlightColor: Colors.indigo.withValues(alpha: 0.1),
          emptySpaceAddIconColor: Colors.indigo.shade200,
          textColor: isDarkMode ? Colors.white70 : Colors.black87,
          taskTextStyle: baseTheme.taskTextStyle.copyWith(color: Colors.white),
        );
      case ThemePreset.standard:
        return baseTheme.copyWith(
          barColorPrimary: Colors.blue.shade700,
          barColorSecondary: Colors.blue[600],
          containedDependencyBackgroundColor: Colors.green.withValues(alpha: 0.15),
          dependencyLineColor: Colors.red.shade700,
          timeRangeHighlightColor: isDarkMode ? Colors.grey[850] : Colors.grey[200],
          emptySpaceHighlightColor: Colors.blue.withValues(alpha: 0.1),
          emptySpaceAddIconColor: Colors.blue.shade700,
          taskTextStyle: baseTheme.taskTextStyle.copyWith(
            fontWeight: FontWeight.bold,
            color: Colors.white, // Ensure good contrast on blue bars
          ),
        );
    }
  }

  void _handleCopyTask(LegacyGanttTask task) {
    _viewModel.handleCopyTask(task);
    _showSnackbar('Copied task: ${task.name}');
  }

  void _handleDeleteTask(LegacyGanttTask task) {
    _viewModel.handleDeleteTask(task);
    _showSnackbar('Deleted task: ${task.name}');
  }

  void _handleClearDependencies(LegacyGanttTask task) {
    _viewModel.clearDependenciesForTask(task);
    _showSnackbar('Cleared all dependencies for ${task.name}');
  }

  Future<void> _showDependencyRemover(BuildContext context, LegacyGanttTask task) async {
    final dependencies = _viewModel.getDependenciesForTask(task);

    final dependencyToRemove = await showDialog<LegacyGanttTaskDependency>(
      context: context,
      builder: (context) => _DependencyManagerDialog(
        title: 'Remove Dependency for "${task.name}"',
        dependencies: dependencies,
        tasks: _viewModel.ganttTasks,
        sourceTask: task,
      ),
    );

    if (dependencyToRemove != null) {
      _viewModel.removeDependency(dependencyToRemove);
      _showSnackbar('Removed dependency');
    }
  }

  void _showSnackbar(String message) => ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(message), duration: const Duration(seconds: 2)),
      );

  // The root of the application uses a ChangeNotifierProvider to make the
  // GanttViewModel available to the entire widget tree below it. This allows
  // any widget to listen to changes in the view model and rebuild accordingly.
  @override
  Widget build(BuildContext context) => ChangeNotifierProvider.value(
        value: _viewModel,
        child: Scaffold(
          body: Consumer<GanttViewModel>(
            builder: (context, vm, child) {
              final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
              final ganttTheme = _buildGanttTheme();

              return Column(
                children: [
                  // --- UI Controls for Example Customization ---
                  Container(
                    padding: const EdgeInsets.all(8.0),
                    color: Theme.of(context).cardColor,
                    child: Wrap(
                      spacing: 16.0,
                      runSpacing: 8.0,
                      alignment: WrapAlignment.center,
                      children: [
                        SegmentedButton<ThemePreset>(
                          segments: const [
                            ButtonSegment(
                                value: ThemePreset.standard, label: Text('Standard'), icon: Icon(Icons.palette)),
                            ButtonSegment(value: ThemePreset.forest, label: Text('Forest'), icon: Icon(Icons.park)),
                            ButtonSegment(
                                value: ThemePreset.midnight,
                                label: Text('Midnight'),
                                icon: Icon(Icons.nightlight_round)),
                          ],
                          selected: {vm.selectedTheme},
                          onSelectionChanged: (newSelection) => vm.setSelectedTheme(newSelection.first),
                        ),
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const Text('Drag & Drop'),
                            Switch(
                              value: vm.dragAndDropEnabled,
                              onChanged: vm.setDragAndDropEnabled,
                            ),
                          ],
                        ),
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const Text('Resize'),
                            Switch(
                              value: vm.resizeEnabled,
                              onChanged: vm.setResizeEnabled,
                            ),
                          ],
                        ),
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const Text('Create Tasks'),
                            Switch(
                              value: vm.createTasksEnabled,
                              onChanged: vm.setCreateTasksEnabled,
                            ),
                          ],
                        ),
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            const Text('Create Dependencies'),
                            Switch(
                              value: vm.dependencyCreationEnabled,
                              onChanged: vm.setDependencyCreationEnabled,
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
                    color: Theme.of(context).cardColor,
                    child: Row(
                      children: [
                        Expanded(
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              const Text('Persons: '),
                              DropdownButton<int>(
                                value: vm.personCount,
                                onChanged: (value) {
                                  if (value != null) vm.setPersonCount(value);
                                },
                                items: List.generate(100, (i) => i + 1)
                                    .map((count) => DropdownMenuItem(value: count, child: Text(count.toString())))
                                    .toList(),
                              ),
                            ],
                          ),
                        ),
                        Expanded(
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              const Text('Jobs: '),
                              DropdownButton<int>(
                                value: vm.jobCount,
                                onChanged: (value) {
                                  if (value != null) vm.setJobCount(value);
                                },
                                items: List.generate(100, (i) => i + 1)
                                    .map((count) => DropdownMenuItem(value: count, child: Text(count.toString())))
                                    .toList(),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                  DashboardHeader(
                    selectedDate: vm.startDate,
                    selectedRange: vm.range,
                    onSelectDate: vm.onSelectDate,
                    onRangeChange: vm.onRangeChange,
                  ),
                  Expanded(
                    child: LayoutBuilder(
                      builder: (context, constraints) {
                        if (vm.gridWidth == null) {
                          WidgetsBinding.instance.addPostFrameCallback((_) {
                            vm.setGridWidth(constraints.maxWidth * 0.4);
                          });
                        }

                        return Row(
                          children: [
                            // Gantt Grid (Left Side)
                            SizedBox(
                              width: vm.gridWidth ?? constraints.maxWidth * 0.4,
                              child: GanttGrid(
                                gridData: vm.visibleGridData,
                                visibleGanttRows: vm.visibleGanttRows,
                                rowMaxStackDepth: vm.rowMaxStackDepth,
                                scrollController: vm.scrollController,
                                onToggleExpansion: vm.toggleExpansion,
                                isDarkMode: isDarkMode,
                                onAddContact: () => vm.addContact(context),
                                onAddLineItem: (parentId) => vm.addLineItem(context, parentId),
                                onSetParentTaskType: vm.setParentTaskType,
                                ganttTasks: vm.ganttTasks,
                              ),
                            ),
                            // Draggable Divider
                            GestureDetector(
                              onHorizontalDragUpdate: (details) {
                                final newWidth = (vm.gridWidth ?? 0) + details.delta.dx;
                                vm.setGridWidth(newWidth.clamp(150.0, constraints.maxWidth - 150.0));
                              },
                              child: MouseRegion(
                                cursor: SystemMouseCursors.resizeLeftRight,
                                child: VerticalDivider(
                                  width: 8,
                                  thickness: 1,
                                  color: Theme.of(context).dividerColor,
                                ),
                              ),
                            ),
                            // Gantt Chart (Right Side)
                            Expanded(
                              child: Column(
                                children: [
                                  Expanded(
                                    child: LayoutBuilder(
                                      builder: (context, chartConstraints) {
                                        // If data is still loading or not set, show a progress indicator
                                        if (vm.ganttTasks.isEmpty && vm.gridData.isEmpty) {
                                          return const Center(child: CircularProgressIndicator());
                                        }

                                        final ganttWidth = vm.calculateGanttWidth(chartConstraints.maxWidth);

                                        return SingleChildScrollView(
                                          scrollDirection: Axis.horizontal,
                                          controller: vm.ganttHorizontalScrollController,
                                          child: SizedBox(
                                            width: ganttWidth,
                                            height: chartConstraints.maxHeight, // Constraints from LayoutBuilder
                                            child: LegacyGanttChartWidget(
                                              scrollController: vm.scrollController, // Link to grid scroll controller
                                              data: vm.ganttTasks,
                                              dependencies: vm.dependencies,
                                              visibleRows: vm.visibleGanttRows,
                                              rowHeight: 27.0,
                                              rowMaxStackDepth: vm.rowMaxStackDepth,
                                              axisHeight: 27.0, // Match grid header height
                                              gridMin: vm.visibleStartDate?.millisecondsSinceEpoch.toDouble(),
                                              gridMax: vm.visibleEndDate?.millisecondsSinceEpoch.toDouble(),
                                              totalGridMin:
                                                  vm.effectiveTotalStartDate?.millisecondsSinceEpoch.toDouble(),
                                              totalGridMax: vm.effectiveTotalEndDate?.millisecondsSinceEpoch.toDouble(),
                                              enableDragAndDrop: vm.dragAndDropEnabled,
                                              enableResize: vm.resizeEnabled,
                                              onTaskUpdate: (task, start, end) {
                                                vm.handleTaskUpdate(task, start, end);
                                                _showSnackbar('Updated ${task.name}');
                                              },
                                              onEmptySpaceClick: (rowId, time) =>
                                                  vm.handleEmptySpaceClick(context, rowId, time),
                                              resizeTooltipDateFormat: (date) =>
                                                  DateFormat('MMM d, h:mm a').format(date.toLocal()),
                                              resizeTooltipBackgroundColor: Colors.purple,
                                              resizeTooltipFontColor: Colors.white,
                                              onTaskHover: (task, globalPosition) =>
                                                  vm.onTaskHover(task, context, globalPosition),
                                              onPressTask: (task) => _showSnackbar('Tapped on task: ${task.name}'),
                                              theme: ganttTheme,
                                              taskContentBuilder: (task) {
                                                if (task.isTimeRangeHighlight) {
                                                  return const SizedBox.shrink(); // Hide content for highlights
                                                }
                                                final barColor = task.color ?? ganttTheme.barColorPrimary;
                                                final textColor =
                                                    ThemeData.estimateBrightnessForColor(barColor) == Brightness.dark
                                                        ? Colors.white
                                                        : Colors.black;
                                                final textStyle = ganttTheme.taskTextStyle.copyWith(color: textColor);

                                                // Define menu items for the context menu
                                                final dependencies = vm.getDependenciesForTask(task);
                                                final availableTasks = vm.getValidDependencyTasks(task);
                                                final hasDependencies = dependencies.isNotEmpty;

                                                final menuItems = <ContextMenuItem>[
                                                  ContextMenuItem(
                                                    caption: 'Copy',
                                                    onTap: () => _handleCopyTask(task),
                                                  ),
                                                  ContextMenuItem(
                                                    caption: 'Delete',
                                                    onTap: () => _handleDeleteTask(task),
                                                  ),
                                                  if (vm.dependencyCreationEnabled) ContextMenuItem.divider,
                                                  if (vm.dependencyCreationEnabled)
                                                    ContextMenuItem(
                                                      caption: 'Add Predecessor',
                                                      submenuBuilder: (context) async {
                                                        if (availableTasks.isEmpty) {
                                                          return [const ContextMenuItem(caption: 'No valid tasks')];
                                                        }
                                                        return availableTasks
                                                            .map((otherTask) => ContextMenuItem(
                                                                  caption: otherTask.name ?? 'Unnamed Task',
                                                                  onTap: () {
                                                                    _viewModel.addDependency(otherTask.id, task.id);
                                                                    _showSnackbar('Added dependency for ${task.name}');
                                                                  },
                                                                ))
                                                            .toList();
                                                      },
                                                    ),
                                                  if (vm.dependencyCreationEnabled)
                                                    ContextMenuItem(
                                                      caption: 'Add Successor',
                                                      submenuBuilder: (context) async {
                                                        if (availableTasks.isEmpty) {
                                                          return [const ContextMenuItem(caption: 'No valid tasks')];
                                                        }
                                                        return availableTasks
                                                            .map((otherTask) => ContextMenuItem(
                                                                  caption: otherTask.name ?? 'Unnamed Task',
                                                                  onTap: () {
                                                                    _viewModel.addDependency(task.id, otherTask.id);
                                                                    _showSnackbar('Added dependency for ${task.name}');
                                                                  },
                                                                ))
                                                            .toList();
                                                      },
                                                    ),
                                                  if (vm.dependencyCreationEnabled && hasDependencies)
                                                    ContextMenuItem.divider,
                                                  if (vm.dependencyCreationEnabled && hasDependencies)
                                                    ContextMenuItem(
                                                      caption: 'Remove Dependency...',
                                                      onTap: () => _showDependencyRemover(context, task),
                                                    ),
                                                  if (vm.dependencyCreationEnabled && hasDependencies)
                                                    ContextMenuItem(
                                                      caption: 'Clear All Dependencies',
                                                      onTap: () => _handleClearDependencies(task),
                                                    ),
                                                ];

                                                return GestureDetector(
                                                  onSecondaryTapUp: (details) {
                                                    showContextMenu(
                                                      context: context,
                                                      menuItems: menuItems,
                                                      tapPosition: details.globalPosition,
                                                    );
                                                  },
                                                  child: LayoutBuilder(builder: (context, constraints) {
                                                    // Define minimum widths for content visibility.
                                                    final bool canShowButton = constraints.maxWidth >= 32;
                                                    final bool canShowText = constraints.maxWidth > 66;

                                                    return Stack(
                                                      children: [
                                                        // Task content (icon and name)
                                                        if (canShowText)
                                                          Padding(
                                                            // Pad to the right to avoid overlapping the options button.
                                                            padding: const EdgeInsets.only(left: 4.0, right: 32.0),
                                                            child: Row(
                                                              children: [
                                                                Icon(
                                                                  task.isSummary
                                                                      ? Icons.summarize_outlined
                                                                      : Icons.task_alt,
                                                                  color: textColor,
                                                                  size: 16,
                                                                ),
                                                                const SizedBox(width: 4),
                                                                Expanded(
                                                                  child: Text(
                                                                    task.name ?? '',
                                                                    style: textStyle,
                                                                    overflow: TextOverflow.ellipsis,
                                                                    softWrap: false,
                                                                  ),
                                                                ),
                                                              ],
                                                            ),
                                                          ),

                                                        // Options menu button
                                                        if (canShowButton)
                                                          Positioned(
                                                            right:
                                                                8, // Inset from the right edge to leave space for resize handle
                                                            top: 0,
                                                            bottom: 0,
                                                            child: Builder(
                                                              builder: (context) => IconButton(
                                                                padding: EdgeInsets.zero,
                                                                icon: Icon(Icons.more_vert, color: textColor, size: 18),
                                                                tooltip: 'Task Options',
                                                                onPressed: () {
                                                                  final RenderBox button =
                                                                      context.findRenderObject() as RenderBox;
                                                                  final Offset offset =
                                                                      button.localToGlobal(Offset.zero);
                                                                  final tapPosition =
                                                                      offset.translate(button.size.width, 0);
                                                                  showContextMenu(
                                                                      context: context,
                                                                      menuItems: menuItems,
                                                                      tapPosition: tapPosition);
                                                                },
                                                              ),
                                                            ),
                                                          ),
                                                      ],
                                                    );
                                                  }),
                                                );
                                              },
                                            ),
                                          ),
                                        );
                                      },
                                    ),
                                  ),
                                  // --- Timeline Scrubber ---
                                  if (vm.totalStartDate != null &&
                                      vm.totalEndDate != null &&
                                      vm.visibleStartDate != null &&
                                      vm.visibleEndDate != null)
                                    Container(
                                      height: 40,
                                      padding: const EdgeInsets.symmetric(vertical: 8.0),
                                      color: Theme.of(context).cardColor,
                                      child: scrubber.LegacyGanttTimelineScrubber(
                                        totalStartDate: vm.totalStartDate!,
                                        totalEndDate: vm.totalEndDate!,
                                        visibleStartDate: vm.visibleStartDate!,
                                        visibleEndDate: vm.visibleEndDate!,
                                        onWindowChanged: vm.onScrubberWindowChanged,
                                        visibleRows: vm.visibleGanttRows.map((row) => row.id).toList(),
                                        rowMaxStackDepth: vm.rowMaxStackDepth,
                                        rowHeight: 27.0,
                                        tasks: vm.ganttTasks
                                            .map((t) => scrubber.LegacyGanttTask(
                                                  id: t.id,
                                                  rowId: t.rowId,
                                                  stackIndex: t.stackIndex,
                                                  start: t.start,
                                                  end: t.end,
                                                  name: t.name,
                                                  color: t.color,
                                                  isOverlapIndicator: t.isOverlapIndicator,
                                                  isTimeRangeHighlight: t.isTimeRangeHighlight,
                                                  isSummary: t.isSummary,
                                                ))
                                            .toList(),
                                        startPadding: const Duration(days: 7),
                                        endPadding: const Duration(days: 7),
                                      ),
                                    ),
                                ],
                              ),
                            )
                          ],
                        );
                      },
                    ),
                  ),
                ],
              );
            },
          ),
        ),
      );
}

/// A dialog to manage (remove) dependencies for a task.
class _DependencyManagerDialog extends StatelessWidget {
  final String title;
  final List<LegacyGanttTaskDependency> dependencies;
  final List<LegacyGanttTask> tasks;
  final LegacyGanttTask sourceTask;

  const _DependencyManagerDialog({
    required this.title,
    required this.dependencies,
    required this.tasks,
    required this.sourceTask,
  });

  String _dependencyText(LegacyGanttTaskDependency dep) {
    final bool isPredecessor = dep.predecessorTaskId == sourceTask.id;
    final otherTaskId = isPredecessor ? dep.successorTaskId : dep.predecessorTaskId;
    final otherTaskResult = tasks.where((t) => t.id == otherTaskId);
    final otherTaskName = otherTaskResult.isEmpty ? 'Unknown Task' : (otherTaskResult.first.name ?? 'Unknown Task');

    final relationship =
        isPredecessor ? '${sourceTask.name} -> $otherTaskName' : '$otherTaskName -> ${sourceTask.name}';

    // Make type name more readable
    final typeName = dep.type.name.replaceAllMapped(RegExp(r'[A-Z]'), (match) => ' ${match.group(0)}').capitalize();

    return '($typeName) $relationship';
  }

  @override
  Widget build(BuildContext context) => AlertDialog(
        title: Text(title),
        content: SizedBox(
          width: double.maxFinite,
          child: dependencies.isEmpty
              ? const Text('No dependencies to remove.')
              : ListView.builder(
                  shrinkWrap: true,
                  itemCount: dependencies.length,
                  itemBuilder: (context, index) {
                    final dep = dependencies[index];
                    return ListTile(
                      title: Text(_dependencyText(dep)),
                      onTap: () => Navigator.of(context).pop(dep),
                    );
                  },
                ),
        ),
        actions: [
          TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')),
        ],
      );
}

extension on String {
  String capitalize() => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}

/// A stateful widget for the "Create Task" dialog.
class _CreateTaskAlertDialog extends StatefulWidget {
  final DateTime initialTime;
  final String resourceName;
  final String rowId;
  final Function(LegacyGanttTask) onCreate;
  final TimeOfDay defaultStartTime;
  final TimeOfDay defaultEndTime;

  const _CreateTaskAlertDialog({
    required this.initialTime,
    required this.resourceName,
    required this.rowId,
    required this.onCreate,
    required this.defaultStartTime,
    required this.defaultEndTime,
  });

  @override
  State<_CreateTaskAlertDialog> createState() => _CreateTaskAlertDialogState();
}

class _CreateTaskAlertDialogState extends State<_CreateTaskAlertDialog> {
  late final TextEditingController _nameController;
  late DateTime _startDate;
  late DateTime _endDate;

  @override
  void initState() {
    super.initState();
    _nameController = TextEditingController(text: 'New Task for ${widget.resourceName}');
    // Select the default text so the user can easily overwrite it.
    _nameController.selection = TextSelection(
      baseOffset: 0,
      extentOffset: _nameController.text.length,
    );

    // Use the date part from where the user clicked, but apply the default times.
    final datePart = widget.initialTime;
    _startDate = DateTime(
      datePart.year,
      datePart.month,
      datePart.day,
      widget.defaultStartTime.hour,
      widget.defaultStartTime.minute,
    );
    _endDate = DateTime(
      datePart.year,
      datePart.month,
      datePart.day,
      widget.defaultEndTime.hour,
      widget.defaultEndTime.minute,
    );

    // Handle overnight case where end time is on the next day.
    if (_endDate.isBefore(_startDate)) {
      _endDate = _endDate.add(const Duration(days: 1));
    }
  }

  @override
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }

  void _submit() {
    if (_nameController.text.isNotEmpty) {
      final newTask = LegacyGanttTask(
          id: 'new_task_${DateTime.now().millisecondsSinceEpoch}',
          rowId: widget.rowId,
          name: _nameController.text,
          start: _startDate,
          end: _endDate);
      widget.onCreate(newTask);
    }
  }

  Future<void> _selectDateTime(BuildContext context, bool isStart) async {
    final initialDate = isStart ? _startDate : _endDate;

    final pickedDate = await showDatePicker(
      context: context,
      initialDate: initialDate,
      firstDate: DateTime(2000),
      lastDate: DateTime(2030),
    );

    if (pickedDate == null || !context.mounted) return;

    final pickedTime = await showTimePicker(
      context: context,
      initialTime: TimeOfDay.fromDateTime(initialDate),
    );

    if (pickedTime == null) return;

    setState(() {
      final newDateTime =
          DateTime(pickedDate.year, pickedDate.month, pickedDate.day, pickedTime.hour, pickedTime.minute);
      if (isStart) {
        _startDate = newDateTime;
        if (_endDate.isBefore(_startDate)) _endDate = _startDate.add(const Duration(hours: 1));
      } else {
        _endDate = newDateTime;
        if (_startDate.isAfter(_endDate)) _startDate = _endDate.subtract(const Duration(hours: 1));
      }
    });
  }

  @override
  Widget build(BuildContext context) => AlertDialog(
        title: Text('Create Task for ${widget.resourceName}'),
        content: Column(mainAxisSize: MainAxisSize.min, children: [
          TextField(
            controller: _nameController,
            autofocus: true,
            decoration: const InputDecoration(labelText: 'Task Name'),
            onSubmitted: (_) => _submit(),
          ),
          const SizedBox(height: 16),
          Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
            const Text('Start:'),
            TextButton(
                onPressed: () => _selectDateTime(context, true),
                child: Text(DateFormat.yMd().add_jm().format(_startDate)))
          ]),
          Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
            const Text('End:'),
            TextButton(
                onPressed: () => _selectDateTime(context, false),
                child: Text(DateFormat.yMd().add_jm().format(_endDate)))
          ]),
        ]),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
          TextButton(onPressed: _submit, child: const Text('Create')),
        ],
      );
}
21
likes
160
points
1.6k
downloads
screenshot

Publisher

verified publisherlegacy-automation.online

Weekly Downloads

A flexible and performant Gantt chart widget for Flutter. Supports interactive drag-and-drop, resizing, dynamic data loading, and extensive theming.

Repository (GitHub)
View/report issues
Contributing

Topics

#gantt #chart #schedule #project-management #timeline

Documentation

API reference

License

MIT (license)

Dependencies

flutter, intl, legacy_timeline_scrubber, provider

More

Packages that depend on legacy_gantt_chart