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:
2026-09-26 11:52:16 +08:00
parent e49b52949c
commit 3cb980b629
125 changed files with 3420 additions and 2853 deletions
+31 -43
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../theme/m3_motion.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -5,18 +6,18 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/attendance_log.dart';
import '../../models/duty_schedule.dart';
import '../../models/leave_of_absence.dart';
import '../../models/attendance_log.model.dart';
import '../../models/duty_schedule.model.dart';
import '../../models/leave_of_absence.model.dart';
import '../../models/live_position.dart';
import '../../models/pass_slip.dart';
import '../../models/profile.dart';
import '../../models/task.dart';
import '../../models/pass_slip.model.dart';
import '../../models/profile.model.dart';
import '../../models/task.model.dart';
import '../../models/task_assignment.dart';
import '../../models/ticket.dart';
import '../../models/ticket.model.dart';
import '../../models/ticket_message.dart';
import '../../models/it_service_request.dart';
import '../../models/it_service_request_assignment.dart';
import '../../models/it_service_request.model.dart';
import '../../models/it_service_request_assignment.model.dart';
import '../../providers/attendance_provider.dart';
import '../../providers/leave_provider.dart';
import '../../providers/pass_slip_provider.dart';
@@ -27,7 +28,7 @@ import '../../providers/whereabouts_provider.dart';
import '../../providers/workforce_provider.dart';
import '../../providers/it_service_request_provider.dart';
import '../../providers/teams_provider.dart';
import '../../models/team.dart';
import '../../models/team.model.dart';
import '../../models/team_member.dart';
import 'dart:math' as math;
import '../../widgets/responsive_body.dart';
@@ -407,10 +408,7 @@ final dashboardMetricsProvider = Provider<AsyncValue<DashboardMetrics>>((ref) {
AppTime.now().difference(livePos.updatedAt) >
const Duration(minutes: 15);
if (stale) {
final diff = AppTime.now().difference(livePos.updatedAt);
final ago = diff.inMinutes < 60
? '${diff.inMinutes}m ago'
: '${diff.inHours}h ago';
final ago = AppTime.relative(livePos.updatedAt);
whereabouts = livePos.inPremise
? 'Last seen in premise \u00b7 $ago'
: 'Last seen outside \u00b7 $ago';
@@ -628,6 +626,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Sound/vibration settings and openAppSettings() don't apply on web.
if (kIsWeb) return;
final prefs = await SharedPreferences.getInstance();
final seen = prefs.getBool('has_seen_notif_showcase') ?? false;
if (!seen) {
@@ -1110,36 +1110,24 @@ class _StaffRow extends StatelessWidget {
);
}
// IT Staff cell: avatar on mobile, name on desktop, with team color dot
Widget staffCell;
if (isMobile) {
staffCell = Row(
mainAxisSize: MainAxisSize.min,
children: [
teamMarker,
const SizedBox(width: 4),
Flexible(
child: Tooltip(
message: row.name,
child: ProfileAvatar(
fullName: row.name,
avatarUrl: row.avatarUrl,
radius: 14,
),
),
// IT Staff cell: name (ellipsized on narrow screens) with team color dot.
// Mobile used initials-only avatars, which are ambiguous and need a
// long-press tooltip to identify anyone.
final staffCell = Row(
mainAxisSize: MainAxisSize.min,
children: [
teamMarker,
const SizedBox(width: 6),
Flexible(
child: Text(
row.name,
style: valueStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
);
} else {
staffCell = Row(
mainAxisSize: MainAxisSize.min,
children: [
teamMarker,
const SizedBox(width: 6),
Flexible(child: Text(row.name, style: valueStyle)),
],
);
}
),
],
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),