IT Service Request
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/it_service_request.dart';
|
||||
import '../models/it_service_request_assignment.dart';
|
||||
import '../models/it_service_request_activity_log.dart';
|
||||
import '../models/it_service_request_action.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'user_offices_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query parameters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class ItServiceRequestQuery {
|
||||
const ItServiceRequestQuery({
|
||||
this.offset = 0,
|
||||
this.limit = 50,
|
||||
this.searchQuery = '',
|
||||
this.officeId,
|
||||
this.status,
|
||||
this.dateRange,
|
||||
});
|
||||
|
||||
final int offset;
|
||||
final int limit;
|
||||
final String searchQuery;
|
||||
final String? officeId;
|
||||
final String? status;
|
||||
final DateTimeRange? dateRange;
|
||||
|
||||
ItServiceRequestQuery copyWith({
|
||||
int? offset,
|
||||
int? limit,
|
||||
String? searchQuery,
|
||||
String? officeId,
|
||||
String? status,
|
||||
DateTimeRange? dateRange,
|
||||
}) {
|
||||
return ItServiceRequestQuery(
|
||||
offset: offset ?? this.offset,
|
||||
limit: limit ?? this.limit,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
officeId: officeId ?? this.officeId,
|
||||
status: status ?? this.status,
|
||||
dateRange: dateRange ?? this.dateRange,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final itServiceRequestQueryProvider = StateProvider<ItServiceRequestQuery>(
|
||||
(ref) => const ItServiceRequestQuery(),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stream providers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final itServiceRequestsProvider = StreamProvider<List<ItServiceRequest>>((ref) {
|
||||
final userId = ref.watch(currentUserIdProvider);
|
||||
if (userId == null) return const Stream.empty();
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
||||
final userOfficesAsync = ref.watch(userOfficesProvider);
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<ItServiceRequest>(
|
||||
stream: client
|
||||
.from('it_service_requests')
|
||||
.stream(primaryKey: ['id'])
|
||||
.order('created_at', ascending: false),
|
||||
onPollData: () async {
|
||||
final data = await client
|
||||
.from('it_service_requests')
|
||||
.select()
|
||||
.order('created_at', ascending: false);
|
||||
return data.map(ItServiceRequest.fromMap).toList();
|
||||
},
|
||||
fromMap: ItServiceRequest.fromMap,
|
||||
channelName: 'it_service_requests',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) {
|
||||
var items = result.data;
|
||||
// Standard users see only requests from their offices or created by them
|
||||
if (profile != null && profile.role == 'standard') {
|
||||
final officeIds = (userOfficesAsync.valueOrNull ?? [])
|
||||
.where((a) => a.userId == userId)
|
||||
.map((a) => a.officeId)
|
||||
.toSet();
|
||||
items = items
|
||||
.where(
|
||||
(r) =>
|
||||
r.creatorId == userId ||
|
||||
(r.officeId != null && officeIds.contains(r.officeId)),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
return items;
|
||||
});
|
||||
});
|
||||
|
||||
final itServiceRequestByIdProvider = Provider.family<ItServiceRequest?, String>(
|
||||
(ref, id) {
|
||||
final requests = ref.watch(itServiceRequestsProvider).valueOrNull;
|
||||
if (requests == null) return null;
|
||||
try {
|
||||
return requests.firstWhere((r) => r.id == id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final itServiceRequestAssignmentsProvider =
|
||||
StreamProvider<List<ItServiceRequestAssignment>>((ref) {
|
||||
final userId = ref.watch(currentUserIdProvider);
|
||||
if (userId == null) return const Stream.empty();
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<ItServiceRequestAssignment>(
|
||||
stream: client
|
||||
.from('it_service_request_assignments')
|
||||
.stream(primaryKey: ['id'])
|
||||
.order('created_at', ascending: false),
|
||||
onPollData: () async {
|
||||
final data = await client
|
||||
.from('it_service_request_assignments')
|
||||
.select()
|
||||
.order('created_at', ascending: false);
|
||||
return data.map(ItServiceRequestAssignment.fromMap).toList();
|
||||
},
|
||||
fromMap: ItServiceRequestAssignment.fromMap,
|
||||
channelName: 'it_service_request_assignments',
|
||||
onStatusChanged: ref
|
||||
.read(realtimeControllerProvider)
|
||||
.handleChannelStatus,
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
});
|
||||
|
||||
final itServiceRequestActivityLogsProvider =
|
||||
StreamProvider.family<List<ItServiceRequestActivityLog>, String>((
|
||||
ref,
|
||||
requestId,
|
||||
) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<ItServiceRequestActivityLog>(
|
||||
stream: client
|
||||
.from('it_service_request_activity_logs')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('request_id', requestId)
|
||||
.order('created_at', ascending: false),
|
||||
onPollData: () async {
|
||||
final data = await client
|
||||
.from('it_service_request_activity_logs')
|
||||
.select()
|
||||
.eq('request_id', requestId)
|
||||
.order('created_at', ascending: false);
|
||||
return data.map(ItServiceRequestActivityLog.fromMap).toList();
|
||||
},
|
||||
fromMap: ItServiceRequestActivityLog.fromMap,
|
||||
channelName: 'isr_activity_logs_$requestId',
|
||||
onStatusChanged: ref
|
||||
.read(realtimeControllerProvider)
|
||||
.handleChannelStatus,
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
});
|
||||
|
||||
final itServiceRequestActionsProvider =
|
||||
StreamProvider.family<List<ItServiceRequestAction>, String>((
|
||||
ref,
|
||||
requestId,
|
||||
) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<ItServiceRequestAction>(
|
||||
stream: client
|
||||
.from('it_service_request_actions')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('request_id', requestId)
|
||||
.order('created_at', ascending: false),
|
||||
onPollData: () async {
|
||||
final data = await client
|
||||
.from('it_service_request_actions')
|
||||
.select()
|
||||
.eq('request_id', requestId)
|
||||
.order('created_at', ascending: false);
|
||||
return data.map(ItServiceRequestAction.fromMap).toList();
|
||||
},
|
||||
fromMap: ItServiceRequestAction.fromMap,
|
||||
channelName: 'isr_actions_$requestId',
|
||||
onStatusChanged: ref
|
||||
.read(realtimeControllerProvider)
|
||||
.handleChannelStatus,
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final itServiceRequestControllerProvider = Provider<ItServiceRequestController>(
|
||||
(ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return ItServiceRequestController(client);
|
||||
},
|
||||
);
|
||||
|
||||
class ItServiceRequestController {
|
||||
ItServiceRequestController(this._client);
|
||||
final SupabaseClient _client;
|
||||
|
||||
/// Creates a new IT Service Request with auto-generated number.
|
||||
Future<Map<String, dynamic>> createRequest({
|
||||
required String eventName,
|
||||
required List<String> services,
|
||||
String? servicesOther,
|
||||
String? officeId,
|
||||
String? requestedBy,
|
||||
String? requestedByUserId,
|
||||
String status = 'draft',
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
|
||||
try {
|
||||
final result = await _client.rpc(
|
||||
'insert_it_service_request_with_number',
|
||||
params: {
|
||||
'p_event_name': eventName,
|
||||
'p_services': services,
|
||||
'p_creator_id': userId,
|
||||
'p_office_id': officeId,
|
||||
'p_requested_by': requestedBy,
|
||||
'p_requested_by_user_id': requestedByUserId,
|
||||
'p_status': status,
|
||||
},
|
||||
);
|
||||
|
||||
final row = result is List
|
||||
? (result.first as Map<String, dynamic>)
|
||||
: result;
|
||||
final requestId = row['id'] as String;
|
||||
|
||||
if (servicesOther != null && servicesOther.isNotEmpty) {
|
||||
await _client
|
||||
.from('it_service_requests')
|
||||
.update({'services_other': servicesOther})
|
||||
.eq('id', requestId);
|
||||
}
|
||||
|
||||
// Activity log
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': userId,
|
||||
'action_type': 'created',
|
||||
});
|
||||
|
||||
return {'id': requestId, 'request_number': row['request_number']};
|
||||
} catch (e) {
|
||||
debugPrint('createRequest error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates IT Service Request fields.
|
||||
Future<void> updateRequest({
|
||||
required String requestId,
|
||||
String? eventName,
|
||||
List<String>? services,
|
||||
String? servicesOther,
|
||||
String? eventDetails,
|
||||
DateTime? eventDate,
|
||||
DateTime? eventEndDate,
|
||||
DateTime? dryRunDate,
|
||||
DateTime? dryRunEndDate,
|
||||
String? contactPerson,
|
||||
String? contactNumber,
|
||||
String? remarks,
|
||||
String? officeId,
|
||||
String? requestedBy,
|
||||
String? requestedByUserId,
|
||||
String? approvedBy,
|
||||
String? approvedByUserId,
|
||||
bool? outsidePremiseAllowed,
|
||||
DateTime? dateTimeReceived,
|
||||
DateTime? dateTimeChecked,
|
||||
}) async {
|
||||
final updates = <String, dynamic>{};
|
||||
if (eventName != null) updates['event_name'] = eventName;
|
||||
if (services != null) updates['services'] = services;
|
||||
if (servicesOther != null) updates['services_other'] = servicesOther;
|
||||
if (eventDetails != null) updates['event_details'] = eventDetails;
|
||||
if (eventDate != null) updates['event_date'] = eventDate.toIso8601String();
|
||||
if (eventEndDate != null) {
|
||||
updates['event_end_date'] = eventEndDate.toIso8601String();
|
||||
}
|
||||
if (dryRunDate != null) {
|
||||
updates['dry_run_date'] = dryRunDate.toIso8601String();
|
||||
}
|
||||
if (dryRunEndDate != null) {
|
||||
updates['dry_run_end_date'] = dryRunEndDate.toIso8601String();
|
||||
}
|
||||
if (contactPerson != null) updates['contact_person'] = contactPerson;
|
||||
if (contactNumber != null) updates['contact_number'] = contactNumber;
|
||||
if (remarks != null) updates['remarks'] = remarks;
|
||||
if (officeId != null) updates['office_id'] = officeId;
|
||||
if (requestedBy != null) updates['requested_by'] = requestedBy;
|
||||
if (requestedByUserId != null) {
|
||||
updates['requested_by_user_id'] = requestedByUserId;
|
||||
}
|
||||
if (approvedBy != null) updates['approved_by'] = approvedBy;
|
||||
if (approvedByUserId != null) {
|
||||
updates['approved_by_user_id'] = approvedByUserId;
|
||||
}
|
||||
if (outsidePremiseAllowed != null) {
|
||||
updates['outside_premise_allowed'] = outsidePremiseAllowed;
|
||||
}
|
||||
if (dateTimeReceived != null) {
|
||||
updates['date_time_received'] = dateTimeReceived.toIso8601String();
|
||||
}
|
||||
if (dateTimeChecked != null) {
|
||||
updates['date_time_checked'] = dateTimeChecked.toIso8601String();
|
||||
}
|
||||
|
||||
if (updates.isEmpty) return;
|
||||
|
||||
await _client
|
||||
.from('it_service_requests')
|
||||
.update(updates)
|
||||
.eq('id', requestId);
|
||||
|
||||
// Log updated fields
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': userId,
|
||||
'action_type': 'updated',
|
||||
'meta': {'fields': updates.keys.toList()},
|
||||
});
|
||||
}
|
||||
|
||||
/// Update only the status of an IT Service Request.
|
||||
Future<void> updateStatus({
|
||||
required String requestId,
|
||||
required String status,
|
||||
String? cancellationReason,
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
final updates = <String, dynamic>{'status': status};
|
||||
|
||||
if (status == 'scheduled') {
|
||||
updates['approved_at'] = DateTime.now().toUtc().toIso8601String();
|
||||
updates['approved_by_user_id'] = userId;
|
||||
}
|
||||
if (status == 'completed') {
|
||||
updates['completed_at'] = DateTime.now().toUtc().toIso8601String();
|
||||
}
|
||||
if (status == 'cancelled') {
|
||||
updates['cancelled_at'] = DateTime.now().toUtc().toIso8601String();
|
||||
if (cancellationReason != null) {
|
||||
updates['cancellation_reason'] = cancellationReason;
|
||||
}
|
||||
}
|
||||
|
||||
await _client
|
||||
.from('it_service_requests')
|
||||
.update(updates)
|
||||
.eq('id', requestId);
|
||||
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': userId,
|
||||
'action_type': 'status_changed',
|
||||
'meta': {'status': status},
|
||||
});
|
||||
}
|
||||
|
||||
/// Approve a request (admin only). Sets status to 'scheduled'.
|
||||
Future<void> approveRequest({
|
||||
required String requestId,
|
||||
required String approverName,
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
await _client
|
||||
.from('it_service_requests')
|
||||
.update({
|
||||
'status': 'scheduled',
|
||||
'approved_by': approverName,
|
||||
'approved_by_user_id': userId,
|
||||
'approved_at': DateTime.now().toUtc().toIso8601String(),
|
||||
})
|
||||
.eq('id', requestId);
|
||||
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': userId,
|
||||
'action_type': 'approved',
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Assignment management
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Future<void> assignStaff({
|
||||
required String requestId,
|
||||
required List<String> userIds,
|
||||
}) async {
|
||||
final actorId = _client.auth.currentUser?.id;
|
||||
final rows = userIds
|
||||
.map((uid) => {'request_id': requestId, 'user_id': uid})
|
||||
.toList();
|
||||
await _client
|
||||
.from('it_service_request_assignments')
|
||||
.upsert(rows, onConflict: 'request_id,user_id');
|
||||
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': actorId,
|
||||
'action_type': 'assigned',
|
||||
'meta': {'user_ids': userIds},
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> unassignStaff({
|
||||
required String requestId,
|
||||
required String userId,
|
||||
}) async {
|
||||
final actorId = _client.auth.currentUser?.id;
|
||||
await _client
|
||||
.from('it_service_request_assignments')
|
||||
.delete()
|
||||
.eq('request_id', requestId)
|
||||
.eq('user_id', userId);
|
||||
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': actorId,
|
||||
'action_type': 'unassigned',
|
||||
'meta': {'user_id': userId},
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Action Taken
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Future<String> createOrUpdateAction({
|
||||
required String requestId,
|
||||
required String actionTaken,
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
|
||||
// Check if action already exists for this user
|
||||
final existing = await _client
|
||||
.from('it_service_request_actions')
|
||||
.select('id')
|
||||
.eq('request_id', requestId)
|
||||
.eq('user_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (existing != null) {
|
||||
await _client
|
||||
.from('it_service_request_actions')
|
||||
.update({'action_taken': actionTaken})
|
||||
.eq('id', existing['id']);
|
||||
return existing['id'] as String;
|
||||
} else {
|
||||
final result = await _client
|
||||
.from('it_service_request_actions')
|
||||
.insert({
|
||||
'request_id': requestId,
|
||||
'user_id': userId,
|
||||
'action_taken': actionTaken,
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
return result['id'] as String;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Evidence Attachments
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Future<String> uploadEvidence({
|
||||
required String requestId,
|
||||
required String actionId,
|
||||
required String fileName,
|
||||
required Uint8List bytes,
|
||||
DateTime? takenAt,
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
|
||||
final path = '$requestId/evidence/$fileName';
|
||||
|
||||
if (kIsWeb) {
|
||||
await _client.storage
|
||||
.from('it_service_attachments')
|
||||
.uploadBinary(
|
||||
path,
|
||||
bytes,
|
||||
fileOptions: const FileOptions(upsert: true),
|
||||
);
|
||||
} else {
|
||||
final tmpDir = Directory.systemTemp;
|
||||
final tmpFile = File('${tmpDir.path}/$fileName');
|
||||
await tmpFile.writeAsBytes(bytes);
|
||||
await _client.storage
|
||||
.from('it_service_attachments')
|
||||
.upload(path, tmpFile, fileOptions: const FileOptions(upsert: true));
|
||||
try {
|
||||
await tmpFile.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
await _client.from('it_service_request_evidence').insert({
|
||||
'request_id': requestId,
|
||||
'action_id': actionId,
|
||||
'user_id': userId,
|
||||
'file_path': path,
|
||||
'file_name': fileName,
|
||||
'taken_at': takenAt?.toIso8601String(),
|
||||
});
|
||||
|
||||
return _client.storage.from('it_service_attachments').getPublicUrl(path);
|
||||
}
|
||||
|
||||
Future<void> deleteEvidence({required String evidenceId}) async {
|
||||
// Get path first
|
||||
final row = await _client
|
||||
.from('it_service_request_evidence')
|
||||
.select('file_path')
|
||||
.eq('id', evidenceId)
|
||||
.single();
|
||||
final path = row['file_path'] as String;
|
||||
|
||||
await _client.storage.from('it_service_attachments').remove([path]);
|
||||
await _client
|
||||
.from('it_service_request_evidence')
|
||||
.delete()
|
||||
.eq('id', evidenceId);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// File Attachments (event files, max 25MB)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Future<String> uploadAttachment({
|
||||
required String requestId,
|
||||
required String fileName,
|
||||
required Uint8List bytes,
|
||||
}) async {
|
||||
if (bytes.length > 25 * 1024 * 1024) {
|
||||
throw Exception('File size exceeds 25MB limit');
|
||||
}
|
||||
final path = '$requestId/attachments/$fileName';
|
||||
|
||||
if (kIsWeb) {
|
||||
await _client.storage
|
||||
.from('it_service_attachments')
|
||||
.uploadBinary(
|
||||
path,
|
||||
bytes,
|
||||
fileOptions: const FileOptions(upsert: true),
|
||||
);
|
||||
} else {
|
||||
final tmpDir = Directory.systemTemp;
|
||||
final tmpFile = File('${tmpDir.path}/$fileName');
|
||||
await tmpFile.writeAsBytes(bytes);
|
||||
await _client.storage
|
||||
.from('it_service_attachments')
|
||||
.upload(path, tmpFile, fileOptions: const FileOptions(upsert: true));
|
||||
try {
|
||||
await tmpFile.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return _client.storage.from('it_service_attachments').getPublicUrl(path);
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> listAttachments(String requestId) async {
|
||||
try {
|
||||
final files = await _client.storage
|
||||
.from('it_service_attachments')
|
||||
.list(path: '$requestId/attachments');
|
||||
return files
|
||||
.map(
|
||||
(f) => {
|
||||
'name': f.name,
|
||||
'url': _client.storage
|
||||
.from('it_service_attachments')
|
||||
.getPublicUrl('$requestId/attachments/${f.name}'),
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAttachment({
|
||||
required String requestId,
|
||||
required String fileName,
|
||||
}) async {
|
||||
final path = '$requestId/attachments/$fileName';
|
||||
await _client.storage.from('it_service_attachments').remove([path]);
|
||||
}
|
||||
|
||||
/// List evidence attachments for a request from the database.
|
||||
Future<List<Map<String, dynamic>>> listEvidence(String requestId) async {
|
||||
final rows = await _client
|
||||
.from('it_service_request_evidence')
|
||||
.select()
|
||||
.eq('request_id', requestId)
|
||||
.order('created_at', ascending: false);
|
||||
return rows
|
||||
.map(
|
||||
(r) => {
|
||||
'id': r['id'],
|
||||
'file_name': r['file_name'],
|
||||
'file_path': r['file_path'],
|
||||
'taken_at': r['taken_at'],
|
||||
'url': _client.storage
|
||||
.from('it_service_attachments')
|
||||
.getPublicUrl(r['file_path'] as String),
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user