Pagination
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/office.dart';
|
||||
import '../models/ticket.dart';
|
||||
@@ -27,15 +28,93 @@ final officesOnceProvider = FutureProvider<List<Office>>((ref) async {
|
||||
.toList();
|
||||
});
|
||||
|
||||
/// Office query parameters for server-side pagination.
|
||||
class OfficeQuery {
|
||||
/// Creates office query parameters.
|
||||
const OfficeQuery({this.offset = 0, this.limit = 50, this.searchQuery = ''});
|
||||
|
||||
/// Offset for pagination.
|
||||
final int offset;
|
||||
|
||||
/// Number of items per page (default: 50).
|
||||
final int limit;
|
||||
|
||||
/// Full text search query.
|
||||
final String searchQuery;
|
||||
|
||||
OfficeQuery copyWith({int? offset, int? limit, String? searchQuery}) {
|
||||
return OfficeQuery(
|
||||
offset: offset ?? this.offset,
|
||||
limit: limit ?? this.limit,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final officesQueryProvider = StateProvider<OfficeQuery>(
|
||||
(ref) => const OfficeQuery(),
|
||||
);
|
||||
|
||||
final officesControllerProvider = Provider<OfficesController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return OfficesController(client);
|
||||
});
|
||||
|
||||
/// Ticket query parameters for server-side pagination and filtering.
|
||||
class TicketQuery {
|
||||
/// Creates ticket query parameters.
|
||||
const TicketQuery({
|
||||
this.offset = 0,
|
||||
this.limit = 50,
|
||||
this.searchQuery = '',
|
||||
this.officeId,
|
||||
this.status,
|
||||
this.dateRange,
|
||||
});
|
||||
|
||||
/// Offset for pagination.
|
||||
final int offset;
|
||||
|
||||
/// Number of items per page (default: 50).
|
||||
final int limit;
|
||||
|
||||
/// Full text search query.
|
||||
final String searchQuery;
|
||||
|
||||
/// Filter by office ID.
|
||||
final String? officeId;
|
||||
|
||||
/// Filter by status.
|
||||
final String? status;
|
||||
|
||||
/// Filter by date range.
|
||||
/// Filter by date range.
|
||||
final DateTimeRange? dateRange;
|
||||
|
||||
TicketQuery copyWith({
|
||||
int? offset,
|
||||
int? limit,
|
||||
String? searchQuery,
|
||||
String? officeId,
|
||||
String? status,
|
||||
DateTimeRange? dateRange,
|
||||
}) {
|
||||
return TicketQuery(
|
||||
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 ticketsProvider = StreamProvider<List<Ticket>>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final profileAsync = ref.watch(currentProfileProvider);
|
||||
final assignmentsAsync = ref.watch(userOfficesProvider);
|
||||
final query = ref.watch(ticketsQueryProvider);
|
||||
|
||||
final profile = profileAsync.valueOrNull;
|
||||
if (profile == null) {
|
||||
@@ -47,33 +126,62 @@ final ticketsProvider = StreamProvider<List<Ticket>>((ref) {
|
||||
profile.role == 'dispatcher' ||
|
||||
profile.role == 'it_staff';
|
||||
|
||||
if (isGlobal) {
|
||||
return client
|
||||
.from('tickets')
|
||||
.stream(primaryKey: ['id'])
|
||||
.order('created_at', ascending: false)
|
||||
.map((rows) => rows.map(Ticket.fromMap).toList());
|
||||
}
|
||||
|
||||
final officeIds =
|
||||
assignmentsAsync.valueOrNull
|
||||
?.where((assignment) => assignment.userId == profile.id)
|
||||
.map((assignment) => assignment.officeId)
|
||||
.toSet()
|
||||
.toList() ??
|
||||
<String>[];
|
||||
if (officeIds.isEmpty) {
|
||||
return Stream.value(const <Ticket>[]);
|
||||
}
|
||||
|
||||
return client
|
||||
// Use stream for realtime updates, then apply pagination & search filters
|
||||
// client-side because `.range(...)` is not supported on the stream builder.
|
||||
final baseStream = client
|
||||
.from('tickets')
|
||||
.stream(primaryKey: ['id'])
|
||||
.inFilter('office_id', officeIds)
|
||||
.order('created_at', ascending: false)
|
||||
.map((rows) => rows.map(Ticket.fromMap).toList());
|
||||
|
||||
return baseStream.map((allTickets) {
|
||||
var list = allTickets;
|
||||
|
||||
if (!isGlobal) {
|
||||
final officeIds =
|
||||
assignmentsAsync.valueOrNull
|
||||
?.where((assignment) => assignment.userId == profile.id)
|
||||
.map((assignment) => assignment.officeId)
|
||||
.toSet()
|
||||
.toList() ??
|
||||
<String>[];
|
||||
if (officeIds.isEmpty) return <Ticket>[];
|
||||
final allowedOffices = officeIds.toSet();
|
||||
list = list.where((t) => allowedOffices.contains(t.officeId)).toList();
|
||||
}
|
||||
|
||||
if (query.officeId != null) {
|
||||
list = list.where((t) => t.officeId == query.officeId).toList();
|
||||
}
|
||||
if (query.status != null) {
|
||||
list = list.where((t) => t.status == query.status).toList();
|
||||
}
|
||||
if (query.searchQuery.isNotEmpty) {
|
||||
final q = query.searchQuery.toLowerCase();
|
||||
list = list
|
||||
.where(
|
||||
(t) =>
|
||||
t.subject.toLowerCase().contains(q) ||
|
||||
t.description.toLowerCase().contains(q),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Sort: newest first
|
||||
list.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
// Pagination
|
||||
final start = query.offset;
|
||||
final end = (start + query.limit).clamp(0, list.length);
|
||||
if (start >= list.length) return <Ticket>[];
|
||||
return list.sublist(start, end);
|
||||
});
|
||||
});
|
||||
|
||||
/// Provider for ticket query parameters.
|
||||
final ticketsQueryProvider = StateProvider<TicketQuery>(
|
||||
(ref) => const TicketQuery(),
|
||||
);
|
||||
|
||||
final ticketMessagesProvider =
|
||||
StreamProvider.family<List<TicketMessage>, String>((ref, ticketId) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
Reference in New Issue
Block a user