QA hardening: security RLS fixes, Flutter 3.47.5 upgrade, UI/validation fixes
Security — enforce write authorization server-side (was UI/RPC-only): - it_service_requests RLS: block cross-office read/edit + self-approve (QA-015) - pass_slips RLS: owner can complete but not self-approve (QA-046) - swap_requests RLS: scope select/update to participants + admin (QA-047) - storage: tighten it_service_attachments + task_attachments write/delete (QA-027) - admin_user_management edge function: allow programmers to manage users (QA-016) Fixes: - workforce generator "uncovered shifts" false alarms (QA-043/044) - network-map VLAN + New-location dialog validation, disabled-until-valid (QA-048) - de-flake time-of-day-dependent dashboard metrics test (QA-045) Toolchain: - upgrade to Flutter 3.47.5 / Dart 3.13.4; font_awesome_flutter 11.0.0, flutter_quill 11.6.0, pdfrx 2.6.5; clear resulting deprecations (QA-002) analyze clean; 139 tests pass; web build succeeds. Report + evidence in docs/qa/. Note: also carries the in-progress Brick model cleanup already present in the working tree. QA-001 (AI keys public in the build) is deferred by owner decision. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -3,17 +3,19 @@ import '../../theme/m3_motion.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:tasq/utils/app_time.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../models/office.dart';
|
||||
import '../../models/office.model.dart';
|
||||
import '../../widgets/office_picker.dart';
|
||||
import '../../models/notification_item.dart';
|
||||
import '../../models/profile.dart';
|
||||
import '../../models/ticket.dart';
|
||||
import '../../models/notification_item.model.dart';
|
||||
import '../../models/profile.model.dart';
|
||||
import '../../models/ticket.model.dart';
|
||||
import '../../providers/notifications_provider.dart';
|
||||
import '../../providers/profile_provider.dart';
|
||||
import '../../providers/tickets_provider.dart';
|
||||
import '../../providers/realtime_controller.dart';
|
||||
import '../../providers/typing_provider.dart';
|
||||
import '../../providers/user_offices_provider.dart';
|
||||
import '../../widgets/mono_text.dart';
|
||||
import '../../widgets/reconnect_overlay.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
@@ -288,7 +290,10 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
||||
TasQColumn<Ticket>(
|
||||
header: 'Ticket ID',
|
||||
technical: true,
|
||||
cellBuilder: (context, ticket) => Text(ticket.id),
|
||||
cellBuilder: (context, ticket) => Tooltip(
|
||||
message: ticket.id,
|
||||
child: Text(_shortId(ticket.id)),
|
||||
),
|
||||
),
|
||||
TasQColumn<Ticket>(
|
||||
header: 'Subject',
|
||||
@@ -362,7 +367,7 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
||||
const SizedBox(height: 2),
|
||||
Text('Filed by: $assigned'),
|
||||
const SizedBox(height: 4),
|
||||
MonoText('ID ${ticket.id}'),
|
||||
MonoText('ID ${_shortId(ticket.id)}'),
|
||||
const SizedBox(height: 2),
|
||||
Text(_formatTimestamp(ticket.createdAt)),
|
||||
],
|
||||
@@ -450,6 +455,17 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
||||
return Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final officesAsync = ref.watch(officesProvider);
|
||||
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
||||
final isGlobal = const {
|
||||
'admin',
|
||||
'programmer',
|
||||
'dispatcher',
|
||||
'it_staff',
|
||||
}.contains(profile?.role);
|
||||
final myOfficeIds = {
|
||||
for (final a in ref.watch(userOfficesProvider).valueOrNull ?? [])
|
||||
if (a.userId == profile?.id) a.officeId,
|
||||
};
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -468,8 +484,18 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
officesAsync.when(
|
||||
data: (offices) {
|
||||
if (offices.isEmpty) return const Text('No offices assigned.');
|
||||
data: (allOffices) {
|
||||
// ticketsProvider only shows non-global users their own
|
||||
// offices' tickets, so only offer those offices here —
|
||||
// otherwise the new ticket vanishes from the filer's list.
|
||||
final offices = isGlobal
|
||||
? allOffices
|
||||
: allOffices
|
||||
.where((o) => myOfficeIds.contains(o.id))
|
||||
.toList();
|
||||
if (offices.isEmpty) {
|
||||
return const Text('No offices assigned.');
|
||||
}
|
||||
final officesSorted = List<Office>.from(offices)
|
||||
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
return OfficeSelectorField(
|
||||
@@ -761,6 +787,11 @@ class _StatusSummaryCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// First 8 chars of the UUID, like a short git hash; the full id is in the
|
||||
/// tooltip. Tickets have no human-friendly number.
|
||||
String _shortId(String id) =>
|
||||
id.length > 8 ? id.substring(0, 8).toUpperCase() : id;
|
||||
|
||||
String _assignedAgent(Map<String, Profile> profileById, String? userId) {
|
||||
if (userId == null || userId.isEmpty) {
|
||||
return 'Unassigned';
|
||||
@@ -772,14 +803,8 @@ String _assignedAgent(Map<String, Profile> profileById, String? userId) {
|
||||
return profile.fullName.isNotEmpty ? profile.fullName : profile.id;
|
||||
}
|
||||
|
||||
String _formatTimestamp(DateTime value) {
|
||||
final year = value.year.toString().padLeft(4, '0');
|
||||
final month = value.month.toString().padLeft(2, '0');
|
||||
final day = value.day.toString().padLeft(2, '0');
|
||||
final hour = value.hour.toString().padLeft(2, '0');
|
||||
final minute = value.minute.toString().padLeft(2, '0');
|
||||
return '$year-$month-$day $hour:$minute';
|
||||
}
|
||||
String _formatTimestamp(DateTime value) =>
|
||||
DateFormat('yyyy-MM-dd HH:mm').format(value);
|
||||
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
const _StatusBadge({required this.status});
|
||||
|
||||
Reference in New Issue
Block a user