Flutter Workgroups

Flutter Workgroups

Continue from previous tutorial: https://hashnotes.hashnode.dev/flutter-login-api-request

[1] Edit PubSpec

Add date time format library i.e. intl (Provides internationalization and localization facilities, including message translation, plurals and genders, date/number formatting and parsing, and bidirectional text.)

name: workgroup_app
description: A flutter workgroup app.
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=2.18.2 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  http: 0.13.0
  intl: ^0.17.0 # DateFormat

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

[2] Prep Workgroup API

Laravel api/WorkgroupController.php:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Workgroup;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class WorkgroupController extends Controller
{
    public function index()
    {
        $user = Auth::user();
        $workgroups = Workgroup::where('mngr_email', $user->email)
            ->get();
        return response()->json($workgroups);
    }

    public function store(Request $request)
    {
        try {
            $validatedData = $request->validate([
                'name' => 'required|string|max:255',
                'refn' => 'required|string',
                'desn' => 'required|string',
                'locn' => 'required|string',
                'estartdate' => 'required|integer',
                'key1' => 'required|string',
                'key2' => 'required|string',
                'key3' => 'required|string',
                'key4' => 'required|string',
                'key5' => 'required|string',
                'n' => 'required|integer',
            ]);

            $user = Auth::user();

            $workgroup = Workgroup::create([
                'mngr_email' => $user->email,
                'name' => $validatedData['name'],
                'refn' => $validatedData['refn'],
                'desn' => $validatedData['desn'],
                'locn' => $validatedData['locn'],
                'estartdate' => $validatedData['estartdate'],
                'key1' => $validatedData['key1'],
                'key2' => $validatedData['key2'],
                'key3' => $validatedData['key3'],
                'key4' => $validatedData['key4'],
                'key5' => $validatedData['key5'],
                'admn' => 0,
                'cord' => 0,
                'oper' => 0,
                'mngr' => $user->id,
                'n' => $validatedData['n'],
            ]);

            return response()->json($workgroup, 201);
        } catch (\Illuminate\Validation\ValidationException $e) {
            return response()->json($e->errors(), 422);
        }
    }

    public function update(Request $request, $workgroup_id)
    {
        $workgroup = Workgroup::findOrFail($workgroup_id);

        // Get the manager's email from the $workgroup
        $managerEmail = $workgroup->mngr_email;

        // Get the authenticated user's email
        $authUserEmail = auth()->user()->email;

        // Compare the manager's email with the authenticated user's email
        if ($managerEmail === $authUserEmail) {
            try {
                $validatedData = $request->validate([
                    'name' => 'required|string|max:255',
                    'refn' => 'required|string',
                    'desn' => 'required|string',
                    'locn' => 'required|string',
                    'estartdate' => 'required|integer',
                    'key1' => 'required|string',
                    'key2' => 'required|string',
                    'key3' => 'required|string',
                    'key4' => 'required|string',
                    'key5' => 'required|string',
                    'n' => 'required|integer',
                ]);

                $workgroup->update($validatedData);

                return response()->json($workgroup,200);
            } catch (\Illuminate\Validation\ValidationException $e) {
                return response()->json($e->errors(), 422);
            }
        }

         else {
            // The manager's email does not match the authenticated user's email
            // You can return an error response or handle the situation accordingly
            return response()->json(['error' => 'You are not authorized to update this workgroup.'], 403);
        }
    }

}

Laravel route api.php:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

use App\Http\Controllers\Api\AuthController;
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);

// Assigning middleware to individual route
Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');


// Assigning middleware to group of routes
use App\Http\Controllers\Api\WorkgroupController;
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/workgroup', [WorkgroupController::class, 'index']);
    Route::post('/workgroup', [WorkgroupController::class, 'store']);    
    Route::put('/workgroup/{workgroup_id}', [WorkgroupController::class, 'update']);
});

[3] Edit Workgroup Screens group

Workgroup Screens group consists of:

(1) Workgroups Screen - list of all workgroups.

(2) Workgroup Screen - editable form of an existing workgroup.

(3) Create Workgroup Screen - editable form for a new workgroup.

File-> lib/modules/workgroups.dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:hive/hive.dart';
import 'package:intl/intl.dart';
import 'workgroupmeetings.dart';

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

  @override
  _WorkgroupsScreenState createState() => _WorkgroupsScreenState();
}

class _WorkgroupsScreenState extends State<WorkgroupsScreen> {
  List<dynamic> _workgroups = [];
  final Box _boxUser = Hive.box("user");

  @override
  void initState() {
    super.initState();
    _fetchWorkgroups();
  }

  Future<void> _fetchWorkgroups() async {
    const url = 'https://demo.razzi.my/spotnet/public/api/workgroup';
    final authToken = await _boxUser.get('user_token');

    try {
      final response = await http.get(
        Uri.parse(url),
        headers: {
          'Authorization': 'Bearer $authToken',
          'Content-Type': 'application/json',
        },
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        setState(() {
          _workgroups = data;
        });
      } else {
        // Handle error
        print('Error fetching workgroups: ${response.statusCode}');
      }
    } catch (e) {
      // Handle network error
      print('Error fetching workgroups: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Workgroups'),
        actions: [
          ElevatedButton(
            onPressed: () {
              // Navigate to the "Create Workgroup" screen
              Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (_) => CreateWorkgroupScreen())).then((_) {
                // This block runs when you have come back to the 1st Page from 2nd.
                _fetchWorkgroups();
              });
            },
            style: ElevatedButton.styleFrom(
              backgroundColor: Color.fromARGB(
                  255, 122, 192, 245), // Set the background color to red
              foregroundColor: Colors.white, // Set the font color to white
              padding:
                  const EdgeInsets.all(8.0), // Adjust the padding as needed
            ),
            child: const Icon(Icons.add),
          ),
        ],
      ),
      body: _workgroups.isEmpty
          ? const Center(child: CircularProgressIndicator())
          : Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16.0),
              child: ListView.builder(
                itemCount: _workgroups.length,
                itemBuilder: (context, index) {
                  final workgroup = _workgroups[index];
                  return ListTile(
                    leading: IconButton(
                      icon: const Icon(Icons.info),
                      onPressed: () {
                        Navigator.push(
                                context,
                                MaterialPageRoute(
                                    builder: (_) =>
                                        WorkgroupScreen(workgroup: workgroup)))
                            .then((_) {
                          // This block runs when you have come back to the 1st Page from 2nd.
                          _fetchWorkgroups();
                        });
                      },
                    ),
                    title: Text('Name: ${workgroup['name']}'),
                    subtitle: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('Reference: ${workgroup['refn']}'),
                        Text('Description: ${workgroup['desn']}'),
                      ],
                    ),
                    trailing: IconButton(
                      icon: const Icon(Icons.chevron_right),
                      onPressed: () {
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (_) =>
                                WorkgroupMeetingsScreen(workgroup: workgroup),
                          ),
                        ).then((_) {
                          // This block runs when you have come back to the 1st Page from 2nd.
                          _fetchWorkgroups();
                        });
                      },
                    ),
                  );
                },
              ),
            ),
    );
  }
}

class WorkgroupScreen extends StatefulWidget {
  final Map<String, dynamic> workgroup;

  const WorkgroupScreen({super.key, required this.workgroup});

  @override
  _WorkgroupScreenState createState() => _WorkgroupScreenState();
}

class _WorkgroupScreenState extends State<WorkgroupScreen> {
  final Box _boxUser = Hive.box("user");
  late TextEditingController _nameController;
  late TextEditingController _refnController;
  late TextEditingController _desnController;
  late TextEditingController _locnController;
  late TextEditingController _startDateController;
  late DateTime _dStartdate;
  late TextEditingController _key1Controller;
  late TextEditingController _key2Controller;
  late TextEditingController _key3Controller;
  late TextEditingController _key4Controller;
  late TextEditingController _key5Controller;
  late TextEditingController _nController;
  bool _isEditing = false;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();

    _nameController = TextEditingController(text: widget.workgroup['name']);
    _refnController = TextEditingController(text: widget.workgroup['refn']);
    _desnController = TextEditingController(text: widget.workgroup['desn']);
    _locnController = TextEditingController(text: widget.workgroup['locn']);
    int _eStartdate = widget.workgroup['estartdate'] ?? 0; // parse epochdate
    _dStartdate = DateTime.fromMillisecondsSinceEpoch(_eStartdate * 1000);
    _startDateController = TextEditingController(
        text: DateFormat('yyyy-MM-dd').format(_dStartdate));
    _key1Controller = TextEditingController(text: widget.workgroup['key1']);
    _key2Controller = TextEditingController(text: widget.workgroup['key2']);
    _key3Controller = TextEditingController(text: widget.workgroup['key3']);
    _key4Controller = TextEditingController(text: widget.workgroup['key4']);
    _key5Controller = TextEditingController(text: widget.workgroup['key5']);
    _nController =
        TextEditingController(text: widget.workgroup['n']?.toString());
  }

  void _toggleEditMode() {
    setState(() {
      _isEditing = !_isEditing;
    });
  }

  Future<void> _saveWorkgroup() async {
    setState(() {
      _isLoading = true;
    });
    try {
      // Prepare the data to be sent
      int n;
      if (int.tryParse(_nController.text) != null) {
        n = int.parse(_nController.text);
      } else {
        n = 0;
      }
      String sStartDate = _startDateController.text;
      DateTime dStartDate = DateTime.parse(sStartDate);
      int eStartDate = dStartDate.millisecondsSinceEpoch ~/ 1000;
      final data = {
        'refn': _refnController.text,
        'desn': _desnController.text,
        'locn': _locnController.text,
        'estartdate': eStartDate,
        'key1': _key1Controller.text,
        'key2': _key2Controller.text,
        'key3': _key3Controller.text,
        'key4': _key4Controller.text,
        'key5': _key5Controller.text,
        'name': _nameController.text,
        'n': n,
      };

      final workgroupId = widget.workgroup['id'];
      final url =
          'https://demo.razzi.my/spotnet/public/api/workgroup/$workgroupId';
      final authToken = await _boxUser.get('user_token');

      // Send the data to the API endpoint
      final response = await http.put(
        Uri.parse(url),
        body: jsonEncode(data),
        headers: {
          'Authorization': 'Bearer $authToken',
          'Content-Type': 'application/json',
        },
      );

      // Check the response status code
      if (response.statusCode == 200) {
        // Workgroup saved successfully
        print('Workgroup saved successfully');
        Navigator.of(context).pop();
      } else {
        // Error saving the workgroup
        print(
            'Error saving workgroup: ${response.statusCode} - ${response.body}');
      }
    } catch (e) {
      // Handle any exceptions that occur during the API call
      print('Error saving workgroup: $e');
    }
    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(children: [
      Scaffold(
        appBar: AppBar(
          title: const Text('Workgroup Details'),
          actions: [
            if (_isEditing)
              IconButton(
                icon: const Icon(Icons.save),
                onPressed: _saveWorkgroup,
              ),
            IconButton(
              icon: Icon(_isEditing ? Icons.cancel : Icons.edit),
              onPressed: _toggleEditMode,
            ),
          ],
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                TextField(
                  controller: _nameController,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Name',
                  ),
                ),
                TextField(
                  controller: _refnController,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Reference',
                  ),
                ),
                TextField(
                  controller: _desnController,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Description',
                  ),
                ),
                TextField(
                  controller: _locnController,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Location',
                  ),
                ),
                TextFormField(
                  controller: _startDateController,
                  enabled: _isEditing,
                  readOnly: true,
                  onTap: () async {
                    final pickedDate = await showDatePicker(
                      context: context,
                      initialDate: _dStartdate,
                      firstDate: DateTime(1900),
                      lastDate: DateTime(2100),
                    );
                    if (pickedDate != null) {
                      setState(() {
                        _startDateController.text =
                            DateFormat('yyyy-MM-dd').format(pickedDate);
                      });
                    }
                  },
                  decoration: const InputDecoration(
                    labelText: 'Start Date',
                    hintText: 'Select start date',
                  ),
                ),
                TextField(
                  controller: _key1Controller,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Key 1',
                  ),
                ),
                TextField(
                  controller: _key2Controller,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Key 2',
                  ),
                ),
                TextField(
                  controller: _key3Controller,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Key 3',
                  ),
                ),
                TextField(
                  controller: _key4Controller,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Key 4',
                  ),
                ),
                TextField(
                  controller: _key5Controller,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'Key 5',
                  ),
                ),
                TextField(
                  controller: _nController,
                  enabled: _isEditing,
                  decoration: const InputDecoration(
                    labelText: 'N',
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
      if (_isLoading)
        Container(
          color: Colors.black.withOpacity(0.5),
          child: Center(
            child: CircularProgressIndicator(),
          ),
        ),
    ]);
  }
}

class CreateWorkgroupScreen extends StatefulWidget {
  @override
  _CreateWorkgroupScreenState createState() => _CreateWorkgroupScreenState();
}

class _CreateWorkgroupScreenState extends State<CreateWorkgroupScreen> {
  final Box _boxUser = Hive.box("user");
  late TextEditingController _nameController;
  late TextEditingController _refnController;
  late TextEditingController _desnController;
  late TextEditingController _locnController;
  late TextEditingController _startDateController;
  late TextEditingController _key1Controller;
  late TextEditingController _key2Controller;
  late TextEditingController _key3Controller;
  late TextEditingController _key4Controller;
  late TextEditingController _key5Controller;
  late TextEditingController _nController;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _nameController = TextEditingController();
    _refnController = TextEditingController();
    _desnController = TextEditingController();
    _locnController = TextEditingController();
    _startDateController = TextEditingController(
        text: DateFormat('yyyy-MM-dd').format(DateTime.now()));
    _key1Controller = TextEditingController(text: '-');
    _key2Controller = TextEditingController(text: '-');
    _key3Controller = TextEditingController(text: '-');
    _key4Controller = TextEditingController(text: '-');
    _key5Controller = TextEditingController(text: '-');
    _nController = TextEditingController(text: (0).toString());
  }

  Future<void> _createWorkgroup() async {
    setState(() {
      _isLoading = true;
    });
    try {
      // Prepare the data to be sent
      int n;
      if (int.tryParse(_nController.text) != null) {
        n = int.parse(_nController.text);
      } else {
        n = 0;
      }
      String sStartDate = _startDateController.text;
      DateTime dStartDate = DateTime.parse(sStartDate);
      int eStartDate = dStartDate.millisecondsSinceEpoch ~/ 1000;
      final data = {
        'refn': _refnController.text,
        'desn': _desnController.text,
        'locn': _locnController.text,
        'estartdate': eStartDate,
        'key1': _key1Controller.text,
        'key2': _key2Controller.text,
        'key3': _key3Controller.text,
        'key4': _key4Controller.text,
        'key5': _key5Controller.text,
        'name': _nameController.text,
        'n': n,
      };

      final url = 'https://demo.razzi.my/spotnet/public/api/workgroup';
      final authToken = await _boxUser.get('user_token');

      // Send the data to the API endpoint
      final response = await http.post(
        Uri.parse(url),
        body: jsonEncode(data),
        headers: {
          'Authorization': 'Bearer $authToken',
          'Content-Type': 'application/json',
        },
      );

      // Check the response status code
      if (response.statusCode == 201) {
        // Workgroup created successfully
        print('Workgroup created successfully');
        Navigator.of(context).pop();
      } else {
        // Error creating the workgroup
        print(
            'Error creating workgroup: ${response.statusCode} - ${response.body}');
      }
    } catch (e) {
      // Handle any exceptions that occur during the API call
      print('Error creating workgroup: $e');
    }
    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(children: [
      Scaffold(
        appBar: AppBar(
          title: const Text('Create Workgroup'),
          actions: [
            IconButton(
              icon: const Icon(Icons.save),
              onPressed: _createWorkgroup,
            ),
          ],
        ),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                TextField(
                  controller: _nameController,
                  decoration: const InputDecoration(
                    labelText: 'Name',
                  ),
                ),
                TextField(
                  controller: _refnController,
                  decoration: const InputDecoration(
                    labelText: 'Reference',
                  ),
                ),
                TextField(
                  controller: _desnController,
                  decoration: const InputDecoration(
                    labelText: 'Description',
                  ),
                ),
                TextField(
                  controller: _locnController,
                  decoration: const InputDecoration(
                    labelText: 'Location',
                  ),
                ),
                TextFormField(
                  controller: _startDateController,
                  readOnly: true,
                  onTap: () async {
                    final pickedDate = await showDatePicker(
                      context: context,
                      initialDate: DateTime.now(),
                      firstDate: DateTime(1900),
                      lastDate: DateTime(2100),
                    );
                    if (pickedDate != null) {
                      setState(() {
                        _startDateController.text =
                            DateFormat('yyyy-MM-dd').format(pickedDate);
                      });
                    }
                  },
                  decoration: const InputDecoration(
                    labelText: 'Start Date',
                    hintText: 'Select start date',
                  ),
                ),
                TextField(
                  controller: _key1Controller,
                  decoration: const InputDecoration(
                    labelText: 'Key 1',
                  ),
                ),
                TextField(
                  controller: _key2Controller,
                  decoration: const InputDecoration(
                    labelText: 'Key 2',
                  ),
                ),
                TextField(
                  controller: _key3Controller,
                  decoration: const InputDecoration(
                    labelText: 'Key 3',
                  ),
                ),
                TextField(
                  controller: _key4Controller,
                  decoration: const InputDecoration(
                    labelText: 'Key 4',
                  ),
                ),
                TextField(
                  controller: _key5Controller,
                  decoration: const InputDecoration(
                    labelText: 'Key 5',
                  ),
                ),
                TextField(
                  controller: _nController,
                  decoration: const InputDecoration(
                    labelText: 'N',
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
      if (_isLoading)
        Container(
          color: Colors.black.withOpacity(0.5),
          child: Center(
            child: CircularProgressIndicator(),
          ),
        ),
    ]);
  }
}

Output for WorkgroupsScreen:

Output for CreateWorkgroupScreen:

Output for WorkgroupScreen:

WorkgroupScreen in Edit mode:

[4] Create a draft of Workgroup Meetings Screen

File-> lib/modules/workgroupmeetings.dart

import 'package:flutter/material.dart';

class WorkgroupMeetingsScreen extends StatefulWidget {
  final Map<String, dynamic> workgroup;

  const WorkgroupMeetingsScreen({super.key, required this.workgroup});

  @override
  _WorkgroupMeetingsScreenState createState() => _WorkgroupMeetingsScreenState();
}

class _WorkgroupMeetingsScreenState extends State<WorkgroupMeetingsScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Workgroup Meetings - ${widget.workgroup['name']}'),
      ),
      body: Center(
        child: Text('Workgroup Meetings for ${widget.workgroup['name']}'),
      ),
    );
  }
}

Output for WorkgroupMeetingsScreen: