From eafa08b28c34248df450dc2865e591d4516e54fd Mon Sep 17 00:00:00 2001 From: ALim Aidrus Date: Sat, 15 Nov 2025 21:36:42 +0800 Subject: [PATCH] add signature module for all --- lib/screens/profile.dart | 219 +++++++++++++++++++++++++++++++++- lib/services/api_service.dart | 14 +++ pubspec.lock | 8 ++ pubspec.yaml | 1 + 4 files changed, 238 insertions(+), 4 deletions(-) diff --git a/lib/screens/profile.dart b/lib/screens/profile.dart index a294deb..e0982f7 100644 --- a/lib/screens/profile.dart +++ b/lib/screens/profile.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'dart:typed_data'; // <-- ADDED IMPORT import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; +import 'package:signature/signature.dart'; // <-- ADDED IMPORT import 'package:environment_monitoring_app/auth_provider.dart'; import 'package:environment_monitoring_app/services/api_service.dart'; @@ -20,10 +22,19 @@ class _ProfileScreenState extends State { bool _isLoading = false; String _errorMessage = ''; File? _profileImageFile; + File? _signatureImageFile; // <-- ADDED STATE VARIABLE // FIX: Use late initialization to retrieve the service instance. late ApiService _apiService; + // --- ADDED SIGNATURE CONTROLLER --- + final SignatureController _signatureController = SignatureController( + penStrokeWidth: 3, + penColor: Colors.black, + exportBackgroundColor: Colors.transparent, // For transparent PNG + ); + // --- END OF CONTROLLER --- + @override void initState() { super.initState(); @@ -32,10 +43,14 @@ class _ProfileScreenState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _apiService = Provider.of(context, listen: false); _loadLocalProfileImage().then((_) { - // If no profile data is available at all, trigger a refresh - if (Provider.of(context, listen: false).profileData == null) { - _refreshProfile(); - } + // --- MODIFIED: Chain signature loading --- + _loadLocalSignatureImage().then((_) { + // If no profile data is available at all, trigger a refresh + if (Provider.of(context, listen: false).profileData == null) { + _refreshProfile(); + } + }); + // --- END MODIFIED --- }); }); } @@ -53,6 +68,7 @@ class _ProfileScreenState extends State { await auth.refreshProfile(); // After syncing, reload the potentially new profile image await _loadLocalProfileImage(); + await _loadLocalSignatureImage(); // <-- ADDED THIS LINE } catch (e) { if (mounted) { setState(() { @@ -94,6 +110,36 @@ class _ProfileScreenState extends State { } } + // --- ADDED NEW FUNCTION --- + /// Loads the signature image from the local cache or downloads it if not present. + Future _loadLocalSignatureImage() async { + final auth = Provider.of(context, listen: false); + // Assumes the field name in your DB/profile JSON is 'signature_path' + final String? serverSignaturePath = auth.profileData?['signature_path']; + + if (serverSignaturePath != null && serverSignaturePath.isNotEmpty) { + final String localFileName = p.basename(serverSignaturePath); + final Directory appDocDir = await getApplicationDocumentsDirectory(); + // Store signatures in their own sub-directory + final String localFilePath = p.join(appDocDir.path, 'signatures', localFileName); + final File localFile = File(localFilePath); + + if (await localFile.exists()) { + if (mounted) setState(() => _signatureImageFile = localFile); + } else { + final String fullImageUrl = ApiService.imageBaseUrl + serverSignaturePath; + // We can re-use the downloadProfilePicture method, just provide a different local path + final downloadedFile = await _apiService.downloadProfilePicture(fullImageUrl, localFilePath); + if (downloadedFile != null && mounted) { + setState(() => _signatureImageFile = downloadedFile); + } + } + } else { + if (mounted) setState(() => _signatureImageFile = null); + } + } + // --- END OF NEW FUNCTION --- + /// Shows a modal bottom sheet for selecting an image source. Future _showImageSourceSelection() async { showModalBottomSheet( @@ -154,6 +200,106 @@ class _ProfileScreenState extends State { } } + // --- ADDED THESE 2 NEW FUNCTIONS --- + /// Shows the signature pad dialog. + Future _showSignaturePad() async { + _signatureController.clear(); // Clear any previous drawings + + final File? signatureFile = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text("Provide Your Signature"), + content: Container( + width: double.maxFinite, + height: 300, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + color: Colors.grey.shade200, // Background for the pad + ), + child: Signature( + controller: _signatureController, + backgroundColor: Colors.white, + ), + ), + actions: [ + TextButton( + child: const Text("Clear"), + onPressed: () { + _signatureController.clear(); + }, + ), + TextButton( + child: const Text("Cancel"), + onPressed: () { + Navigator.pop(dialogContext, null); + }, + ), + ElevatedButton( + child: const Text("Save"), + onPressed: () async { + if (_signatureController.isNotEmpty) { + final Uint8List? data = await _signatureController.toPngBytes(); + if (data != null) { + final tempDir = await getTemporaryDirectory(); + final file = File(p.join(tempDir.path, "signature_${DateTime.now().millisecondsSinceEpoch}.png")); + await file.writeAsBytes(data); + Navigator.pop(dialogContext, file); + } + } else { + // Show a snackbar if the signature is empty + ScaffoldMessenger.of(dialogContext).showSnackBar( + const SnackBar( + content: Text("Please provide a signature first."), + duration: Duration(seconds: 2), + ), + ); + } + }, + ), + ], + ); + }, + ); + + // After dialog is closed, check if we got a file + if (signatureFile != null) { + await _uploadSignature(signatureFile); + } + } + + /// Uploads the signature file and refreshes the profile. + Future _uploadSignature(File signatureFile) async { + setState(() => _isLoading = true); + + final uploadResult = await _apiService.uploadSignature(signatureFile); + + if (mounted) { + if (uploadResult['success']) { + // Refresh profile to get new signature_path and display it + await _refreshProfile(); + _showSnackBar("Signature updated successfully.", isError: false); + } else { + setState(() { + _errorMessage = uploadResult['message'] ?? 'Failed to upload signature.'; + }); + _showSnackBar(_errorMessage, isError: true); + } + setState(() => _isLoading = false); + } + + // Clean up temp file + try { + if (await signatureFile.exists()) { + await signatureFile.delete(); + } + } catch (e) { + debugPrint("Error deleting temp signature file: $e"); + } + } + // --- END OF NEW FUNCTIONS --- + void _showSnackBar(String message, {bool isError = false}) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -260,6 +406,12 @@ class _ProfileScreenState extends State { _buildProfileDetail(context, "Position:", profileData['position_name']), ]), const SizedBox(height: 24), + + // --- ADDED THIS SECTION --- + _buildSignatureSection(context), + const SizedBox(height: 24), + // --- END OF ADDED SECTION --- + _buildProfileSection(context, "Account Status", [ _buildProfileDetail(context, "Account Status:", profileData['account_status']), _buildProfileDetail(context, "Registered On:", profileData['date_registered']), @@ -338,6 +490,65 @@ class _ProfileScreenState extends State { ); } + // --- ADDED NEW WIDGET METHOD --- + Widget _buildSignatureSection(BuildContext context) { + return Card( + elevation: 4, + margin: const EdgeInsets.symmetric(vertical: 8.0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Signature", + style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor), + ), + const Divider(height: 20, thickness: 1.5), + Center( + child: Container( + width: double.infinity, + height: 150, + decoration: BoxDecoration( + color: Colors.grey.shade200, + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: _signatureImageFile != null + ? Image.file( + _signatureImageFile!, + fit: BoxFit.contain, + ) + : Center( + child: Text( + "No signature set.", + style: TextStyle(color: Colors.grey.shade700), + ), + ), + ), + ), + ), + const SizedBox(height: 16), + Center( + child: ElevatedButton.icon( + onPressed: _isLoading ? null : _showSignaturePad, + icon: const Icon(Icons.edit_note), + label: const Text("Update Signature"), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + ) + ], + ), + ), + ); + } + // --- END OF NEW WIDGET METHOD --- + Widget _buildProfileDetail(BuildContext context, String label, dynamic value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 2b1675d..130085b 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -150,6 +150,20 @@ class ApiService { files: {'profile_picture': imageFile}); } + // --- ADDED THIS NEW METHOD --- + /// Uploads the user's signature as a transparent PNG file. + Future> uploadSignature(File imageFile) async { + final baseUrl = await _serverConfigService.getActiveApiUrl(); + // We assume the endpoint is 'profile/upload-signature' + // and the server expects the file field name to be 'signature'. + return _baseService.postMultipart( + baseUrl: baseUrl, + endpoint: 'profile/upload-signature', + fields: {}, + files: {'signature': imageFile}); + } + // --- END OF NEW METHOD --- + Future> refreshProfile() async { debugPrint('ApiService: Refreshing profile data from server...'); final result = await getProfile(); diff --git a/pubspec.lock b/pubspec.lock index 1ec82ff..ba410ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -801,6 +801,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + signature: + dependency: "direct main" + description: + name: signature + sha256: "8056e091ad59c2eb5735fee975ec649d0caf8ce802bb1ffb1e0955b00a6d0daa" + url: "https://pub.dev" + source: hosted + version: "5.5.0" simple_barcode_scanner: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b29a62f..c0cd41e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: flutter_pdfview: ^1.3.2 dio: ^5.4.3+1 toggle_switch: ^2.3.0 + signature: ^5.4.1 # --- Device & Hardware Access --- image_picker: ^1.0.7