* Task/Ticket desktop dialogs — widened from 520 → 600dp, and tasks now uses SizedBox(width: 600) instead of ConstrainedBox(maxWidth:) which was the root cause of the "still small" issue (max-only never forced expansion)
* Pass Slip + Leave — both dialogs gained an isSheet flag; callers now branch on AppBreakpoints.tablet: bottom sheet on mobile, dialog on desktop
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -219,6 +221,7 @@ final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
||||
'[attendanceLogsProvider] reconnected — syncing ${pendingUpdatesMap.length} attendance update(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
final filesToDelete = <String>[];
|
||||
for (final entry in pendingUpdatesMap.entries) {
|
||||
final attendanceId = entry.key;
|
||||
final fields = Map<String, dynamic>.from(entry.value);
|
||||
@@ -243,12 +246,64 @@ final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If a verification photo was queued to disk while offline, upload
|
||||
// the bytes first and append the resulting public URL to `fields`.
|
||||
final localPath = fields.remove('_local_photo_path') as String?;
|
||||
final isCheckOut = fields.remove('_is_checkout') as bool? ?? false;
|
||||
final fileName = fields.remove('_file_name') as String?;
|
||||
String? pendingFileToDelete;
|
||||
if (localPath != null && !kIsWeb) {
|
||||
try {
|
||||
final file = File(localPath);
|
||||
if (await file.exists()) {
|
||||
final bytes = await file.readAsBytes();
|
||||
final userId = client.auth.currentUser?.id;
|
||||
if (userId != null) {
|
||||
final ext = (fileName ?? 'photo.jpg')
|
||||
.split('.')
|
||||
.last
|
||||
.toLowerCase();
|
||||
final prefix = isCheckOut ? 'checkout' : 'checkin';
|
||||
final storagePath =
|
||||
'$userId/${prefix}_$attendanceId.$ext';
|
||||
await client.storage
|
||||
.from('attendance-verification')
|
||||
.uploadBinary(
|
||||
storagePath,
|
||||
bytes,
|
||||
fileOptions: const FileOptions(upsert: true),
|
||||
);
|
||||
final url = client.storage
|
||||
.from('attendance-verification')
|
||||
.getPublicUrl(storagePath);
|
||||
final column = isCheckOut
|
||||
? 'check_out_verification_photo_url'
|
||||
: 'check_in_verification_photo_url';
|
||||
fields[column] = url;
|
||||
pendingFileToDelete = localPath;
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[attendanceLogsProvider] local photo missing for id=$attendanceId — replaying status only',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Photo upload failed — leave entry in queue for next reconnect.
|
||||
debugPrint(
|
||||
'[attendanceLogsProvider] failed to upload local photo id=$attendanceId: $e',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await client
|
||||
.from('attendance_logs')
|
||||
.update(fields)
|
||||
.eq('id', attendanceId);
|
||||
syncedIds.add(attendanceId);
|
||||
if (pendingFileToDelete != null) {
|
||||
filesToDelete.add(pendingFileToDelete);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[attendanceLogsProvider] failed to sync update id=$attendanceId: $e',
|
||||
@@ -256,6 +311,12 @@ final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Best-effort cleanup of uploaded local photo files.
|
||||
for (final path in filesToDelete) {
|
||||
try {
|
||||
await File(path).delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remaining = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingAttendanceUpdatesProvider),
|
||||
@@ -408,11 +469,38 @@ class AttendanceController {
|
||||
.eq('id', attendanceId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
// Queue status update without photo URL — photo bytes are not persisted
|
||||
// across sessions; user will need to re-upload photo when online.
|
||||
// Persist photo bytes to disk so the reconnect-replay path can upload
|
||||
// them later. Disk write only on mobile/desktop — `path_provider` is
|
||||
// unavailable on web, where we fall back to status-only queueing.
|
||||
if (!kIsWeb) {
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final pendingDir = Directory('${dir.path}/verification_pending');
|
||||
if (!await pendingDir.exists()) {
|
||||
await pendingDir.create(recursive: true);
|
||||
}
|
||||
final localPath = '${pendingDir.path}/${attendanceId}_$prefix.$ext';
|
||||
await File(localPath).writeAsBytes(bytes);
|
||||
_queueAttendanceUpdate(attendanceId, {
|
||||
'verification_status': status,
|
||||
'_local_photo_path': localPath,
|
||||
'_is_checkout': isCheckOut,
|
||||
'_file_name': fileName,
|
||||
});
|
||||
debugPrint(
|
||||
'[AttendanceController] uploadVerification queued offline with photo: $attendanceId',
|
||||
);
|
||||
return;
|
||||
} catch (writeErr) {
|
||||
debugPrint(
|
||||
'[AttendanceController] failed to persist offline photo for $attendanceId: $writeErr — falling back to status-only queue',
|
||||
);
|
||||
}
|
||||
}
|
||||
// Web or disk-write failure: queue status only.
|
||||
_queueAttendanceUpdate(attendanceId, {'verification_status': status});
|
||||
debugPrint(
|
||||
'[AttendanceController] uploadVerification status queued offline (photo not persisted): $attendanceId',
|
||||
'[AttendanceController] uploadVerification status queued offline (no photo): $attendanceId',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user