Flutter Workgroups

Mohamad's interest is in Programming (Mobile, Web, Database and Machine Learning). He is studying at the Center For Artificial Intelligence Technology (CAIT), Universiti Kebangsaan Malaysia (UKM).
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:
