Offline Support
This commit is contained in:
@@ -35,6 +35,24 @@
|
||||
},
|
||||
"createdAt": "2026-04-08T22:52:50.281Z",
|
||||
"updatedAt": "2026-04-08T22:52:50.281Z"
|
||||
},
|
||||
"swarm-1777077971732-eh7t50": {
|
||||
"swarmId": "swarm-1777077971732-eh7t50",
|
||||
"topology": "hierarchical-mesh",
|
||||
"maxAgents": 15,
|
||||
"status": "running",
|
||||
"agents": [],
|
||||
"tasks": [],
|
||||
"config": {
|
||||
"topology": "hierarchical-mesh",
|
||||
"maxAgents": 15,
|
||||
"strategy": "specialized",
|
||||
"communicationProtocol": "message-bus",
|
||||
"autoScaling": true,
|
||||
"consensusMechanism": "majority"
|
||||
},
|
||||
"createdAt": "2026-04-25T00:46:11.732Z",
|
||||
"updatedAt": "2026-04-25T00:46:11.732Z"
|
||||
}
|
||||
},
|
||||
"version": "3.0.0"
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
"type": "command",
|
||||
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/hook-handler.cjs route",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"shell": "powershell",
|
||||
"statusMessage": "Loading architecture context...",
|
||||
"command": "$f = \"$env:CLAUDE_PROJECT_DIR/graphify-out/GRAPH_REPORT.md\"; if (Test-Path $f) { $c = Get-Content -Raw $f; $o = @{ hookSpecificOutput = @{ hookEventName = 'UserPromptSubmit'; additionalContext = \"=== GRAPHIFY ARCHITECTURE CONTEXT ===`n$c`n=== END GRAPHIFY CONTEXT ===\" } } | ConvertTo-Json -Depth 5 -Compress; Write-Output $o }"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -67,6 +73,13 @@
|
||||
"type": "command",
|
||||
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/auto-memory-hook.mjs import",
|
||||
"timeout": 8000
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"shell": "powershell",
|
||||
"async": true,
|
||||
"statusMessage": "Starting workers and graphify...",
|
||||
"command": "$branch = git branch --show-current; Write-Host \"[Hook] Branch: $branch\"; if (!(Get-NetTCPConnection -LocalPort 37777 -ErrorAction SilentlyContinue)) { Start-Process cmd -ArgumentList '/c','npx','claude-mem','start' -WindowStyle Hidden -ErrorAction SilentlyContinue }; Start-Process claude-flow -ArgumentList 'daemon','start' -WindowStyle Hidden -ErrorAction SilentlyContinue; Start-Process graphify -ArgumentList '--watch' -WindowStyle Minimized -ErrorAction SilentlyContinue; $t=0; while (!(Get-NetTCPConnection -LocalPort 37777 -ErrorAction SilentlyContinue) -and $t -lt 20) { Start-Sleep 1; $t++ }; if (Get-NetTCPConnection -LocalPort 37777 -ErrorAction SilentlyContinue) { Start-Process cmd -ArgumentList '/c','start','\"\"','\"http://localhost:37777\"' -WindowStyle Hidden; Write-Host \"[Hook] Opened claude-mem dashboard\" } else { Write-Host \"[Hook] claude-mem not running on :37777 (needs Bun + plugin install — run 'npx claude-mem install' outside Claude Code)\" }; $graphHtml = \"$env:CLAUDE_PROJECT_DIR\\graphify-out\\graph.html\"; if (Test-Path $graphHtml) { Start-Process cmd -ArgumentList '/c','start','\"\"',\"`\"$graphHtml`\"\" -WindowStyle Hidden; Write-Host \"[Hook] Opened graphify visualization\" }"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -89,6 +102,13 @@
|
||||
"type": "command",
|
||||
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/auto-memory-hook.mjs sync",
|
||||
"timeout": 10000
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"shell": "powershell",
|
||||
"async": true,
|
||||
"statusMessage": "Updating architecture graph...",
|
||||
"command": "Write-Host \"[Hook] Regenerating graphify...\"; graphify 2>$null; $wshell = New-Object -ComObject WScript.Shell; $wshell.Popup(\"Claude finished. Architecture graph updated.\", 5, \"Claude Code Task Complete\", 64)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+12
@@ -46,3 +46,15 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Firebase service account credentials (never commit)
|
||||
supabase/functions/**/service-account.json
|
||||
supabase/functions/service-account.json
|
||||
|
||||
# graphify knowledge graph output
|
||||
graphify-out/
|
||||
|
||||
# claude-flow agent DB artifacts
|
||||
.swarm/
|
||||
agentdb.rvf
|
||||
ruvector.db
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
# Auto-generated Brick files — suppress lint warnings from external generators.
|
||||
- lib/brick/*.g.dart
|
||||
- lib/brick/**/*.g.dart
|
||||
- lib/brick/db/*.dart
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
brick_offline_first_with_supabase_build|offline_first_with_supabase_generator:
|
||||
enabled: true
|
||||
generate_for:
|
||||
- lib/models/**.dart
|
||||
options:
|
||||
# SQLite migrations are placed in lib/brick/db/
|
||||
sqlite_migration_path: lib/brick/db
|
||||
# Adapters are placed in lib/brick/adapters/
|
||||
adapter_path: lib/brick/adapters
|
||||
# The generated model dictionary file
|
||||
output_path: lib/brick
|
||||
+11
-2
@@ -5,17 +5,25 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'routing/app_router.dart';
|
||||
import 'models/profile.dart';
|
||||
import 'providers/connectivity_provider.dart';
|
||||
import 'providers/profile_provider.dart';
|
||||
import 'services/background_location_service.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'utils/snackbar.dart';
|
||||
import 'widgets/ios_install_prompt.dart';
|
||||
import 'widgets/offline_banner.dart';
|
||||
|
||||
class TasqApp extends ConsumerWidget {
|
||||
const TasqApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Keep the connectivity monitor alive at the root so the global online
|
||||
// check is wired into StreamRecoveryWrapper before any provider subscribes
|
||||
// to a Supabase realtime stream. Without this watch the autoDispose
|
||||
// provider only stays alive while OfflineBanner is mounted, which is too
|
||||
// late for offline-cold-launch detection.
|
||||
ref.watch(connectivityMonitorProvider);
|
||||
final router = ref.watch(appRouterProvider);
|
||||
|
||||
// Ensure background service is running if the profile indicates tracking.
|
||||
@@ -48,8 +56,9 @@ class TasqApp extends ConsumerWidget {
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
FlutterQuillLocalizations.delegate,
|
||||
],
|
||||
builder: (context, child) =>
|
||||
IosInstallPrompt(child: child ?? const SizedBox.shrink()),
|
||||
builder: (context, child) => OfflineBanner(
|
||||
child: IosInstallPrompt(child: child ?? const SizedBox.shrink()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<Announcement> _$AnnouncementFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Announcement(
|
||||
id: data['id'] as String,
|
||||
authorId: data['author_id'] as String,
|
||||
title: data['title'] as String,
|
||||
body: data['body'] as String,
|
||||
visibleRoles: data['visible_roles'].toList().cast<String>(),
|
||||
isTemplate: data['is_template'] as bool,
|
||||
templateId: data['template_id'] == null
|
||||
? null
|
||||
: data['template_id'] as String?,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
updatedAt: DateTime.parse(data['updated_at'] as String),
|
||||
bannerEnabled: data['banner_enabled'] as bool,
|
||||
bannerShowAt: data['banner_show_at'] == null
|
||||
? null
|
||||
: data['banner_show_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['banner_show_at'] as String),
|
||||
bannerHideAt: data['banner_hide_at'] == null
|
||||
? null
|
||||
: data['banner_hide_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['banner_hide_at'] as String),
|
||||
pushIntervalMinutes: data['push_interval_minutes'] == null
|
||||
? null
|
||||
: data['push_interval_minutes'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$AnnouncementToSupabase(
|
||||
Announcement instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'author_id': instance.authorId,
|
||||
'title': instance.title,
|
||||
'body': instance.body,
|
||||
'visible_roles': instance.visibleRoles,
|
||||
'is_template': instance.isTemplate,
|
||||
'template_id': instance.templateId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'banner_enabled': instance.bannerEnabled,
|
||||
'banner_show_at': instance.bannerShowAt?.toIso8601String(),
|
||||
'banner_hide_at': instance.bannerHideAt?.toIso8601String(),
|
||||
'push_interval_minutes': instance.pushIntervalMinutes,
|
||||
};
|
||||
}
|
||||
|
||||
Future<Announcement> _$AnnouncementFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Announcement(
|
||||
id: data['id'] as String,
|
||||
authorId: data['author_id'] as String,
|
||||
title: data['title'] as String,
|
||||
body: data['body'] as String,
|
||||
visibleRoles: jsonDecode(data['visible_roles']).toList().cast<String>(),
|
||||
isTemplate: data['is_template'] == 1,
|
||||
templateId: data['template_id'] == null
|
||||
? null
|
||||
: data['template_id'] as String?,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
updatedAt: DateTime.parse(data['updated_at'] as String),
|
||||
bannerEnabled: data['banner_enabled'] == 1,
|
||||
bannerShowAt: data['banner_show_at'] == null
|
||||
? null
|
||||
: data['banner_show_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['banner_show_at'] as String),
|
||||
bannerHideAt: data['banner_hide_at'] == null
|
||||
? null
|
||||
: data['banner_hide_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['banner_hide_at'] as String),
|
||||
pushIntervalMinutes: data['push_interval_minutes'] == null
|
||||
? null
|
||||
: data['push_interval_minutes'] as int?,
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$AnnouncementToSqlite(
|
||||
Announcement instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'author_id': instance.authorId,
|
||||
'title': instance.title,
|
||||
'body': instance.body,
|
||||
'visible_roles': jsonEncode(instance.visibleRoles),
|
||||
'is_template': instance.isTemplate ? 1 : 0,
|
||||
'template_id': instance.templateId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'banner_enabled': instance.bannerEnabled ? 1 : 0,
|
||||
'banner_show_at': instance.bannerShowAt?.toIso8601String(),
|
||||
'banner_hide_at': instance.bannerHideAt?.toIso8601String(),
|
||||
'push_interval_minutes': instance.pushIntervalMinutes,
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [Announcement]
|
||||
class AnnouncementAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<Announcement> {
|
||||
AnnouncementAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'announcements';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'authorId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'author_id',
|
||||
),
|
||||
'title': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'title',
|
||||
),
|
||||
'body': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'body',
|
||||
),
|
||||
'visibleRoles': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'visible_roles',
|
||||
),
|
||||
'isTemplate': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'is_template',
|
||||
),
|
||||
'templateId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'template_id',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'updatedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'updated_at',
|
||||
),
|
||||
'bannerEnabled': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'banner_enabled',
|
||||
),
|
||||
'bannerShowAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'banner_show_at',
|
||||
),
|
||||
'bannerHideAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'banner_hide_at',
|
||||
),
|
||||
'pushIntervalMinutes': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'push_interval_minutes',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'authorId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'author_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'title': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'title',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'body': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'body',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'visibleRoles': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'visible_roles',
|
||||
iterable: true,
|
||||
type: String,
|
||||
),
|
||||
'isTemplate': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'is_template',
|
||||
iterable: false,
|
||||
type: bool,
|
||||
),
|
||||
'templateId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'template_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'updatedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'updated_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'bannerEnabled': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'banner_enabled',
|
||||
iterable: false,
|
||||
type: bool,
|
||||
),
|
||||
'bannerShowAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'banner_show_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'bannerHideAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'banner_hide_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'pushIntervalMinutes': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'push_interval_minutes',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
Announcement instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'Announcement';
|
||||
|
||||
@override
|
||||
Future<Announcement> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AnnouncementFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
Announcement input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AnnouncementToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Announcement> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AnnouncementFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
Announcement input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AnnouncementToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<AnnouncementComment> _$AnnouncementCommentFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return AnnouncementComment(
|
||||
id: data['id'] as String,
|
||||
announcementId: data['announcement_id'] as String,
|
||||
authorId: data['author_id'] as String,
|
||||
body: data['body'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$AnnouncementCommentToSupabase(
|
||||
AnnouncementComment instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'announcement_id': instance.announcementId,
|
||||
'author_id': instance.authorId,
|
||||
'body': instance.body,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<AnnouncementComment> _$AnnouncementCommentFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return AnnouncementComment(
|
||||
id: data['id'] as String,
|
||||
announcementId: data['announcement_id'] as String,
|
||||
authorId: data['author_id'] as String,
|
||||
body: data['body'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$AnnouncementCommentToSqlite(
|
||||
AnnouncementComment instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'announcement_id': instance.announcementId,
|
||||
'author_id': instance.authorId,
|
||||
'body': instance.body,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [AnnouncementComment]
|
||||
class AnnouncementCommentAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<AnnouncementComment> {
|
||||
AnnouncementCommentAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'announcement_comments';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'announcementId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'announcement_id',
|
||||
),
|
||||
'authorId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'author_id',
|
||||
),
|
||||
'body': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'body',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'announcementId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'announcement_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'authorId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'author_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'body': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'body',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
AnnouncementComment instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'AnnouncementComment';
|
||||
|
||||
@override
|
||||
Future<AnnouncementComment> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AnnouncementCommentFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
AnnouncementComment input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AnnouncementCommentToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<AnnouncementComment> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AnnouncementCommentFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
AnnouncementComment input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AnnouncementCommentToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<AttendanceLog> _$AttendanceLogFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return AttendanceLog(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
dutyScheduleId: data['duty_schedule_id'] as String,
|
||||
shiftType: data['shift_type'] as String,
|
||||
checkInAt: DateTime.parse(data['check_in_at'] as String),
|
||||
checkInLat: data['check_in_lat'] as double,
|
||||
checkInLng: data['check_in_lng'] as double,
|
||||
checkOutAt: data['check_out_at'] == null
|
||||
? null
|
||||
: data['check_out_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['check_out_at'] as String),
|
||||
checkOutLat: data['check_out_lat'] == null
|
||||
? null
|
||||
: data['check_out_lat'] as double?,
|
||||
checkOutLng: data['check_out_lng'] == null
|
||||
? null
|
||||
: data['check_out_lng'] as double?,
|
||||
justification: data['justification'] == null
|
||||
? null
|
||||
: data['justification'] as String?,
|
||||
checkOutJustification: data['check_out_justification'] == null
|
||||
? null
|
||||
: data['check_out_justification'] as String?,
|
||||
verificationStatus: data['verification_status'] as String,
|
||||
checkInVerificationPhotoUrl: data['check_in_verification_photo_url'] == null
|
||||
? null
|
||||
: data['check_in_verification_photo_url'] as String?,
|
||||
checkOutVerificationPhotoUrl:
|
||||
data['check_out_verification_photo_url'] == null
|
||||
? null
|
||||
: data['check_out_verification_photo_url'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$AttendanceLogToSupabase(
|
||||
AttendanceLog instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'duty_schedule_id': instance.dutyScheduleId,
|
||||
'shift_type': instance.shiftType,
|
||||
'check_in_at': instance.checkInAt.toIso8601String(),
|
||||
'check_in_lat': instance.checkInLat,
|
||||
'check_in_lng': instance.checkInLng,
|
||||
'check_out_at': instance.checkOutAt?.toIso8601String(),
|
||||
'check_out_lat': instance.checkOutLat,
|
||||
'check_out_lng': instance.checkOutLng,
|
||||
'justification': instance.justification,
|
||||
'check_out_justification': instance.checkOutJustification,
|
||||
'verification_status': instance.verificationStatus,
|
||||
'check_in_verification_photo_url': instance.checkInVerificationPhotoUrl,
|
||||
'check_out_verification_photo_url': instance.checkOutVerificationPhotoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
Future<AttendanceLog> _$AttendanceLogFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return AttendanceLog(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
dutyScheduleId: data['duty_schedule_id'] as String,
|
||||
shiftType: data['shift_type'] as String,
|
||||
checkInAt: DateTime.parse(data['check_in_at'] as String),
|
||||
checkInLat: data['check_in_lat'] as double,
|
||||
checkInLng: data['check_in_lng'] as double,
|
||||
checkOutAt: data['check_out_at'] == null
|
||||
? null
|
||||
: data['check_out_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['check_out_at'] as String),
|
||||
checkOutLat: data['check_out_lat'] == null
|
||||
? null
|
||||
: data['check_out_lat'] as double?,
|
||||
checkOutLng: data['check_out_lng'] == null
|
||||
? null
|
||||
: data['check_out_lng'] as double?,
|
||||
justification: data['justification'] == null
|
||||
? null
|
||||
: data['justification'] as String?,
|
||||
checkOutJustification: data['check_out_justification'] == null
|
||||
? null
|
||||
: data['check_out_justification'] as String?,
|
||||
verificationStatus: data['verification_status'] as String,
|
||||
checkInVerificationPhotoUrl: data['check_in_verification_photo_url'] == null
|
||||
? null
|
||||
: data['check_in_verification_photo_url'] as String?,
|
||||
checkOutVerificationPhotoUrl:
|
||||
data['check_out_verification_photo_url'] == null
|
||||
? null
|
||||
: data['check_out_verification_photo_url'] as String?,
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$AttendanceLogToSqlite(
|
||||
AttendanceLog instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'duty_schedule_id': instance.dutyScheduleId,
|
||||
'shift_type': instance.shiftType,
|
||||
'check_in_at': instance.checkInAt.toIso8601String(),
|
||||
'check_in_lat': instance.checkInLat,
|
||||
'check_in_lng': instance.checkInLng,
|
||||
'check_out_at': instance.checkOutAt?.toIso8601String(),
|
||||
'check_out_lat': instance.checkOutLat,
|
||||
'check_out_lng': instance.checkOutLng,
|
||||
'justification': instance.justification,
|
||||
'check_out_justification': instance.checkOutJustification,
|
||||
'verification_status': instance.verificationStatus,
|
||||
'check_in_verification_photo_url': instance.checkInVerificationPhotoUrl,
|
||||
'check_out_verification_photo_url': instance.checkOutVerificationPhotoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [AttendanceLog]
|
||||
class AttendanceLogAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<AttendanceLog> {
|
||||
AttendanceLogAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'attendance_logs';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'userId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
),
|
||||
'dutyScheduleId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'duty_schedule_id',
|
||||
),
|
||||
'shiftType': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'shift_type',
|
||||
),
|
||||
'checkInAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_at',
|
||||
),
|
||||
'checkInLat': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_lat',
|
||||
),
|
||||
'checkInLng': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_lng',
|
||||
),
|
||||
'checkOutAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_at',
|
||||
),
|
||||
'checkOutLat': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_lat',
|
||||
),
|
||||
'checkOutLng': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_lng',
|
||||
),
|
||||
'justification': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'justification',
|
||||
),
|
||||
'checkOutJustification': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_justification',
|
||||
),
|
||||
'verificationStatus': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'verification_status',
|
||||
),
|
||||
'checkInVerificationPhotoUrl': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_verification_photo_url',
|
||||
),
|
||||
'checkOutVerificationPhotoUrl': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_verification_photo_url',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'userId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'dutyScheduleId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'duty_schedule_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'shiftType': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'shift_type',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'checkInAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'checkInLat': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_lat',
|
||||
iterable: false,
|
||||
type: double,
|
||||
),
|
||||
'checkInLng': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_lng',
|
||||
iterable: false,
|
||||
type: double,
|
||||
),
|
||||
'checkOutAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'checkOutLat': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_lat',
|
||||
iterable: false,
|
||||
type: double,
|
||||
),
|
||||
'checkOutLng': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_lng',
|
||||
iterable: false,
|
||||
type: double,
|
||||
),
|
||||
'justification': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'justification',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'checkOutJustification': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_justification',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'verificationStatus': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'verification_status',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'checkInVerificationPhotoUrl': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_verification_photo_url',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'checkOutVerificationPhotoUrl': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_out_verification_photo_url',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
AttendanceLog instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'AttendanceLog';
|
||||
|
||||
@override
|
||||
Future<AttendanceLog> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AttendanceLogFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
AttendanceLog input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AttendanceLogToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<AttendanceLog> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AttendanceLogFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
AttendanceLog input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$AttendanceLogToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<ChatMessage> _$ChatMessageFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ChatMessage(
|
||||
id: data['id'] as String,
|
||||
threadId: data['thread_id'] as String,
|
||||
senderId: data['sender_id'] as String,
|
||||
body: data['body'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ChatMessageToSupabase(
|
||||
ChatMessage instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'thread_id': instance.threadId,
|
||||
'sender_id': instance.senderId,
|
||||
'body': instance.body,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<ChatMessage> _$ChatMessageFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ChatMessage(
|
||||
id: data['id'] as String,
|
||||
threadId: data['thread_id'] as String,
|
||||
senderId: data['sender_id'] as String,
|
||||
body: data['body'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ChatMessageToSqlite(
|
||||
ChatMessage instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'thread_id': instance.threadId,
|
||||
'sender_id': instance.senderId,
|
||||
'body': instance.body,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [ChatMessage]
|
||||
class ChatMessageAdapter extends OfflineFirstWithSupabaseAdapter<ChatMessage> {
|
||||
ChatMessageAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'chat_messages';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'threadId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'thread_id',
|
||||
),
|
||||
'senderId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'sender_id',
|
||||
),
|
||||
'body': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'body',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'threadId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'thread_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'senderId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'sender_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'body': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'body',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
ChatMessage instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'ChatMessage';
|
||||
|
||||
@override
|
||||
Future<ChatMessage> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ChatMessageFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
ChatMessage input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ChatMessageToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<ChatMessage> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ChatMessageFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
ChatMessage input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ChatMessageToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<DutySchedule> _$DutyScheduleFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return DutySchedule(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
shiftType: data['shift_type'] as String,
|
||||
startTime: DateTime.parse(data['start_time'] as String),
|
||||
endTime: DateTime.parse(data['end_time'] as String),
|
||||
status: data['status'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
checkInAt: data['check_in_at'] == null
|
||||
? null
|
||||
: data['check_in_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['check_in_at'] as String),
|
||||
relieverIds: data['reliever_ids'].toList().cast<String>(),
|
||||
swapRequestId: data['swap_request_id'] == null
|
||||
? null
|
||||
: data['swap_request_id'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$DutyScheduleToSupabase(
|
||||
DutySchedule instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'shift_type': instance.shiftType,
|
||||
'start_time': instance.startTime.toIso8601String(),
|
||||
'end_time': instance.endTime.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'check_in_at': instance.checkInAt?.toIso8601String(),
|
||||
'reliever_ids': instance.relieverIds,
|
||||
'swap_request_id': instance.swapRequestId,
|
||||
};
|
||||
}
|
||||
|
||||
Future<DutySchedule> _$DutyScheduleFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return DutySchedule(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
shiftType: data['shift_type'] as String,
|
||||
startTime: DateTime.parse(data['start_time'] as String),
|
||||
endTime: DateTime.parse(data['end_time'] as String),
|
||||
status: data['status'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
checkInAt: data['check_in_at'] == null
|
||||
? null
|
||||
: data['check_in_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['check_in_at'] as String),
|
||||
relieverIds: jsonDecode(data['reliever_ids']).toList().cast<String>(),
|
||||
swapRequestId: data['swap_request_id'] == null
|
||||
? null
|
||||
: data['swap_request_id'] as String?,
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$DutyScheduleToSqlite(
|
||||
DutySchedule instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'shift_type': instance.shiftType,
|
||||
'start_time': instance.startTime.toIso8601String(),
|
||||
'end_time': instance.endTime.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'check_in_at': instance.checkInAt?.toIso8601String(),
|
||||
'reliever_ids': jsonEncode(instance.relieverIds),
|
||||
'swap_request_id': instance.swapRequestId,
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [DutySchedule]
|
||||
class DutyScheduleAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<DutySchedule> {
|
||||
DutyScheduleAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'duty_schedules';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'userId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
),
|
||||
'shiftType': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'shift_type',
|
||||
),
|
||||
'startTime': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'start_time',
|
||||
),
|
||||
'endTime': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'end_time',
|
||||
),
|
||||
'status': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'checkInAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_at',
|
||||
),
|
||||
'relieverIds': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'reliever_ids',
|
||||
),
|
||||
'swapRequestId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'swap_request_id',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'userId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'shiftType': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'shift_type',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'startTime': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'start_time',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'endTime': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'end_time',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'status': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'checkInAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'check_in_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'relieverIds': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'reliever_ids',
|
||||
iterable: true,
|
||||
type: String,
|
||||
),
|
||||
'swapRequestId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'swap_request_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
DutySchedule instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'DutySchedule';
|
||||
|
||||
@override
|
||||
Future<DutySchedule> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$DutyScheduleFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
DutySchedule input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$DutyScheduleToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<DutySchedule> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$DutyScheduleFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
DutySchedule input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$DutyScheduleToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<ItServiceRequestAction> _$ItServiceRequestActionFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ItServiceRequestAction(
|
||||
id: data['id'] as String,
|
||||
requestId: data['request_id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
actionTaken: data['action_taken'] == null
|
||||
? null
|
||||
: data['action_taken'] as String?,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
updatedAt: DateTime.parse(data['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ItServiceRequestActionToSupabase(
|
||||
ItServiceRequestAction instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'request_id': instance.requestId,
|
||||
'user_id': instance.userId,
|
||||
'action_taken': instance.actionTaken,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<ItServiceRequestAction> _$ItServiceRequestActionFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ItServiceRequestAction(
|
||||
id: data['id'] as String,
|
||||
requestId: data['request_id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
actionTaken: data['action_taken'] == null
|
||||
? null
|
||||
: data['action_taken'] as String?,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
updatedAt: DateTime.parse(data['updated_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ItServiceRequestActionToSqlite(
|
||||
ItServiceRequestAction instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'request_id': instance.requestId,
|
||||
'user_id': instance.userId,
|
||||
'action_taken': instance.actionTaken,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [ItServiceRequestAction]
|
||||
class ItServiceRequestActionAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<ItServiceRequestAction> {
|
||||
ItServiceRequestActionAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'it_service_request_actions';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'requestId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_id',
|
||||
),
|
||||
'userId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
),
|
||||
'actionTaken': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'action_taken',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'updatedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'updated_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'userId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'actionTaken': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'action_taken',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'updatedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'updated_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
ItServiceRequestAction instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'ItServiceRequestAction';
|
||||
|
||||
@override
|
||||
Future<ItServiceRequestAction> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestActionFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
ItServiceRequestAction input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestActionToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<ItServiceRequestAction> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestActionFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
ItServiceRequestAction input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestActionToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<ItServiceRequestActivityLog> _$ItServiceRequestActivityLogFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ItServiceRequestActivityLog(
|
||||
id: data['id'] as String,
|
||||
requestId: data['request_id'] as String,
|
||||
actorId: data['actor_id'] == null ? null : data['actor_id'] as String?,
|
||||
actionType: data['action_type'] as String,
|
||||
meta: data['meta'] == null ? null : data['meta'],
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ItServiceRequestActivityLogToSupabase(
|
||||
ItServiceRequestActivityLog instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'request_id': instance.requestId,
|
||||
'actor_id': instance.actorId,
|
||||
'action_type': instance.actionType,
|
||||
'meta': instance.meta,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<ItServiceRequestActivityLog> _$ItServiceRequestActivityLogFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ItServiceRequestActivityLog(
|
||||
id: data['id'] as String,
|
||||
requestId: data['request_id'] as String,
|
||||
actorId: data['actor_id'] == null ? null : data['actor_id'] as String?,
|
||||
actionType: data['action_type'] as String,
|
||||
meta: data['meta'] == null ? null : jsonDecode(data['meta']),
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ItServiceRequestActivityLogToSqlite(
|
||||
ItServiceRequestActivityLog instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'request_id': instance.requestId,
|
||||
'actor_id': instance.actorId,
|
||||
'action_type': instance.actionType,
|
||||
'meta': instance.meta != null ? jsonEncode(instance.meta) : null,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [ItServiceRequestActivityLog]
|
||||
class ItServiceRequestActivityLogAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<ItServiceRequestActivityLog> {
|
||||
ItServiceRequestActivityLogAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'it_service_request_activity_logs';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'requestId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_id',
|
||||
),
|
||||
'actorId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'actor_id',
|
||||
),
|
||||
'actionType': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'action_type',
|
||||
),
|
||||
'meta': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'meta',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'actorId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'actor_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'actionType': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'action_type',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'meta': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'meta',
|
||||
iterable: false,
|
||||
type: Map,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
ItServiceRequestActivityLog instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'ItServiceRequestActivityLog';
|
||||
|
||||
@override
|
||||
Future<ItServiceRequestActivityLog> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestActivityLogFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
ItServiceRequestActivityLog input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestActivityLogToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<ItServiceRequestActivityLog> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestActivityLogFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
ItServiceRequestActivityLog input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestActivityLogToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<ItServiceRequest> _$ItServiceRequestFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ItServiceRequest(
|
||||
id: data['id'] as String,
|
||||
requestNumber: data['request_number'] == null
|
||||
? null
|
||||
: data['request_number'] as String?,
|
||||
services: data['services'].toList().cast<String>(),
|
||||
servicesOther: data['services_other'] == null
|
||||
? null
|
||||
: data['services_other'] as String?,
|
||||
eventName: data['event_name'] as String,
|
||||
eventDetails: data['event_details'] == null
|
||||
? null
|
||||
: data['event_details'] as String?,
|
||||
eventDate: data['event_date'] == null
|
||||
? null
|
||||
: data['event_date'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['event_date'] as String),
|
||||
eventEndDate: data['event_end_date'] == null
|
||||
? null
|
||||
: data['event_end_date'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['event_end_date'] as String),
|
||||
dryRunDate: data['dry_run_date'] == null
|
||||
? null
|
||||
: data['dry_run_date'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['dry_run_date'] as String),
|
||||
dryRunEndDate: data['dry_run_end_date'] == null
|
||||
? null
|
||||
: data['dry_run_end_date'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['dry_run_end_date'] as String),
|
||||
contactPerson: data['contact_person'] == null
|
||||
? null
|
||||
: data['contact_person'] as String?,
|
||||
contactNumber: data['contact_number'] == null
|
||||
? null
|
||||
: data['contact_number'] as String?,
|
||||
remarks: data['remarks'] == null ? null : data['remarks'] as String?,
|
||||
officeId: data['office_id'] == null ? null : data['office_id'] as String?,
|
||||
requestedBy: data['requested_by'] == null
|
||||
? null
|
||||
: data['requested_by'] as String?,
|
||||
requestedByUserId: data['requested_by_user_id'] == null
|
||||
? null
|
||||
: data['requested_by_user_id'] as String?,
|
||||
approvedBy: data['approved_by'] == null
|
||||
? null
|
||||
: data['approved_by'] as String?,
|
||||
approvedByUserId: data['approved_by_user_id'] == null
|
||||
? null
|
||||
: data['approved_by_user_id'] as String?,
|
||||
approvedAt: data['approved_at'] == null
|
||||
? null
|
||||
: data['approved_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['approved_at'] as String),
|
||||
status: data['status'] as String,
|
||||
outsidePremiseAllowed: data['outside_premise_allowed'] as bool,
|
||||
cancellationReason: data['cancellation_reason'] == null
|
||||
? null
|
||||
: data['cancellation_reason'] as String?,
|
||||
cancelledAt: data['cancelled_at'] == null
|
||||
? null
|
||||
: data['cancelled_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['cancelled_at'] as String),
|
||||
creatorId: data['creator_id'] == null
|
||||
? null
|
||||
: data['creator_id'] as String?,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
updatedAt: DateTime.parse(data['updated_at'] as String),
|
||||
completedAt: data['completed_at'] == null
|
||||
? null
|
||||
: data['completed_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['completed_at'] as String),
|
||||
dateTimeReceived: data['date_time_received'] == null
|
||||
? null
|
||||
: data['date_time_received'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['date_time_received'] as String),
|
||||
dateTimeChecked: data['date_time_checked'] == null
|
||||
? null
|
||||
: data['date_time_checked'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['date_time_checked'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ItServiceRequestToSupabase(
|
||||
ItServiceRequest instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'request_number': instance.requestNumber,
|
||||
'services': instance.services,
|
||||
'services_other': instance.servicesOther,
|
||||
'event_name': instance.eventName,
|
||||
'event_details': instance.eventDetails,
|
||||
'event_date': instance.eventDate?.toIso8601String(),
|
||||
'event_end_date': instance.eventEndDate?.toIso8601String(),
|
||||
'dry_run_date': instance.dryRunDate?.toIso8601String(),
|
||||
'dry_run_end_date': instance.dryRunEndDate?.toIso8601String(),
|
||||
'contact_person': instance.contactPerson,
|
||||
'contact_number': instance.contactNumber,
|
||||
'remarks': instance.remarks,
|
||||
'office_id': instance.officeId,
|
||||
'requested_by': instance.requestedBy,
|
||||
'requested_by_user_id': instance.requestedByUserId,
|
||||
'approved_by': instance.approvedBy,
|
||||
'approved_by_user_id': instance.approvedByUserId,
|
||||
'approved_at': instance.approvedAt?.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'outside_premise_allowed': instance.outsidePremiseAllowed,
|
||||
'cancellation_reason': instance.cancellationReason,
|
||||
'cancelled_at': instance.cancelledAt?.toIso8601String(),
|
||||
'creator_id': instance.creatorId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'completed_at': instance.completedAt?.toIso8601String(),
|
||||
'date_time_received': instance.dateTimeReceived?.toIso8601String(),
|
||||
'date_time_checked': instance.dateTimeChecked?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<ItServiceRequest> _$ItServiceRequestFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ItServiceRequest(
|
||||
id: data['id'] as String,
|
||||
requestNumber: data['request_number'] == null
|
||||
? null
|
||||
: data['request_number'] as String?,
|
||||
services: jsonDecode(data['services']).toList().cast<String>(),
|
||||
servicesOther: data['services_other'] == null
|
||||
? null
|
||||
: data['services_other'] as String?,
|
||||
eventName: data['event_name'] as String,
|
||||
eventDetails: data['event_details'] == null
|
||||
? null
|
||||
: data['event_details'] as String?,
|
||||
eventDate: data['event_date'] == null
|
||||
? null
|
||||
: data['event_date'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['event_date'] as String),
|
||||
eventEndDate: data['event_end_date'] == null
|
||||
? null
|
||||
: data['event_end_date'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['event_end_date'] as String),
|
||||
dryRunDate: data['dry_run_date'] == null
|
||||
? null
|
||||
: data['dry_run_date'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['dry_run_date'] as String),
|
||||
dryRunEndDate: data['dry_run_end_date'] == null
|
||||
? null
|
||||
: data['dry_run_end_date'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['dry_run_end_date'] as String),
|
||||
contactPerson: data['contact_person'] == null
|
||||
? null
|
||||
: data['contact_person'] as String?,
|
||||
contactNumber: data['contact_number'] == null
|
||||
? null
|
||||
: data['contact_number'] as String?,
|
||||
remarks: data['remarks'] == null ? null : data['remarks'] as String?,
|
||||
officeId: data['office_id'] == null ? null : data['office_id'] as String?,
|
||||
requestedBy: data['requested_by'] == null
|
||||
? null
|
||||
: data['requested_by'] as String?,
|
||||
requestedByUserId: data['requested_by_user_id'] == null
|
||||
? null
|
||||
: data['requested_by_user_id'] as String?,
|
||||
approvedBy: data['approved_by'] == null
|
||||
? null
|
||||
: data['approved_by'] as String?,
|
||||
approvedByUserId: data['approved_by_user_id'] == null
|
||||
? null
|
||||
: data['approved_by_user_id'] as String?,
|
||||
approvedAt: data['approved_at'] == null
|
||||
? null
|
||||
: data['approved_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['approved_at'] as String),
|
||||
status: data['status'] as String,
|
||||
outsidePremiseAllowed: data['outside_premise_allowed'] == 1,
|
||||
cancellationReason: data['cancellation_reason'] == null
|
||||
? null
|
||||
: data['cancellation_reason'] as String?,
|
||||
cancelledAt: data['cancelled_at'] == null
|
||||
? null
|
||||
: data['cancelled_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['cancelled_at'] as String),
|
||||
creatorId: data['creator_id'] == null
|
||||
? null
|
||||
: data['creator_id'] as String?,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
updatedAt: DateTime.parse(data['updated_at'] as String),
|
||||
completedAt: data['completed_at'] == null
|
||||
? null
|
||||
: data['completed_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['completed_at'] as String),
|
||||
dateTimeReceived: data['date_time_received'] == null
|
||||
? null
|
||||
: data['date_time_received'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['date_time_received'] as String),
|
||||
dateTimeChecked: data['date_time_checked'] == null
|
||||
? null
|
||||
: data['date_time_checked'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['date_time_checked'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ItServiceRequestToSqlite(
|
||||
ItServiceRequest instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'request_number': instance.requestNumber,
|
||||
'services': jsonEncode(instance.services),
|
||||
'services_other': instance.servicesOther,
|
||||
'event_name': instance.eventName,
|
||||
'event_details': instance.eventDetails,
|
||||
'event_date': instance.eventDate?.toIso8601String(),
|
||||
'event_end_date': instance.eventEndDate?.toIso8601String(),
|
||||
'dry_run_date': instance.dryRunDate?.toIso8601String(),
|
||||
'dry_run_end_date': instance.dryRunEndDate?.toIso8601String(),
|
||||
'contact_person': instance.contactPerson,
|
||||
'contact_number': instance.contactNumber,
|
||||
'remarks': instance.remarks,
|
||||
'office_id': instance.officeId,
|
||||
'requested_by': instance.requestedBy,
|
||||
'requested_by_user_id': instance.requestedByUserId,
|
||||
'approved_by': instance.approvedBy,
|
||||
'approved_by_user_id': instance.approvedByUserId,
|
||||
'approved_at': instance.approvedAt?.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'outside_premise_allowed': instance.outsidePremiseAllowed ? 1 : 0,
|
||||
'cancellation_reason': instance.cancellationReason,
|
||||
'cancelled_at': instance.cancelledAt?.toIso8601String(),
|
||||
'creator_id': instance.creatorId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'completed_at': instance.completedAt?.toIso8601String(),
|
||||
'date_time_received': instance.dateTimeReceived?.toIso8601String(),
|
||||
'date_time_checked': instance.dateTimeChecked?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [ItServiceRequest]
|
||||
class ItServiceRequestAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<ItServiceRequest> {
|
||||
ItServiceRequestAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'it_service_requests';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'requestNumber': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_number',
|
||||
),
|
||||
'services': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'services',
|
||||
),
|
||||
'servicesOther': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'services_other',
|
||||
),
|
||||
'eventName': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'event_name',
|
||||
),
|
||||
'eventDetails': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'event_details',
|
||||
),
|
||||
'eventDate': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'event_date',
|
||||
),
|
||||
'eventEndDate': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'event_end_date',
|
||||
),
|
||||
'dryRunDate': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'dry_run_date',
|
||||
),
|
||||
'dryRunEndDate': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'dry_run_end_date',
|
||||
),
|
||||
'contactPerson': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'contact_person',
|
||||
),
|
||||
'contactNumber': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'contact_number',
|
||||
),
|
||||
'remarks': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'remarks',
|
||||
),
|
||||
'officeId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'office_id',
|
||||
),
|
||||
'requestedBy': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_by',
|
||||
),
|
||||
'requestedByUserId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_by_user_id',
|
||||
),
|
||||
'approvedBy': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_by',
|
||||
),
|
||||
'approvedByUserId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_by_user_id',
|
||||
),
|
||||
'approvedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_at',
|
||||
),
|
||||
'status': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
),
|
||||
'outsidePremiseAllowed': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'outside_premise_allowed',
|
||||
),
|
||||
'cancellationReason': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'cancellation_reason',
|
||||
),
|
||||
'cancelledAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'cancelled_at',
|
||||
),
|
||||
'creatorId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'creator_id',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'updatedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'updated_at',
|
||||
),
|
||||
'completedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'completed_at',
|
||||
),
|
||||
'dateTimeReceived': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'date_time_received',
|
||||
),
|
||||
'dateTimeChecked': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'date_time_checked',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestNumber': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_number',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'services': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'services',
|
||||
iterable: true,
|
||||
type: String,
|
||||
),
|
||||
'servicesOther': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'services_other',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'eventName': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'event_name',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'eventDetails': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'event_details',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'eventDate': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'event_date',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'eventEndDate': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'event_end_date',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'dryRunDate': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'dry_run_date',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'dryRunEndDate': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'dry_run_end_date',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'contactPerson': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'contact_person',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'contactNumber': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'contact_number',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'remarks': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'remarks',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'officeId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'office_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestedBy': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_by',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestedByUserId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_by_user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'approvedBy': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_by',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'approvedByUserId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_by_user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'approvedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'status': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'outsidePremiseAllowed': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'outside_premise_allowed',
|
||||
iterable: false,
|
||||
type: bool,
|
||||
),
|
||||
'cancellationReason': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'cancellation_reason',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'cancelledAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'cancelled_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'creatorId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'creator_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'updatedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'updated_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'completedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'completed_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'dateTimeReceived': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'date_time_received',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'dateTimeChecked': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'date_time_checked',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
ItServiceRequest instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'ItServiceRequest';
|
||||
|
||||
@override
|
||||
Future<ItServiceRequest> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
ItServiceRequest input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<ItServiceRequest> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
ItServiceRequest input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<ItServiceRequestAssignment> _$ItServiceRequestAssignmentFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ItServiceRequestAssignment(
|
||||
id: data['id'] as String,
|
||||
requestId: data['request_id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ItServiceRequestAssignmentToSupabase(
|
||||
ItServiceRequestAssignment instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'request_id': instance.requestId,
|
||||
'user_id': instance.userId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<ItServiceRequestAssignment> _$ItServiceRequestAssignmentFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return ItServiceRequestAssignment(
|
||||
id: data['id'] as String,
|
||||
requestId: data['request_id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ItServiceRequestAssignmentToSqlite(
|
||||
ItServiceRequestAssignment instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'request_id': instance.requestId,
|
||||
'user_id': instance.userId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [ItServiceRequestAssignment]
|
||||
class ItServiceRequestAssignmentAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<ItServiceRequestAssignment> {
|
||||
ItServiceRequestAssignmentAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'it_service_request_assignments';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'requestId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_id',
|
||||
),
|
||||
'userId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'userId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
ItServiceRequestAssignment instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'ItServiceRequestAssignment';
|
||||
|
||||
@override
|
||||
Future<ItServiceRequestAssignment> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestAssignmentFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
ItServiceRequestAssignment input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestAssignmentToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<ItServiceRequestAssignment> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestAssignmentFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
ItServiceRequestAssignment input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ItServiceRequestAssignmentToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<LeaveOfAbsence> _$LeaveOfAbsenceFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return LeaveOfAbsence(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
leaveType: data['leave_type'] as String,
|
||||
justification: data['justification'] as String,
|
||||
startTime: DateTime.parse(data['start_time'] as String),
|
||||
endTime: DateTime.parse(data['end_time'] as String),
|
||||
status: data['status'] as String,
|
||||
filedBy: data['filed_by'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$LeaveOfAbsenceToSupabase(
|
||||
LeaveOfAbsence instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'leave_type': instance.leaveType,
|
||||
'justification': instance.justification,
|
||||
'start_time': instance.startTime.toIso8601String(),
|
||||
'end_time': instance.endTime.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'filed_by': instance.filedBy,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<LeaveOfAbsence> _$LeaveOfAbsenceFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return LeaveOfAbsence(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
leaveType: data['leave_type'] as String,
|
||||
justification: data['justification'] as String,
|
||||
startTime: DateTime.parse(data['start_time'] as String),
|
||||
endTime: DateTime.parse(data['end_time'] as String),
|
||||
status: data['status'] as String,
|
||||
filedBy: data['filed_by'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$LeaveOfAbsenceToSqlite(
|
||||
LeaveOfAbsence instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'leave_type': instance.leaveType,
|
||||
'justification': instance.justification,
|
||||
'start_time': instance.startTime.toIso8601String(),
|
||||
'end_time': instance.endTime.toIso8601String(),
|
||||
'status': instance.status,
|
||||
'filed_by': instance.filedBy,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [LeaveOfAbsence]
|
||||
class LeaveOfAbsenceAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<LeaveOfAbsence> {
|
||||
LeaveOfAbsenceAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'leave_of_absence';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'userId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
),
|
||||
'leaveType': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'leave_type',
|
||||
),
|
||||
'justification': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'justification',
|
||||
),
|
||||
'startTime': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'start_time',
|
||||
),
|
||||
'endTime': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'end_time',
|
||||
),
|
||||
'status': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
),
|
||||
'filedBy': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'filed_by',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'userId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'leaveType': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'leave_type',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'justification': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'justification',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'startTime': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'start_time',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'endTime': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'end_time',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'status': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'filedBy': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'filed_by',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
LeaveOfAbsence instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'LeaveOfAbsence';
|
||||
|
||||
@override
|
||||
Future<LeaveOfAbsence> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$LeaveOfAbsenceFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
LeaveOfAbsence input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$LeaveOfAbsenceToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<LeaveOfAbsence> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$LeaveOfAbsenceFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
LeaveOfAbsence input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$LeaveOfAbsenceToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<NotificationItem> _$NotificationItemFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return NotificationItem(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
actorId: data['actor_id'] == null ? null : data['actor_id'] as String?,
|
||||
ticketId: data['ticket_id'] == null ? null : data['ticket_id'] as String?,
|
||||
taskId: data['task_id'] == null ? null : data['task_id'] as String?,
|
||||
leaveId: data['leave_id'] == null ? null : data['leave_id'] as String?,
|
||||
passSlipId: data['pass_slip_id'] == null
|
||||
? null
|
||||
: data['pass_slip_id'] as String?,
|
||||
itServiceRequestId: data['it_service_request_id'] == null
|
||||
? null
|
||||
: data['it_service_request_id'] as String?,
|
||||
announcementId: data['announcement_id'] == null
|
||||
? null
|
||||
: data['announcement_id'] as String?,
|
||||
messageId: data['message_id'] == null ? null : data['message_id'] as int?,
|
||||
type: data['type'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
readAt: data['read_at'] == null
|
||||
? null
|
||||
: data['read_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['read_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$NotificationItemToSupabase(
|
||||
NotificationItem instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'actor_id': instance.actorId,
|
||||
'ticket_id': instance.ticketId,
|
||||
'task_id': instance.taskId,
|
||||
'leave_id': instance.leaveId,
|
||||
'pass_slip_id': instance.passSlipId,
|
||||
'it_service_request_id': instance.itServiceRequestId,
|
||||
'announcement_id': instance.announcementId,
|
||||
'message_id': instance.messageId,
|
||||
'type': instance.type,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'read_at': instance.readAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<NotificationItem> _$NotificationItemFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return NotificationItem(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
actorId: data['actor_id'] == null ? null : data['actor_id'] as String?,
|
||||
ticketId: data['ticket_id'] == null ? null : data['ticket_id'] as String?,
|
||||
taskId: data['task_id'] == null ? null : data['task_id'] as String?,
|
||||
leaveId: data['leave_id'] == null ? null : data['leave_id'] as String?,
|
||||
passSlipId: data['pass_slip_id'] == null
|
||||
? null
|
||||
: data['pass_slip_id'] as String?,
|
||||
itServiceRequestId: data['it_service_request_id'] == null
|
||||
? null
|
||||
: data['it_service_request_id'] as String?,
|
||||
announcementId: data['announcement_id'] == null
|
||||
? null
|
||||
: data['announcement_id'] as String?,
|
||||
messageId: data['message_id'] == null ? null : data['message_id'] as int?,
|
||||
type: data['type'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
readAt: data['read_at'] == null
|
||||
? null
|
||||
: data['read_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['read_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$NotificationItemToSqlite(
|
||||
NotificationItem instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'actor_id': instance.actorId,
|
||||
'ticket_id': instance.ticketId,
|
||||
'task_id': instance.taskId,
|
||||
'leave_id': instance.leaveId,
|
||||
'pass_slip_id': instance.passSlipId,
|
||||
'it_service_request_id': instance.itServiceRequestId,
|
||||
'announcement_id': instance.announcementId,
|
||||
'message_id': instance.messageId,
|
||||
'type': instance.type,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'read_at': instance.readAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [NotificationItem]
|
||||
class NotificationItemAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<NotificationItem> {
|
||||
NotificationItemAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'notifications';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'userId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
),
|
||||
'actorId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'actor_id',
|
||||
),
|
||||
'ticketId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'ticket_id',
|
||||
),
|
||||
'taskId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'task_id',
|
||||
),
|
||||
'leaveId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'leave_id',
|
||||
),
|
||||
'passSlipId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'pass_slip_id',
|
||||
),
|
||||
'itServiceRequestId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'it_service_request_id',
|
||||
),
|
||||
'announcementId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'announcement_id',
|
||||
),
|
||||
'messageId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'message_id',
|
||||
),
|
||||
'type': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'type',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'readAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'read_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'userId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'actorId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'actor_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'ticketId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'ticket_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'taskId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'task_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'leaveId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'leave_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'passSlipId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'pass_slip_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'itServiceRequestId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'it_service_request_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'announcementId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'announcement_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'messageId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'message_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'type': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'type',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'readAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'read_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
NotificationItem instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'NotificationItem';
|
||||
|
||||
@override
|
||||
Future<NotificationItem> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$NotificationItemFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
NotificationItem input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$NotificationItemToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<NotificationItem> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$NotificationItemFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
NotificationItem input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$NotificationItemToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<Office> _$OfficeFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Office(
|
||||
id: data['id'] as String,
|
||||
name: data['name'] as String,
|
||||
serviceId: data['service_id'] == null
|
||||
? null
|
||||
: data['service_id'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$OfficeToSupabase(
|
||||
Office instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'service_id': instance.serviceId,
|
||||
};
|
||||
}
|
||||
|
||||
Future<Office> _$OfficeFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Office(
|
||||
id: data['id'] as String,
|
||||
name: data['name'] as String,
|
||||
serviceId: data['service_id'] == null
|
||||
? null
|
||||
: data['service_id'] as String?,
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$OfficeToSqlite(
|
||||
Office instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'service_id': instance.serviceId,
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [Office]
|
||||
class OfficeAdapter extends OfflineFirstWithSupabaseAdapter<Office> {
|
||||
OfficeAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'offices';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'name': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'name',
|
||||
),
|
||||
'serviceId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'service_id',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'name': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'name',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'serviceId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'service_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
Office instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'Office';
|
||||
|
||||
@override
|
||||
Future<Office> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$OfficeFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
Office input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$OfficeToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Office> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$OfficeFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
Office input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async =>
|
||||
await _$OfficeToSqlite(input, provider: provider, repository: repository);
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<PassSlip> _$PassSlipFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return PassSlip(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
dutyScheduleId: data['duty_schedule_id'] as String,
|
||||
reason: data['reason'] as String,
|
||||
status: data['status'] as String,
|
||||
requestedAt: DateTime.parse(data['requested_at'] as String),
|
||||
approvedBy: data['approved_by'] == null
|
||||
? null
|
||||
: data['approved_by'] as String?,
|
||||
approvedAt: data['approved_at'] == null
|
||||
? null
|
||||
: data['approved_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['approved_at'] as String),
|
||||
slipStart: data['slip_start'] == null
|
||||
? null
|
||||
: data['slip_start'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['slip_start'] as String),
|
||||
slipEnd: data['slip_end'] == null
|
||||
? null
|
||||
: data['slip_end'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['slip_end'] as String),
|
||||
requestedStart: data['requested_start'] == null
|
||||
? null
|
||||
: data['requested_start'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['requested_start'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$PassSlipToSupabase(
|
||||
PassSlip instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'duty_schedule_id': instance.dutyScheduleId,
|
||||
'reason': instance.reason,
|
||||
'status': instance.status,
|
||||
'requested_at': instance.requestedAt.toIso8601String(),
|
||||
'approved_by': instance.approvedBy,
|
||||
'approved_at': instance.approvedAt?.toIso8601String(),
|
||||
'slip_start': instance.slipStart?.toIso8601String(),
|
||||
'slip_end': instance.slipEnd?.toIso8601String(),
|
||||
'requested_start': instance.requestedStart?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<PassSlip> _$PassSlipFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return PassSlip(
|
||||
id: data['id'] as String,
|
||||
userId: data['user_id'] as String,
|
||||
dutyScheduleId: data['duty_schedule_id'] as String,
|
||||
reason: data['reason'] as String,
|
||||
status: data['status'] as String,
|
||||
requestedAt: DateTime.parse(data['requested_at'] as String),
|
||||
approvedBy: data['approved_by'] == null
|
||||
? null
|
||||
: data['approved_by'] as String?,
|
||||
approvedAt: data['approved_at'] == null
|
||||
? null
|
||||
: data['approved_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['approved_at'] as String),
|
||||
slipStart: data['slip_start'] == null
|
||||
? null
|
||||
: data['slip_start'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['slip_start'] as String),
|
||||
slipEnd: data['slip_end'] == null
|
||||
? null
|
||||
: data['slip_end'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['slip_end'] as String),
|
||||
requestedStart: data['requested_start'] == null
|
||||
? null
|
||||
: data['requested_start'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['requested_start'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$PassSlipToSqlite(
|
||||
PassSlip instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'user_id': instance.userId,
|
||||
'duty_schedule_id': instance.dutyScheduleId,
|
||||
'reason': instance.reason,
|
||||
'status': instance.status,
|
||||
'requested_at': instance.requestedAt.toIso8601String(),
|
||||
'approved_by': instance.approvedBy,
|
||||
'approved_at': instance.approvedAt?.toIso8601String(),
|
||||
'slip_start': instance.slipStart?.toIso8601String(),
|
||||
'slip_end': instance.slipEnd?.toIso8601String(),
|
||||
'requested_start': instance.requestedStart?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [PassSlip]
|
||||
class PassSlipAdapter extends OfflineFirstWithSupabaseAdapter<PassSlip> {
|
||||
PassSlipAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'pass_slips';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'userId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
),
|
||||
'dutyScheduleId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'duty_schedule_id',
|
||||
),
|
||||
'reason': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'reason',
|
||||
),
|
||||
'status': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
),
|
||||
'requestedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_at',
|
||||
),
|
||||
'approvedBy': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_by',
|
||||
),
|
||||
'approvedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_at',
|
||||
),
|
||||
'slipStart': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'slip_start',
|
||||
),
|
||||
'slipEnd': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'slip_end',
|
||||
),
|
||||
'requestedStart': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_start',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'userId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'user_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'dutyScheduleId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'duty_schedule_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'reason': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'reason',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'status': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'approvedBy': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_by',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'approvedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'slipStart': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'slip_start',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'slipEnd': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'slip_end',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'requestedStart': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_start',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
PassSlip instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'PassSlip';
|
||||
|
||||
@override
|
||||
Future<PassSlip> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$PassSlipFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
PassSlip input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$PassSlipToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<PassSlip> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$PassSlipFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
PassSlip input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$PassSlipToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<Profile> _$ProfileFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Profile(
|
||||
id: data['id'] as String,
|
||||
role: data['role'] as String,
|
||||
fullName: data['full_name'] as String,
|
||||
religion: data['religion'] as String,
|
||||
allowTracking: data['allow_tracking'] as bool,
|
||||
avatarUrl: data['avatar_url'] == null
|
||||
? null
|
||||
: data['avatar_url'] as String?,
|
||||
facePhotoUrl: data['face_photo_url'] == null
|
||||
? null
|
||||
: data['face_photo_url'] as String?,
|
||||
faceEnrolledAt: data['face_enrolled_at'] == null
|
||||
? null
|
||||
: data['face_enrolled_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['face_enrolled_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ProfileToSupabase(
|
||||
Profile instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'role': instance.role,
|
||||
'full_name': instance.fullName,
|
||||
'religion': instance.religion,
|
||||
'allow_tracking': instance.allowTracking,
|
||||
'avatar_url': instance.avatarUrl,
|
||||
'face_photo_url': instance.facePhotoUrl,
|
||||
'face_enrolled_at': instance.faceEnrolledAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<Profile> _$ProfileFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Profile(
|
||||
id: data['id'] as String,
|
||||
role: data['role'] as String,
|
||||
fullName: data['full_name'] as String,
|
||||
religion: data['religion'] as String,
|
||||
allowTracking: data['allow_tracking'] == 1,
|
||||
avatarUrl: data['avatar_url'] == null
|
||||
? null
|
||||
: data['avatar_url'] as String?,
|
||||
facePhotoUrl: data['face_photo_url'] == null
|
||||
? null
|
||||
: data['face_photo_url'] as String?,
|
||||
faceEnrolledAt: data['face_enrolled_at'] == null
|
||||
? null
|
||||
: data['face_enrolled_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['face_enrolled_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ProfileToSqlite(
|
||||
Profile instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'role': instance.role,
|
||||
'full_name': instance.fullName,
|
||||
'religion': instance.religion,
|
||||
'allow_tracking': instance.allowTracking ? 1 : 0,
|
||||
'avatar_url': instance.avatarUrl,
|
||||
'face_photo_url': instance.facePhotoUrl,
|
||||
'face_enrolled_at': instance.faceEnrolledAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [Profile]
|
||||
class ProfileAdapter extends OfflineFirstWithSupabaseAdapter<Profile> {
|
||||
ProfileAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'profiles';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'role': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'role',
|
||||
),
|
||||
'fullName': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'full_name',
|
||||
),
|
||||
'religion': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'religion',
|
||||
),
|
||||
'allowTracking': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'allow_tracking',
|
||||
),
|
||||
'avatarUrl': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'avatar_url',
|
||||
),
|
||||
'facePhotoUrl': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'face_photo_url',
|
||||
),
|
||||
'faceEnrolledAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'face_enrolled_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'role': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'role',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'fullName': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'full_name',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'religion': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'religion',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'allowTracking': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'allow_tracking',
|
||||
iterable: false,
|
||||
type: bool,
|
||||
),
|
||||
'avatarUrl': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'avatar_url',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'facePhotoUrl': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'face_photo_url',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'faceEnrolledAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'face_enrolled_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
Profile instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'Profile';
|
||||
|
||||
@override
|
||||
Future<Profile> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ProfileFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
Profile input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ProfileToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Profile> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ProfileFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
Profile input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ProfileToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<Service> _$ServiceFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Service(id: data['id'] as String, name: data['name'] as String);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ServiceToSupabase(
|
||||
Service instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {'id': instance.id, 'name': instance.name};
|
||||
}
|
||||
|
||||
Future<Service> _$ServiceFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Service(id: data['id'] as String, name: data['name'] as String)
|
||||
..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$ServiceToSqlite(
|
||||
Service instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {'id': instance.id, 'name': instance.name};
|
||||
}
|
||||
|
||||
/// Construct a [Service]
|
||||
class ServiceAdapter extends OfflineFirstWithSupabaseAdapter<Service> {
|
||||
ServiceAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'services';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'name': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'name',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'name': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'name',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
Service instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'Service';
|
||||
|
||||
@override
|
||||
Future<Service> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ServiceFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
Service input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ServiceToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Service> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ServiceFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
Service input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$ServiceToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<SwapRequest> _$SwapRequestFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return SwapRequest(
|
||||
id: data['id'] as String,
|
||||
requesterId: data['requester_id'] as String,
|
||||
recipientId: data['recipient_id'] as String,
|
||||
requesterScheduleId: data['requester_schedule_id'] as String,
|
||||
targetScheduleId: data['target_shift_id'] == null
|
||||
? null
|
||||
: data['target_shift_id'] as String?,
|
||||
status: data['status'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
updatedAt: data['updated_at'] == null
|
||||
? null
|
||||
: data['updated_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['updated_at'] as String),
|
||||
chatThreadId: data['chat_thread_id'] == null
|
||||
? null
|
||||
: data['chat_thread_id'] as String?,
|
||||
shiftType: data['shift_type'] == null
|
||||
? null
|
||||
: data['shift_type'] as String?,
|
||||
shiftStartTime: data['shift_start_time'] == null
|
||||
? null
|
||||
: data['shift_start_time'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['shift_start_time'] as String),
|
||||
relieverIds: data['reliever_ids'] == null
|
||||
? null
|
||||
: data['reliever_ids']?.toList().cast<String>(),
|
||||
approvedBy: data['approved_by'] == null
|
||||
? null
|
||||
: data['approved_by'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$SwapRequestToSupabase(
|
||||
SwapRequest instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'requester_id': instance.requesterId,
|
||||
'recipient_id': instance.recipientId,
|
||||
'requester_schedule_id': instance.requesterScheduleId,
|
||||
'target_shift_id': instance.targetScheduleId,
|
||||
'status': instance.status,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||
'chat_thread_id': instance.chatThreadId,
|
||||
'shift_type': instance.shiftType,
|
||||
'shift_start_time': instance.shiftStartTime?.toIso8601String(),
|
||||
'reliever_ids': instance.relieverIds,
|
||||
'approved_by': instance.approvedBy,
|
||||
};
|
||||
}
|
||||
|
||||
Future<SwapRequest> _$SwapRequestFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return SwapRequest(
|
||||
id: data['id'] as String,
|
||||
requesterId: data['requester_id'] as String,
|
||||
recipientId: data['recipient_id'] as String,
|
||||
requesterScheduleId: data['requester_schedule_id'] as String,
|
||||
targetScheduleId: data['target_schedule_id'] == null
|
||||
? null
|
||||
: data['target_schedule_id'] as String?,
|
||||
status: data['status'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
updatedAt: data['updated_at'] == null
|
||||
? null
|
||||
: data['updated_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['updated_at'] as String),
|
||||
chatThreadId: data['chat_thread_id'] == null
|
||||
? null
|
||||
: data['chat_thread_id'] as String?,
|
||||
shiftType: data['shift_type'] == null
|
||||
? null
|
||||
: data['shift_type'] as String?,
|
||||
shiftStartTime: data['shift_start_time'] == null
|
||||
? null
|
||||
: data['shift_start_time'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['shift_start_time'] as String),
|
||||
relieverIds: data['reliever_ids'] == null
|
||||
? null
|
||||
: jsonDecode(data['reliever_ids']).toList().cast<String>(),
|
||||
approvedBy: data['approved_by'] == null
|
||||
? null
|
||||
: data['approved_by'] as String?,
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$SwapRequestToSqlite(
|
||||
SwapRequest instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'requester_id': instance.requesterId,
|
||||
'recipient_id': instance.recipientId,
|
||||
'requester_schedule_id': instance.requesterScheduleId,
|
||||
'target_schedule_id': instance.targetScheduleId,
|
||||
'status': instance.status,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||
'chat_thread_id': instance.chatThreadId,
|
||||
'shift_type': instance.shiftType,
|
||||
'shift_start_time': instance.shiftStartTime?.toIso8601String(),
|
||||
'reliever_ids': instance.relieverIds == null
|
||||
? null
|
||||
: jsonEncode(instance.relieverIds),
|
||||
'approved_by': instance.approvedBy,
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [SwapRequest]
|
||||
class SwapRequestAdapter extends OfflineFirstWithSupabaseAdapter<SwapRequest> {
|
||||
SwapRequestAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'swap_requests';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'requesterId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requester_id',
|
||||
),
|
||||
'recipientId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'recipient_id',
|
||||
),
|
||||
'requesterScheduleId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requester_schedule_id',
|
||||
),
|
||||
'targetScheduleId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'target_shift_id',
|
||||
),
|
||||
'status': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'updatedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'updated_at',
|
||||
),
|
||||
'chatThreadId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'chat_thread_id',
|
||||
),
|
||||
'shiftType': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'shift_type',
|
||||
),
|
||||
'shiftStartTime': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'shift_start_time',
|
||||
),
|
||||
'relieverIds': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'reliever_ids',
|
||||
),
|
||||
'approvedBy': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_by',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requesterId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requester_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'recipientId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'recipient_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requesterScheduleId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requester_schedule_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'targetScheduleId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'target_schedule_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'status': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'updatedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'updated_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'chatThreadId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'chat_thread_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'shiftType': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'shift_type',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'shiftStartTime': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'shift_start_time',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'relieverIds': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'reliever_ids',
|
||||
iterable: true,
|
||||
type: String,
|
||||
),
|
||||
'approvedBy': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'approved_by',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
SwapRequest instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'SwapRequest';
|
||||
|
||||
@override
|
||||
Future<SwapRequest> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$SwapRequestFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
SwapRequest input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$SwapRequestToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<SwapRequest> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$SwapRequestFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
SwapRequest input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$SwapRequestToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<TaskActivityLog> _$TaskActivityLogFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return TaskActivityLog(
|
||||
id: data['id'] as String,
|
||||
taskId: data['task_id'] as String,
|
||||
actorId: data['actor_id'] == null ? null : data['actor_id'] as String?,
|
||||
actionType: data['action_type'] as String,
|
||||
meta: data['meta'] == null ? null : data['meta'],
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$TaskActivityLogToSupabase(
|
||||
TaskActivityLog instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'task_id': instance.taskId,
|
||||
'actor_id': instance.actorId,
|
||||
'action_type': instance.actionType,
|
||||
'meta': instance.meta,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<TaskActivityLog> _$TaskActivityLogFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return TaskActivityLog(
|
||||
id: data['id'] as String,
|
||||
taskId: data['task_id'] as String,
|
||||
actorId: data['actor_id'] == null ? null : data['actor_id'] as String?,
|
||||
actionType: data['action_type'] as String,
|
||||
meta: data['meta'] == null ? null : jsonDecode(data['meta']),
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$TaskActivityLogToSqlite(
|
||||
TaskActivityLog instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'task_id': instance.taskId,
|
||||
'actor_id': instance.actorId,
|
||||
'action_type': instance.actionType,
|
||||
'meta': instance.meta != null ? jsonEncode(instance.meta) : null,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [TaskActivityLog]
|
||||
class TaskActivityLogAdapter
|
||||
extends OfflineFirstWithSupabaseAdapter<TaskActivityLog> {
|
||||
TaskActivityLogAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'task_activity_logs';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'taskId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'task_id',
|
||||
),
|
||||
'actorId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'actor_id',
|
||||
),
|
||||
'actionType': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'action_type',
|
||||
),
|
||||
'meta': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'meta',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'taskId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'task_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'actorId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'actor_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'actionType': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'action_type',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'meta': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'meta',
|
||||
iterable: false,
|
||||
type: Map,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
TaskActivityLog instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'TaskActivityLog';
|
||||
|
||||
@override
|
||||
Future<TaskActivityLog> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TaskActivityLogFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
TaskActivityLog input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TaskActivityLogToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<TaskActivityLog> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TaskActivityLogFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
TaskActivityLog input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TaskActivityLogToSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<Task> _$TaskFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Task(
|
||||
id: data['id'] as String,
|
||||
ticketId: data['ticket_id'] == null ? null : data['ticket_id'] as String?,
|
||||
taskNumber: data['task_number'] == null
|
||||
? null
|
||||
: data['task_number'] as String?,
|
||||
title: data['title'] as String,
|
||||
description: data['description'] as String,
|
||||
officeId: data['office_id'] == null ? null : data['office_id'] as String?,
|
||||
status: data['status'] as String,
|
||||
priority: data['priority'] as int,
|
||||
queueOrder: data['queue_order'] == null
|
||||
? null
|
||||
: data['queue_order'] as int?,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
creatorId: data['creator_id'] == null
|
||||
? null
|
||||
: data['creator_id'] as String?,
|
||||
startedAt: data['started_at'] == null
|
||||
? null
|
||||
: data['started_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['started_at'] as String),
|
||||
completedAt: data['completed_at'] == null
|
||||
? null
|
||||
: data['completed_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['completed_at'] as String),
|
||||
requestedBy: data['requested_by'] == null
|
||||
? null
|
||||
: data['requested_by'] as String?,
|
||||
notedBy: data['noted_by'] == null ? null : data['noted_by'] as String?,
|
||||
receivedBy: data['received_by'] == null
|
||||
? null
|
||||
: data['received_by'] as String?,
|
||||
requestType: data['request_type'] == null
|
||||
? null
|
||||
: data['request_type'] as String?,
|
||||
requestTypeOther: data['request_type_other'] == null
|
||||
? null
|
||||
: data['request_type_other'] as String?,
|
||||
requestCategory: data['request_category'] == null
|
||||
? null
|
||||
: data['request_category'] as String?,
|
||||
actionTaken: data['action_taken'] == null
|
||||
? null
|
||||
: data['action_taken'] as String?,
|
||||
cancellationReason: data['cancellation_reason'] == null
|
||||
? null
|
||||
: data['cancellation_reason'] as String?,
|
||||
cancelledAt: data['cancelled_at'] == null
|
||||
? null
|
||||
: data['cancelled_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['cancelled_at'] as String),
|
||||
itJobPrinted: data['it_job_printed'] as bool,
|
||||
itJobPrintedAt: data['it_job_printed_at'] == null
|
||||
? null
|
||||
: data['it_job_printed_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['it_job_printed_at'] as String),
|
||||
itJobReceivedById: data['it_job_received_by_id'] == null
|
||||
? null
|
||||
: data['it_job_received_by_id'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$TaskToSupabase(
|
||||
Task instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'ticket_id': instance.ticketId,
|
||||
'task_number': instance.taskNumber,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'office_id': instance.officeId,
|
||||
'status': instance.status,
|
||||
'priority': instance.priority,
|
||||
'queue_order': instance.queueOrder,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'creator_id': instance.creatorId,
|
||||
'started_at': instance.startedAt?.toIso8601String(),
|
||||
'completed_at': instance.completedAt?.toIso8601String(),
|
||||
'requested_by': instance.requestedBy,
|
||||
'noted_by': instance.notedBy,
|
||||
'received_by': instance.receivedBy,
|
||||
'request_type': instance.requestType,
|
||||
'request_type_other': instance.requestTypeOther,
|
||||
'request_category': instance.requestCategory,
|
||||
'action_taken': instance.actionTaken,
|
||||
'cancellation_reason': instance.cancellationReason,
|
||||
'cancelled_at': instance.cancelledAt?.toIso8601String(),
|
||||
'it_job_printed': instance.itJobPrinted,
|
||||
'it_job_printed_at': instance.itJobPrintedAt?.toIso8601String(),
|
||||
'it_job_received_by_id': instance.itJobReceivedById,
|
||||
};
|
||||
}
|
||||
|
||||
Future<Task> _$TaskFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Task(
|
||||
id: data['id'] as String,
|
||||
ticketId: data['ticket_id'] == null ? null : data['ticket_id'] as String?,
|
||||
taskNumber: data['task_number'] == null
|
||||
? null
|
||||
: data['task_number'] as String?,
|
||||
title: data['title'] as String,
|
||||
description: data['description'] as String,
|
||||
officeId: data['office_id'] == null ? null : data['office_id'] as String?,
|
||||
status: data['status'] as String,
|
||||
priority: data['priority'] as int,
|
||||
queueOrder: data['queue_order'] == null
|
||||
? null
|
||||
: data['queue_order'] as int?,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
creatorId: data['creator_id'] == null
|
||||
? null
|
||||
: data['creator_id'] as String?,
|
||||
startedAt: data['started_at'] == null
|
||||
? null
|
||||
: data['started_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['started_at'] as String),
|
||||
completedAt: data['completed_at'] == null
|
||||
? null
|
||||
: data['completed_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['completed_at'] as String),
|
||||
requestedBy: data['requested_by'] == null
|
||||
? null
|
||||
: data['requested_by'] as String?,
|
||||
notedBy: data['noted_by'] == null ? null : data['noted_by'] as String?,
|
||||
receivedBy: data['received_by'] == null
|
||||
? null
|
||||
: data['received_by'] as String?,
|
||||
requestType: data['request_type'] == null
|
||||
? null
|
||||
: data['request_type'] as String?,
|
||||
requestTypeOther: data['request_type_other'] == null
|
||||
? null
|
||||
: data['request_type_other'] as String?,
|
||||
requestCategory: data['request_category'] == null
|
||||
? null
|
||||
: data['request_category'] as String?,
|
||||
actionTaken: data['action_taken'] == null
|
||||
? null
|
||||
: data['action_taken'] as String?,
|
||||
cancellationReason: data['cancellation_reason'] == null
|
||||
? null
|
||||
: data['cancellation_reason'] as String?,
|
||||
cancelledAt: data['cancelled_at'] == null
|
||||
? null
|
||||
: data['cancelled_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['cancelled_at'] as String),
|
||||
itJobPrinted: data['it_job_printed'] == 1,
|
||||
itJobPrintedAt: data['it_job_printed_at'] == null
|
||||
? null
|
||||
: data['it_job_printed_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['it_job_printed_at'] as String),
|
||||
itJobReceivedById: data['it_job_received_by_id'] == null
|
||||
? null
|
||||
: data['it_job_received_by_id'] as String?,
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$TaskToSqlite(
|
||||
Task instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'ticket_id': instance.ticketId,
|
||||
'task_number': instance.taskNumber,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'office_id': instance.officeId,
|
||||
'status': instance.status,
|
||||
'priority': instance.priority,
|
||||
'queue_order': instance.queueOrder,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'creator_id': instance.creatorId,
|
||||
'started_at': instance.startedAt?.toIso8601String(),
|
||||
'completed_at': instance.completedAt?.toIso8601String(),
|
||||
'requested_by': instance.requestedBy,
|
||||
'noted_by': instance.notedBy,
|
||||
'received_by': instance.receivedBy,
|
||||
'request_type': instance.requestType,
|
||||
'request_type_other': instance.requestTypeOther,
|
||||
'request_category': instance.requestCategory,
|
||||
'action_taken': instance.actionTaken,
|
||||
'cancellation_reason': instance.cancellationReason,
|
||||
'cancelled_at': instance.cancelledAt?.toIso8601String(),
|
||||
'it_job_printed': instance.itJobPrinted ? 1 : 0,
|
||||
'it_job_printed_at': instance.itJobPrintedAt?.toIso8601String(),
|
||||
'it_job_received_by_id': instance.itJobReceivedById,
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [Task]
|
||||
class TaskAdapter extends OfflineFirstWithSupabaseAdapter<Task> {
|
||||
TaskAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'tasks';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'ticketId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'ticket_id',
|
||||
),
|
||||
'taskNumber': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'task_number',
|
||||
),
|
||||
'title': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'title',
|
||||
),
|
||||
'description': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'description',
|
||||
),
|
||||
'officeId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'office_id',
|
||||
),
|
||||
'status': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
),
|
||||
'priority': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'priority',
|
||||
),
|
||||
'queueOrder': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'queue_order',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'creatorId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'creator_id',
|
||||
),
|
||||
'startedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'started_at',
|
||||
),
|
||||
'completedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'completed_at',
|
||||
),
|
||||
'requestedBy': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_by',
|
||||
),
|
||||
'notedBy': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'noted_by',
|
||||
),
|
||||
'receivedBy': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'received_by',
|
||||
),
|
||||
'requestType': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_type',
|
||||
),
|
||||
'requestTypeOther': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_type_other',
|
||||
),
|
||||
'requestCategory': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_category',
|
||||
),
|
||||
'actionTaken': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'action_taken',
|
||||
),
|
||||
'cancellationReason': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'cancellation_reason',
|
||||
),
|
||||
'cancelledAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'cancelled_at',
|
||||
),
|
||||
'itJobPrinted': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'it_job_printed',
|
||||
),
|
||||
'itJobPrintedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'it_job_printed_at',
|
||||
),
|
||||
'itJobReceivedById': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'it_job_received_by_id',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'ticketId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'ticket_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'taskNumber': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'task_number',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'title': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'title',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'description': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'description',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'officeId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'office_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'status': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'priority': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'priority',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'queueOrder': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'queue_order',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'creatorId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'creator_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'startedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'started_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'completedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'completed_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'requestedBy': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'requested_by',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'notedBy': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'noted_by',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'receivedBy': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'received_by',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestType': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_type',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestTypeOther': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_type_other',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'requestCategory': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'request_category',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'actionTaken': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'action_taken',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'cancellationReason': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'cancellation_reason',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'cancelledAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'cancelled_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'itJobPrinted': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'it_job_printed',
|
||||
iterable: false,
|
||||
type: bool,
|
||||
),
|
||||
'itJobPrintedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'it_job_printed_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'itJobReceivedById': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'it_job_received_by_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
Task instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'Task';
|
||||
|
||||
@override
|
||||
Future<Task> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TaskFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
Task input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async =>
|
||||
await _$TaskToSupabase(input, provider: provider, repository: repository);
|
||||
@override
|
||||
Future<Task> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async =>
|
||||
await _$TaskFromSqlite(input, provider: provider, repository: repository);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
Task input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async =>
|
||||
await _$TaskToSqlite(input, provider: provider, repository: repository);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<Team> _$TeamFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Team(
|
||||
id: data['id'] as String,
|
||||
name: data['name'] as String,
|
||||
leaderId: data['leader_id'] as String,
|
||||
officeIds: data['office_ids'].toList().cast<String>(),
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
color: data['color'] == null ? null : data['color'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$TeamToSupabase(
|
||||
Team instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'leader_id': instance.leaderId,
|
||||
'office_ids': instance.officeIds,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'color': instance.color,
|
||||
};
|
||||
}
|
||||
|
||||
Future<Team> _$TeamFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Team(
|
||||
id: data['id'] as String,
|
||||
name: data['name'] as String,
|
||||
leaderId: data['leader_id'] as String,
|
||||
officeIds: jsonDecode(data['office_ids']).toList().cast<String>(),
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
color: data['color'] == null ? null : data['color'] as String?,
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$TeamToSqlite(
|
||||
Team instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'leader_id': instance.leaderId,
|
||||
'office_ids': jsonEncode(instance.officeIds),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'color': instance.color,
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [Team]
|
||||
class TeamAdapter extends OfflineFirstWithSupabaseAdapter<Team> {
|
||||
TeamAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'teams';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'name': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'name',
|
||||
),
|
||||
'leaderId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'leader_id',
|
||||
),
|
||||
'officeIds': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'office_ids',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'color': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'color',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'name': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'name',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'leaderId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'leader_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'officeIds': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'office_ids',
|
||||
iterable: true,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'color': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'color',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
Team instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'Team';
|
||||
|
||||
@override
|
||||
Future<Team> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TeamFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
Team input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async =>
|
||||
await _$TeamToSupabase(input, provider: provider, repository: repository);
|
||||
@override
|
||||
Future<Team> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async =>
|
||||
await _$TeamFromSqlite(input, provider: provider, repository: repository);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
Team input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async =>
|
||||
await _$TeamToSqlite(input, provider: provider, repository: repository);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
part of '../brick.g.dart';
|
||||
|
||||
Future<Ticket> _$TicketFromSupabase(
|
||||
Map<String, dynamic> data, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Ticket(
|
||||
id: data['id'] as String,
|
||||
subject: data['subject'] as String,
|
||||
description: data['description'] as String,
|
||||
officeId: data['office_id'] as String,
|
||||
status: data['status'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
creatorId: data['creator_id'] == null
|
||||
? null
|
||||
: data['creator_id'] as String?,
|
||||
respondedAt: data['responded_at'] == null
|
||||
? null
|
||||
: data['responded_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['responded_at'] as String),
|
||||
promotedAt: data['promoted_at'] == null
|
||||
? null
|
||||
: data['promoted_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['promoted_at'] as String),
|
||||
closedAt: data['closed_at'] == null
|
||||
? null
|
||||
: data['closed_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['closed_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$TicketToSupabase(
|
||||
Ticket instance, {
|
||||
required SupabaseProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'subject': instance.subject,
|
||||
'description': instance.description,
|
||||
'office_id': instance.officeId,
|
||||
'status': instance.status,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'creator_id': instance.creatorId,
|
||||
'responded_at': instance.respondedAt?.toIso8601String(),
|
||||
'promoted_at': instance.promotedAt?.toIso8601String(),
|
||||
'closed_at': instance.closedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
Future<Ticket> _$TicketFromSqlite(
|
||||
Map<String, dynamic> data, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return Ticket(
|
||||
id: data['id'] as String,
|
||||
subject: data['subject'] as String,
|
||||
description: data['description'] as String,
|
||||
officeId: data['office_id'] as String,
|
||||
status: data['status'] as String,
|
||||
createdAt: DateTime.parse(data['created_at'] as String),
|
||||
creatorId: data['creator_id'] == null
|
||||
? null
|
||||
: data['creator_id'] as String?,
|
||||
respondedAt: data['responded_at'] == null
|
||||
? null
|
||||
: data['responded_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['responded_at'] as String),
|
||||
promotedAt: data['promoted_at'] == null
|
||||
? null
|
||||
: data['promoted_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['promoted_at'] as String),
|
||||
closedAt: data['closed_at'] == null
|
||||
? null
|
||||
: data['closed_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(data['closed_at'] as String),
|
||||
)..primaryKey = data['_brick_id'] as int;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _$TicketToSqlite(
|
||||
Ticket instance, {
|
||||
required SqliteProvider provider,
|
||||
OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async {
|
||||
return {
|
||||
'id': instance.id,
|
||||
'subject': instance.subject,
|
||||
'description': instance.description,
|
||||
'office_id': instance.officeId,
|
||||
'status': instance.status,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'creator_id': instance.creatorId,
|
||||
'responded_at': instance.respondedAt?.toIso8601String(),
|
||||
'promoted_at': instance.promotedAt?.toIso8601String(),
|
||||
'closed_at': instance.closedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Construct a [Ticket]
|
||||
class TicketAdapter extends OfflineFirstWithSupabaseAdapter<Ticket> {
|
||||
TicketAdapter();
|
||||
|
||||
@override
|
||||
final supabaseTableName = 'tickets';
|
||||
@override
|
||||
final defaultToNull = true;
|
||||
@override
|
||||
final fieldsToSupabaseColumns = {
|
||||
'id': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
),
|
||||
'subject': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'subject',
|
||||
),
|
||||
'description': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'description',
|
||||
),
|
||||
'officeId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'office_id',
|
||||
),
|
||||
'status': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
),
|
||||
'createdAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
),
|
||||
'creatorId': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'creator_id',
|
||||
),
|
||||
'respondedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'responded_at',
|
||||
),
|
||||
'promotedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'promoted_at',
|
||||
),
|
||||
'closedAt': const RuntimeSupabaseColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'closed_at',
|
||||
),
|
||||
};
|
||||
@override
|
||||
final ignoreDuplicates = false;
|
||||
@override
|
||||
final uniqueFields = {};
|
||||
@override
|
||||
final Map<String, RuntimeSqliteColumnDefinition> fieldsToSqliteColumns = {
|
||||
'primaryKey': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: '_brick_id',
|
||||
iterable: false,
|
||||
type: int,
|
||||
),
|
||||
'id': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'subject': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'subject',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'description': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'description',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'officeId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'office_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'status': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'status',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'createdAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'created_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'creatorId': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'creator_id',
|
||||
iterable: false,
|
||||
type: String,
|
||||
),
|
||||
'respondedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'responded_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'promotedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'promoted_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
'closedAt': const RuntimeSqliteColumnDefinition(
|
||||
association: false,
|
||||
columnName: 'closed_at',
|
||||
iterable: false,
|
||||
type: DateTime,
|
||||
),
|
||||
};
|
||||
@override
|
||||
Future<int?> primaryKeyByUniqueColumns(
|
||||
Ticket instance,
|
||||
DatabaseExecutor executor,
|
||||
) async => instance.primaryKey;
|
||||
@override
|
||||
final String tableName = 'Ticket';
|
||||
|
||||
@override
|
||||
Future<Ticket> fromSupabase(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TicketFromSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSupabase(
|
||||
Ticket input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TicketToSupabase(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Ticket> fromSqlite(
|
||||
Map<String, dynamic> input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async => await _$TicketFromSqlite(
|
||||
input,
|
||||
provider: provider,
|
||||
repository: repository,
|
||||
);
|
||||
@override
|
||||
Future<Map<String, dynamic>> toSqlite(
|
||||
Ticket input, {
|
||||
required provider,
|
||||
covariant OfflineFirstWithSupabaseRepository? repository,
|
||||
}) async =>
|
||||
await _$TicketToSqlite(input, provider: provider, repository: repository);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// ignore: unused_import, unused_shown_name, unnecessary_import
|
||||
import 'package:brick_core/query.dart';
|
||||
// ignore: unused_import, unused_shown_name, unnecessary_import
|
||||
import 'package:brick_sqlite/db.dart';
|
||||
// ignore: unused_import, unused_shown_name, unnecessary_import
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
// ignore: unused_import, unused_shown_name, unnecessary_import
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
// ignore: unused_import, unused_shown_name, unnecessary_import
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
// ignore: unused_import, unused_shown_name, unnecessary_import
|
||||
import '../utils/app_time.dart';
|
||||
// ignore: unused_import, unused_shown_name, unnecessary_import
|
||||
import 'package:tasq/models/team_member.dart';// GENERATED CODE DO NOT EDIT
|
||||
// ignore: unused_import
|
||||
import 'dart:convert';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart' show SqliteModel, SqliteAdapter, SqliteModelDictionary, RuntimeSqliteColumnDefinition, SqliteProvider;
|
||||
import 'package:brick_supabase/brick_supabase.dart' show SupabaseProvider, SupabaseModel, SupabaseAdapter, SupabaseModelDictionary;
|
||||
// ignore: unused_import, unused_shown_name
|
||||
import 'package:brick_offline_first/brick_offline_first.dart' show RuntimeOfflineFirstDefinition;
|
||||
// ignore: unused_import, unused_shown_name
|
||||
import 'package:sqflite_common/sqlite_api.dart' show DatabaseExecutor;
|
||||
|
||||
import '../models/announcement.model.dart';
|
||||
import '../models/announcement_comment.model.dart';
|
||||
import '../models/attendance_log.model.dart';
|
||||
import '../models/chat_message.model.dart';
|
||||
import '../models/duty_schedule.model.dart';
|
||||
import '../models/it_service_request.model.dart';
|
||||
import '../models/it_service_request_action.model.dart';
|
||||
import '../models/it_service_request_activity_log.model.dart';
|
||||
import '../models/it_service_request_assignment.model.dart';
|
||||
import '../models/leave_of_absence.model.dart';
|
||||
import '../models/notification_item.model.dart';
|
||||
import '../models/office.model.dart';
|
||||
import '../models/pass_slip.model.dart';
|
||||
import '../models/profile.model.dart';
|
||||
import '../models/service.model.dart';
|
||||
import '../models/swap_request.model.dart';
|
||||
import '../models/task.model.dart';
|
||||
import '../models/task_activity_log.model.dart';
|
||||
import '../models/team.model.dart';
|
||||
import '../models/ticket.model.dart';
|
||||
|
||||
part 'adapters/announcement_adapter.g.dart';
|
||||
part 'adapters/announcement_comment_adapter.g.dart';
|
||||
part 'adapters/attendance_log_adapter.g.dart';
|
||||
part 'adapters/chat_message_adapter.g.dart';
|
||||
part 'adapters/duty_schedule_adapter.g.dart';
|
||||
part 'adapters/it_service_request_adapter.g.dart';
|
||||
part 'adapters/it_service_request_action_adapter.g.dart';
|
||||
part 'adapters/it_service_request_activity_log_adapter.g.dart';
|
||||
part 'adapters/it_service_request_assignment_adapter.g.dart';
|
||||
part 'adapters/leave_of_absence_adapter.g.dart';
|
||||
part 'adapters/notification_item_adapter.g.dart';
|
||||
part 'adapters/office_adapter.g.dart';
|
||||
part 'adapters/pass_slip_adapter.g.dart';
|
||||
part 'adapters/profile_adapter.g.dart';
|
||||
part 'adapters/service_adapter.g.dart';
|
||||
part 'adapters/swap_request_adapter.g.dart';
|
||||
part 'adapters/task_adapter.g.dart';
|
||||
part 'adapters/task_activity_log_adapter.g.dart';
|
||||
part 'adapters/team_adapter.g.dart';
|
||||
part 'adapters/ticket_adapter.g.dart';
|
||||
|
||||
/// Supabase mappings should only be used when initializing a [SupabaseProvider]
|
||||
final Map<Type, SupabaseAdapter<SupabaseModel>> supabaseMappings = {
|
||||
Announcement: AnnouncementAdapter(),
|
||||
AnnouncementComment: AnnouncementCommentAdapter(),
|
||||
AttendanceLog: AttendanceLogAdapter(),
|
||||
ChatMessage: ChatMessageAdapter(),
|
||||
DutySchedule: DutyScheduleAdapter(),
|
||||
ItServiceRequest: ItServiceRequestAdapter(),
|
||||
ItServiceRequestAction: ItServiceRequestActionAdapter(),
|
||||
ItServiceRequestActivityLog: ItServiceRequestActivityLogAdapter(),
|
||||
ItServiceRequestAssignment: ItServiceRequestAssignmentAdapter(),
|
||||
LeaveOfAbsence: LeaveOfAbsenceAdapter(),
|
||||
NotificationItem: NotificationItemAdapter(),
|
||||
Office: OfficeAdapter(),
|
||||
PassSlip: PassSlipAdapter(),
|
||||
Profile: ProfileAdapter(),
|
||||
Service: ServiceAdapter(),
|
||||
SwapRequest: SwapRequestAdapter(),
|
||||
Task: TaskAdapter(),
|
||||
TaskActivityLog: TaskActivityLogAdapter(),
|
||||
Team: TeamAdapter(),
|
||||
Ticket: TicketAdapter()
|
||||
};
|
||||
final supabaseModelDictionary = SupabaseModelDictionary(supabaseMappings);
|
||||
|
||||
/// Sqlite mappings should only be used when initializing a [SqliteProvider]
|
||||
final Map<Type, SqliteAdapter<SqliteModel>> sqliteMappings = {
|
||||
Announcement: AnnouncementAdapter(),
|
||||
AnnouncementComment: AnnouncementCommentAdapter(),
|
||||
AttendanceLog: AttendanceLogAdapter(),
|
||||
ChatMessage: ChatMessageAdapter(),
|
||||
DutySchedule: DutyScheduleAdapter(),
|
||||
ItServiceRequest: ItServiceRequestAdapter(),
|
||||
ItServiceRequestAction: ItServiceRequestActionAdapter(),
|
||||
ItServiceRequestActivityLog: ItServiceRequestActivityLogAdapter(),
|
||||
ItServiceRequestAssignment: ItServiceRequestAssignmentAdapter(),
|
||||
LeaveOfAbsence: LeaveOfAbsenceAdapter(),
|
||||
NotificationItem: NotificationItemAdapter(),
|
||||
Office: OfficeAdapter(),
|
||||
PassSlip: PassSlipAdapter(),
|
||||
Profile: ProfileAdapter(),
|
||||
Service: ServiceAdapter(),
|
||||
SwapRequest: SwapRequestAdapter(),
|
||||
Task: TaskAdapter(),
|
||||
TaskActivityLog: TaskActivityLogAdapter(),
|
||||
Team: TeamAdapter(),
|
||||
Ticket: TicketAdapter()
|
||||
};
|
||||
final sqliteModelDictionary = SqliteModelDictionary(sqliteMappings);
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
|
||||
import 'repository.dart';
|
||||
|
||||
/// Returns the locally cached list of [T] from Brick SQLite, or `[]` if Brick
|
||||
/// isn't configured or the read fails. Never throws.
|
||||
///
|
||||
/// Use this as `onOfflineData` for [StreamRecoveryWrapper] so that screens
|
||||
/// render data from the local cache when the device is offline:
|
||||
///
|
||||
/// ```dart
|
||||
/// onOfflineData: () => cachedListFromBrick<Task>(),
|
||||
/// ```
|
||||
Future<List<T>> cachedListFromBrick<T extends OfflineFirstWithSupabaseModel>() async {
|
||||
if (!AppRepository.isConfigured) {
|
||||
debugPrint('[cachedListFromBrick<$T>] BRICK NOT CONFIGURED — returning []');
|
||||
return const [];
|
||||
}
|
||||
try {
|
||||
final rows = await AppRepository.instance.get<T>(
|
||||
policy: OfflineFirstGetPolicy.localOnly,
|
||||
);
|
||||
if (rows.isEmpty) {
|
||||
debugPrint('[cachedListFromBrick<$T>] empty cache (0 rows)');
|
||||
}
|
||||
return rows;
|
||||
} catch (e) {
|
||||
debugPrint('[cachedListFromBrick<$T>] failed: $e');
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a batch of model rows into Brick's local SQLite cache as a
|
||||
/// fire-and-forget side effect. Errors are caught and logged.
|
||||
///
|
||||
/// Use this as `onCacheMirror` for [StreamRecoveryWrapper] so that every
|
||||
/// realtime emission keeps the local cache fresh — required for the cold-
|
||||
/// launch-offline experience.
|
||||
///
|
||||
/// ```dart
|
||||
/// onCacheMirror: (rows) => mirrorBatchToBrick<Task>(rows, tag: 'tasks'),
|
||||
/// ```
|
||||
void mirrorBatchToBrick<T extends OfflineFirstWithSupabaseModel>(
|
||||
List<T> rows, {
|
||||
String tag = 'mirror',
|
||||
}) {
|
||||
if (!AppRepository.isConfigured) return;
|
||||
// Use sqliteProvider directly (local-only write). AppRepository.upsert is
|
||||
// bidirectional — it re-POSTs rows to Supabase, which triggers RLS
|
||||
// violations for other users' records and deadlocks from concurrent writes.
|
||||
// Sequential processing prevents SQLite lock contention across providers.
|
||||
// ignore: discarded_futures
|
||||
Future(() async {
|
||||
for (final row in rows) {
|
||||
try {
|
||||
await AppRepository.instance.sqliteProvider
|
||||
.upsert<T>(row, repository: AppRepository.instance);
|
||||
} catch (e) {
|
||||
debugPrint('[$tag] mirror upsert<$T> failed: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../models/announcement.model.dart';
|
||||
import '../models/announcement_comment.model.dart';
|
||||
import '../models/attendance_log.model.dart';
|
||||
import '../models/duty_schedule.model.dart';
|
||||
import '../models/it_service_request.model.dart';
|
||||
import '../models/leave_of_absence.model.dart';
|
||||
import '../models/notification_item.model.dart';
|
||||
import '../models/office.model.dart';
|
||||
import '../models/pass_slip.model.dart';
|
||||
import '../models/profile.model.dart';
|
||||
import '../models/service.model.dart';
|
||||
import '../models/swap_request.model.dart';
|
||||
import '../models/task.model.dart';
|
||||
import '../models/team.model.dart';
|
||||
import '../models/ticket.model.dart';
|
||||
import 'repository.dart';
|
||||
|
||||
/// Proactively seeds the local Brick SQLite cache with the data the app needs
|
||||
/// to render correctly when launched offline.
|
||||
///
|
||||
/// Triggered:
|
||||
/// - After successful login.
|
||||
/// - After the connectivity monitor flips offline → online.
|
||||
/// - On a 1-hour timer in the foreground (defensive refresh).
|
||||
///
|
||||
/// Never throws — a single table failure must not abort the rest. Each table
|
||||
/// is wrapped in try/catch and only logs the error.
|
||||
///
|
||||
/// Web is a no-op because Brick's SQLite is unavailable on web.
|
||||
class CacheWarmer {
|
||||
CacheWarmer._();
|
||||
|
||||
/// Window of mutable data to keep locally. 30 days is enough for the
|
||||
/// "month of tasks/tickets" the user requested without bloating SQLite.
|
||||
static const Duration window = Duration(days: 30);
|
||||
|
||||
/// SharedPreferences keys for association tables (no PK → not Brick models).
|
||||
static const _kTeamMembersKey = 'cache_team_members_json';
|
||||
static const _kUserOfficesKey = 'cache_user_offices_json';
|
||||
|
||||
/// Foreground timer handle so [start]/[stop] is idempotent.
|
||||
static Timer? _refreshTimer;
|
||||
|
||||
/// True when at least one warm has completed since app start.
|
||||
static bool _hasWarmedOnce = false;
|
||||
static bool get hasWarmedOnce => _hasWarmedOnce;
|
||||
|
||||
/// Single-flight guard to prevent overlapping warms.
|
||||
static Future<void>? _inflight;
|
||||
|
||||
/// Warm every cacheable table. Safe to call repeatedly.
|
||||
static Future<void> warmAll(SupabaseClient client) {
|
||||
if (kIsWeb) return Future.value();
|
||||
if (!AppRepository.isConfigured) return Future.value();
|
||||
final existing = _inflight;
|
||||
if (existing != null) return existing;
|
||||
|
||||
final future = _runWarmAll(client);
|
||||
_inflight = future;
|
||||
return future.whenComplete(() => _inflight = null);
|
||||
}
|
||||
|
||||
static Future<void> _runWarmAll(SupabaseClient client) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final since = DateTime.now().toUtc().subtract(window);
|
||||
final sinceIso = since.toIso8601String();
|
||||
|
||||
debugPrint('[CacheWarmer] starting (since=$sinceIso)');
|
||||
|
||||
// Run reference + per-module warms in parallel — a single failure in one
|
||||
// table must not block the others.
|
||||
await Future.wait<void>([
|
||||
_safe('reference', () => _warmReference(client)),
|
||||
_safe('tasks', () => _warmTasks(client, sinceIso)),
|
||||
_safe('tickets', () => _warmTickets(client, sinceIso)),
|
||||
_safe('itsr', () => _warmItServiceRequests(client, sinceIso)),
|
||||
_safe('announcements', () => _warmAnnouncements(client, sinceIso)),
|
||||
_safe('attendance', () => _warmAttendance(client, sinceIso)),
|
||||
_safe('pass_slips', () => _warmPassSlips(client, sinceIso)),
|
||||
_safe('leaves', () => _warmLeaves(client, sinceIso)),
|
||||
_safe('duty_schedules', () => _warmDutySchedules(client, sinceIso)),
|
||||
_safe('swap_requests', () => _warmSwapRequests(client, sinceIso)),
|
||||
_safe('notifications', () => _warmNotifications(client, sinceIso)),
|
||||
]);
|
||||
|
||||
_hasWarmedOnce = true;
|
||||
stopwatch.stop();
|
||||
debugPrint('[CacheWarmer] done in ${stopwatch.elapsedMilliseconds}ms');
|
||||
}
|
||||
|
||||
/// Schedule a periodic re-warm. Idempotent.
|
||||
static void startPeriodicRefresh(SupabaseClient client) {
|
||||
if (kIsWeb) return;
|
||||
_refreshTimer?.cancel();
|
||||
_refreshTimer = Timer.periodic(const Duration(hours: 1), (_) {
|
||||
warmAll(client);
|
||||
});
|
||||
}
|
||||
|
||||
/// Cancel periodic re-warm (e.g. on sign-out).
|
||||
static void stopPeriodicRefresh() {
|
||||
_refreshTimer?.cancel();
|
||||
_refreshTimer = null;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Reference data — small, rarely-mutating. Pulled in full.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
static Future<void> _warmReference(SupabaseClient client) async {
|
||||
await Future.wait<void>([
|
||||
_safe('offices', () async {
|
||||
final rows = await client.from('offices').select();
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Office>(Office.fromMap(row));
|
||||
}
|
||||
}),
|
||||
_safe('services', () async {
|
||||
final rows = await client.from('services').select();
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Service>(Service.fromMap(row));
|
||||
}
|
||||
}),
|
||||
_safe('profiles', () async {
|
||||
final rows = await client.from('profiles').select();
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Profile>(Profile.fromMap(row));
|
||||
}
|
||||
}),
|
||||
_safe('teams', () async {
|
||||
final rows = await client.from('teams').select();
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Team>(Team.fromMap(row));
|
||||
}
|
||||
}),
|
||||
// Association tables have composite PKs, so we cache them as JSON in
|
||||
// SharedPreferences instead of Brick. The list is small and read-only.
|
||||
_safe('team_members', () async {
|
||||
final rows = await client.from('team_members').select();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kTeamMembersKey, jsonEncode(rows));
|
||||
}),
|
||||
_safe('user_offices', () async {
|
||||
final rows = await client.from('user_offices').select();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kUserOfficesKey, jsonEncode(rows));
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Read cached team_members rows from SharedPreferences.
|
||||
/// Returns the same shape as the Supabase select (`[{team_id, user_id}, ...]`).
|
||||
static Future<List<Map<String, dynamic>>> readCachedTeamMembers() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_kTeamMembersKey);
|
||||
if (raw == null) return const [];
|
||||
try {
|
||||
final list = jsonDecode(raw);
|
||||
if (list is! List) return const [];
|
||||
return list.cast<Map<String, dynamic>>();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Read cached user_offices rows from SharedPreferences.
|
||||
static Future<List<Map<String, dynamic>>> readCachedUserOffices() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_kUserOfficesKey);
|
||||
if (raw == null) return const [];
|
||||
try {
|
||||
final list = jsonDecode(raw);
|
||||
if (list is! List) return const [];
|
||||
return list.cast<Map<String, dynamic>>();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Per-module warms — windowed by created_at >= since
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
static Future<void> _warmTasks(SupabaseClient client, String sinceIso) async {
|
||||
final rows = await client
|
||||
.from('tasks')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Task>(Task.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmTickets(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('tickets')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Ticket>(Ticket.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmItServiceRequests(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('it_service_requests')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<ItServiceRequest>(ItServiceRequest.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmAnnouncements(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('announcements')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<Announcement>(Announcement.fromMap(row));
|
||||
}
|
||||
// Also pull comments for the windowed announcements so detail screens have
|
||||
// them offline.
|
||||
try {
|
||||
final commentRows = await client
|
||||
.from('announcement_comments')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in commentRows) {
|
||||
await _localUpsert<AnnouncementComment>(
|
||||
AnnouncementComment.fromMap(row),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CacheWarmer] announcement_comments failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmAttendance(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('attendance_logs')
|
||||
.select()
|
||||
.gte('check_in_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<AttendanceLog>(AttendanceLog.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmPassSlips(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('pass_slips')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<PassSlip>(PassSlip.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmLeaves(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('leave_of_absence')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<LeaveOfAbsence>(LeaveOfAbsence.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmDutySchedules(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
// Duty schedules are upcoming-focused; pull a wider window so the user
|
||||
// can see today + the next several weeks while offline.
|
||||
final rows = await client
|
||||
.from('duty_schedules')
|
||||
.select()
|
||||
.gte('start_time', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<DutySchedule>(DutySchedule.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmSwapRequests(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('swap_requests')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<SwapRequest>(SwapRequest.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _warmNotifications(
|
||||
SupabaseClient client,
|
||||
String sinceIso,
|
||||
) async {
|
||||
final rows = await client
|
||||
.from('notifications')
|
||||
.select()
|
||||
.gte('created_at', sinceIso);
|
||||
for (final row in rows) {
|
||||
await _localUpsert<NotificationItem>(NotificationItem.fromMap(row));
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// Writes [model] to local SQLite only, bypassing Brick's bidirectional
|
||||
/// sync. AppRepository.upsert would re-POST rows to Supabase, causing RLS
|
||||
/// violations for other users' records and deadlocks from concurrent writes.
|
||||
static Future<void> _localUpsert<T extends OfflineFirstWithSupabaseModel>(
|
||||
T model,
|
||||
) async {
|
||||
await AppRepository.instance.sqliteProvider
|
||||
.upsert<T>(model, repository: AppRepository.instance);
|
||||
}
|
||||
|
||||
static Future<void> _safe(String label, Future<void> Function() task) async {
|
||||
try {
|
||||
await task();
|
||||
} catch (e, st) {
|
||||
debugPrint('[CacheWarmer] $label failed: $e');
|
||||
assert(() {
|
||||
debugPrint('$st');
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
// GENERATED CODE EDIT WITH CAUTION
|
||||
// THIS FILE **WILL NOT** BE REGENERATED
|
||||
// This file should be version controlled and can be manually edited.
|
||||
part of 'schema.g.dart';
|
||||
|
||||
// While migrations are intelligently created, the difference between some commands, such as
|
||||
// DropTable vs. RenameTable, cannot be determined. For this reason, please review migrations after
|
||||
// they are created to ensure the correct inference was made.
|
||||
|
||||
// The migration version must **always** mirror the file name
|
||||
|
||||
const List<MigrationCommand> _migration_20260411041808_up = [
|
||||
InsertTable('Announcement'),
|
||||
InsertTable('AnnouncementComment'),
|
||||
InsertTable('AttendanceLog'),
|
||||
InsertTable('ChatMessage'),
|
||||
InsertTable('DutySchedule'),
|
||||
InsertTable('ItServiceRequest'),
|
||||
InsertTable('ItServiceRequestAction'),
|
||||
InsertTable('ItServiceRequestActivityLog'),
|
||||
InsertTable('ItServiceRequestAssignment'),
|
||||
InsertTable('LeaveOfAbsence'),
|
||||
InsertTable('NotificationItem'),
|
||||
InsertTable('Office'),
|
||||
InsertTable('PassSlip'),
|
||||
InsertTable('Profile'),
|
||||
InsertTable('Service'),
|
||||
InsertTable('SwapRequest'),
|
||||
InsertTable('Task'),
|
||||
InsertTable('TaskActivityLog'),
|
||||
InsertTable('Team'),
|
||||
InsertTable('Ticket'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'Announcement'),
|
||||
InsertColumn('author_id', Column.varchar, onTable: 'Announcement'),
|
||||
InsertColumn('title', Column.varchar, onTable: 'Announcement'),
|
||||
InsertColumn('body', Column.varchar, onTable: 'Announcement'),
|
||||
InsertColumn('visible_roles', Column.varchar, onTable: 'Announcement'),
|
||||
InsertColumn('is_template', Column.boolean, onTable: 'Announcement'),
|
||||
InsertColumn('template_id', Column.varchar, onTable: 'Announcement'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'Announcement'),
|
||||
InsertColumn('updated_at', Column.datetime, onTable: 'Announcement'),
|
||||
InsertColumn('banner_enabled', Column.boolean, onTable: 'Announcement'),
|
||||
InsertColumn('banner_show_at', Column.datetime, onTable: 'Announcement'),
|
||||
InsertColumn('banner_hide_at', Column.datetime, onTable: 'Announcement'),
|
||||
InsertColumn('push_interval_minutes', Column.integer, onTable: 'Announcement'),
|
||||
InsertColumn('is_banner_active', Column.boolean, onTable: 'Announcement'),
|
||||
InsertColumn('hash_code', Column.integer, onTable: 'Announcement'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'AnnouncementComment'),
|
||||
InsertColumn('announcement_id', Column.varchar, onTable: 'AnnouncementComment'),
|
||||
InsertColumn('author_id', Column.varchar, onTable: 'AnnouncementComment'),
|
||||
InsertColumn('body', Column.varchar, onTable: 'AnnouncementComment'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'AnnouncementComment'),
|
||||
InsertColumn('hash_code', Column.integer, onTable: 'AnnouncementComment'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('user_id', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('duty_schedule_id', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('shift_type', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_in_at', Column.datetime, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_in_lat', Column.Double, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_in_lng', Column.Double, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_out_at', Column.datetime, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_out_lat', Column.Double, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_out_lng', Column.Double, onTable: 'AttendanceLog'),
|
||||
InsertColumn('justification', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_out_justification', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('verification_status', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_in_verification_photo_url', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('check_out_verification_photo_url', Column.varchar, onTable: 'AttendanceLog'),
|
||||
InsertColumn('is_checked_out', Column.boolean, onTable: 'AttendanceLog'),
|
||||
InsertColumn('is_verified', Column.boolean, onTable: 'AttendanceLog'),
|
||||
InsertColumn('is_unverified', Column.boolean, onTable: 'AttendanceLog'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'ChatMessage'),
|
||||
InsertColumn('thread_id', Column.varchar, onTable: 'ChatMessage'),
|
||||
InsertColumn('sender_id', Column.varchar, onTable: 'ChatMessage'),
|
||||
InsertColumn('body', Column.varchar, onTable: 'ChatMessage'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'ChatMessage'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'DutySchedule'),
|
||||
InsertColumn('user_id', Column.varchar, onTable: 'DutySchedule'),
|
||||
InsertColumn('shift_type', Column.varchar, onTable: 'DutySchedule'),
|
||||
InsertColumn('start_time', Column.datetime, onTable: 'DutySchedule'),
|
||||
InsertColumn('end_time', Column.datetime, onTable: 'DutySchedule'),
|
||||
InsertColumn('status', Column.varchar, onTable: 'DutySchedule'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'DutySchedule'),
|
||||
InsertColumn('check_in_at', Column.datetime, onTable: 'DutySchedule'),
|
||||
InsertColumn('reliever_ids', Column.varchar, onTable: 'DutySchedule'),
|
||||
InsertColumn('swap_request_id', Column.varchar, onTable: 'DutySchedule'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('request_number', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('services', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('services_other', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('event_name', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('event_details', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('event_date', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('event_end_date', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('dry_run_date', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('dry_run_end_date', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('contact_person', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('contact_number', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('remarks', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('office_id', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('requested_by', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('requested_by_user_id', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('approved_by', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('approved_by_user_id', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('approved_at', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('status', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('outside_premise_allowed', Column.boolean, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('cancellation_reason', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('cancelled_at', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('creator_id', Column.varchar, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('updated_at', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('completed_at', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('date_time_received', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('date_time_checked', Column.datetime, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('hash_code', Column.integer, onTable: 'ItServiceRequest'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'ItServiceRequestAction'),
|
||||
InsertColumn('request_id', Column.varchar, onTable: 'ItServiceRequestAction'),
|
||||
InsertColumn('user_id', Column.varchar, onTable: 'ItServiceRequestAction'),
|
||||
InsertColumn('action_taken', Column.varchar, onTable: 'ItServiceRequestAction'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'ItServiceRequestAction'),
|
||||
InsertColumn('updated_at', Column.datetime, onTable: 'ItServiceRequestAction'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'ItServiceRequestActivityLog'),
|
||||
InsertColumn('request_id', Column.varchar, onTable: 'ItServiceRequestActivityLog'),
|
||||
InsertColumn('actor_id', Column.varchar, onTable: 'ItServiceRequestActivityLog'),
|
||||
InsertColumn('action_type', Column.varchar, onTable: 'ItServiceRequestActivityLog'),
|
||||
InsertColumn('meta', Column.varchar, onTable: 'ItServiceRequestActivityLog'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'ItServiceRequestActivityLog'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'ItServiceRequestAssignment'),
|
||||
InsertColumn('request_id', Column.varchar, onTable: 'ItServiceRequestAssignment'),
|
||||
InsertColumn('user_id', Column.varchar, onTable: 'ItServiceRequestAssignment'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'ItServiceRequestAssignment'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('user_id', Column.varchar, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('leave_type', Column.varchar, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('justification', Column.varchar, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('start_time', Column.datetime, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('end_time', Column.datetime, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('status', Column.varchar, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('filed_by', Column.varchar, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('leave_type_label', Column.varchar, onTable: 'LeaveOfAbsence'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('user_id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('actor_id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('ticket_id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('task_id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('leave_id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('pass_slip_id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('it_service_request_id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('announcement_id', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('message_id', Column.integer, onTable: 'NotificationItem'),
|
||||
InsertColumn('type', Column.varchar, onTable: 'NotificationItem'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'NotificationItem'),
|
||||
InsertColumn('read_at', Column.datetime, onTable: 'NotificationItem'),
|
||||
InsertColumn('is_unread', Column.boolean, onTable: 'NotificationItem'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'Office'),
|
||||
InsertColumn('name', Column.varchar, onTable: 'Office'),
|
||||
InsertColumn('service_id', Column.varchar, onTable: 'Office'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'PassSlip'),
|
||||
InsertColumn('user_id', Column.varchar, onTable: 'PassSlip'),
|
||||
InsertColumn('duty_schedule_id', Column.varchar, onTable: 'PassSlip'),
|
||||
InsertColumn('reason', Column.varchar, onTable: 'PassSlip'),
|
||||
InsertColumn('status', Column.varchar, onTable: 'PassSlip'),
|
||||
InsertColumn('requested_at', Column.datetime, onTable: 'PassSlip'),
|
||||
InsertColumn('approved_by', Column.varchar, onTable: 'PassSlip'),
|
||||
InsertColumn('approved_at', Column.datetime, onTable: 'PassSlip'),
|
||||
InsertColumn('slip_start', Column.datetime, onTable: 'PassSlip'),
|
||||
InsertColumn('slip_end', Column.datetime, onTable: 'PassSlip'),
|
||||
InsertColumn('requested_start', Column.datetime, onTable: 'PassSlip'),
|
||||
InsertColumn('is_active', Column.boolean, onTable: 'PassSlip'),
|
||||
InsertColumn('is_exceeded', Column.boolean, onTable: 'PassSlip'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'Profile'),
|
||||
InsertColumn('role', Column.varchar, onTable: 'Profile'),
|
||||
InsertColumn('full_name', Column.varchar, onTable: 'Profile'),
|
||||
InsertColumn('religion', Column.varchar, onTable: 'Profile'),
|
||||
InsertColumn('allow_tracking', Column.boolean, onTable: 'Profile'),
|
||||
InsertColumn('avatar_url', Column.varchar, onTable: 'Profile'),
|
||||
InsertColumn('face_photo_url', Column.varchar, onTable: 'Profile'),
|
||||
InsertColumn('face_enrolled_at', Column.datetime, onTable: 'Profile'),
|
||||
InsertColumn('has_face_enrolled', Column.boolean, onTable: 'Profile'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'Service'),
|
||||
InsertColumn('name', Column.varchar, onTable: 'Service'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('requester_id', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('recipient_id', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('requester_schedule_id', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('target_schedule_id', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('status', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'SwapRequest'),
|
||||
InsertColumn('updated_at', Column.datetime, onTable: 'SwapRequest'),
|
||||
InsertColumn('chat_thread_id', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('shift_type', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('shift_start_time', Column.datetime, onTable: 'SwapRequest'),
|
||||
InsertColumn('reliever_ids', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('approved_by', Column.varchar, onTable: 'SwapRequest'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('ticket_id', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('task_number', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('title', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('description', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('office_id', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('status', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('priority', Column.integer, onTable: 'Task'),
|
||||
InsertColumn('queue_order', Column.integer, onTable: 'Task'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'Task'),
|
||||
InsertColumn('creator_id', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('started_at', Column.datetime, onTable: 'Task'),
|
||||
InsertColumn('completed_at', Column.datetime, onTable: 'Task'),
|
||||
InsertColumn('requested_by', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('noted_by', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('received_by', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('request_type', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('request_type_other', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('request_category', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('action_taken', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('cancellation_reason', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('cancelled_at', Column.datetime, onTable: 'Task'),
|
||||
InsertColumn('it_job_printed', Column.boolean, onTable: 'Task'),
|
||||
InsertColumn('it_job_printed_at', Column.datetime, onTable: 'Task'),
|
||||
InsertColumn('it_job_received_by_id', Column.varchar, onTable: 'Task'),
|
||||
InsertColumn('hash_code', Column.integer, onTable: 'Task'),
|
||||
InsertColumn('has_incomplete_details', Column.boolean, onTable: 'Task'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'TaskActivityLog'),
|
||||
InsertColumn('task_id', Column.varchar, onTable: 'TaskActivityLog'),
|
||||
InsertColumn('actor_id', Column.varchar, onTable: 'TaskActivityLog'),
|
||||
InsertColumn('action_type', Column.varchar, onTable: 'TaskActivityLog'),
|
||||
InsertColumn('meta', Column.varchar, onTable: 'TaskActivityLog'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'TaskActivityLog'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'Team'),
|
||||
InsertColumn('name', Column.varchar, onTable: 'Team'),
|
||||
InsertColumn('leader_id', Column.varchar, onTable: 'Team'),
|
||||
InsertColumn('office_ids', Column.varchar, onTable: 'Team'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'Team'),
|
||||
InsertColumn('color', Column.varchar, onTable: 'Team'),
|
||||
InsertColumn('id', Column.varchar, onTable: 'Ticket'),
|
||||
InsertColumn('subject', Column.varchar, onTable: 'Ticket'),
|
||||
InsertColumn('description', Column.varchar, onTable: 'Ticket'),
|
||||
InsertColumn('office_id', Column.varchar, onTable: 'Ticket'),
|
||||
InsertColumn('status', Column.varchar, onTable: 'Ticket'),
|
||||
InsertColumn('created_at', Column.datetime, onTable: 'Ticket'),
|
||||
InsertColumn('creator_id', Column.varchar, onTable: 'Ticket'),
|
||||
InsertColumn('responded_at', Column.datetime, onTable: 'Ticket'),
|
||||
InsertColumn('promoted_at', Column.datetime, onTable: 'Ticket'),
|
||||
InsertColumn('closed_at', Column.datetime, onTable: 'Ticket'),
|
||||
InsertColumn('hash_code', Column.integer, onTable: 'Ticket')
|
||||
];
|
||||
|
||||
const List<MigrationCommand> _migration_20260411041808_down = [
|
||||
DropTable('Announcement'),
|
||||
DropTable('AnnouncementComment'),
|
||||
DropTable('AttendanceLog'),
|
||||
DropTable('ChatMessage'),
|
||||
DropTable('DutySchedule'),
|
||||
DropTable('ItServiceRequest'),
|
||||
DropTable('ItServiceRequestAction'),
|
||||
DropTable('ItServiceRequestActivityLog'),
|
||||
DropTable('ItServiceRequestAssignment'),
|
||||
DropTable('LeaveOfAbsence'),
|
||||
DropTable('NotificationItem'),
|
||||
DropTable('Office'),
|
||||
DropTable('PassSlip'),
|
||||
DropTable('Profile'),
|
||||
DropTable('Service'),
|
||||
DropTable('SwapRequest'),
|
||||
DropTable('Task'),
|
||||
DropTable('TaskActivityLog'),
|
||||
DropTable('Team'),
|
||||
DropTable('Ticket'),
|
||||
DropColumn('id', onTable: 'Announcement'),
|
||||
DropColumn('author_id', onTable: 'Announcement'),
|
||||
DropColumn('title', onTable: 'Announcement'),
|
||||
DropColumn('body', onTable: 'Announcement'),
|
||||
DropColumn('visible_roles', onTable: 'Announcement'),
|
||||
DropColumn('is_template', onTable: 'Announcement'),
|
||||
DropColumn('template_id', onTable: 'Announcement'),
|
||||
DropColumn('created_at', onTable: 'Announcement'),
|
||||
DropColumn('updated_at', onTable: 'Announcement'),
|
||||
DropColumn('banner_enabled', onTable: 'Announcement'),
|
||||
DropColumn('banner_show_at', onTable: 'Announcement'),
|
||||
DropColumn('banner_hide_at', onTable: 'Announcement'),
|
||||
DropColumn('push_interval_minutes', onTable: 'Announcement'),
|
||||
DropColumn('is_banner_active', onTable: 'Announcement'),
|
||||
DropColumn('hash_code', onTable: 'Announcement'),
|
||||
DropColumn('id', onTable: 'AnnouncementComment'),
|
||||
DropColumn('announcement_id', onTable: 'AnnouncementComment'),
|
||||
DropColumn('author_id', onTable: 'AnnouncementComment'),
|
||||
DropColumn('body', onTable: 'AnnouncementComment'),
|
||||
DropColumn('created_at', onTable: 'AnnouncementComment'),
|
||||
DropColumn('hash_code', onTable: 'AnnouncementComment'),
|
||||
DropColumn('id', onTable: 'AttendanceLog'),
|
||||
DropColumn('user_id', onTable: 'AttendanceLog'),
|
||||
DropColumn('duty_schedule_id', onTable: 'AttendanceLog'),
|
||||
DropColumn('shift_type', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_in_at', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_in_lat', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_in_lng', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_out_at', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_out_lat', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_out_lng', onTable: 'AttendanceLog'),
|
||||
DropColumn('justification', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_out_justification', onTable: 'AttendanceLog'),
|
||||
DropColumn('verification_status', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_in_verification_photo_url', onTable: 'AttendanceLog'),
|
||||
DropColumn('check_out_verification_photo_url', onTable: 'AttendanceLog'),
|
||||
DropColumn('is_checked_out', onTable: 'AttendanceLog'),
|
||||
DropColumn('is_verified', onTable: 'AttendanceLog'),
|
||||
DropColumn('is_unverified', onTable: 'AttendanceLog'),
|
||||
DropColumn('id', onTable: 'ChatMessage'),
|
||||
DropColumn('thread_id', onTable: 'ChatMessage'),
|
||||
DropColumn('sender_id', onTable: 'ChatMessage'),
|
||||
DropColumn('body', onTable: 'ChatMessage'),
|
||||
DropColumn('created_at', onTable: 'ChatMessage'),
|
||||
DropColumn('id', onTable: 'DutySchedule'),
|
||||
DropColumn('user_id', onTable: 'DutySchedule'),
|
||||
DropColumn('shift_type', onTable: 'DutySchedule'),
|
||||
DropColumn('start_time', onTable: 'DutySchedule'),
|
||||
DropColumn('end_time', onTable: 'DutySchedule'),
|
||||
DropColumn('status', onTable: 'DutySchedule'),
|
||||
DropColumn('created_at', onTable: 'DutySchedule'),
|
||||
DropColumn('check_in_at', onTable: 'DutySchedule'),
|
||||
DropColumn('reliever_ids', onTable: 'DutySchedule'),
|
||||
DropColumn('swap_request_id', onTable: 'DutySchedule'),
|
||||
DropColumn('id', onTable: 'ItServiceRequest'),
|
||||
DropColumn('request_number', onTable: 'ItServiceRequest'),
|
||||
DropColumn('services', onTable: 'ItServiceRequest'),
|
||||
DropColumn('services_other', onTable: 'ItServiceRequest'),
|
||||
DropColumn('event_name', onTable: 'ItServiceRequest'),
|
||||
DropColumn('event_details', onTable: 'ItServiceRequest'),
|
||||
DropColumn('event_date', onTable: 'ItServiceRequest'),
|
||||
DropColumn('event_end_date', onTable: 'ItServiceRequest'),
|
||||
DropColumn('dry_run_date', onTable: 'ItServiceRequest'),
|
||||
DropColumn('dry_run_end_date', onTable: 'ItServiceRequest'),
|
||||
DropColumn('contact_person', onTable: 'ItServiceRequest'),
|
||||
DropColumn('contact_number', onTable: 'ItServiceRequest'),
|
||||
DropColumn('remarks', onTable: 'ItServiceRequest'),
|
||||
DropColumn('office_id', onTable: 'ItServiceRequest'),
|
||||
DropColumn('requested_by', onTable: 'ItServiceRequest'),
|
||||
DropColumn('requested_by_user_id', onTable: 'ItServiceRequest'),
|
||||
DropColumn('approved_by', onTable: 'ItServiceRequest'),
|
||||
DropColumn('approved_by_user_id', onTable: 'ItServiceRequest'),
|
||||
DropColumn('approved_at', onTable: 'ItServiceRequest'),
|
||||
DropColumn('status', onTable: 'ItServiceRequest'),
|
||||
DropColumn('outside_premise_allowed', onTable: 'ItServiceRequest'),
|
||||
DropColumn('cancellation_reason', onTable: 'ItServiceRequest'),
|
||||
DropColumn('cancelled_at', onTable: 'ItServiceRequest'),
|
||||
DropColumn('creator_id', onTable: 'ItServiceRequest'),
|
||||
DropColumn('created_at', onTable: 'ItServiceRequest'),
|
||||
DropColumn('updated_at', onTable: 'ItServiceRequest'),
|
||||
DropColumn('completed_at', onTable: 'ItServiceRequest'),
|
||||
DropColumn('date_time_received', onTable: 'ItServiceRequest'),
|
||||
DropColumn('date_time_checked', onTable: 'ItServiceRequest'),
|
||||
DropColumn('hash_code', onTable: 'ItServiceRequest'),
|
||||
DropColumn('id', onTable: 'ItServiceRequestAction'),
|
||||
DropColumn('request_id', onTable: 'ItServiceRequestAction'),
|
||||
DropColumn('user_id', onTable: 'ItServiceRequestAction'),
|
||||
DropColumn('action_taken', onTable: 'ItServiceRequestAction'),
|
||||
DropColumn('created_at', onTable: 'ItServiceRequestAction'),
|
||||
DropColumn('updated_at', onTable: 'ItServiceRequestAction'),
|
||||
DropColumn('id', onTable: 'ItServiceRequestActivityLog'),
|
||||
DropColumn('request_id', onTable: 'ItServiceRequestActivityLog'),
|
||||
DropColumn('actor_id', onTable: 'ItServiceRequestActivityLog'),
|
||||
DropColumn('action_type', onTable: 'ItServiceRequestActivityLog'),
|
||||
DropColumn('meta', onTable: 'ItServiceRequestActivityLog'),
|
||||
DropColumn('created_at', onTable: 'ItServiceRequestActivityLog'),
|
||||
DropColumn('id', onTable: 'ItServiceRequestAssignment'),
|
||||
DropColumn('request_id', onTable: 'ItServiceRequestAssignment'),
|
||||
DropColumn('user_id', onTable: 'ItServiceRequestAssignment'),
|
||||
DropColumn('created_at', onTable: 'ItServiceRequestAssignment'),
|
||||
DropColumn('id', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('user_id', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('leave_type', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('justification', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('start_time', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('end_time', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('status', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('filed_by', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('created_at', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('leave_type_label', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('id', onTable: 'NotificationItem'),
|
||||
DropColumn('user_id', onTable: 'NotificationItem'),
|
||||
DropColumn('actor_id', onTable: 'NotificationItem'),
|
||||
DropColumn('ticket_id', onTable: 'NotificationItem'),
|
||||
DropColumn('task_id', onTable: 'NotificationItem'),
|
||||
DropColumn('leave_id', onTable: 'NotificationItem'),
|
||||
DropColumn('pass_slip_id', onTable: 'NotificationItem'),
|
||||
DropColumn('it_service_request_id', onTable: 'NotificationItem'),
|
||||
DropColumn('announcement_id', onTable: 'NotificationItem'),
|
||||
DropColumn('message_id', onTable: 'NotificationItem'),
|
||||
DropColumn('type', onTable: 'NotificationItem'),
|
||||
DropColumn('created_at', onTable: 'NotificationItem'),
|
||||
DropColumn('read_at', onTable: 'NotificationItem'),
|
||||
DropColumn('is_unread', onTable: 'NotificationItem'),
|
||||
DropColumn('id', onTable: 'Office'),
|
||||
DropColumn('name', onTable: 'Office'),
|
||||
DropColumn('service_id', onTable: 'Office'),
|
||||
DropColumn('id', onTable: 'PassSlip'),
|
||||
DropColumn('user_id', onTable: 'PassSlip'),
|
||||
DropColumn('duty_schedule_id', onTable: 'PassSlip'),
|
||||
DropColumn('reason', onTable: 'PassSlip'),
|
||||
DropColumn('status', onTable: 'PassSlip'),
|
||||
DropColumn('requested_at', onTable: 'PassSlip'),
|
||||
DropColumn('approved_by', onTable: 'PassSlip'),
|
||||
DropColumn('approved_at', onTable: 'PassSlip'),
|
||||
DropColumn('slip_start', onTable: 'PassSlip'),
|
||||
DropColumn('slip_end', onTable: 'PassSlip'),
|
||||
DropColumn('requested_start', onTable: 'PassSlip'),
|
||||
DropColumn('is_active', onTable: 'PassSlip'),
|
||||
DropColumn('is_exceeded', onTable: 'PassSlip'),
|
||||
DropColumn('id', onTable: 'Profile'),
|
||||
DropColumn('role', onTable: 'Profile'),
|
||||
DropColumn('full_name', onTable: 'Profile'),
|
||||
DropColumn('religion', onTable: 'Profile'),
|
||||
DropColumn('allow_tracking', onTable: 'Profile'),
|
||||
DropColumn('avatar_url', onTable: 'Profile'),
|
||||
DropColumn('face_photo_url', onTable: 'Profile'),
|
||||
DropColumn('face_enrolled_at', onTable: 'Profile'),
|
||||
DropColumn('has_face_enrolled', onTable: 'Profile'),
|
||||
DropColumn('id', onTable: 'Service'),
|
||||
DropColumn('name', onTable: 'Service'),
|
||||
DropColumn('id', onTable: 'SwapRequest'),
|
||||
DropColumn('requester_id', onTable: 'SwapRequest'),
|
||||
DropColumn('recipient_id', onTable: 'SwapRequest'),
|
||||
DropColumn('requester_schedule_id', onTable: 'SwapRequest'),
|
||||
DropColumn('target_schedule_id', onTable: 'SwapRequest'),
|
||||
DropColumn('status', onTable: 'SwapRequest'),
|
||||
DropColumn('created_at', onTable: 'SwapRequest'),
|
||||
DropColumn('updated_at', onTable: 'SwapRequest'),
|
||||
DropColumn('chat_thread_id', onTable: 'SwapRequest'),
|
||||
DropColumn('shift_type', onTable: 'SwapRequest'),
|
||||
DropColumn('shift_start_time', onTable: 'SwapRequest'),
|
||||
DropColumn('reliever_ids', onTable: 'SwapRequest'),
|
||||
DropColumn('approved_by', onTable: 'SwapRequest'),
|
||||
DropColumn('id', onTable: 'Task'),
|
||||
DropColumn('ticket_id', onTable: 'Task'),
|
||||
DropColumn('task_number', onTable: 'Task'),
|
||||
DropColumn('title', onTable: 'Task'),
|
||||
DropColumn('description', onTable: 'Task'),
|
||||
DropColumn('office_id', onTable: 'Task'),
|
||||
DropColumn('status', onTable: 'Task'),
|
||||
DropColumn('priority', onTable: 'Task'),
|
||||
DropColumn('queue_order', onTable: 'Task'),
|
||||
DropColumn('created_at', onTable: 'Task'),
|
||||
DropColumn('creator_id', onTable: 'Task'),
|
||||
DropColumn('started_at', onTable: 'Task'),
|
||||
DropColumn('completed_at', onTable: 'Task'),
|
||||
DropColumn('requested_by', onTable: 'Task'),
|
||||
DropColumn('noted_by', onTable: 'Task'),
|
||||
DropColumn('received_by', onTable: 'Task'),
|
||||
DropColumn('request_type', onTable: 'Task'),
|
||||
DropColumn('request_type_other', onTable: 'Task'),
|
||||
DropColumn('request_category', onTable: 'Task'),
|
||||
DropColumn('action_taken', onTable: 'Task'),
|
||||
DropColumn('cancellation_reason', onTable: 'Task'),
|
||||
DropColumn('cancelled_at', onTable: 'Task'),
|
||||
DropColumn('it_job_printed', onTable: 'Task'),
|
||||
DropColumn('it_job_printed_at', onTable: 'Task'),
|
||||
DropColumn('it_job_received_by_id', onTable: 'Task'),
|
||||
DropColumn('hash_code', onTable: 'Task'),
|
||||
DropColumn('has_incomplete_details', onTable: 'Task'),
|
||||
DropColumn('id', onTable: 'TaskActivityLog'),
|
||||
DropColumn('task_id', onTable: 'TaskActivityLog'),
|
||||
DropColumn('actor_id', onTable: 'TaskActivityLog'),
|
||||
DropColumn('action_type', onTable: 'TaskActivityLog'),
|
||||
DropColumn('meta', onTable: 'TaskActivityLog'),
|
||||
DropColumn('created_at', onTable: 'TaskActivityLog'),
|
||||
DropColumn('id', onTable: 'Team'),
|
||||
DropColumn('name', onTable: 'Team'),
|
||||
DropColumn('leader_id', onTable: 'Team'),
|
||||
DropColumn('office_ids', onTable: 'Team'),
|
||||
DropColumn('created_at', onTable: 'Team'),
|
||||
DropColumn('color', onTable: 'Team'),
|
||||
DropColumn('id', onTable: 'Ticket'),
|
||||
DropColumn('subject', onTable: 'Ticket'),
|
||||
DropColumn('description', onTable: 'Ticket'),
|
||||
DropColumn('office_id', onTable: 'Ticket'),
|
||||
DropColumn('status', onTable: 'Ticket'),
|
||||
DropColumn('created_at', onTable: 'Ticket'),
|
||||
DropColumn('creator_id', onTable: 'Ticket'),
|
||||
DropColumn('responded_at', onTable: 'Ticket'),
|
||||
DropColumn('promoted_at', onTable: 'Ticket'),
|
||||
DropColumn('closed_at', onTable: 'Ticket'),
|
||||
DropColumn('hash_code', onTable: 'Ticket')
|
||||
];
|
||||
|
||||
//
|
||||
// DO NOT EDIT BELOW THIS LINE
|
||||
//
|
||||
|
||||
@Migratable(
|
||||
version: '20260411041808',
|
||||
up: _migration_20260411041808_up,
|
||||
down: _migration_20260411041808_down,
|
||||
)
|
||||
class Migration20260411041808 extends Migration {
|
||||
const Migration20260411041808()
|
||||
: super(
|
||||
version: 20260411041808,
|
||||
up: _migration_20260411041808_up,
|
||||
down: _migration_20260411041808_down,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// GENERATED CODE EDIT WITH CAUTION
|
||||
// THIS FILE **WILL NOT** BE REGENERATED
|
||||
// This file should be version controlled and can be manually edited.
|
||||
part of 'schema.g.dart';
|
||||
|
||||
// While migrations are intelligently created, the difference between some commands, such as
|
||||
// DropTable vs. RenameTable, cannot be determined. For this reason, please review migrations after
|
||||
// they are created to ensure the correct inference was made.
|
||||
|
||||
// The migration version must **always** mirror the file name
|
||||
|
||||
const List<MigrationCommand> _migration_20260411042152_up = [
|
||||
DropColumn('is_banner_active', onTable: 'Announcement'),
|
||||
DropColumn('hash_code', onTable: 'Announcement'),
|
||||
DropColumn('hash_code', onTable: 'AnnouncementComment'),
|
||||
DropColumn('is_checked_out', onTable: 'AttendanceLog'),
|
||||
DropColumn('is_verified', onTable: 'AttendanceLog'),
|
||||
DropColumn('is_unverified', onTable: 'AttendanceLog'),
|
||||
DropColumn('hash_code', onTable: 'ItServiceRequest'),
|
||||
DropColumn('leave_type_label', onTable: 'LeaveOfAbsence'),
|
||||
DropColumn('is_unread', onTable: 'NotificationItem'),
|
||||
DropColumn('is_active', onTable: 'PassSlip'),
|
||||
DropColumn('is_exceeded', onTable: 'PassSlip'),
|
||||
DropColumn('has_face_enrolled', onTable: 'Profile'),
|
||||
DropColumn('hash_code', onTable: 'Task'),
|
||||
DropColumn('has_incomplete_details', onTable: 'Task'),
|
||||
DropColumn('hash_code', onTable: 'Ticket')
|
||||
];
|
||||
|
||||
const List<MigrationCommand> _migration_20260411042152_down = [
|
||||
|
||||
];
|
||||
|
||||
//
|
||||
// DO NOT EDIT BELOW THIS LINE
|
||||
//
|
||||
|
||||
@Migratable(
|
||||
version: '20260411042152',
|
||||
up: _migration_20260411042152_up,
|
||||
down: _migration_20260411042152_down,
|
||||
)
|
||||
class Migration20260411042152 extends Migration {
|
||||
const Migration20260411042152()
|
||||
: super(
|
||||
version: 20260411042152,
|
||||
up: _migration_20260411042152_up,
|
||||
down: _migration_20260411042152_down,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
// GENERATED CODE DO NOT EDIT
|
||||
// This file should be version controlled
|
||||
import 'package:brick_sqlite/db.dart';
|
||||
part '20260411041808.migration.dart';
|
||||
part '20260411042152.migration.dart';
|
||||
|
||||
/// All intelligently-generated migrations from all `@Migratable` classes on disk
|
||||
final migrations = <Migration>{
|
||||
const Migration20260411041808(),
|
||||
const Migration20260411042152(),
|
||||
};
|
||||
|
||||
/// A consumable database structure including the latest generated migration.
|
||||
final schema = Schema(
|
||||
20260411042152,
|
||||
generatorVersion: 1,
|
||||
tables: <SchemaTable>{
|
||||
SchemaTable(
|
||||
'Announcement',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('author_id', Column.varchar),
|
||||
SchemaColumn('title', Column.varchar),
|
||||
SchemaColumn('body', Column.varchar),
|
||||
SchemaColumn('visible_roles', Column.varchar),
|
||||
SchemaColumn('is_template', Column.boolean),
|
||||
SchemaColumn('template_id', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('updated_at', Column.datetime),
|
||||
SchemaColumn('banner_enabled', Column.boolean),
|
||||
SchemaColumn('banner_show_at', Column.datetime),
|
||||
SchemaColumn('banner_hide_at', Column.datetime),
|
||||
SchemaColumn('push_interval_minutes', Column.integer),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'AnnouncementComment',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('announcement_id', Column.varchar),
|
||||
SchemaColumn('author_id', Column.varchar),
|
||||
SchemaColumn('body', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'AttendanceLog',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('user_id', Column.varchar),
|
||||
SchemaColumn('duty_schedule_id', Column.varchar),
|
||||
SchemaColumn('shift_type', Column.varchar),
|
||||
SchemaColumn('check_in_at', Column.datetime),
|
||||
SchemaColumn('check_in_lat', Column.Double),
|
||||
SchemaColumn('check_in_lng', Column.Double),
|
||||
SchemaColumn('check_out_at', Column.datetime),
|
||||
SchemaColumn('check_out_lat', Column.Double),
|
||||
SchemaColumn('check_out_lng', Column.Double),
|
||||
SchemaColumn('justification', Column.varchar),
|
||||
SchemaColumn('check_out_justification', Column.varchar),
|
||||
SchemaColumn('verification_status', Column.varchar),
|
||||
SchemaColumn('check_in_verification_photo_url', Column.varchar),
|
||||
SchemaColumn('check_out_verification_photo_url', Column.varchar),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'ChatMessage',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('thread_id', Column.varchar),
|
||||
SchemaColumn('sender_id', Column.varchar),
|
||||
SchemaColumn('body', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'DutySchedule',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('user_id', Column.varchar),
|
||||
SchemaColumn('shift_type', Column.varchar),
|
||||
SchemaColumn('start_time', Column.datetime),
|
||||
SchemaColumn('end_time', Column.datetime),
|
||||
SchemaColumn('status', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('check_in_at', Column.datetime),
|
||||
SchemaColumn('reliever_ids', Column.varchar),
|
||||
SchemaColumn('swap_request_id', Column.varchar),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'ItServiceRequest',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('request_number', Column.varchar),
|
||||
SchemaColumn('services', Column.varchar),
|
||||
SchemaColumn('services_other', Column.varchar),
|
||||
SchemaColumn('event_name', Column.varchar),
|
||||
SchemaColumn('event_details', Column.varchar),
|
||||
SchemaColumn('event_date', Column.datetime),
|
||||
SchemaColumn('event_end_date', Column.datetime),
|
||||
SchemaColumn('dry_run_date', Column.datetime),
|
||||
SchemaColumn('dry_run_end_date', Column.datetime),
|
||||
SchemaColumn('contact_person', Column.varchar),
|
||||
SchemaColumn('contact_number', Column.varchar),
|
||||
SchemaColumn('remarks', Column.varchar),
|
||||
SchemaColumn('office_id', Column.varchar),
|
||||
SchemaColumn('requested_by', Column.varchar),
|
||||
SchemaColumn('requested_by_user_id', Column.varchar),
|
||||
SchemaColumn('approved_by', Column.varchar),
|
||||
SchemaColumn('approved_by_user_id', Column.varchar),
|
||||
SchemaColumn('approved_at', Column.datetime),
|
||||
SchemaColumn('status', Column.varchar),
|
||||
SchemaColumn('outside_premise_allowed', Column.boolean),
|
||||
SchemaColumn('cancellation_reason', Column.varchar),
|
||||
SchemaColumn('cancelled_at', Column.datetime),
|
||||
SchemaColumn('creator_id', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('updated_at', Column.datetime),
|
||||
SchemaColumn('completed_at', Column.datetime),
|
||||
SchemaColumn('date_time_received', Column.datetime),
|
||||
SchemaColumn('date_time_checked', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'ItServiceRequestAction',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('request_id', Column.varchar),
|
||||
SchemaColumn('user_id', Column.varchar),
|
||||
SchemaColumn('action_taken', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('updated_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'ItServiceRequestActivityLog',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('request_id', Column.varchar),
|
||||
SchemaColumn('actor_id', Column.varchar),
|
||||
SchemaColumn('action_type', Column.varchar),
|
||||
SchemaColumn('meta', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'ItServiceRequestAssignment',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('request_id', Column.varchar),
|
||||
SchemaColumn('user_id', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'LeaveOfAbsence',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('user_id', Column.varchar),
|
||||
SchemaColumn('leave_type', Column.varchar),
|
||||
SchemaColumn('justification', Column.varchar),
|
||||
SchemaColumn('start_time', Column.datetime),
|
||||
SchemaColumn('end_time', Column.datetime),
|
||||
SchemaColumn('status', Column.varchar),
|
||||
SchemaColumn('filed_by', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'NotificationItem',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('user_id', Column.varchar),
|
||||
SchemaColumn('actor_id', Column.varchar),
|
||||
SchemaColumn('ticket_id', Column.varchar),
|
||||
SchemaColumn('task_id', Column.varchar),
|
||||
SchemaColumn('leave_id', Column.varchar),
|
||||
SchemaColumn('pass_slip_id', Column.varchar),
|
||||
SchemaColumn('it_service_request_id', Column.varchar),
|
||||
SchemaColumn('announcement_id', Column.varchar),
|
||||
SchemaColumn('message_id', Column.integer),
|
||||
SchemaColumn('type', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('read_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'Office',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('name', Column.varchar),
|
||||
SchemaColumn('service_id', Column.varchar),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'PassSlip',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('user_id', Column.varchar),
|
||||
SchemaColumn('duty_schedule_id', Column.varchar),
|
||||
SchemaColumn('reason', Column.varchar),
|
||||
SchemaColumn('status', Column.varchar),
|
||||
SchemaColumn('requested_at', Column.datetime),
|
||||
SchemaColumn('approved_by', Column.varchar),
|
||||
SchemaColumn('approved_at', Column.datetime),
|
||||
SchemaColumn('slip_start', Column.datetime),
|
||||
SchemaColumn('slip_end', Column.datetime),
|
||||
SchemaColumn('requested_start', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'Profile',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('role', Column.varchar),
|
||||
SchemaColumn('full_name', Column.varchar),
|
||||
SchemaColumn('religion', Column.varchar),
|
||||
SchemaColumn('allow_tracking', Column.boolean),
|
||||
SchemaColumn('avatar_url', Column.varchar),
|
||||
SchemaColumn('face_photo_url', Column.varchar),
|
||||
SchemaColumn('face_enrolled_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'Service',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('name', Column.varchar),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'SwapRequest',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('requester_id', Column.varchar),
|
||||
SchemaColumn('recipient_id', Column.varchar),
|
||||
SchemaColumn('requester_schedule_id', Column.varchar),
|
||||
SchemaColumn('target_schedule_id', Column.varchar),
|
||||
SchemaColumn('status', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('updated_at', Column.datetime),
|
||||
SchemaColumn('chat_thread_id', Column.varchar),
|
||||
SchemaColumn('shift_type', Column.varchar),
|
||||
SchemaColumn('shift_start_time', Column.datetime),
|
||||
SchemaColumn('reliever_ids', Column.varchar),
|
||||
SchemaColumn('approved_by', Column.varchar),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'Task',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('ticket_id', Column.varchar),
|
||||
SchemaColumn('task_number', Column.varchar),
|
||||
SchemaColumn('title', Column.varchar),
|
||||
SchemaColumn('description', Column.varchar),
|
||||
SchemaColumn('office_id', Column.varchar),
|
||||
SchemaColumn('status', Column.varchar),
|
||||
SchemaColumn('priority', Column.integer),
|
||||
SchemaColumn('queue_order', Column.integer),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('creator_id', Column.varchar),
|
||||
SchemaColumn('started_at', Column.datetime),
|
||||
SchemaColumn('completed_at', Column.datetime),
|
||||
SchemaColumn('requested_by', Column.varchar),
|
||||
SchemaColumn('noted_by', Column.varchar),
|
||||
SchemaColumn('received_by', Column.varchar),
|
||||
SchemaColumn('request_type', Column.varchar),
|
||||
SchemaColumn('request_type_other', Column.varchar),
|
||||
SchemaColumn('request_category', Column.varchar),
|
||||
SchemaColumn('action_taken', Column.varchar),
|
||||
SchemaColumn('cancellation_reason', Column.varchar),
|
||||
SchemaColumn('cancelled_at', Column.datetime),
|
||||
SchemaColumn('it_job_printed', Column.boolean),
|
||||
SchemaColumn('it_job_printed_at', Column.datetime),
|
||||
SchemaColumn('it_job_received_by_id', Column.varchar),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'TaskActivityLog',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('task_id', Column.varchar),
|
||||
SchemaColumn('actor_id', Column.varchar),
|
||||
SchemaColumn('action_type', Column.varchar),
|
||||
SchemaColumn('meta', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'Team',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('name', Column.varchar),
|
||||
SchemaColumn('leader_id', Column.varchar),
|
||||
SchemaColumn('office_ids', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('color', Column.varchar),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
SchemaTable(
|
||||
'Ticket',
|
||||
columns: <SchemaColumn>{
|
||||
SchemaColumn(
|
||||
'_brick_id',
|
||||
Column.integer,
|
||||
autoincrement: true,
|
||||
nullable: false,
|
||||
isPrimaryKey: true,
|
||||
),
|
||||
SchemaColumn('id', Column.varchar),
|
||||
SchemaColumn('subject', Column.varchar),
|
||||
SchemaColumn('description', Column.varchar),
|
||||
SchemaColumn('office_id', Column.varchar),
|
||||
SchemaColumn('status', Column.varchar),
|
||||
SchemaColumn('created_at', Column.datetime),
|
||||
SchemaColumn('creator_id', Column.varchar),
|
||||
SchemaColumn('responded_at', Column.datetime),
|
||||
SchemaColumn('promoted_at', Column.datetime),
|
||||
SchemaColumn('closed_at', Column.datetime),
|
||||
},
|
||||
indices: <SchemaIndex>{},
|
||||
),
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_offline_first_with_rest/offline_queue.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_sqlite/memory_cache_provider.dart';
|
||||
// Hide the brick_supabase @Supabase annotation to avoid conflict with
|
||||
// supabase_flutter's Supabase singleton class.
|
||||
import 'package:brick_supabase/brick_supabase.dart' hide Supabase;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb, debugPrint;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:sqflite/sqflite.dart' show databaseFactory;
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
// Generated by `flutter pub run build_runner build`.
|
||||
// Run: flutter pub run build_runner build --delete-conflicting-outputs
|
||||
// ignore: uri_does_not_exist
|
||||
import 'brick.g.dart';
|
||||
// migrations + schema are generated into db/schema.g.dart
|
||||
// ignore: uri_does_not_exist
|
||||
import 'db/schema.g.dart';
|
||||
|
||||
export 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'
|
||||
show OfflineFirstWithSupabaseModel;
|
||||
export 'package:brick_supabase/brick_supabase.dart'
|
||||
show SupabaseSerializable;
|
||||
export 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart'
|
||||
show ConnectOfflineFirstWithSupabase;
|
||||
export 'package:brick_sqlite/brick_sqlite.dart' show SqliteSerializable;
|
||||
export 'package:brick_offline_first/brick_offline_first.dart'
|
||||
show OfflineFirstGetPolicy;
|
||||
|
||||
/// Singleton offline-first repository backed by Supabase + local SQLite.
|
||||
///
|
||||
/// Initialization sequence in [main]:
|
||||
/// ```dart
|
||||
/// // 1. Get the Brick HTTP client (mobile only; null on web)
|
||||
/// final httpClient = AppRepository.createHttpClient();
|
||||
///
|
||||
/// // 2. Initialize Supabase WITH the Brick HTTP client so all REST
|
||||
/// // writes are intercepted and queued when offline.
|
||||
/// await Supabase.initialize(url: ..., anonKey: ..., httpClient: httpClient);
|
||||
///
|
||||
/// // 3. Configure the repository (reads model dictionary from brick.g.dart)
|
||||
/// await AppRepository.configure();
|
||||
/// ```
|
||||
class AppRepository extends OfflineFirstWithSupabaseRepository {
|
||||
static AppRepository? _instance;
|
||||
static RestOfflineRequestQueue? _pendingQueue;
|
||||
|
||||
/// The singleton. Throws if [configure] was not called first.
|
||||
static AppRepository get instance {
|
||||
assert(_instance != null, 'Call AppRepository.configure() first');
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// True when this platform supports offline SQLite storage (i.e. not web).
|
||||
static bool get isOfflineSupported => !kIsWeb;
|
||||
|
||||
/// True after [configure] has completed successfully.
|
||||
static bool get isConfigured => _instance != null;
|
||||
|
||||
/// The Brick HTTP offline queue. `null` before [configure] or on web.
|
||||
///
|
||||
/// Call [RestOfflineRequestQueue.stop] when offline to silence retry logs
|
||||
/// and avoid hammering a network that is known to be unavailable.
|
||||
/// Call [RestOfflineRequestQueue.start] when back online to replay queued
|
||||
/// writes. The stored requests are preserved in both states.
|
||||
static RestOfflineRequestQueue? get offlineQueue =>
|
||||
_instance?.offlineRequestQueue;
|
||||
|
||||
AppRepository._({
|
||||
required super.supabaseProvider,
|
||||
required super.sqliteProvider,
|
||||
required super.migrations,
|
||||
required super.offlineRequestQueue,
|
||||
super.memoryCacheProvider,
|
||||
});
|
||||
|
||||
/// Creates and returns the Brick-managed HTTP client to pass to
|
||||
/// [Supabase.initialize]. Returns `null` on web (SQLite unavailable).
|
||||
///
|
||||
/// Must be called *before* [Supabase.initialize] and *before* [configure].
|
||||
static http.Client? createHttpClient() {
|
||||
if (kIsWeb) return null;
|
||||
final (client, queue) = OfflineFirstWithSupabaseRepository.clientQueue(
|
||||
databaseFactory: databaseFactory,
|
||||
// Only retry on truly transient server errors.
|
||||
// 400/409 indicate a permanent failure (bad payload or conflict) and
|
||||
// should NOT be retried — they will never self-heal and cause infinite
|
||||
// retry loops that spam the logs.
|
||||
reattemptForStatusCodes: const [408, 429, 500, 502, 503, 504],
|
||||
);
|
||||
_pendingQueue = queue;
|
||||
return client;
|
||||
}
|
||||
|
||||
/// Configures the repository singleton.
|
||||
///
|
||||
/// Must be called *after* [Supabase.initialize]. Reads the Brick-generated
|
||||
/// model dictionaries from `brick.g.dart` (created by build_runner).
|
||||
static Future<void> configure() async {
|
||||
if (_instance != null) return;
|
||||
if (kIsWeb) {
|
||||
debugPrint('[AppRepository] web — offline queue disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
final queue = _pendingQueue;
|
||||
if (queue == null) {
|
||||
throw StateError(
|
||||
'Call AppRepository.createHttpClient() before configure()',
|
||||
);
|
||||
}
|
||||
|
||||
_instance = AppRepository._(
|
||||
supabaseProvider: SupabaseProvider(
|
||||
Supabase.instance.client,
|
||||
modelDictionary: supabaseModelDictionary,
|
||||
),
|
||||
sqliteProvider: SqliteProvider(
|
||||
'tasq_offline.sqlite',
|
||||
databaseFactory: databaseFactory,
|
||||
modelDictionary: sqliteModelDictionary,
|
||||
),
|
||||
migrations: migrations,
|
||||
offlineRequestQueue: queue,
|
||||
memoryCacheProvider: MemoryCacheProvider(),
|
||||
);
|
||||
|
||||
await _instance!.initialize();
|
||||
_pendingQueue = null;
|
||||
debugPrint('[AppRepository] initialized — offline queue active');
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors a Supabase realtime stream into the Brick local SQLite cache.
|
||||
///
|
||||
/// Wrap any `client.from('t').stream(...)` source so every emission is
|
||||
/// upserted into Brick. The cache stays fresh during normal online use, which
|
||||
/// means a cold launch with no network can read a recent snapshot via
|
||||
/// `OfflineFirstGetPolicy.localOnly` instead of rendering an empty list.
|
||||
///
|
||||
/// Cache writes are fire-and-forget — they never block UI updates and any
|
||||
/// upsert error is silently logged (mirroring is best-effort).
|
||||
extension AppRepositoryStreamMirror on AppRepository {
|
||||
Stream<List<T>> mirroredStream<T extends OfflineFirstWithSupabaseModel>(
|
||||
Stream<List<T>> source,
|
||||
) async* {
|
||||
await for (final batch in source) {
|
||||
for (final item in batch) {
|
||||
// Fire-and-forget; never block the UI on cache writes.
|
||||
// ignore: discarded_futures
|
||||
upsert<T>(item).catchError((Object e) {
|
||||
debugPrint('[AppRepository] mirror upsert<$T> failed: $e');
|
||||
return item;
|
||||
});
|
||||
}
|
||||
yield batch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'firebase_options.dart';
|
||||
// removed unused imports
|
||||
import 'app.dart';
|
||||
import 'brick/cache_warmer.dart';
|
||||
import 'brick/repository.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'providers/notifications_provider.dart';
|
||||
import 'providers/notification_navigation_provider.dart';
|
||||
@@ -276,10 +278,16 @@ Future<void> main() async {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Brick's HTTP client BEFORE Supabase.initialize so that all
|
||||
// Supabase REST writes are intercepted and queued when offline.
|
||||
// Returns null on web (SQLite unavailable there).
|
||||
final brickHttpClient = AppRepository.createHttpClient();
|
||||
|
||||
try {
|
||||
await Supabase.initialize(
|
||||
url: supabaseUrl,
|
||||
anonKey: supabaseAnonKey,
|
||||
httpClient: brickHttpClient,
|
||||
).timeout(const Duration(seconds: 20));
|
||||
} catch (e) {
|
||||
debugPrint('Supabase init failed or timed out: $e');
|
||||
@@ -287,6 +295,16 @@ Future<void> main() async {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure the Brick repository after Supabase is ready.
|
||||
try {
|
||||
await AppRepository.configure();
|
||||
} catch (e, st) {
|
||||
debugPrint('AppRepository configure FAILED: $e\n$st');
|
||||
}
|
||||
debugPrint(
|
||||
'[Boot] AppRepository.isConfigured=${AppRepository.isConfigured}',
|
||||
);
|
||||
|
||||
// Initialize background location service (flutter_background_service)
|
||||
if (!kIsWeb) {
|
||||
try {
|
||||
@@ -302,6 +320,17 @@ Future<void> main() async {
|
||||
// Run this after runApp so startup is not blocked by network/token ops.
|
||||
final supaClient = Supabase.instance.client;
|
||||
|
||||
// Cold-launch with a persisted session: AuthChangeEvent.signedIn does NOT
|
||||
// fire on session rehydration (only initialSession/tokenRefreshed do), so
|
||||
// the auth-change listener below misses cold-starts. Warm the Brick mirror
|
||||
// here in the background so cached lists stay fresh between sessions.
|
||||
// CacheWarmer.warmAll is no-op-safe offline (REST calls just fail).
|
||||
if (supaClient.auth.currentUser != null && AppRepository.isConfigured) {
|
||||
debugPrint('[Boot] Persisted session detected — warming Brick cache');
|
||||
CacheWarmer.warmAll(supaClient);
|
||||
CacheWarmer.startPeriodicRefresh(supaClient);
|
||||
}
|
||||
|
||||
// listen for auth changes to register/unregister token accordingly
|
||||
supaClient.auth.onAuthStateChange.listen((data) async {
|
||||
final event = data.event;
|
||||
@@ -347,6 +376,16 @@ Future<void> main() async {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger a cache warm immediately when the user signs in so the local
|
||||
// Brick SQLite mirror is populated for the next cold-launch. Fire-and-
|
||||
// forget — never block the auth flow on the warmer.
|
||||
if (event == AuthChangeEvent.signedIn) {
|
||||
CacheWarmer.warmAll(supaClient);
|
||||
CacheWarmer.startPeriodicRefresh(supaClient);
|
||||
} else if (event == AuthChangeEvent.signedOut) {
|
||||
CacheWarmer.stopPeriodicRefresh();
|
||||
}
|
||||
|
||||
String? token;
|
||||
try {
|
||||
token = await FirebaseMessaging.instance.getToken();
|
||||
|
||||
@@ -1,111 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class Announcement {
|
||||
Announcement({
|
||||
required this.id,
|
||||
required this.authorId,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.visibleRoles,
|
||||
required this.isTemplate,
|
||||
this.templateId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.bannerEnabled,
|
||||
this.bannerShowAt,
|
||||
this.bannerHideAt,
|
||||
this.pushIntervalMinutes,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String authorId;
|
||||
final String title;
|
||||
final String body;
|
||||
final List<String> visibleRoles;
|
||||
final bool isTemplate;
|
||||
final String? templateId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Whether a persistent banner is shown at the top of the announcements screen.
|
||||
final bool bannerEnabled;
|
||||
|
||||
/// When the banner should start showing. [null] means immediately.
|
||||
final DateTime? bannerShowAt;
|
||||
|
||||
/// When the banner should stop showing. [null] means it requires a manual
|
||||
/// turn-off by the poster or an admin.
|
||||
final DateTime? bannerHideAt;
|
||||
|
||||
/// How often (in minutes) a scheduled push notification is sent while the
|
||||
/// banner is active. [null] means no scheduled push. Max is 1440 (daily).
|
||||
final int? pushIntervalMinutes;
|
||||
|
||||
/// Whether the banner is currently active (visible) based on the current time.
|
||||
bool get isBannerActive {
|
||||
if (!bannerEnabled) return false;
|
||||
final now = AppTime.now();
|
||||
if (bannerShowAt != null && now.isBefore(bannerShowAt!)) return false;
|
||||
if (bannerHideAt != null && now.isAfter(bannerHideAt!)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Announcement &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
authorId == other.authorId &&
|
||||
title == other.title &&
|
||||
body == other.body &&
|
||||
isTemplate == other.isTemplate &&
|
||||
templateId == other.templateId &&
|
||||
bannerEnabled == other.bannerEnabled &&
|
||||
bannerShowAt == other.bannerShowAt &&
|
||||
bannerHideAt == other.bannerHideAt &&
|
||||
pushIntervalMinutes == other.pushIntervalMinutes &&
|
||||
createdAt == other.createdAt &&
|
||||
updatedAt == other.updatedAt;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
authorId,
|
||||
title,
|
||||
body,
|
||||
isTemplate,
|
||||
templateId,
|
||||
bannerEnabled,
|
||||
bannerShowAt,
|
||||
bannerHideAt,
|
||||
pushIntervalMinutes,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
|
||||
factory Announcement.fromMap(Map<String, dynamic> map) {
|
||||
final rolesRaw = map['visible_roles'];
|
||||
final roles = rolesRaw is List ? rolesRaw.cast<String>() : <String>[];
|
||||
|
||||
return Announcement(
|
||||
id: map['id'] as String,
|
||||
authorId: map['author_id'] as String,
|
||||
title: map['title'] as String? ?? '',
|
||||
body: map['body'] as String? ?? '',
|
||||
visibleRoles: roles,
|
||||
isTemplate: map['is_template'] as bool? ?? false,
|
||||
templateId: map['template_id'] as String?,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
updatedAt: AppTime.parse(map['updated_at'] as String),
|
||||
bannerEnabled: map['banner_enabled'] as bool? ?? false,
|
||||
bannerShowAt: map['banner_show_at'] != null
|
||||
? AppTime.parse(map['banner_show_at'] as String)
|
||||
: null,
|
||||
bannerHideAt: map['banner_hide_at'] != null
|
||||
? AppTime.parse(map['banner_hide_at'] as String)
|
||||
: null,
|
||||
pushIntervalMinutes: map['push_interval_minutes'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'announcement.model.dart';
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'announcements'),
|
||||
)
|
||||
class Announcement extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String authorId;
|
||||
final String title;
|
||||
final String body;
|
||||
final List<String> visibleRoles;
|
||||
final bool isTemplate;
|
||||
final String? templateId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final bool bannerEnabled;
|
||||
final DateTime? bannerShowAt;
|
||||
final DateTime? bannerHideAt;
|
||||
final int? pushIntervalMinutes;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get isBannerActive {
|
||||
if (!bannerEnabled) return false;
|
||||
final now = AppTime.now();
|
||||
if (bannerShowAt != null && now.isBefore(bannerShowAt!)) return false;
|
||||
if (bannerHideAt != null && now.isAfter(bannerHideAt!)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Announcement({
|
||||
required this.id,
|
||||
required this.authorId,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.visibleRoles,
|
||||
required this.isTemplate,
|
||||
this.templateId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.bannerEnabled,
|
||||
this.bannerShowAt,
|
||||
this.bannerHideAt,
|
||||
this.pushIntervalMinutes,
|
||||
});
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Announcement &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id && authorId == other.authorId &&
|
||||
title == other.title && body == other.body &&
|
||||
isTemplate == other.isTemplate && templateId == other.templateId &&
|
||||
bannerEnabled == other.bannerEnabled &&
|
||||
bannerShowAt == other.bannerShowAt &&
|
||||
bannerHideAt == other.bannerHideAt &&
|
||||
pushIntervalMinutes == other.pushIntervalMinutes &&
|
||||
createdAt == other.createdAt && updatedAt == other.updatedAt;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(id, authorId, title, body, isTemplate,
|
||||
templateId, bannerEnabled, bannerShowAt, bannerHideAt,
|
||||
pushIntervalMinutes, createdAt, updatedAt);
|
||||
|
||||
factory Announcement.fromMap(Map<String, dynamic> map) {
|
||||
final rolesRaw = map['visible_roles'];
|
||||
final roles = rolesRaw is List ? rolesRaw.cast<String>() : <String>[];
|
||||
return Announcement(
|
||||
id: map['id'] as String,
|
||||
authorId: map['author_id'] as String,
|
||||
title: map['title'] as String? ?? '',
|
||||
body: map['body'] as String? ?? '',
|
||||
visibleRoles: roles,
|
||||
isTemplate: map['is_template'] as bool? ?? false,
|
||||
templateId: map['template_id'] as String?,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
updatedAt: AppTime.parse(map['updated_at'] as String),
|
||||
bannerEnabled: map['banner_enabled'] as bool? ?? false,
|
||||
bannerShowAt: map['banner_show_at'] != null
|
||||
? AppTime.parse(map['banner_show_at'] as String) : null,
|
||||
bannerHideAt: map['banner_hide_at'] != null
|
||||
? AppTime.parse(map['banner_hide_at'] as String) : null,
|
||||
pushIntervalMinutes: map['push_interval_minutes'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class AnnouncementComment {
|
||||
AnnouncementComment({
|
||||
required this.id,
|
||||
required this.announcementId,
|
||||
required this.authorId,
|
||||
required this.body,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String announcementId;
|
||||
final String authorId;
|
||||
final String body;
|
||||
final DateTime createdAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AnnouncementComment &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
announcementId == other.announcementId &&
|
||||
authorId == other.authorId &&
|
||||
body == other.body &&
|
||||
createdAt == other.createdAt;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
announcementId,
|
||||
authorId,
|
||||
body,
|
||||
createdAt,
|
||||
);
|
||||
|
||||
factory AnnouncementComment.fromMap(Map<String, dynamic> map) {
|
||||
return AnnouncementComment(
|
||||
id: map['id'] as String,
|
||||
announcementId: map['announcement_id'] as String,
|
||||
authorId: map['author_id'] as String,
|
||||
body: map['body'] as String? ?? '',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'announcement_comment.model.dart';
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'announcement_comments'),
|
||||
)
|
||||
class AnnouncementComment extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String announcementId;
|
||||
final String authorId;
|
||||
final String body;
|
||||
final DateTime createdAt;
|
||||
|
||||
AnnouncementComment({
|
||||
required this.id,
|
||||
required this.announcementId,
|
||||
required this.authorId,
|
||||
required this.body,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AnnouncementComment &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id && announcementId == other.announcementId &&
|
||||
authorId == other.authorId && body == other.body &&
|
||||
createdAt == other.createdAt;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(id, announcementId, authorId, body, createdAt);
|
||||
|
||||
factory AnnouncementComment.fromMap(Map<String, dynamic> map) {
|
||||
return AnnouncementComment(
|
||||
id: map['id'] as String,
|
||||
announcementId: map['announcement_id'] as String,
|
||||
authorId: map['author_id'] as String,
|
||||
body: map['body'] as String? ?? '',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class AttendanceLog {
|
||||
AttendanceLog({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.dutyScheduleId,
|
||||
required this.shiftType,
|
||||
required this.checkInAt,
|
||||
required this.checkInLat,
|
||||
required this.checkInLng,
|
||||
this.checkOutAt,
|
||||
this.checkOutLat,
|
||||
this.checkOutLng,
|
||||
this.justification,
|
||||
this.checkOutJustification,
|
||||
this.verificationStatus = 'pending',
|
||||
this.checkInVerificationPhotoUrl,
|
||||
this.checkOutVerificationPhotoUrl,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String dutyScheduleId;
|
||||
final String shiftType;
|
||||
final DateTime checkInAt;
|
||||
final double checkInLat;
|
||||
final double checkInLng;
|
||||
final DateTime? checkOutAt;
|
||||
final double? checkOutLat;
|
||||
final double? checkOutLng;
|
||||
final String? justification;
|
||||
final String? checkOutJustification;
|
||||
final String verificationStatus; // pending, verified, unverified, skipped
|
||||
final String? checkInVerificationPhotoUrl;
|
||||
final String? checkOutVerificationPhotoUrl;
|
||||
|
||||
bool get isCheckedOut => checkOutAt != null;
|
||||
bool get isVerified => verificationStatus == 'verified';
|
||||
bool get isUnverified =>
|
||||
verificationStatus == 'unverified' || verificationStatus == 'skipped';
|
||||
|
||||
factory AttendanceLog.fromMap(Map<String, dynamic> map) {
|
||||
// Support both old single-column and new dual-column schemas.
|
||||
final legacyUrl = map['verification_photo_url'] as String?;
|
||||
return AttendanceLog(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
dutyScheduleId: map['duty_schedule_id'] as String,
|
||||
shiftType: map['shift_type'] as String? ?? 'normal',
|
||||
checkInAt: AppTime.parse(map['check_in_at'] as String),
|
||||
checkInLat: (map['check_in_lat'] as num).toDouble(),
|
||||
checkInLng: (map['check_in_lng'] as num).toDouble(),
|
||||
checkOutAt: map['check_out_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['check_out_at'] as String),
|
||||
checkOutLat: (map['check_out_lat'] as num?)?.toDouble(),
|
||||
checkOutLng: (map['check_out_lng'] as num?)?.toDouble(),
|
||||
justification: map['justification'] as String?,
|
||||
checkOutJustification: map['check_out_justification'] as String?,
|
||||
verificationStatus: map['verification_status'] as String? ?? 'pending',
|
||||
checkInVerificationPhotoUrl:
|
||||
map['check_in_verification_photo_url'] as String? ?? legacyUrl,
|
||||
checkOutVerificationPhotoUrl:
|
||||
map['check_out_verification_photo_url'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'attendance_log.model.dart';
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'attendance_logs'),
|
||||
)
|
||||
class AttendanceLog extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String dutyScheduleId;
|
||||
final String shiftType;
|
||||
final DateTime checkInAt;
|
||||
final double checkInLat;
|
||||
final double checkInLng;
|
||||
final DateTime? checkOutAt;
|
||||
final double? checkOutLat;
|
||||
final double? checkOutLng;
|
||||
final String? justification;
|
||||
final String? checkOutJustification;
|
||||
final String verificationStatus;
|
||||
final String? checkInVerificationPhotoUrl;
|
||||
final String? checkOutVerificationPhotoUrl;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get isCheckedOut => checkOutAt != null;
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get isVerified => verificationStatus == 'verified';
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get isUnverified =>
|
||||
verificationStatus == 'unverified' || verificationStatus == 'skipped';
|
||||
|
||||
AttendanceLog({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.dutyScheduleId,
|
||||
required this.shiftType,
|
||||
required this.checkInAt,
|
||||
required this.checkInLat,
|
||||
required this.checkInLng,
|
||||
this.checkOutAt,
|
||||
this.checkOutLat,
|
||||
this.checkOutLng,
|
||||
this.justification,
|
||||
this.checkOutJustification,
|
||||
this.verificationStatus = 'pending',
|
||||
this.checkInVerificationPhotoUrl,
|
||||
this.checkOutVerificationPhotoUrl,
|
||||
});
|
||||
|
||||
factory AttendanceLog.fromMap(Map<String, dynamic> map) {
|
||||
final legacyUrl = map['verification_photo_url'] as String?;
|
||||
return AttendanceLog(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
dutyScheduleId: map['duty_schedule_id'] as String,
|
||||
shiftType: map['shift_type'] as String? ?? 'normal',
|
||||
checkInAt: AppTime.parse(map['check_in_at'] as String),
|
||||
checkInLat: (map['check_in_lat'] as num).toDouble(),
|
||||
checkInLng: (map['check_in_lng'] as num).toDouble(),
|
||||
checkOutAt: map['check_out_at'] == null
|
||||
? null : AppTime.parse(map['check_out_at'] as String),
|
||||
checkOutLat: (map['check_out_lat'] as num?)?.toDouble(),
|
||||
checkOutLng: (map['check_out_lng'] as num?)?.toDouble(),
|
||||
justification: map['justification'] as String?,
|
||||
checkOutJustification: map['check_out_justification'] as String?,
|
||||
verificationStatus: map['verification_status'] as String? ?? 'pending',
|
||||
checkInVerificationPhotoUrl:
|
||||
map['check_in_verification_photo_url'] as String? ?? legacyUrl,
|
||||
checkOutVerificationPhotoUrl:
|
||||
map['check_out_verification_photo_url'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class ChatMessage {
|
||||
ChatMessage({
|
||||
required this.id,
|
||||
required this.threadId,
|
||||
required this.senderId,
|
||||
required this.body,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String threadId;
|
||||
final String senderId;
|
||||
final String body;
|
||||
final DateTime createdAt;
|
||||
|
||||
factory ChatMessage.fromMap(Map<String, dynamic> map) {
|
||||
return ChatMessage(
|
||||
id: map['id'] as String,
|
||||
threadId: map['thread_id'] as String,
|
||||
senderId: map['sender_id'] as String,
|
||||
body: map['body'] as String,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'thread_id': threadId,
|
||||
'sender_id': senderId,
|
||||
'body': body,
|
||||
};
|
||||
}
|
||||
export 'chat_message.model.dart';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'chat_messages'),
|
||||
)
|
||||
class ChatMessage extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String threadId;
|
||||
final String senderId;
|
||||
final String body;
|
||||
final DateTime createdAt;
|
||||
|
||||
ChatMessage({
|
||||
required this.id,
|
||||
required this.threadId,
|
||||
required this.senderId,
|
||||
required this.body,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ChatMessage.fromMap(Map<String, dynamic> map) {
|
||||
return ChatMessage(
|
||||
id: map['id'] as String,
|
||||
threadId: map['thread_id'] as String,
|
||||
senderId: map['sender_id'] as String,
|
||||
body: map['body'] as String,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'thread_id': threadId,
|
||||
'sender_id': senderId,
|
||||
'body': body,
|
||||
};
|
||||
}
|
||||
@@ -1,55 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class DutySchedule {
|
||||
DutySchedule({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.shiftType,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
required this.checkInAt,
|
||||
required this.checkInLocation,
|
||||
required this.relieverIds,
|
||||
this.swapRequestId,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String shiftType;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final String status;
|
||||
final DateTime createdAt;
|
||||
final DateTime? checkInAt;
|
||||
final Object? checkInLocation;
|
||||
final List<String> relieverIds;
|
||||
final String? swapRequestId;
|
||||
|
||||
factory DutySchedule.fromMap(Map<String, dynamic> map) {
|
||||
final relieversRaw = map['reliever_ids'];
|
||||
final relievers = relieversRaw is List
|
||||
? relieversRaw
|
||||
.where((e) => e != null)
|
||||
.map((entry) => entry.toString())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList()
|
||||
: <String>[];
|
||||
return DutySchedule(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
shiftType: map['shift_type'] as String? ?? 'normal',
|
||||
startTime: AppTime.parse(map['start_time'] as String),
|
||||
endTime: AppTime.parse(map['end_time'] as String),
|
||||
status: map['status'] as String? ?? 'scheduled',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
checkInAt: map['check_in_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['check_in_at'] as String),
|
||||
checkInLocation: map['check_in_location'],
|
||||
relieverIds: relievers,
|
||||
swapRequestId: map['swap_request_id'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'duty_schedule.model.dart';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'duty_schedules'),
|
||||
)
|
||||
class DutySchedule extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String shiftType;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final String status;
|
||||
final DateTime createdAt;
|
||||
final DateTime? checkInAt;
|
||||
|
||||
// PostGIS geometry — Brick cannot serialize Object; ignored in local cache.
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
final Object? checkInLocation;
|
||||
|
||||
final List<String> relieverIds;
|
||||
final String? swapRequestId;
|
||||
|
||||
DutySchedule({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.shiftType,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
this.checkInAt,
|
||||
this.checkInLocation,
|
||||
required this.relieverIds,
|
||||
this.swapRequestId,
|
||||
});
|
||||
|
||||
factory DutySchedule.fromMap(Map<String, dynamic> map) {
|
||||
final relieversRaw = map['reliever_ids'];
|
||||
final relievers = relieversRaw is List
|
||||
? relieversRaw
|
||||
.where((e) => e != null)
|
||||
.map((entry) => entry.toString())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList()
|
||||
: <String>[];
|
||||
return DutySchedule(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
shiftType: map['shift_type'] as String? ?? 'normal',
|
||||
startTime: AppTime.parse(map['start_time'] as String),
|
||||
endTime: AppTime.parse(map['end_time'] as String),
|
||||
status: map['status'] as String? ?? 'scheduled',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
checkInAt: map['check_in_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['check_in_at'] as String),
|
||||
checkInLocation: map['check_in_location'],
|
||||
relieverIds: relievers,
|
||||
swapRequestId: map['swap_request_id'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,259 +1 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
/// Available IT services from the form.
|
||||
class ItServiceType {
|
||||
static const fbLiveStream = 'fb_live_stream';
|
||||
static const videoRecording = 'video_recording';
|
||||
static const technicalAssistance = 'technical_assistance';
|
||||
static const wifi = 'wifi';
|
||||
static const others = 'others';
|
||||
|
||||
static const all = [
|
||||
fbLiveStream,
|
||||
videoRecording,
|
||||
technicalAssistance,
|
||||
wifi,
|
||||
others,
|
||||
];
|
||||
|
||||
static String label(String type) {
|
||||
switch (type) {
|
||||
case fbLiveStream:
|
||||
return 'FB Live Stream';
|
||||
case videoRecording:
|
||||
return 'Video Recording';
|
||||
case technicalAssistance:
|
||||
return 'Technical Assistance';
|
||||
case wifi:
|
||||
return 'WiFi';
|
||||
case others:
|
||||
return 'Others';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Status lifecycle for an IT Service Request.
|
||||
class ItServiceRequestStatus {
|
||||
static const draft = 'draft';
|
||||
static const pendingApproval = 'pending_approval';
|
||||
static const scheduled = 'scheduled';
|
||||
static const inProgressDryRun = 'in_progress_dry_run';
|
||||
static const inProgress = 'in_progress';
|
||||
static const completed = 'completed';
|
||||
static const cancelled = 'cancelled';
|
||||
|
||||
static const all = [
|
||||
draft,
|
||||
pendingApproval,
|
||||
scheduled,
|
||||
inProgressDryRun,
|
||||
inProgress,
|
||||
completed,
|
||||
cancelled,
|
||||
];
|
||||
|
||||
static String label(String status) {
|
||||
switch (status) {
|
||||
case draft:
|
||||
return 'Draft';
|
||||
case pendingApproval:
|
||||
return 'Pending Approval';
|
||||
case scheduled:
|
||||
return 'Scheduled';
|
||||
case inProgressDryRun:
|
||||
return 'In Progress (Dry Run)';
|
||||
case inProgress:
|
||||
return 'In Progress';
|
||||
case completed:
|
||||
return 'Completed';
|
||||
case cancelled:
|
||||
return 'Cancelled';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ItServiceRequest {
|
||||
ItServiceRequest({
|
||||
required this.id,
|
||||
this.requestNumber,
|
||||
required this.services,
|
||||
this.servicesOther,
|
||||
required this.eventName,
|
||||
this.eventDetails,
|
||||
this.eventDate,
|
||||
this.eventEndDate,
|
||||
this.dryRunDate,
|
||||
this.dryRunEndDate,
|
||||
this.contactPerson,
|
||||
this.contactNumber,
|
||||
this.remarks,
|
||||
this.officeId,
|
||||
this.requestedBy,
|
||||
this.requestedByUserId,
|
||||
this.approvedBy,
|
||||
this.approvedByUserId,
|
||||
this.approvedAt,
|
||||
required this.status,
|
||||
required this.outsidePremiseAllowed,
|
||||
this.cancellationReason,
|
||||
this.cancelledAt,
|
||||
this.creatorId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.completedAt,
|
||||
this.dateTimeReceived,
|
||||
this.dateTimeChecked,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String? requestNumber;
|
||||
final List<String> services;
|
||||
final String? servicesOther;
|
||||
final String eventName;
|
||||
final String? eventDetails; // Quill Delta JSON
|
||||
final DateTime? eventDate;
|
||||
final DateTime? eventEndDate;
|
||||
final DateTime? dryRunDate;
|
||||
final DateTime? dryRunEndDate;
|
||||
final String? contactPerson;
|
||||
final String? contactNumber;
|
||||
final String? remarks; // Quill Delta JSON
|
||||
final String? officeId;
|
||||
final String? requestedBy;
|
||||
final String? requestedByUserId;
|
||||
final String? approvedBy;
|
||||
final String? approvedByUserId;
|
||||
final DateTime? approvedAt;
|
||||
final String status;
|
||||
final bool outsidePremiseAllowed;
|
||||
final String? cancellationReason;
|
||||
final DateTime? cancelledAt;
|
||||
final String? creatorId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? completedAt;
|
||||
final DateTime? dateTimeReceived;
|
||||
final DateTime? dateTimeChecked;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ItServiceRequest &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
requestNumber == other.requestNumber &&
|
||||
status == other.status &&
|
||||
updatedAt == other.updatedAt;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, requestNumber, status, updatedAt);
|
||||
|
||||
/// Whether the request is on a day that would allow geofence override.
|
||||
bool isGeofenceOverrideActive(DateTime now) {
|
||||
if (!outsidePremiseAllowed) return false;
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
// Active on dry run date or event date
|
||||
if (dryRunDate != null) {
|
||||
final dryDay = DateTime(
|
||||
dryRunDate!.year,
|
||||
dryRunDate!.month,
|
||||
dryRunDate!.day,
|
||||
);
|
||||
if (!today.isBefore(dryDay) &&
|
||||
today.isBefore(dryDay.add(const Duration(days: 1)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (eventDate != null) {
|
||||
final eventDay = DateTime(
|
||||
eventDate!.year,
|
||||
eventDate!.month,
|
||||
eventDate!.day,
|
||||
);
|
||||
if (!today.isBefore(eventDay) &&
|
||||
today.isBefore(eventDay.add(const Duration(days: 1)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
factory ItServiceRequest.fromMap(Map<String, dynamic> map) {
|
||||
List<String> parseServices(dynamic raw) {
|
||||
if (raw is List) return raw.map((e) => e.toString()).toList();
|
||||
if (raw is String) {
|
||||
// Handle PostgreSQL array format: {a,b,c}
|
||||
final trimmed = raw.replaceAll('{', '').replaceAll('}', '');
|
||||
if (trimmed.isEmpty) return [];
|
||||
return trimmed.split(',').map((e) => e.trim()).toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
String? quillField(dynamic raw) {
|
||||
if (raw == null) return null;
|
||||
if (raw is String) return raw;
|
||||
try {
|
||||
return jsonEncode(raw);
|
||||
} catch (_) {
|
||||
return raw.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return ItServiceRequest(
|
||||
id: map['id'] as String,
|
||||
requestNumber: map['request_number'] as String?,
|
||||
services: parseServices(map['services']),
|
||||
servicesOther: map['services_other'] as String?,
|
||||
eventName: map['event_name'] as String? ?? '',
|
||||
eventDetails: quillField(map['event_details']),
|
||||
eventDate: map['event_date'] == null
|
||||
? null
|
||||
: AppTime.parse(map['event_date'] as String),
|
||||
eventEndDate: map['event_end_date'] == null
|
||||
? null
|
||||
: AppTime.parse(map['event_end_date'] as String),
|
||||
dryRunDate: map['dry_run_date'] == null
|
||||
? null
|
||||
: AppTime.parse(map['dry_run_date'] as String),
|
||||
dryRunEndDate: map['dry_run_end_date'] == null
|
||||
? null
|
||||
: AppTime.parse(map['dry_run_end_date'] as String),
|
||||
contactPerson: map['contact_person'] as String?,
|
||||
contactNumber: map['contact_number'] as String?,
|
||||
remarks: quillField(map['remarks']),
|
||||
officeId: map['office_id'] as String?,
|
||||
requestedBy: map['requested_by'] as String?,
|
||||
requestedByUserId: map['requested_by_user_id'] as String?,
|
||||
approvedBy: map['approved_by'] as String?,
|
||||
approvedByUserId: map['approved_by_user_id'] as String?,
|
||||
approvedAt: map['approved_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['approved_at'] as String),
|
||||
status: map['status'] as String? ?? 'draft',
|
||||
outsidePremiseAllowed: map['outside_premise_allowed'] as bool? ?? false,
|
||||
cancellationReason: map['cancellation_reason'] as String?,
|
||||
cancelledAt: map['cancelled_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['cancelled_at'] as String),
|
||||
creatorId: map['creator_id'] as String?,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
updatedAt: AppTime.parse(map['updated_at'] as String),
|
||||
completedAt: map['completed_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['completed_at'] as String),
|
||||
dateTimeReceived: map['date_time_received'] == null
|
||||
? null
|
||||
: AppTime.parse(map['date_time_received'] as String),
|
||||
dateTimeChecked: map['date_time_checked'] == null
|
||||
? null
|
||||
: AppTime.parse(map['date_time_checked'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'it_service_request.model.dart';
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
/// Available IT services from the form.
|
||||
class ItServiceType {
|
||||
static const fbLiveStream = 'fb_live_stream';
|
||||
static const videoRecording = 'video_recording';
|
||||
static const technicalAssistance = 'technical_assistance';
|
||||
static const wifi = 'wifi';
|
||||
static const others = 'others';
|
||||
|
||||
static const all = [
|
||||
fbLiveStream,
|
||||
videoRecording,
|
||||
technicalAssistance,
|
||||
wifi,
|
||||
others,
|
||||
];
|
||||
|
||||
static String label(String type) {
|
||||
switch (type) {
|
||||
case fbLiveStream:
|
||||
return 'FB Live Stream';
|
||||
case videoRecording:
|
||||
return 'Video Recording';
|
||||
case technicalAssistance:
|
||||
return 'Technical Assistance';
|
||||
case wifi:
|
||||
return 'WiFi';
|
||||
case others:
|
||||
return 'Others';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Status lifecycle for an IT Service Request.
|
||||
class ItServiceRequestStatus {
|
||||
static const draft = 'draft';
|
||||
static const pendingApproval = 'pending_approval';
|
||||
static const scheduled = 'scheduled';
|
||||
static const inProgressDryRun = 'in_progress_dry_run';
|
||||
static const inProgress = 'in_progress';
|
||||
static const completed = 'completed';
|
||||
static const cancelled = 'cancelled';
|
||||
|
||||
static const all = [
|
||||
draft,
|
||||
pendingApproval,
|
||||
scheduled,
|
||||
inProgressDryRun,
|
||||
inProgress,
|
||||
completed,
|
||||
cancelled,
|
||||
];
|
||||
|
||||
static String label(String status) {
|
||||
switch (status) {
|
||||
case draft:
|
||||
return 'Draft';
|
||||
case pendingApproval:
|
||||
return 'Pending Approval';
|
||||
case scheduled:
|
||||
return 'Scheduled';
|
||||
case inProgressDryRun:
|
||||
return 'In Progress (Dry Run)';
|
||||
case inProgress:
|
||||
return 'In Progress';
|
||||
case completed:
|
||||
return 'Completed';
|
||||
case cancelled:
|
||||
return 'Cancelled';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'it_service_requests'),
|
||||
)
|
||||
class ItServiceRequest extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String? requestNumber;
|
||||
final List<String> services;
|
||||
final String? servicesOther;
|
||||
final String eventName;
|
||||
// Quill Delta JSON stored as plain strings.
|
||||
final String? eventDetails;
|
||||
final DateTime? eventDate;
|
||||
final DateTime? eventEndDate;
|
||||
final DateTime? dryRunDate;
|
||||
final DateTime? dryRunEndDate;
|
||||
final String? contactPerson;
|
||||
final String? contactNumber;
|
||||
final String? remarks;
|
||||
final String? officeId;
|
||||
final String? requestedBy;
|
||||
final String? requestedByUserId;
|
||||
final String? approvedBy;
|
||||
final String? approvedByUserId;
|
||||
final DateTime? approvedAt;
|
||||
final String status;
|
||||
final bool outsidePremiseAllowed;
|
||||
final String? cancellationReason;
|
||||
final DateTime? cancelledAt;
|
||||
final String? creatorId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? completedAt;
|
||||
final DateTime? dateTimeReceived;
|
||||
final DateTime? dateTimeChecked;
|
||||
|
||||
ItServiceRequest({
|
||||
required this.id,
|
||||
this.requestNumber,
|
||||
required this.services,
|
||||
this.servicesOther,
|
||||
required this.eventName,
|
||||
this.eventDetails,
|
||||
this.eventDate,
|
||||
this.eventEndDate,
|
||||
this.dryRunDate,
|
||||
this.dryRunEndDate,
|
||||
this.contactPerson,
|
||||
this.contactNumber,
|
||||
this.remarks,
|
||||
this.officeId,
|
||||
this.requestedBy,
|
||||
this.requestedByUserId,
|
||||
this.approvedBy,
|
||||
this.approvedByUserId,
|
||||
this.approvedAt,
|
||||
required this.status,
|
||||
required this.outsidePremiseAllowed,
|
||||
this.cancellationReason,
|
||||
this.cancelledAt,
|
||||
this.creatorId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.completedAt,
|
||||
this.dateTimeReceived,
|
||||
this.dateTimeChecked,
|
||||
});
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ItServiceRequest &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
requestNumber == other.requestNumber &&
|
||||
status == other.status &&
|
||||
updatedAt == other.updatedAt;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(id, requestNumber, status, updatedAt);
|
||||
|
||||
bool isGeofenceOverrideActive(DateTime now) {
|
||||
if (!outsidePremiseAllowed) return false;
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
if (dryRunDate != null) {
|
||||
final dryDay = DateTime(
|
||||
dryRunDate!.year,
|
||||
dryRunDate!.month,
|
||||
dryRunDate!.day,
|
||||
);
|
||||
if (!today.isBefore(dryDay) &&
|
||||
today.isBefore(dryDay.add(const Duration(days: 1)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (eventDate != null) {
|
||||
final eventDay = DateTime(
|
||||
eventDate!.year,
|
||||
eventDate!.month,
|
||||
eventDate!.day,
|
||||
);
|
||||
if (!today.isBefore(eventDay) &&
|
||||
today.isBefore(eventDay.add(const Duration(days: 1)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
factory ItServiceRequest.fromMap(Map<String, dynamic> map) {
|
||||
List<String> parseServices(dynamic raw) {
|
||||
if (raw is List) return raw.map((e) => e.toString()).toList();
|
||||
if (raw is String) {
|
||||
final trimmed = raw.replaceAll('{', '').replaceAll('}', '');
|
||||
if (trimmed.isEmpty) return [];
|
||||
return trimmed.split(',').map((e) => e.trim()).toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
String? quillField(dynamic raw) {
|
||||
if (raw == null) return null;
|
||||
if (raw is String) return raw;
|
||||
try {
|
||||
return jsonEncode(raw);
|
||||
} catch (_) {
|
||||
return raw.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return ItServiceRequest(
|
||||
id: map['id'] as String,
|
||||
requestNumber: map['request_number'] as String?,
|
||||
services: parseServices(map['services']),
|
||||
servicesOther: map['services_other'] as String?,
|
||||
eventName: map['event_name'] as String? ?? '',
|
||||
eventDetails: quillField(map['event_details']),
|
||||
eventDate: map['event_date'] == null
|
||||
? null
|
||||
: AppTime.parse(map['event_date'] as String),
|
||||
eventEndDate: map['event_end_date'] == null
|
||||
? null
|
||||
: AppTime.parse(map['event_end_date'] as String),
|
||||
dryRunDate: map['dry_run_date'] == null
|
||||
? null
|
||||
: AppTime.parse(map['dry_run_date'] as String),
|
||||
dryRunEndDate: map['dry_run_end_date'] == null
|
||||
? null
|
||||
: AppTime.parse(map['dry_run_end_date'] as String),
|
||||
contactPerson: map['contact_person'] as String?,
|
||||
contactNumber: map['contact_number'] as String?,
|
||||
remarks: quillField(map['remarks']),
|
||||
officeId: map['office_id'] as String?,
|
||||
requestedBy: map['requested_by'] as String?,
|
||||
requestedByUserId: map['requested_by_user_id'] as String?,
|
||||
approvedBy: map['approved_by'] as String?,
|
||||
approvedByUserId: map['approved_by_user_id'] as String?,
|
||||
approvedAt: map['approved_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['approved_at'] as String),
|
||||
status: map['status'] as String? ?? 'draft',
|
||||
outsidePremiseAllowed: map['outside_premise_allowed'] as bool? ?? false,
|
||||
cancellationReason: map['cancellation_reason'] as String?,
|
||||
cancelledAt: map['cancelled_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['cancelled_at'] as String),
|
||||
creatorId: map['creator_id'] as String?,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
updatedAt: AppTime.parse(map['updated_at'] as String),
|
||||
completedAt: map['completed_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['completed_at'] as String),
|
||||
dateTimeReceived: map['date_time_received'] == null
|
||||
? null
|
||||
: AppTime.parse(map['date_time_received'] as String),
|
||||
dateTimeChecked: map['date_time_checked'] == null
|
||||
? null
|
||||
: AppTime.parse(map['date_time_checked'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class ItServiceRequestAction {
|
||||
ItServiceRequestAction({
|
||||
required this.id,
|
||||
required this.requestId,
|
||||
required this.userId,
|
||||
this.actionTaken,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String requestId;
|
||||
final String userId;
|
||||
final String? actionTaken; // Quill Delta JSON
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
factory ItServiceRequestAction.fromMap(Map<String, dynamic> map) {
|
||||
String? quillField(dynamic raw) {
|
||||
if (raw == null) return null;
|
||||
if (raw is String) return raw;
|
||||
try {
|
||||
return jsonEncode(raw);
|
||||
} catch (_) {
|
||||
return raw.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return ItServiceRequestAction(
|
||||
id: map['id'] as String,
|
||||
requestId: map['request_id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
actionTaken: quillField(map['action_taken']),
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
updatedAt: AppTime.parse(map['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'it_service_request_action.model.dart';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'it_service_request_actions'),
|
||||
)
|
||||
class ItServiceRequestAction extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String requestId;
|
||||
final String userId;
|
||||
// Quill Delta JSON stored as plain string.
|
||||
final String? actionTaken;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
ItServiceRequestAction({
|
||||
required this.id,
|
||||
required this.requestId,
|
||||
required this.userId,
|
||||
this.actionTaken,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory ItServiceRequestAction.fromMap(Map<String, dynamic> map) {
|
||||
String? quillField(dynamic raw) {
|
||||
if (raw == null) return null;
|
||||
if (raw is String) return raw;
|
||||
try {
|
||||
return jsonEncode(raw);
|
||||
} catch (_) {
|
||||
return raw.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return ItServiceRequestAction(
|
||||
id: map['id'] as String,
|
||||
requestId: map['request_id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
actionTaken: quillField(map['action_taken']),
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
updatedAt: AppTime.parse(map['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class ItServiceRequestActivityLog {
|
||||
ItServiceRequestActivityLog({
|
||||
required this.id,
|
||||
required this.requestId,
|
||||
this.actorId,
|
||||
required this.actionType,
|
||||
this.meta,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String requestId;
|
||||
final String? actorId;
|
||||
final String actionType;
|
||||
final Map<String, dynamic>? meta;
|
||||
final DateTime createdAt;
|
||||
|
||||
factory ItServiceRequestActivityLog.fromMap(Map<String, dynamic> map) {
|
||||
final rawId = map['id'];
|
||||
final rawRequestId = map['request_id'];
|
||||
String id = rawId == null ? '' : rawId.toString();
|
||||
String requestId = rawRequestId == null ? '' : rawRequestId.toString();
|
||||
final actorId = map['actor_id']?.toString();
|
||||
final actionType = (map['action_type'] as String?) ?? 'unknown';
|
||||
|
||||
Map<String, dynamic>? meta;
|
||||
final rawMeta = map['meta'];
|
||||
if (rawMeta is Map<String, dynamic>) {
|
||||
meta = rawMeta;
|
||||
} else if (rawMeta is Map) {
|
||||
try {
|
||||
meta = rawMeta.map((k, v) => MapEntry(k.toString(), v));
|
||||
} catch (_) {
|
||||
meta = null;
|
||||
}
|
||||
} else if (rawMeta is String && rawMeta.isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(rawMeta);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
meta = decoded;
|
||||
} else if (decoded is Map) {
|
||||
meta = decoded.map((k, v) => MapEntry(k.toString(), v));
|
||||
}
|
||||
} catch (_) {
|
||||
meta = null;
|
||||
}
|
||||
}
|
||||
|
||||
DateTime createdAt;
|
||||
final rawCreated = map['created_at'];
|
||||
if (rawCreated is String) {
|
||||
try {
|
||||
createdAt = AppTime.parse(rawCreated);
|
||||
} catch (_) {
|
||||
createdAt = AppTime.now();
|
||||
}
|
||||
} else {
|
||||
createdAt = AppTime.now();
|
||||
}
|
||||
|
||||
return ItServiceRequestActivityLog(
|
||||
id: id,
|
||||
requestId: requestId,
|
||||
actorId: actorId,
|
||||
actionType: actionType,
|
||||
meta: meta,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'it_service_request_activity_log.model.dart';
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
// meta: Map<String, dynamic>? is stored as JSON text in SQLite by Brick.
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'it_service_request_activity_logs'),
|
||||
)
|
||||
class ItServiceRequestActivityLog extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String requestId;
|
||||
final String? actorId;
|
||||
final String actionType;
|
||||
final Map<String, dynamic>? meta;
|
||||
final DateTime createdAt;
|
||||
|
||||
ItServiceRequestActivityLog({
|
||||
required this.id,
|
||||
required this.requestId,
|
||||
this.actorId,
|
||||
required this.actionType,
|
||||
this.meta,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ItServiceRequestActivityLog.fromMap(Map<String, dynamic> map) {
|
||||
final rawId = map['id'];
|
||||
final rawRequestId = map['request_id'];
|
||||
String id = rawId == null ? '' : rawId.toString();
|
||||
String requestId = rawRequestId == null ? '' : rawRequestId.toString();
|
||||
final actorId = map['actor_id']?.toString();
|
||||
final actionType = (map['action_type'] as String?) ?? 'unknown';
|
||||
|
||||
Map<String, dynamic>? meta;
|
||||
final rawMeta = map['meta'];
|
||||
if (rawMeta is Map<String, dynamic>) {
|
||||
meta = rawMeta;
|
||||
} else if (rawMeta is Map) {
|
||||
try {
|
||||
meta = rawMeta.map((k, v) => MapEntry(k.toString(), v));
|
||||
} catch (_) {
|
||||
meta = null;
|
||||
}
|
||||
} else if (rawMeta is String && rawMeta.isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(rawMeta);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
meta = decoded;
|
||||
} else if (decoded is Map) {
|
||||
meta = decoded.map((k, v) => MapEntry(k.toString(), v));
|
||||
}
|
||||
} catch (_) {
|
||||
meta = null;
|
||||
}
|
||||
}
|
||||
|
||||
DateTime createdAt;
|
||||
final rawCreated = map['created_at'];
|
||||
if (rawCreated is String) {
|
||||
try {
|
||||
createdAt = AppTime.parse(rawCreated);
|
||||
} catch (_) {
|
||||
createdAt = AppTime.now();
|
||||
}
|
||||
} else {
|
||||
createdAt = AppTime.now();
|
||||
}
|
||||
|
||||
return ItServiceRequestActivityLog(
|
||||
id: id,
|
||||
requestId: requestId,
|
||||
actorId: actorId,
|
||||
actionType: actionType,
|
||||
meta: meta,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class ItServiceRequestAssignment {
|
||||
ItServiceRequestAssignment({
|
||||
required this.id,
|
||||
required this.requestId,
|
||||
required this.userId,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String requestId;
|
||||
final String userId;
|
||||
final DateTime createdAt;
|
||||
|
||||
factory ItServiceRequestAssignment.fromMap(Map<String, dynamic> map) {
|
||||
return ItServiceRequestAssignment(
|
||||
id: map['id'] as String,
|
||||
requestId: map['request_id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'it_service_request_assignment.model.dart';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'it_service_request_assignments'),
|
||||
)
|
||||
class ItServiceRequestAssignment extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String requestId;
|
||||
final String userId;
|
||||
final DateTime createdAt;
|
||||
|
||||
ItServiceRequestAssignment({
|
||||
required this.id,
|
||||
required this.requestId,
|
||||
required this.userId,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ItServiceRequestAssignment.fromMap(Map<String, dynamic> map) {
|
||||
return ItServiceRequestAssignment(
|
||||
id: map['id'] as String,
|
||||
requestId: map['request_id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class LeaveOfAbsence {
|
||||
LeaveOfAbsence({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.leaveType,
|
||||
required this.justification,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.status,
|
||||
required this.filedBy,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String leaveType;
|
||||
final String justification;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final String status;
|
||||
final String filedBy;
|
||||
final DateTime createdAt;
|
||||
|
||||
factory LeaveOfAbsence.fromMap(Map<String, dynamic> map) {
|
||||
return LeaveOfAbsence(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
leaveType: map['leave_type'] as String,
|
||||
justification: map['justification'] as String,
|
||||
startTime: AppTime.parse(map['start_time'] as String),
|
||||
endTime: AppTime.parse(map['end_time'] as String),
|
||||
status: map['status'] as String? ?? 'pending',
|
||||
filedBy: map['filed_by'] as String,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
String get leaveTypeLabel {
|
||||
switch (leaveType) {
|
||||
case 'emergency_leave':
|
||||
return 'Emergency Leave';
|
||||
case 'parental_leave':
|
||||
return 'Parental Leave';
|
||||
case 'sick_leave':
|
||||
return 'Sick Leave';
|
||||
case 'vacation_leave':
|
||||
return 'Vacation Leave';
|
||||
default:
|
||||
return leaveType;
|
||||
}
|
||||
}
|
||||
}
|
||||
export 'leave_of_absence.model.dart';
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
// Supabase table is singular: 'leave_of_absence' (not 'leave_of_absences').
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'leave_of_absence'),
|
||||
)
|
||||
class LeaveOfAbsence extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String leaveType;
|
||||
final String justification;
|
||||
final DateTime startTime;
|
||||
final DateTime endTime;
|
||||
final String status;
|
||||
final String filedBy;
|
||||
final DateTime createdAt;
|
||||
|
||||
LeaveOfAbsence({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.leaveType,
|
||||
required this.justification,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.status,
|
||||
required this.filedBy,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory LeaveOfAbsence.fromMap(Map<String, dynamic> map) {
|
||||
return LeaveOfAbsence(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
leaveType: map['leave_type'] as String,
|
||||
justification: map['justification'] as String,
|
||||
startTime: AppTime.parse(map['start_time'] as String),
|
||||
endTime: AppTime.parse(map['end_time'] as String),
|
||||
status: map['status'] as String? ?? 'pending',
|
||||
filedBy: map['filed_by'] as String,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
String get leaveTypeLabel {
|
||||
switch (leaveType) {
|
||||
case 'emergency_leave': return 'Emergency Leave';
|
||||
case 'parental_leave': return 'Parental Leave';
|
||||
case 'sick_leave': return 'Sick Leave';
|
||||
case 'vacation_leave': return 'Vacation Leave';
|
||||
default: return leaveType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class NotificationItem {
|
||||
NotificationItem({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.actorId,
|
||||
required this.ticketId,
|
||||
required this.taskId,
|
||||
this.leaveId,
|
||||
this.passSlipId,
|
||||
required this.itServiceRequestId,
|
||||
this.announcementId,
|
||||
required this.messageId,
|
||||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.readAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String? actorId;
|
||||
final String? ticketId;
|
||||
final String? taskId;
|
||||
final String? leaveId;
|
||||
final String? passSlipId;
|
||||
final String? itServiceRequestId;
|
||||
final String? announcementId;
|
||||
final int? messageId;
|
||||
final String type;
|
||||
final DateTime createdAt;
|
||||
final DateTime? readAt;
|
||||
|
||||
bool get isUnread => readAt == null;
|
||||
|
||||
factory NotificationItem.fromMap(Map<String, dynamic> map) {
|
||||
return NotificationItem(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
actorId: map['actor_id'] as String?,
|
||||
ticketId: map['ticket_id'] as String?,
|
||||
taskId: map['task_id'] as String?,
|
||||
leaveId: map['leave_id'] as String?,
|
||||
passSlipId: map['pass_slip_id'] as String?,
|
||||
itServiceRequestId: map['it_service_request_id'] as String?,
|
||||
announcementId: map['announcement_id'] as String?,
|
||||
messageId: map['message_id'] as int?,
|
||||
type: map['type'] as String? ?? 'mention',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
readAt: map['read_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['read_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'notification_item.model.dart';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
// Supabase table is 'notifications' (not 'notification_items').
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'notifications'),
|
||||
)
|
||||
class NotificationItem extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String? actorId;
|
||||
final String? ticketId;
|
||||
final String? taskId;
|
||||
final String? leaveId;
|
||||
final String? passSlipId;
|
||||
final String? itServiceRequestId;
|
||||
final String? announcementId;
|
||||
final int? messageId;
|
||||
final String type;
|
||||
final DateTime createdAt;
|
||||
final DateTime? readAt;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get isUnread => readAt == null;
|
||||
|
||||
NotificationItem({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.actorId,
|
||||
required this.ticketId,
|
||||
required this.taskId,
|
||||
this.leaveId,
|
||||
this.passSlipId,
|
||||
required this.itServiceRequestId,
|
||||
this.announcementId,
|
||||
required this.messageId,
|
||||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.readAt,
|
||||
});
|
||||
|
||||
factory NotificationItem.fromMap(Map<String, dynamic> map) {
|
||||
return NotificationItem(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
actorId: map['actor_id'] as String?,
|
||||
ticketId: map['ticket_id'] as String?,
|
||||
taskId: map['task_id'] as String?,
|
||||
leaveId: map['leave_id'] as String?,
|
||||
passSlipId: map['pass_slip_id'] as String?,
|
||||
itServiceRequestId: map['it_service_request_id'] as String?,
|
||||
announcementId: map['announcement_id'] as String?,
|
||||
messageId: map['message_id'] as int?,
|
||||
type: map['type'] as String? ?? 'mention',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
readAt: map['read_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['read_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-15
@@ -1,15 +1 @@
|
||||
class Office {
|
||||
Office({required this.id, required this.name, this.serviceId});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String? serviceId;
|
||||
|
||||
factory Office.fromMap(Map<String, dynamic> map) {
|
||||
return Office(
|
||||
id: map['id'] as String,
|
||||
name: map['name'] as String? ?? '',
|
||||
serviceId: map['service_id'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'office.model.dart';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'offices'),
|
||||
)
|
||||
class Office extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? serviceId;
|
||||
|
||||
Office({required this.id, required this.name, this.serviceId});
|
||||
|
||||
factory Office.fromMap(Map<String, dynamic> map) {
|
||||
return Office(
|
||||
id: map['id'] as String,
|
||||
name: map['name'] as String? ?? '',
|
||||
serviceId: map['service_id'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class PassSlip {
|
||||
PassSlip({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.dutyScheduleId,
|
||||
required this.reason,
|
||||
required this.status,
|
||||
required this.requestedAt,
|
||||
this.approvedBy,
|
||||
this.approvedAt,
|
||||
this.slipStart,
|
||||
this.slipEnd,
|
||||
this.requestedStart,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String dutyScheduleId;
|
||||
final String reason;
|
||||
final String status; // 'pending', 'approved', 'rejected', 'completed'
|
||||
final DateTime requestedAt;
|
||||
final String? approvedBy;
|
||||
final DateTime? approvedAt;
|
||||
final DateTime? slipStart;
|
||||
final DateTime? slipEnd;
|
||||
final DateTime? requestedStart;
|
||||
|
||||
/// Whether the slip is active (approved but not yet completed).
|
||||
bool get isActive => status == 'approved' && slipEnd == null;
|
||||
|
||||
/// Whether the active slip has exceeded 1 hour.
|
||||
bool get isExceeded =>
|
||||
isActive &&
|
||||
slipStart != null &&
|
||||
AppTime.now().difference(slipStart!) > const Duration(hours: 1);
|
||||
|
||||
factory PassSlip.fromMap(Map<String, dynamic> map) {
|
||||
return PassSlip(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
dutyScheduleId: map['duty_schedule_id'] as String,
|
||||
reason: map['reason'] as String,
|
||||
status: map['status'] as String? ?? 'pending',
|
||||
requestedAt: AppTime.parse(map['requested_at'] as String),
|
||||
approvedBy: map['approved_by'] as String?,
|
||||
approvedAt: map['approved_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['approved_at'] as String),
|
||||
slipStart: map['slip_start'] == null
|
||||
? null
|
||||
: AppTime.parse(map['slip_start'] as String),
|
||||
slipEnd: map['slip_end'] == null
|
||||
? null
|
||||
: AppTime.parse(map['slip_end'] as String),
|
||||
requestedStart: map['requested_start'] == null
|
||||
? null
|
||||
: AppTime.parse(map['requested_start'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'pass_slip.model.dart';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'pass_slips'),
|
||||
)
|
||||
class PassSlip extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String dutyScheduleId;
|
||||
final String reason;
|
||||
final String status;
|
||||
final DateTime requestedAt;
|
||||
final String? approvedBy;
|
||||
final DateTime? approvedAt;
|
||||
final DateTime? slipStart;
|
||||
final DateTime? slipEnd;
|
||||
final DateTime? requestedStart;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get isActive => status == 'approved' && slipEnd == null;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get isExceeded =>
|
||||
isActive &&
|
||||
slipStart != null &&
|
||||
AppTime.now().difference(slipStart!) > const Duration(hours: 1);
|
||||
|
||||
PassSlip({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.dutyScheduleId,
|
||||
required this.reason,
|
||||
required this.status,
|
||||
required this.requestedAt,
|
||||
this.approvedBy,
|
||||
this.approvedAt,
|
||||
this.slipStart,
|
||||
this.slipEnd,
|
||||
this.requestedStart,
|
||||
});
|
||||
|
||||
factory PassSlip.fromMap(Map<String, dynamic> map) {
|
||||
return PassSlip(
|
||||
id: map['id'] as String,
|
||||
userId: map['user_id'] as String,
|
||||
dutyScheduleId: map['duty_schedule_id'] as String,
|
||||
reason: map['reason'] as String,
|
||||
status: map['status'] as String? ?? 'pending',
|
||||
requestedAt: AppTime.parse(map['requested_at'] as String),
|
||||
approvedBy: map['approved_by'] as String?,
|
||||
approvedAt: map['approved_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['approved_at'] as String),
|
||||
slipStart: map['slip_start'] == null
|
||||
? null
|
||||
: AppTime.parse(map['slip_start'] as String),
|
||||
slipEnd: map['slip_end'] == null
|
||||
? null
|
||||
: AppTime.parse(map['slip_end'] as String),
|
||||
requestedStart: map['requested_start'] == null
|
||||
? null
|
||||
: AppTime.parse(map['requested_start'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-38
@@ -1,38 +1 @@
|
||||
class Profile {
|
||||
Profile({
|
||||
required this.id,
|
||||
required this.role,
|
||||
required this.fullName,
|
||||
this.religion = 'catholic',
|
||||
this.allowTracking = false,
|
||||
this.avatarUrl,
|
||||
this.facePhotoUrl,
|
||||
this.faceEnrolledAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String role;
|
||||
final String fullName;
|
||||
final String religion;
|
||||
final bool allowTracking;
|
||||
final String? avatarUrl;
|
||||
final String? facePhotoUrl;
|
||||
final DateTime? faceEnrolledAt;
|
||||
|
||||
bool get hasFaceEnrolled => facePhotoUrl != null && faceEnrolledAt != null;
|
||||
|
||||
factory Profile.fromMap(Map<String, dynamic> map) {
|
||||
return Profile(
|
||||
id: map['id'] as String,
|
||||
role: map['role'] as String? ?? 'standard',
|
||||
fullName: map['full_name'] as String? ?? '',
|
||||
religion: map['religion'] as String? ?? 'catholic',
|
||||
allowTracking: map['allow_tracking'] as bool? ?? false,
|
||||
avatarUrl: map['avatar_url'] as String?,
|
||||
facePhotoUrl: map['face_photo_url'] as String?,
|
||||
faceEnrolledAt: map['face_enrolled_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(map['face_enrolled_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'profile.model.dart';
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'profiles'),
|
||||
)
|
||||
class Profile extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String role;
|
||||
final String fullName;
|
||||
final String religion;
|
||||
final bool allowTracking;
|
||||
final String? avatarUrl;
|
||||
final String? facePhotoUrl;
|
||||
final DateTime? faceEnrolledAt;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get hasFaceEnrolled => facePhotoUrl != null && faceEnrolledAt != null;
|
||||
|
||||
Profile({
|
||||
required this.id,
|
||||
required this.role,
|
||||
required this.fullName,
|
||||
this.religion = 'catholic',
|
||||
this.allowTracking = false,
|
||||
this.avatarUrl,
|
||||
this.facePhotoUrl,
|
||||
this.faceEnrolledAt,
|
||||
});
|
||||
|
||||
factory Profile.fromMap(Map<String, dynamic> map) {
|
||||
return Profile(
|
||||
id: map['id'] as String,
|
||||
role: map['role'] as String? ?? 'standard',
|
||||
fullName: map['full_name'] as String? ?? '',
|
||||
religion: map['religion'] as String? ?? 'catholic',
|
||||
allowTracking: map['allow_tracking'] as bool? ?? false,
|
||||
avatarUrl: map['avatar_url'] as String?,
|
||||
facePhotoUrl: map['face_photo_url'] as String?,
|
||||
faceEnrolledAt: map['face_enrolled_at'] == null
|
||||
? null
|
||||
: DateTime.tryParse(map['face_enrolled_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-10
@@ -1,10 +1 @@
|
||||
class Service {
|
||||
Service({required this.id, required this.name});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
factory Service.fromMap(Map<String, dynamic> map) {
|
||||
return Service(id: map['id'] as String, name: map['name'] as String? ?? '');
|
||||
}
|
||||
}
|
||||
export 'service.model.dart';
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'services'),
|
||||
)
|
||||
class Service extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
Service({required this.id, required this.name});
|
||||
|
||||
factory Service.fromMap(Map<String, dynamic> map) {
|
||||
return Service(
|
||||
id: map['id'] as String,
|
||||
name: map['name'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class SwapRequest {
|
||||
SwapRequest({
|
||||
required this.id,
|
||||
required this.requesterId,
|
||||
required this.recipientId,
|
||||
required this.requesterScheduleId,
|
||||
required this.targetScheduleId,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.chatThreadId,
|
||||
this.shiftType,
|
||||
this.shiftStartTime,
|
||||
this.relieverIds,
|
||||
this.approvedBy,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String requesterId;
|
||||
final String recipientId;
|
||||
final String requesterScheduleId; // previously `shiftId`
|
||||
final String? targetScheduleId;
|
||||
final String status;
|
||||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? chatThreadId;
|
||||
final String? shiftType;
|
||||
final DateTime? shiftStartTime;
|
||||
final List<String>? relieverIds;
|
||||
final String? approvedBy;
|
||||
|
||||
factory SwapRequest.fromMap(Map<String, dynamic> map) {
|
||||
return SwapRequest(
|
||||
id: map['id'] as String,
|
||||
requesterId: map['requester_id'] as String,
|
||||
recipientId: map['recipient_id'] as String,
|
||||
requesterScheduleId:
|
||||
(map['requester_schedule_id'] as String?) ??
|
||||
(map['shift_id'] as String),
|
||||
targetScheduleId: map['target_shift_id'] as String?,
|
||||
status: map['status'] as String? ?? 'pending',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
updatedAt: map['updated_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['updated_at'] as String),
|
||||
chatThreadId: map['chat_thread_id'] as String?,
|
||||
shiftType: map['shift_type'] as String?,
|
||||
shiftStartTime: map['shift_start_time'] == null
|
||||
? null
|
||||
: AppTime.parse(map['shift_start_time'] as String),
|
||||
relieverIds: map['reliever_ids'] is List
|
||||
? (map['reliever_ids'] as List)
|
||||
.where((e) => e != null)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: const <String>[],
|
||||
approvedBy: map['approved_by'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'swap_request.model.dart';
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'swap_requests'),
|
||||
)
|
||||
class SwapRequest extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String requesterId;
|
||||
final String recipientId;
|
||||
final String requesterScheduleId;
|
||||
|
||||
// DB column is 'target_shift_id' (legacy name differs from Dart field).
|
||||
@Supabase(name: 'target_shift_id')
|
||||
final String? targetScheduleId;
|
||||
|
||||
final String status;
|
||||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? chatThreadId;
|
||||
final String? shiftType;
|
||||
final DateTime? shiftStartTime;
|
||||
final List<String>? relieverIds;
|
||||
final String? approvedBy;
|
||||
|
||||
SwapRequest({
|
||||
required this.id,
|
||||
required this.requesterId,
|
||||
required this.recipientId,
|
||||
required this.requesterScheduleId,
|
||||
required this.targetScheduleId,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.chatThreadId,
|
||||
this.shiftType,
|
||||
this.shiftStartTime,
|
||||
this.relieverIds,
|
||||
this.approvedBy,
|
||||
});
|
||||
|
||||
factory SwapRequest.fromMap(Map<String, dynamic> map) {
|
||||
return SwapRequest(
|
||||
id: map['id'] as String,
|
||||
requesterId: map['requester_id'] as String,
|
||||
recipientId: map['recipient_id'] as String,
|
||||
requesterScheduleId:
|
||||
(map['requester_schedule_id'] as String?) ??
|
||||
(map['shift_id'] as String),
|
||||
targetScheduleId: map['target_shift_id'] as String?,
|
||||
status: map['status'] as String? ?? 'pending',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
updatedAt: map['updated_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['updated_at'] as String),
|
||||
chatThreadId: map['chat_thread_id'] as String?,
|
||||
shiftType: map['shift_type'] as String?,
|
||||
shiftStartTime: map['shift_start_time'] == null
|
||||
? null
|
||||
: AppTime.parse(map['shift_start_time'] as String),
|
||||
relieverIds: map['reliever_ids'] is List
|
||||
? (map['reliever_ids'] as List)
|
||||
.where((e) => e != null)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: const <String>[],
|
||||
approvedBy: map['approved_by'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-186
@@ -1,186 +1 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class Task {
|
||||
Task({
|
||||
required this.id,
|
||||
required this.ticketId,
|
||||
required this.taskNumber,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.officeId,
|
||||
required this.status,
|
||||
required this.priority,
|
||||
required this.queueOrder,
|
||||
required this.createdAt,
|
||||
required this.creatorId,
|
||||
required this.startedAt,
|
||||
required this.completedAt,
|
||||
// new optional metadata fields
|
||||
this.requestedBy,
|
||||
this.notedBy,
|
||||
this.receivedBy,
|
||||
this.requestType,
|
||||
this.requestTypeOther,
|
||||
this.requestCategory,
|
||||
this.actionTaken,
|
||||
this.cancellationReason,
|
||||
this.cancelledAt,
|
||||
this.itJobPrinted = false,
|
||||
this.itJobPrintedAt,
|
||||
this.itJobReceivedById,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String? ticketId;
|
||||
final String? taskNumber;
|
||||
final String title;
|
||||
final String description;
|
||||
final String? officeId;
|
||||
final String status;
|
||||
final int priority;
|
||||
final int? queueOrder;
|
||||
final DateTime createdAt;
|
||||
final String? creatorId;
|
||||
final DateTime? startedAt;
|
||||
final DateTime? completedAt;
|
||||
|
||||
// Optional client/user metadata
|
||||
final String? requestedBy;
|
||||
final String? notedBy;
|
||||
final String? receivedBy;
|
||||
|
||||
/// Optional request metadata added later in lifecycle.
|
||||
final String? requestType;
|
||||
final String? requestTypeOther;
|
||||
final String? requestCategory;
|
||||
// JSON serialized rich text for action taken (Quill Delta JSON encoded)
|
||||
final String? actionTaken;
|
||||
|
||||
/// Cancellation details when a task was cancelled.
|
||||
final String? cancellationReason;
|
||||
final DateTime? cancelledAt;
|
||||
|
||||
/// Whether the printed IT Job has been submitted.
|
||||
final bool itJobPrinted;
|
||||
final DateTime? itJobPrintedAt;
|
||||
final String? itJobReceivedById;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Task &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
ticketId == other.ticketId &&
|
||||
taskNumber == other.taskNumber &&
|
||||
title == other.title &&
|
||||
description == other.description &&
|
||||
officeId == other.officeId &&
|
||||
status == other.status &&
|
||||
priority == other.priority &&
|
||||
queueOrder == other.queueOrder &&
|
||||
createdAt == other.createdAt &&
|
||||
creatorId == other.creatorId &&
|
||||
startedAt == other.startedAt &&
|
||||
completedAt == other.completedAt &&
|
||||
requestedBy == other.requestedBy &&
|
||||
notedBy == other.notedBy &&
|
||||
receivedBy == other.receivedBy &&
|
||||
requestType == other.requestType &&
|
||||
requestTypeOther == other.requestTypeOther &&
|
||||
requestCategory == other.requestCategory &&
|
||||
actionTaken == other.actionTaken &&
|
||||
cancellationReason == other.cancellationReason &&
|
||||
cancelledAt == other.cancelledAt &&
|
||||
itJobPrinted == other.itJobPrinted &&
|
||||
itJobPrintedAt == other.itJobPrintedAt &&
|
||||
itJobReceivedById == other.itJobReceivedById;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
ticketId,
|
||||
taskNumber,
|
||||
title,
|
||||
description,
|
||||
officeId,
|
||||
status,
|
||||
priority,
|
||||
queueOrder,
|
||||
createdAt,
|
||||
creatorId,
|
||||
startedAt,
|
||||
completedAt,
|
||||
requestedBy,
|
||||
notedBy,
|
||||
receivedBy,
|
||||
requestType,
|
||||
requestTypeOther,
|
||||
requestCategory,
|
||||
// Object.hash supports max 20 positional args; combine remainder.
|
||||
Object.hash(actionTaken, cancellationReason, cancelledAt,
|
||||
itJobPrinted, itJobPrintedAt, itJobReceivedById),
|
||||
);
|
||||
|
||||
/// Helper that indicates whether a completed task still has missing
|
||||
/// metadata such as signatories or action details. The parameter is used
|
||||
/// by UI to surface a warning icon/banner when a task has been closed but
|
||||
/// the user skipped filling out all fields.
|
||||
bool get hasIncompleteDetails {
|
||||
if (status != 'completed') return false;
|
||||
bool empty(String? v) => v == null || v.trim().isEmpty;
|
||||
return empty(requestedBy) ||
|
||||
empty(notedBy) ||
|
||||
empty(receivedBy) ||
|
||||
empty(actionTaken);
|
||||
}
|
||||
|
||||
factory Task.fromMap(Map<String, dynamic> map) {
|
||||
return Task(
|
||||
id: map['id'] as String,
|
||||
ticketId: map['ticket_id'] as String?,
|
||||
taskNumber: map['task_number'] as String?,
|
||||
title: map['title'] as String? ?? 'Task',
|
||||
description: map['description'] as String? ?? '',
|
||||
officeId: map['office_id'] as String?,
|
||||
status: map['status'] as String? ?? 'queued',
|
||||
priority: map['priority'] as int? ?? 1,
|
||||
queueOrder: map['queue_order'] as int?,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
creatorId: map['creator_id'] as String?,
|
||||
startedAt: map['started_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['started_at'] as String),
|
||||
completedAt: map['completed_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['completed_at'] as String),
|
||||
requestType: map['request_type'] as String?,
|
||||
requestTypeOther: map['request_type_other'] as String?,
|
||||
requestCategory: map['request_category'] as String?,
|
||||
requestedBy: map['requested_by'] as String?,
|
||||
notedBy: map['noted_by'] as String?,
|
||||
receivedBy: map['received_by'] as String?,
|
||||
cancellationReason: map['cancellation_reason'] as String?,
|
||||
cancelledAt: map['cancelled_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['cancelled_at'] as String),
|
||||
itJobPrinted: map['it_job_printed'] as bool? ?? false,
|
||||
itJobPrintedAt: map['it_job_printed_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['it_job_printed_at'] as String),
|
||||
itJobReceivedById: map['it_job_received_by_id'] as String?,
|
||||
actionTaken: (() {
|
||||
final at = map['action_taken'];
|
||||
if (at == null) return null;
|
||||
if (at is String) return at;
|
||||
try {
|
||||
return jsonEncode(at);
|
||||
} catch (_) {
|
||||
return at.toString();
|
||||
}
|
||||
})(),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'task.model.dart';
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'tasks'),
|
||||
)
|
||||
class Task extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String? ticketId;
|
||||
final String? taskNumber;
|
||||
final String title;
|
||||
final String description;
|
||||
final String? officeId;
|
||||
final String status;
|
||||
final int priority;
|
||||
final int? queueOrder;
|
||||
final DateTime createdAt;
|
||||
final String? creatorId;
|
||||
final DateTime? startedAt;
|
||||
final DateTime? completedAt;
|
||||
final String? requestedBy;
|
||||
final String? notedBy;
|
||||
final String? receivedBy;
|
||||
final String? requestType;
|
||||
final String? requestTypeOther;
|
||||
final String? requestCategory;
|
||||
// JSON-encoded Quill Delta stored as plain string.
|
||||
final String? actionTaken;
|
||||
final String? cancellationReason;
|
||||
final DateTime? cancelledAt;
|
||||
final bool itJobPrinted;
|
||||
final DateTime? itJobPrintedAt;
|
||||
final String? itJobReceivedById;
|
||||
|
||||
Task({
|
||||
required this.id,
|
||||
required this.ticketId,
|
||||
required this.taskNumber,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.officeId,
|
||||
required this.status,
|
||||
required this.priority,
|
||||
required this.queueOrder,
|
||||
required this.createdAt,
|
||||
required this.creatorId,
|
||||
required this.startedAt,
|
||||
required this.completedAt,
|
||||
this.requestedBy,
|
||||
this.notedBy,
|
||||
this.receivedBy,
|
||||
this.requestType,
|
||||
this.requestTypeOther,
|
||||
this.requestCategory,
|
||||
this.actionTaken,
|
||||
this.cancellationReason,
|
||||
this.cancelledAt,
|
||||
this.itJobPrinted = false,
|
||||
this.itJobPrintedAt,
|
||||
this.itJobReceivedById,
|
||||
});
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Task &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
ticketId == other.ticketId &&
|
||||
taskNumber == other.taskNumber &&
|
||||
title == other.title &&
|
||||
description == other.description &&
|
||||
officeId == other.officeId &&
|
||||
status == other.status &&
|
||||
priority == other.priority &&
|
||||
queueOrder == other.queueOrder &&
|
||||
createdAt == other.createdAt &&
|
||||
creatorId == other.creatorId &&
|
||||
startedAt == other.startedAt &&
|
||||
completedAt == other.completedAt &&
|
||||
requestedBy == other.requestedBy &&
|
||||
notedBy == other.notedBy &&
|
||||
receivedBy == other.receivedBy &&
|
||||
requestType == other.requestType &&
|
||||
requestTypeOther == other.requestTypeOther &&
|
||||
requestCategory == other.requestCategory &&
|
||||
actionTaken == other.actionTaken &&
|
||||
cancellationReason == other.cancellationReason &&
|
||||
cancelledAt == other.cancelledAt &&
|
||||
itJobPrinted == other.itJobPrinted &&
|
||||
itJobPrintedAt == other.itJobPrintedAt &&
|
||||
itJobReceivedById == other.itJobReceivedById;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id, ticketId, taskNumber, title, description, officeId, status, priority,
|
||||
queueOrder, createdAt, creatorId, startedAt, completedAt, requestedBy,
|
||||
notedBy, receivedBy, requestType, requestTypeOther, requestCategory,
|
||||
Object.hash(actionTaken, cancellationReason, cancelledAt,
|
||||
itJobPrinted, itJobPrintedAt, itJobReceivedById),
|
||||
);
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
bool get hasIncompleteDetails {
|
||||
if (status != 'completed') return false;
|
||||
bool empty(String? v) => v == null || v.trim().isEmpty;
|
||||
return empty(requestedBy) || empty(notedBy) ||
|
||||
empty(receivedBy) || empty(actionTaken);
|
||||
}
|
||||
|
||||
factory Task.fromMap(Map<String, dynamic> map) {
|
||||
return Task(
|
||||
id: map['id'] as String,
|
||||
ticketId: map['ticket_id'] as String?,
|
||||
taskNumber: map['task_number'] as String?,
|
||||
title: map['title'] as String? ?? 'Task',
|
||||
description: map['description'] as String? ?? '',
|
||||
officeId: map['office_id'] as String?,
|
||||
status: map['status'] as String? ?? 'queued',
|
||||
priority: map['priority'] as int? ?? 1,
|
||||
queueOrder: map['queue_order'] as int?,
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
creatorId: map['creator_id'] as String?,
|
||||
startedAt: map['started_at'] == null
|
||||
? null : AppTime.parse(map['started_at'] as String),
|
||||
completedAt: map['completed_at'] == null
|
||||
? null : AppTime.parse(map['completed_at'] as String),
|
||||
requestType: map['request_type'] as String?,
|
||||
requestTypeOther: map['request_type_other'] as String?,
|
||||
requestCategory: map['request_category'] as String?,
|
||||
requestedBy: map['requested_by'] as String?,
|
||||
notedBy: map['noted_by'] as String?,
|
||||
receivedBy: map['received_by'] as String?,
|
||||
cancellationReason: map['cancellation_reason'] as String?,
|
||||
cancelledAt: map['cancelled_at'] == null
|
||||
? null : AppTime.parse(map['cancelled_at'] as String),
|
||||
itJobPrinted: map['it_job_printed'] as bool? ?? false,
|
||||
itJobPrintedAt: map['it_job_printed_at'] == null
|
||||
? null : AppTime.parse(map['it_job_printed_at'] as String),
|
||||
itJobReceivedById: map['it_job_received_by_id'] as String?,
|
||||
actionTaken: (() {
|
||||
final at = map['action_taken'];
|
||||
if (at == null) return null;
|
||||
if (at is String) return at;
|
||||
try { return jsonEncode(at); } catch (_) { return at.toString(); }
|
||||
})(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +1 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class TaskActivityLog {
|
||||
TaskActivityLog({
|
||||
required this.id,
|
||||
required this.taskId,
|
||||
this.actorId,
|
||||
required this.actionType,
|
||||
this.meta,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String taskId;
|
||||
final String? actorId;
|
||||
final String actionType; // created, assigned, reassigned, started, completed
|
||||
final Map<String, dynamic>? meta;
|
||||
final DateTime createdAt;
|
||||
|
||||
factory TaskActivityLog.fromMap(Map<String, dynamic> map) {
|
||||
// id and task_id may be returned as int or String depending on DB
|
||||
final rawId = map['id'];
|
||||
final rawTaskId = map['task_id'];
|
||||
|
||||
String id = rawId == null ? '' : rawId.toString();
|
||||
String taskId = rawTaskId == null ? '' : rawTaskId.toString();
|
||||
|
||||
// actor_id is nullable
|
||||
final actorId = map['actor_id']?.toString();
|
||||
|
||||
// action_type fallback
|
||||
final actionType = (map['action_type'] as String?) ?? 'unknown';
|
||||
|
||||
// meta may be a Map, Map<dynamic,dynamic>, JSON-encoded string, List, or null
|
||||
Map<String, dynamic>? meta;
|
||||
final rawMeta = map['meta'];
|
||||
if (rawMeta is Map<String, dynamic>) {
|
||||
meta = rawMeta;
|
||||
} else if (rawMeta is Map) {
|
||||
// convert dynamic-key map to Map<String, dynamic>
|
||||
try {
|
||||
meta = rawMeta.map((k, v) => MapEntry(k.toString(), v));
|
||||
} catch (_) {
|
||||
meta = null;
|
||||
}
|
||||
} else if (rawMeta is String && rawMeta.isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(rawMeta);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
meta = decoded;
|
||||
} else if (decoded is Map) {
|
||||
meta = decoded.map((k, v) => MapEntry(k.toString(), v));
|
||||
}
|
||||
} catch (_) {
|
||||
meta = null;
|
||||
}
|
||||
} else {
|
||||
meta = null;
|
||||
}
|
||||
|
||||
// created_at may be ISO string, DateTime, or numeric (seconds/millis since epoch)
|
||||
final rawCreated = map['created_at'];
|
||||
DateTime createdAt;
|
||||
if (rawCreated is DateTime) {
|
||||
createdAt = AppTime.toAppTime(rawCreated);
|
||||
} else if (rawCreated is String) {
|
||||
try {
|
||||
createdAt = AppTime.parse(rawCreated);
|
||||
} catch (_) {
|
||||
createdAt = AppTime.now();
|
||||
}
|
||||
} else if (rawCreated is int) {
|
||||
// assume seconds or milliseconds
|
||||
if (rawCreated > 1e12) {
|
||||
// likely microseconds or nanoseconds - treat as milliseconds
|
||||
createdAt = AppTime.toAppTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(rawCreated),
|
||||
);
|
||||
} else if (rawCreated > 1e10) {
|
||||
createdAt = AppTime.toAppTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(rawCreated),
|
||||
);
|
||||
} else {
|
||||
createdAt = AppTime.toAppTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(rawCreated * 1000),
|
||||
);
|
||||
}
|
||||
} else if (rawCreated is double) {
|
||||
final asInt = rawCreated.toInt();
|
||||
createdAt = AppTime.toAppTime(DateTime.fromMillisecondsSinceEpoch(asInt));
|
||||
} else {
|
||||
createdAt = AppTime.now();
|
||||
}
|
||||
|
||||
return TaskActivityLog(
|
||||
id: id,
|
||||
taskId: taskId,
|
||||
actorId: actorId,
|
||||
actionType: actionType,
|
||||
meta: meta,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'task_activity_log.model.dart';
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
// meta: Map<String, dynamic>? is stored as JSON text in SQLite by Brick.
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'task_activity_logs'),
|
||||
)
|
||||
class TaskActivityLog extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String taskId;
|
||||
final String? actorId;
|
||||
final String actionType;
|
||||
final Map<String, dynamic>? meta;
|
||||
final DateTime createdAt;
|
||||
|
||||
TaskActivityLog({
|
||||
required this.id,
|
||||
required this.taskId,
|
||||
this.actorId,
|
||||
required this.actionType,
|
||||
this.meta,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory TaskActivityLog.fromMap(Map<String, dynamic> map) {
|
||||
final rawId = map['id'];
|
||||
final rawTaskId = map['task_id'];
|
||||
|
||||
String id = rawId == null ? '' : rawId.toString();
|
||||
String taskId = rawTaskId == null ? '' : rawTaskId.toString();
|
||||
final actorId = map['actor_id']?.toString();
|
||||
final actionType = (map['action_type'] as String?) ?? 'unknown';
|
||||
|
||||
Map<String, dynamic>? meta;
|
||||
final rawMeta = map['meta'];
|
||||
if (rawMeta is Map<String, dynamic>) {
|
||||
meta = rawMeta;
|
||||
} else if (rawMeta is Map) {
|
||||
try {
|
||||
meta = rawMeta.map((k, v) => MapEntry(k.toString(), v));
|
||||
} catch (_) {
|
||||
meta = null;
|
||||
}
|
||||
} else if (rawMeta is String && rawMeta.isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(rawMeta);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
meta = decoded;
|
||||
} else if (decoded is Map) {
|
||||
meta = decoded.map((k, v) => MapEntry(k.toString(), v));
|
||||
}
|
||||
} catch (_) {
|
||||
meta = null;
|
||||
}
|
||||
}
|
||||
|
||||
final rawCreated = map['created_at'];
|
||||
DateTime createdAt;
|
||||
if (rawCreated is DateTime) {
|
||||
createdAt = AppTime.toAppTime(rawCreated);
|
||||
} else if (rawCreated is String) {
|
||||
try {
|
||||
createdAt = AppTime.parse(rawCreated);
|
||||
} catch (_) {
|
||||
createdAt = AppTime.now();
|
||||
}
|
||||
} else if (rawCreated is int) {
|
||||
if (rawCreated > 1e12) {
|
||||
createdAt = AppTime.toAppTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(rawCreated),
|
||||
);
|
||||
} else if (rawCreated > 1e10) {
|
||||
createdAt = AppTime.toAppTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(rawCreated),
|
||||
);
|
||||
} else {
|
||||
createdAt = AppTime.toAppTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(rawCreated * 1000),
|
||||
);
|
||||
}
|
||||
} else if (rawCreated is double) {
|
||||
createdAt = AppTime.toAppTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(rawCreated.toInt()),
|
||||
);
|
||||
} else {
|
||||
createdAt = AppTime.now();
|
||||
}
|
||||
|
||||
return TaskActivityLog(
|
||||
id: id,
|
||||
taskId: taskId,
|
||||
actorId: actorId,
|
||||
actionType: actionType,
|
||||
meta: meta,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-42
@@ -1,42 +1 @@
|
||||
// Extension to add members property to Team
|
||||
import '../models/team_member.dart';
|
||||
|
||||
extension TeamMembersExtension on Team {
|
||||
List<String> members(List<TeamMember> allMembers) {
|
||||
return allMembers
|
||||
.where((m) => m.teamId == id)
|
||||
.map((m) => m.userId)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
class Team {
|
||||
Team({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.leaderId,
|
||||
required this.officeIds,
|
||||
required this.createdAt,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String leaderId;
|
||||
final List<String> officeIds;
|
||||
final DateTime createdAt;
|
||||
final String? color;
|
||||
|
||||
factory Team.fromMap(Map<String, dynamic> map) {
|
||||
return Team(
|
||||
id: map['id'] as String,
|
||||
name: map['name'] as String? ?? '',
|
||||
leaderId: map['leader_id'] as String? ?? '',
|
||||
officeIds:
|
||||
(map['office_ids'] as List?)?.map((e) => e.toString()).toList() ??
|
||||
<String>[],
|
||||
createdAt: DateTime.parse(map['created_at'] as String),
|
||||
color: map['color'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'team.model.dart';
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import 'package:tasq/models/team_member.dart';
|
||||
|
||||
extension TeamMembersExtension on Team {
|
||||
List<String> members(List<TeamMember> allMembers) {
|
||||
return allMembers
|
||||
.where((m) => m.teamId == id)
|
||||
.map((m) => m.userId)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'teams'),
|
||||
)
|
||||
class Team extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String leaderId;
|
||||
final List<String> officeIds;
|
||||
final DateTime createdAt;
|
||||
final String? color;
|
||||
|
||||
Team({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.leaderId,
|
||||
required this.officeIds,
|
||||
required this.createdAt,
|
||||
this.color,
|
||||
});
|
||||
|
||||
factory Team.fromMap(Map<String, dynamic> map) {
|
||||
return Team(
|
||||
id: map['id'] as String,
|
||||
name: map['name'] as String? ?? '',
|
||||
leaderId: map['leader_id'] as String? ?? '',
|
||||
officeIds:
|
||||
(map['office_ids'] as List?)?.map((e) => e.toString()).toList() ??
|
||||
<String>[],
|
||||
createdAt: DateTime.parse(map['created_at'] as String),
|
||||
color: map['color'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-78
@@ -1,78 +1 @@
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
class Ticket {
|
||||
Ticket({
|
||||
required this.id,
|
||||
required this.subject,
|
||||
required this.description,
|
||||
required this.officeId,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
required this.creatorId,
|
||||
required this.respondedAt,
|
||||
required this.promotedAt,
|
||||
required this.closedAt,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String subject;
|
||||
final String description;
|
||||
final String officeId;
|
||||
final String status;
|
||||
final DateTime createdAt;
|
||||
final String? creatorId;
|
||||
final DateTime? respondedAt;
|
||||
final DateTime? promotedAt;
|
||||
final DateTime? closedAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Ticket &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
subject == other.subject &&
|
||||
description == other.description &&
|
||||
officeId == other.officeId &&
|
||||
status == other.status &&
|
||||
createdAt == other.createdAt &&
|
||||
creatorId == other.creatorId &&
|
||||
respondedAt == other.respondedAt &&
|
||||
promotedAt == other.promotedAt &&
|
||||
closedAt == other.closedAt;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
subject,
|
||||
description,
|
||||
officeId,
|
||||
status,
|
||||
createdAt,
|
||||
creatorId,
|
||||
respondedAt,
|
||||
promotedAt,
|
||||
closedAt,
|
||||
);
|
||||
|
||||
factory Ticket.fromMap(Map<String, dynamic> map) {
|
||||
return Ticket(
|
||||
id: map['id'] as String,
|
||||
subject: map['subject'] as String? ?? '',
|
||||
description: map['description'] as String? ?? '',
|
||||
officeId: map['office_id'] as String? ?? '',
|
||||
status: map['status'] as String? ?? 'pending',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
creatorId: map['creator_id'] as String?,
|
||||
respondedAt: map['responded_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['responded_at'] as String),
|
||||
promotedAt: map['promoted_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['promoted_at'] as String),
|
||||
closedAt: map['closed_at'] == null
|
||||
? null
|
||||
: AppTime.parse(map['closed_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'ticket.model.dart';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:brick_offline_first_with_supabase/brick_offline_first_with_supabase.dart';
|
||||
import 'package:brick_sqlite/brick_sqlite.dart';
|
||||
import 'package:brick_supabase/brick_supabase.dart';
|
||||
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
@ConnectOfflineFirstWithSupabase(
|
||||
supabaseConfig: SupabaseSerializable(tableName: 'tickets'),
|
||||
)
|
||||
class Ticket extends OfflineFirstWithSupabaseModel {
|
||||
final String id;
|
||||
final String subject;
|
||||
final String description;
|
||||
final String officeId;
|
||||
final String status;
|
||||
final DateTime createdAt;
|
||||
final String? creatorId;
|
||||
final DateTime? respondedAt;
|
||||
final DateTime? promotedAt;
|
||||
final DateTime? closedAt;
|
||||
|
||||
Ticket({
|
||||
required this.id,
|
||||
required this.subject,
|
||||
required this.description,
|
||||
required this.officeId,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
required this.creatorId,
|
||||
required this.respondedAt,
|
||||
required this.promotedAt,
|
||||
required this.closedAt,
|
||||
});
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Ticket &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id && subject == other.subject &&
|
||||
description == other.description && officeId == other.officeId &&
|
||||
status == other.status && createdAt == other.createdAt &&
|
||||
creatorId == other.creatorId && respondedAt == other.respondedAt &&
|
||||
promotedAt == other.promotedAt && closedAt == other.closedAt;
|
||||
|
||||
@Supabase(ignore: true)
|
||||
@Sqlite(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(id, subject, description, officeId, status,
|
||||
createdAt, creatorId, respondedAt, promotedAt, closedAt);
|
||||
|
||||
factory Ticket.fromMap(Map<String, dynamic> map) {
|
||||
return Ticket(
|
||||
id: map['id'] as String,
|
||||
subject: map['subject'] as String? ?? '',
|
||||
description: map['description'] as String? ?? '',
|
||||
officeId: map['office_id'] as String? ?? '',
|
||||
status: map['status'] as String? ?? 'pending',
|
||||
createdAt: AppTime.parse(map['created_at'] as String),
|
||||
creatorId: map['creator_id'] as String?,
|
||||
respondedAt: map['responded_at'] == null
|
||||
? null : AppTime.parse(map['responded_at'] as String),
|
||||
promotedAt: map['promoted_at'] == null
|
||||
? null : AppTime.parse(map['promoted_at'] as String),
|
||||
closedAt: map['closed_at'] == null
|
||||
? null : AppTime.parse(map['closed_at'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/announcement.dart';
|
||||
import '../models/announcement_comment.dart';
|
||||
import '../utils/app_time.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'connectivity_provider.dart';
|
||||
import 'notifications_provider.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
/// Maximum rows fetched per poll. Announcements are lower-volume than tasks,
|
||||
/// so 100 is sufficient; adjust if orgs routinely exceed this.
|
||||
/// Maximum rows fetched per poll.
|
||||
const int kAnnouncementsPageLimit = 100;
|
||||
|
||||
/// Thrown when an announcement or comment is saved successfully but
|
||||
@@ -24,6 +27,26 @@ class AnnouncementNotificationException implements Exception {
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Announcements created offline that haven't been confirmed by Supabase yet.
|
||||
final offlinePendingAnnouncementsProvider =
|
||||
StateProvider<List<Announcement>>((ref) => const []);
|
||||
|
||||
/// Field-level updates to existing announcements made offline, keyed by id.
|
||||
final offlinePendingAnnouncementUpdatesProvider =
|
||||
StateProvider<Map<String, Map<String, dynamic>>>((ref) => const {});
|
||||
|
||||
/// Comments added offline, not yet confirmed by Supabase.
|
||||
final offlinePendingAnnouncementCommentsProvider =
|
||||
StateProvider<List<AnnouncementComment>>((ref) => const []);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read providers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Streams all announcements visible to the current user (RLS filtered).
|
||||
final announcementsProvider = StreamProvider<List<Announcement>>((ref) {
|
||||
final userId = ref.watch(currentUserIdProvider);
|
||||
@@ -31,6 +54,53 @@ final announcementsProvider = StreamProvider<List<Announcement>>((ref) {
|
||||
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
final pendingNew = ref.watch(offlinePendingAnnouncementsProvider);
|
||||
final pendingUpdates = ref.watch(offlinePendingAnnouncementUpdatesProvider);
|
||||
|
||||
List<Announcement> applyPending(List<Announcement> rows) {
|
||||
final byId = {for (final r in rows) r.id: r};
|
||||
pendingUpdates.forEach((id, fields) {
|
||||
final existing = byId[id];
|
||||
if (existing == null) return;
|
||||
byId[id] = Announcement(
|
||||
id: existing.id,
|
||||
authorId: existing.authorId,
|
||||
title: (fields['title'] as String?) ?? existing.title,
|
||||
body: (fields['body'] as String?) ?? existing.body,
|
||||
visibleRoles: (fields['visible_roles'] as List<String>?) ??
|
||||
existing.visibleRoles,
|
||||
isTemplate: (fields['is_template'] as bool?) ?? existing.isTemplate,
|
||||
templateId: fields.containsKey('template_id')
|
||||
? fields['template_id'] as String?
|
||||
: existing.templateId,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: fields.containsKey('updated_at')
|
||||
? AppTime.parse(fields['updated_at'] as String)
|
||||
: existing.updatedAt,
|
||||
bannerEnabled:
|
||||
(fields['banner_enabled'] as bool?) ?? existing.bannerEnabled,
|
||||
bannerShowAt: fields.containsKey('banner_show_at')
|
||||
? (fields['banner_show_at'] != null
|
||||
? AppTime.parse(fields['banner_show_at'] as String)
|
||||
: null)
|
||||
: existing.bannerShowAt,
|
||||
bannerHideAt: fields.containsKey('banner_hide_at')
|
||||
? (fields['banner_hide_at'] != null
|
||||
? AppTime.parse(fields['banner_hide_at'] as String)
|
||||
: null)
|
||||
: existing.bannerHideAt,
|
||||
pushIntervalMinutes: fields.containsKey('push_interval_minutes')
|
||||
? fields['push_interval_minutes'] as int?
|
||||
: existing.pushIntervalMinutes,
|
||||
);
|
||||
});
|
||||
for (final p in pendingNew) {
|
||||
byId.putIfAbsent(p.id, () => p);
|
||||
}
|
||||
return byId.values.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<Announcement>(
|
||||
stream: client
|
||||
.from('announcements')
|
||||
@@ -47,10 +117,138 @@ final announcementsProvider = StreamProvider<List<Announcement>>((ref) {
|
||||
fromMap: Announcement.fromMap,
|
||||
channelName: 'announcements',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<Announcement>();
|
||||
return applyPending(all);
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<Announcement>(rows, tag: 'announcements'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
|
||||
// Reconnect-replay: POST queued items directly, bypassing Brick's queue.
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
|
||||
// 1. Replay offline-created announcements
|
||||
final pendingList =
|
||||
List<Announcement>.from(ref.read(offlinePendingAnnouncementsProvider));
|
||||
if (pendingList.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[announcementsProvider] reconnected — syncing ${pendingList.length} announcement(s)',
|
||||
);
|
||||
final synced = <String>[];
|
||||
for (final a in pendingList) {
|
||||
try {
|
||||
await client.from('announcements').insert({
|
||||
'id': a.id,
|
||||
'author_id': a.authorId,
|
||||
'title': a.title,
|
||||
'body': a.body,
|
||||
'visible_roles': a.visibleRoles,
|
||||
'is_template': a.isTemplate,
|
||||
'template_id': a.templateId,
|
||||
'banner_enabled': a.bannerEnabled,
|
||||
'banner_show_at': a.bannerShowAt?.toIso8601String(),
|
||||
'banner_hide_at': a.bannerHideAt?.toIso8601String(),
|
||||
'push_interval_minutes': a.pushIntervalMinutes,
|
||||
});
|
||||
synced.add(a.id);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
synced.add(a.id);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[announcementsProvider] failed to sync id=${a.id}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = ref
|
||||
.read(offlinePendingAnnouncementsProvider)
|
||||
.where((a) => !synced.contains(a.id))
|
||||
.toList();
|
||||
ref.read(offlinePendingAnnouncementsProvider.notifier).state =
|
||||
List.unmodifiable(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Replay offline announcement updates
|
||||
final pendingUpdatesMap = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingAnnouncementUpdatesProvider),
|
||||
);
|
||||
if (pendingUpdatesMap.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[announcementsProvider] reconnected — syncing ${pendingUpdatesMap.length} update(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final entry in pendingUpdatesMap.entries) {
|
||||
try {
|
||||
await client
|
||||
.from('announcements')
|
||||
.update(entry.value)
|
||||
.eq('id', entry.key);
|
||||
syncedIds.add(entry.key);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[announcementsProvider] failed to sync update id=${entry.key}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remaining = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingAnnouncementUpdatesProvider),
|
||||
);
|
||||
for (final id in syncedIds) { remaining.remove(id); }
|
||||
ref.read(offlinePendingAnnouncementUpdatesProvider.notifier).state =
|
||||
remaining;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Replay offline comments
|
||||
final pendingComments = List<AnnouncementComment>.from(
|
||||
ref.read(offlinePendingAnnouncementCommentsProvider),
|
||||
);
|
||||
if (pendingComments.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[announcementsProvider] reconnected — syncing ${pendingComments.length} comment(s)',
|
||||
);
|
||||
final synced = <String>[];
|
||||
for (final c in pendingComments) {
|
||||
try {
|
||||
await client.from('announcement_comments').insert({
|
||||
'id': c.id,
|
||||
'announcement_id': c.announcementId,
|
||||
'author_id': c.authorId,
|
||||
'body': c.body,
|
||||
});
|
||||
synced.add(c.id);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
synced.add(c.id);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[announcementsProvider] failed to sync comment id=${c.id}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = ref
|
||||
.read(offlinePendingAnnouncementCommentsProvider)
|
||||
.where((c) => !synced.contains(c.id))
|
||||
.toList();
|
||||
ref.read(offlinePendingAnnouncementCommentsProvider.notifier).state =
|
||||
List.unmodifiable(remaining);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
/// Streams comments for a specific announcement.
|
||||
@@ -61,6 +259,20 @@ final announcementCommentsProvider =
|
||||
) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
final pendingComments =
|
||||
ref.watch(offlinePendingAnnouncementCommentsProvider);
|
||||
|
||||
List<AnnouncementComment> applyPending(List<AnnouncementComment> rows) {
|
||||
final byId = {for (final r in rows) r.id: r};
|
||||
for (final c in pendingComments) {
|
||||
if (c.announcementId == announcementId) {
|
||||
byId.putIfAbsent(c.id, () => c);
|
||||
}
|
||||
}
|
||||
return byId.values.toList()
|
||||
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<AnnouncementComment>(
|
||||
stream: client
|
||||
.from('announcement_comments')
|
||||
@@ -78,37 +290,51 @@ final announcementCommentsProvider =
|
||||
fromMap: AnnouncementComment.fromMap,
|
||||
channelName: 'announcement_comments_$announcementId',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<AnnouncementComment>();
|
||||
final filtered = all
|
||||
.where((c) => c.announcementId == announcementId)
|
||||
.toList()
|
||||
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
return applyPending(filtered);
|
||||
},
|
||||
onCacheMirror: (rows) => mirrorBatchToBrick<AnnouncementComment>(
|
||||
rows,
|
||||
tag: 'announcement_comments'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
/// Active banner announcements for the current user.
|
||||
/// Returns only non-template announcements whose banner is currently in its
|
||||
/// active time window ([Announcement.isBannerActive]).
|
||||
final activeBannerAnnouncementsProvider = Provider<List<Announcement>>((ref) {
|
||||
final all = ref.watch(announcementsProvider).valueOrNull ?? [];
|
||||
return all.where((a) => !a.isTemplate && a.isBannerActive).toList();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final announcementsControllerProvider =
|
||||
Provider<AnnouncementsController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final notifCtrl = ref.watch(notificationsControllerProvider);
|
||||
return AnnouncementsController(client, notifCtrl);
|
||||
return AnnouncementsController(client, notifCtrl, ref);
|
||||
});
|
||||
|
||||
class AnnouncementsController {
|
||||
AnnouncementsController(this._client, this._notifCtrl);
|
||||
AnnouncementsController(this._client, this._notifCtrl, [this._ref]);
|
||||
|
||||
// _client is declared dynamic to allow test doubles that mimic only the
|
||||
// subset of methods used by this class. In production it is a SupabaseClient.
|
||||
// _client is declared dynamic to allow test doubles.
|
||||
// ignore: avoid_dynamic_calls
|
||||
final dynamic _client;
|
||||
final dynamic _notifCtrl;
|
||||
final Ref? _ref;
|
||||
|
||||
/// Create a new announcement and send push notifications to target users.
|
||||
/// Offline: queues a local row and returns silently.
|
||||
Future<void> createAnnouncement({
|
||||
required String title,
|
||||
required String body,
|
||||
@@ -120,10 +346,14 @@ class AnnouncementsController {
|
||||
DateTime? bannerHideAt,
|
||||
int? pushIntervalMinutes,
|
||||
}) async {
|
||||
final authorId = _client.auth.currentUser?.id;
|
||||
final authorId = _client.auth.currentUser?.id as String?;
|
||||
if (authorId == null) return;
|
||||
|
||||
final id = const Uuid().v4();
|
||||
final now = AppTime.now();
|
||||
|
||||
final row = {
|
||||
'id': id,
|
||||
'author_id': authorId,
|
||||
'title': title,
|
||||
'body': body,
|
||||
@@ -136,23 +366,39 @@ class AnnouncementsController {
|
||||
'push_interval_minutes': pushIntervalMinutes,
|
||||
};
|
||||
|
||||
final result = await _client
|
||||
.from('announcements')
|
||||
.insert(row)
|
||||
.select('id')
|
||||
.single();
|
||||
final announcementId = result['id'] as String;
|
||||
String? announcementId;
|
||||
try {
|
||||
final result = await _client
|
||||
.from('announcements')
|
||||
.insert(row)
|
||||
.select('id')
|
||||
.single();
|
||||
announcementId = result['id'] as String;
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queuePendingAnnouncement(
|
||||
Announcement(
|
||||
id: id,
|
||||
authorId: authorId,
|
||||
title: title,
|
||||
body: body,
|
||||
visibleRoles: visibleRoles,
|
||||
isTemplate: isTemplate,
|
||||
templateId: templateId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
bannerEnabled: bannerEnabled,
|
||||
bannerShowAt: bannerShowAt,
|
||||
bannerHideAt: bannerHideAt,
|
||||
pushIntervalMinutes: pushIntervalMinutes,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't send notifications for templates (they are drafts for reuse)
|
||||
if (isTemplate) return;
|
||||
|
||||
// Skip the one-time creation push when a scheduled banner push is
|
||||
// configured. The banner scheduler will send the first push on its own
|
||||
// interval, so firing an extra push here would result in two back-to-back
|
||||
// notifications for the same announcement.
|
||||
if (bannerEnabled && pushIntervalMinutes != null) return;
|
||||
|
||||
// Query users whose role matches visible_roles, excluding the author
|
||||
try {
|
||||
final profiles = await _client
|
||||
.from('profiles')
|
||||
@@ -160,9 +406,8 @@ class AnnouncementsController {
|
||||
.inFilter('role', visibleRoles);
|
||||
final userIds = (profiles as List)
|
||||
.map((p) => p['id'] as String)
|
||||
.where((id) => id != authorId)
|
||||
.where((uid) => uid != authorId)
|
||||
.toList();
|
||||
|
||||
if (userIds.isEmpty) return;
|
||||
|
||||
await _notifCtrl.createNotification(
|
||||
@@ -183,7 +428,7 @@ class AnnouncementsController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing announcement.
|
||||
/// Update an existing announcement. Offline: queues the field patch.
|
||||
Future<void> updateAnnouncement({
|
||||
required String id,
|
||||
required String title,
|
||||
@@ -221,11 +466,16 @@ class AnnouncementsController {
|
||||
} else if (clearPushInterval) {
|
||||
payload['push_interval_minutes'] = null;
|
||||
}
|
||||
await _client.from('announcements').update(payload).eq('id', id);
|
||||
|
||||
try {
|
||||
await _client.from('announcements').update(payload).eq('id', id);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueAnnouncementUpdate(id, payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update only the banner settings on an existing announcement.
|
||||
/// Intended for the "Manage Banner" popup available to the poster and admins.
|
||||
/// Update only the banner settings. Offline: queues the field patch.
|
||||
Future<void> updateBannerSettings({
|
||||
required String id,
|
||||
required bool bannerEnabled,
|
||||
@@ -233,25 +483,37 @@ class AnnouncementsController {
|
||||
DateTime? bannerHideAt,
|
||||
int? pushIntervalMinutes,
|
||||
}) async {
|
||||
await _client.from('announcements').update({
|
||||
final payload = <String, dynamic>{
|
||||
'banner_enabled': bannerEnabled,
|
||||
'banner_show_at': bannerShowAt?.toUtc().toIso8601String(),
|
||||
'banner_hide_at': bannerHideAt?.toUtc().toIso8601String(),
|
||||
'push_interval_minutes': pushIntervalMinutes,
|
||||
'updated_at': AppTime.nowUtc().toIso8601String(),
|
||||
}).eq('id', id);
|
||||
};
|
||||
try {
|
||||
await _client.from('announcements').update(payload).eq('id', id);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueAnnouncementUpdate(id, payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// Immediately stops a banner by setting [banner_hide_at] to now.
|
||||
/// Usable by the poster or an admin.
|
||||
/// Immediately stops a banner by setting banner_hide_at to now.
|
||||
/// Offline: queues the field patch.
|
||||
Future<void> dismissBanner(String id) async {
|
||||
await _client.from('announcements').update({
|
||||
final payload = <String, dynamic>{
|
||||
'banner_hide_at': AppTime.nowUtc().toIso8601String(),
|
||||
'updated_at': AppTime.nowUtc().toIso8601String(),
|
||||
}).eq('id', id);
|
||||
};
|
||||
try {
|
||||
await _client.from('announcements').update(payload).eq('id', id);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueAnnouncementUpdate(id, payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete an announcement.
|
||||
/// Delete an announcement. Online-only — destructive actions are not queued.
|
||||
Future<void> deleteAnnouncement(String id) async {
|
||||
await _client.from('announcements').delete().eq('id', id);
|
||||
}
|
||||
@@ -264,27 +526,41 @@ class AnnouncementsController {
|
||||
.eq('id', templateId)
|
||||
.maybeSingle();
|
||||
if (data == null) return null;
|
||||
return Announcement.fromMap(data);
|
||||
return Announcement.fromMap(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// Add a comment to an announcement.
|
||||
/// Notifies the announcement author and all previous commenters.
|
||||
/// Add a comment. Offline: queues the comment locally.
|
||||
Future<void> addComment({
|
||||
required String announcementId,
|
||||
required String body,
|
||||
}) async {
|
||||
final authorId = _client.auth.currentUser?.id;
|
||||
final authorId = _client.auth.currentUser?.id as String?;
|
||||
if (authorId == null) return;
|
||||
|
||||
await _client.from('announcement_comments').insert({
|
||||
'announcement_id': announcementId,
|
||||
'author_id': authorId,
|
||||
'body': body,
|
||||
});
|
||||
final id = const Uuid().v4();
|
||||
|
||||
try {
|
||||
await _client.from('announcement_comments').insert({
|
||||
'id': id,
|
||||
'announcement_id': announcementId,
|
||||
'author_id': authorId,
|
||||
'body': body,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queuePendingComment(
|
||||
AnnouncementComment(
|
||||
id: id,
|
||||
announcementId: announcementId,
|
||||
authorId: authorId,
|
||||
body: body,
|
||||
createdAt: AppTime.now(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify announcement author + previous commenters
|
||||
try {
|
||||
// Get the announcement author and title
|
||||
final announcement = await _client
|
||||
.from('announcements')
|
||||
.select('author_id, title')
|
||||
@@ -296,23 +572,18 @@ class AnnouncementsController {
|
||||
final announcementTitle =
|
||||
announcement['title'] as String? ?? 'an announcement';
|
||||
|
||||
// Get all unique commenters on this announcement
|
||||
final comments = await _client
|
||||
.from('announcement_comments')
|
||||
.select('author_id')
|
||||
.eq('announcement_id', announcementId);
|
||||
final commenterIds = (comments as List)
|
||||
.map((c) => c['author_id'] as String)
|
||||
.toSet();
|
||||
final commenterIds =
|
||||
(comments as List).map((c) => c['author_id'] as String).toSet();
|
||||
|
||||
// Combine author + commenters, exclude self
|
||||
final notifyIds = <String>{announcementAuthorId, ...commenterIds}
|
||||
.where((id) => id != authorId)
|
||||
.where((uid) => uid != authorId)
|
||||
.toList();
|
||||
|
||||
if (notifyIds.isEmpty) return;
|
||||
|
||||
// Fetch commenter's display name for a human-readable push body
|
||||
final commenterData = await _client
|
||||
.from('profiles')
|
||||
.select('full_name')
|
||||
@@ -340,10 +611,9 @@ class AnnouncementsController {
|
||||
}
|
||||
|
||||
/// Re-sends push notifications for an existing announcement.
|
||||
/// Intended for use by admins or the announcement author.
|
||||
Future<void> resendAnnouncementNotification(
|
||||
Announcement announcement) async {
|
||||
final actorId = _client.auth.currentUser?.id;
|
||||
final actorId = _client.auth.currentUser?.id as String?;
|
||||
if (actorId == null) return;
|
||||
if (announcement.visibleRoles.isEmpty) return;
|
||||
|
||||
@@ -354,9 +624,8 @@ class AnnouncementsController {
|
||||
.inFilter('role', announcement.visibleRoles);
|
||||
final userIds = (profiles as List)
|
||||
.map((p) => p['id'] as String)
|
||||
.where((id) => id != actorId)
|
||||
.where((uid) => uid != actorId)
|
||||
.toList();
|
||||
|
||||
if (userIds.isEmpty) return;
|
||||
|
||||
await _notifCtrl.createNotification(
|
||||
@@ -379,8 +648,47 @@ class AnnouncementsController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a comment.
|
||||
/// Delete a comment. Online-only — destructive actions are not queued.
|
||||
Future<void> deleteComment(String id) async {
|
||||
await _client.from('announcement_comments').delete().eq('id', id);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queue helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void _queuePendingAnnouncement(Announcement announcement) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = List<Announcement>.from(
|
||||
ref.read(offlinePendingAnnouncementsProvider),
|
||||
);
|
||||
current.add(announcement);
|
||||
ref.read(offlinePendingAnnouncementsProvider.notifier).state =
|
||||
List.unmodifiable(current);
|
||||
mirrorBatchToBrick<Announcement>([announcement], tag: 'offline_announcement');
|
||||
}
|
||||
|
||||
void _queueAnnouncementUpdate(String id, Map<String, dynamic> fields) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingAnnouncementUpdatesProvider),
|
||||
);
|
||||
current[id] = {...?current[id], ...fields};
|
||||
ref.read(offlinePendingAnnouncementUpdatesProvider.notifier).state = current;
|
||||
}
|
||||
|
||||
void _queuePendingComment(AnnouncementComment comment) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = List<AnnouncementComment>.from(
|
||||
ref.read(offlinePendingAnnouncementCommentsProvider),
|
||||
);
|
||||
current.add(comment);
|
||||
ref.read(offlinePendingAnnouncementCommentsProvider.notifier).state =
|
||||
List.unmodifiable(current);
|
||||
mirrorBatchToBrick<AnnouncementComment>([comment],
|
||||
tag: 'offline_announcement_comment');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/attendance_log.dart';
|
||||
import '../utils/app_time.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'connectivity_provider.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'reports_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
|
||||
/// Date range for attendance logbook, defaults to "Last 7 Days".
|
||||
/// Date range for attendance logbook, defaults to "Today".
|
||||
final attendanceDateRangeProvider = StateProvider<ReportDateRange>((ref) {
|
||||
final now = AppTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
@@ -25,12 +30,32 @@ final attendanceDateRangeProvider = StateProvider<ReportDateRange>((ref) {
|
||||
/// Filter for logbook users (multi-select). If empty, all users are shown.
|
||||
final attendanceUserFilterProvider = StateProvider<List<String>>((ref) => []);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Check-ins created offline (typed model — merged into live stream).
|
||||
final offlinePendingCheckInsProvider =
|
||||
StateProvider<List<AttendanceLog>>((ref) => const []);
|
||||
|
||||
/// Raw RPC params for offline check-ins — used for reconnect replay.
|
||||
/// Each entry: {p_duty_id, p_lat, p_lng, localId}
|
||||
final offlinePendingCheckInRawProvider =
|
||||
StateProvider<List<Map<String, dynamic>>>((ref) => const []);
|
||||
|
||||
/// Attendance field updates made offline, keyed by attendance log id.
|
||||
/// Covers: checkouts, verification status updates, skipVerification.
|
||||
final offlinePendingAttendanceUpdatesProvider =
|
||||
StateProvider<Map<String, Map<String, dynamic>>>((ref) => const {});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// All visible attendance logs (own for standard, all for admin/dispatcher/it_staff).
|
||||
final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
// Use .select() so the stream is only recreated when the user id or role
|
||||
// actually changes (not on avatar/name edits, etc.).
|
||||
final profileId = ref.watch(
|
||||
currentProfileProvider.select((p) => p.valueOrNull?.id),
|
||||
);
|
||||
@@ -45,6 +70,56 @@ final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
||||
profileRole == 'dispatcher' ||
|
||||
profileRole == 'it_staff';
|
||||
|
||||
final pendingCheckIns = ref.watch(offlinePendingCheckInsProvider);
|
||||
final pendingUpdates = ref.watch(offlinePendingAttendanceUpdatesProvider);
|
||||
|
||||
List<AttendanceLog> applyPending(List<AttendanceLog> rows) {
|
||||
final byId = {for (final r in rows) r.id: r};
|
||||
|
||||
// Apply pending checkouts and other field updates
|
||||
pendingUpdates.forEach((id, fields) {
|
||||
final existing = byId[id];
|
||||
if (existing == null) return;
|
||||
byId[id] = AttendanceLog(
|
||||
id: existing.id,
|
||||
userId: existing.userId,
|
||||
dutyScheduleId: existing.dutyScheduleId,
|
||||
shiftType: existing.shiftType,
|
||||
checkInAt: existing.checkInAt,
|
||||
checkInLat: existing.checkInLat,
|
||||
checkInLng: existing.checkInLng,
|
||||
checkOutAt: fields.containsKey('check_out_at')
|
||||
? (fields['check_out_at'] != null
|
||||
? AppTime.parse(fields['check_out_at'] as String)
|
||||
: null)
|
||||
: existing.checkOutAt,
|
||||
checkOutLat: (fields['check_out_lat'] as num?)?.toDouble() ??
|
||||
existing.checkOutLat,
|
||||
checkOutLng: (fields['check_out_lng'] as num?)?.toDouble() ??
|
||||
existing.checkOutLng,
|
||||
justification: existing.justification,
|
||||
checkOutJustification:
|
||||
(fields['check_out_justification'] as String?) ??
|
||||
existing.checkOutJustification,
|
||||
verificationStatus:
|
||||
(fields['verification_status'] as String?) ??
|
||||
existing.verificationStatus,
|
||||
checkInVerificationPhotoUrl: existing.checkInVerificationPhotoUrl,
|
||||
checkOutVerificationPhotoUrl: existing.checkOutVerificationPhotoUrl,
|
||||
);
|
||||
});
|
||||
|
||||
// Append offline check-ins not yet on server
|
||||
for (final p in pendingCheckIns) {
|
||||
if (hasFullAccess || p.userId == profileId) {
|
||||
byId.putIfAbsent(p.id, () => p);
|
||||
}
|
||||
}
|
||||
|
||||
return byId.values.toList()
|
||||
..sort((a, b) => b.checkInAt.compareTo(a.checkInAt));
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<AttendanceLog>(
|
||||
stream: hasFullAccess
|
||||
? client
|
||||
@@ -68,55 +143,225 @@ final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
||||
fromMap: AttendanceLog.fromMap,
|
||||
channelName: 'attendance_logs',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<AttendanceLog>();
|
||||
final filtered = hasFullAccess
|
||||
? all
|
||||
: all.where((log) => log.userId == profileId).toList();
|
||||
filtered.sort((a, b) => b.checkInAt.compareTo(a.checkInAt));
|
||||
return applyPending(filtered);
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<AttendanceLog>(rows, tag: 'attendance_logs'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
|
||||
// Reconnect-replay: POST/PATCH queued items directly, bypassing Brick's queue.
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
|
||||
// 1. Replay offline check-ins
|
||||
final pendingRaw = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingCheckInRawProvider),
|
||||
);
|
||||
if (pendingRaw.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[attendanceLogsProvider] reconnected — syncing ${pendingRaw.length} check-in(s)',
|
||||
);
|
||||
final syncedLocalIds = <String>[];
|
||||
for (final params in pendingRaw) {
|
||||
final localId = params['localId'] as String;
|
||||
try {
|
||||
await client.rpc(
|
||||
'attendance_check_in',
|
||||
params: {
|
||||
'p_duty_id': params['p_duty_id'],
|
||||
'p_lat': params['p_lat'],
|
||||
'p_lng': params['p_lng'],
|
||||
},
|
||||
);
|
||||
syncedLocalIds.add(localId);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
syncedLocalIds.add(localId);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[attendanceLogsProvider] failed to sync check-in localId=$localId: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (syncedLocalIds.isNotEmpty) {
|
||||
final remainingRaw = ref
|
||||
.read(offlinePendingCheckInRawProvider)
|
||||
.where((p) => !syncedLocalIds.contains(p['localId'] as String))
|
||||
.toList();
|
||||
ref.read(offlinePendingCheckInRawProvider.notifier).state =
|
||||
List.unmodifiable(remainingRaw);
|
||||
|
||||
final remainingModels = ref
|
||||
.read(offlinePendingCheckInsProvider)
|
||||
.where((l) => !syncedLocalIds.contains(l.id))
|
||||
.toList();
|
||||
ref.read(offlinePendingCheckInsProvider.notifier).state =
|
||||
List.unmodifiable(remainingModels);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Replay pending checkouts and field updates (direct PATCH)
|
||||
final pendingUpdatesMap = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingAttendanceUpdatesProvider),
|
||||
);
|
||||
if (pendingUpdatesMap.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[attendanceLogsProvider] reconnected — syncing ${pendingUpdatesMap.length} attendance update(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final entry in pendingUpdatesMap.entries) {
|
||||
final attendanceId = entry.key;
|
||||
final fields = Map<String, dynamic>.from(entry.value);
|
||||
|
||||
// If this was a checkout via RPC, call the RPC instead of a raw PATCH
|
||||
if (fields.containsKey('_rpc_checkout')) {
|
||||
fields.remove('_rpc_checkout');
|
||||
try {
|
||||
await client.rpc(
|
||||
'attendance_check_out',
|
||||
params: {
|
||||
'p_attendance_id': attendanceId,
|
||||
'p_lat': fields['check_out_lat'],
|
||||
'p_lng': fields['check_out_lng'],
|
||||
'p_justification': fields['check_out_justification'],
|
||||
},
|
||||
);
|
||||
syncedIds.add(attendanceId);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[attendanceLogsProvider] failed to sync checkout id=$attendanceId: $e',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await client
|
||||
.from('attendance_logs')
|
||||
.update(fields)
|
||||
.eq('id', attendanceId);
|
||||
syncedIds.add(attendanceId);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[attendanceLogsProvider] failed to sync update id=$attendanceId: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remaining = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingAttendanceUpdatesProvider),
|
||||
);
|
||||
for (final id in syncedIds) {
|
||||
remaining.remove(id);
|
||||
}
|
||||
ref.read(offlinePendingAttendanceUpdatesProvider.notifier).state =
|
||||
remaining;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final attendanceControllerProvider = Provider<AttendanceController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return AttendanceController(client);
|
||||
return AttendanceController(client, ref);
|
||||
});
|
||||
|
||||
class AttendanceController {
|
||||
AttendanceController(this._client);
|
||||
AttendanceController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
/// Check in to a duty schedule. Returns the attendance log ID.
|
||||
/// Offline: queues locally via StateProvider, returns local UUID.
|
||||
Future<String?> checkIn({
|
||||
required String dutyScheduleId,
|
||||
required double lat,
|
||||
required double lng,
|
||||
String shiftType = 'normal',
|
||||
}) async {
|
||||
final data = await _client.rpc(
|
||||
'attendance_check_in',
|
||||
params: {'p_duty_id': dutyScheduleId, 'p_lat': lat, 'p_lng': lng},
|
||||
);
|
||||
return data as String?;
|
||||
try {
|
||||
final data = await _client.rpc(
|
||||
'attendance_check_in',
|
||||
params: {'p_duty_id': dutyScheduleId, 'p_lat': lat, 'p_lng': lng},
|
||||
);
|
||||
return data as String?;
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) rethrow;
|
||||
|
||||
final id = const Uuid().v4();
|
||||
final log = AttendanceLog(
|
||||
id: id,
|
||||
userId: userId,
|
||||
dutyScheduleId: dutyScheduleId,
|
||||
shiftType: shiftType,
|
||||
checkInAt: AppTime.now(),
|
||||
checkInLat: lat,
|
||||
checkInLng: lng,
|
||||
verificationStatus: 'pending',
|
||||
);
|
||||
|
||||
_queuePendingCheckIn(
|
||||
log,
|
||||
{'p_duty_id': dutyScheduleId, 'p_lat': lat, 'p_lng': lng, 'localId': id},
|
||||
);
|
||||
debugPrint('[AttendanceController] checkIn queued offline: $id');
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check out from an attendance log.
|
||||
/// Check out from an attendance log. Offline: queues the checkout params.
|
||||
Future<void> checkOut({
|
||||
required String attendanceId,
|
||||
required double lat,
|
||||
required double lng,
|
||||
String? justification,
|
||||
}) async {
|
||||
await _client.rpc(
|
||||
'attendance_check_out',
|
||||
params: {
|
||||
'p_attendance_id': attendanceId,
|
||||
'p_lat': lat,
|
||||
'p_lng': lng,
|
||||
'p_justification': justification,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await _client.rpc(
|
||||
'attendance_check_out',
|
||||
params: {
|
||||
'p_attendance_id': attendanceId,
|
||||
'p_lat': lat,
|
||||
'p_lng': lng,
|
||||
'p_justification': justification,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
|
||||
_queueAttendanceUpdate(attendanceId, {
|
||||
'_rpc_checkout': true,
|
||||
'check_out_at': AppTime.now().toIso8601String(),
|
||||
'check_out_lat': lat,
|
||||
'check_out_lng': lng,
|
||||
'check_out_justification': justification,
|
||||
});
|
||||
debugPrint(
|
||||
'[AttendanceController] checkOut queued offline: $attendanceId',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Overtime check-in (no pre-existing schedule required).
|
||||
/// Creates an overtime duty schedule + attendance log in one RPC call.
|
||||
/// Overtime check-in (online-only — requires server-side schedule creation).
|
||||
Future<String?> overtimeCheckIn({
|
||||
required double lat,
|
||||
required double lng,
|
||||
@@ -130,41 +375,99 @@ class AttendanceController {
|
||||
}
|
||||
|
||||
/// Upload a verification selfie and update the attendance log.
|
||||
/// Offline: queues the status update only (photo bytes are not persisted).
|
||||
Future<void> uploadVerification({
|
||||
required String attendanceId,
|
||||
required Uint8List bytes,
|
||||
required String fileName,
|
||||
required String status, // 'verified', 'unverified'
|
||||
required String status,
|
||||
bool isCheckOut = false,
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser!.id;
|
||||
final ext = fileName.split('.').last.toLowerCase();
|
||||
final prefix = isCheckOut ? 'checkout' : 'checkin';
|
||||
final path = '$userId/${prefix}_$attendanceId.$ext';
|
||||
await _client.storage
|
||||
.from('attendance-verification')
|
||||
.uploadBinary(
|
||||
path,
|
||||
bytes,
|
||||
fileOptions: const FileOptions(upsert: true),
|
||||
);
|
||||
final url = _client.storage
|
||||
.from('attendance-verification')
|
||||
.getPublicUrl(path);
|
||||
final column = isCheckOut
|
||||
? 'check_out_verification_photo_url'
|
||||
: 'check_in_verification_photo_url';
|
||||
await _client
|
||||
.from('attendance_logs')
|
||||
.update({'verification_status': status, column: url})
|
||||
.eq('id', attendanceId);
|
||||
|
||||
try {
|
||||
await _client.storage
|
||||
.from('attendance-verification')
|
||||
.uploadBinary(
|
||||
path,
|
||||
bytes,
|
||||
fileOptions: const FileOptions(upsert: true),
|
||||
);
|
||||
final url = _client.storage
|
||||
.from('attendance-verification')
|
||||
.getPublicUrl(path);
|
||||
final column = isCheckOut
|
||||
? 'check_out_verification_photo_url'
|
||||
: 'check_in_verification_photo_url';
|
||||
await _client
|
||||
.from('attendance_logs')
|
||||
.update({'verification_status': status, column: url})
|
||||
.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.
|
||||
_queueAttendanceUpdate(attendanceId, {'verification_status': status});
|
||||
debugPrint(
|
||||
'[AttendanceController] uploadVerification status queued offline (photo not persisted): $attendanceId',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark an attendance log as skipped verification.
|
||||
/// Mark an attendance log as skipped verification. Offline: queues the patch.
|
||||
Future<void> skipVerification(String attendanceId) async {
|
||||
await _client
|
||||
.from('attendance_logs')
|
||||
.update({'verification_status': 'skipped'})
|
||||
.eq('id', attendanceId);
|
||||
try {
|
||||
await _client
|
||||
.from('attendance_logs')
|
||||
.update({'verification_status': 'skipped'})
|
||||
.eq('id', attendanceId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueAttendanceUpdate(attendanceId, {'verification_status': 'skipped'});
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Queue helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
void _queuePendingCheckIn(
|
||||
AttendanceLog log,
|
||||
Map<String, dynamic> rpcParams,
|
||||
) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
|
||||
final currentModels = List<AttendanceLog>.from(
|
||||
ref.read(offlinePendingCheckInsProvider),
|
||||
);
|
||||
currentModels.add(log);
|
||||
ref.read(offlinePendingCheckInsProvider.notifier).state =
|
||||
List.unmodifiable(currentModels);
|
||||
|
||||
final currentRaw = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingCheckInRawProvider),
|
||||
);
|
||||
currentRaw.add(rpcParams);
|
||||
ref.read(offlinePendingCheckInRawProvider.notifier).state =
|
||||
List.unmodifiable(currentRaw);
|
||||
|
||||
mirrorBatchToBrick<AttendanceLog>([log], tag: 'offline_checkin');
|
||||
}
|
||||
|
||||
void _queueAttendanceUpdate(
|
||||
String attendanceId,
|
||||
Map<String, dynamic> fields,
|
||||
) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingAttendanceUpdatesProvider),
|
||||
);
|
||||
current[attendanceId] = {...?current[attendanceId], ...fields};
|
||||
ref.read(offlinePendingAttendanceUpdatesProvider.notifier).state = current;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/chat_message.dart';
|
||||
import '../utils/app_time.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'connectivity_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Pending chat messages keyed by threadId.
|
||||
/// Messages sync on reconnect when the thread's provider is active.
|
||||
final offlinePendingChatMessagesProvider =
|
||||
StateProvider<Map<String, List<ChatMessage>>>((ref) => const {});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Real-time chat messages for a swap request thread.
|
||||
final chatMessagesProvider = StreamProvider.family<List<ChatMessage>, String>((
|
||||
ref,
|
||||
@@ -13,6 +32,19 @@ final chatMessagesProvider = StreamProvider.family<List<ChatMessage>, String>((
|
||||
) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
final pendingByThread = ref.watch(offlinePendingChatMessagesProvider);
|
||||
final pendingForThread = pendingByThread[threadId] ?? const [];
|
||||
|
||||
List<ChatMessage> applyPending(List<ChatMessage> rows) {
|
||||
if (pendingForThread.isEmpty) return rows;
|
||||
final byId = {for (final r in rows) r.id: r};
|
||||
for (final p in pendingForThread) {
|
||||
byId.putIfAbsent(p.id, () => p);
|
||||
}
|
||||
return byId.values.toList()
|
||||
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<ChatMessage>(
|
||||
stream: client
|
||||
.from('chat_messages')
|
||||
@@ -30,32 +62,126 @@ final chatMessagesProvider = StreamProvider.family<List<ChatMessage>, String>((
|
||||
fromMap: ChatMessage.fromMap,
|
||||
channelName: 'chat_messages_$threadId',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<ChatMessage>();
|
||||
final filtered = all
|
||||
.where((m) => m.threadId == threadId)
|
||||
.toList()
|
||||
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
return applyPending(filtered);
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<ChatMessage>(rows, tag: 'chat_messages'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
|
||||
// Reconnect-replay: fires when this thread's screen is active.
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
|
||||
final pending = List<ChatMessage>.from(
|
||||
(ref.read(offlinePendingChatMessagesProvider)[threadId] ?? []),
|
||||
);
|
||||
if (pending.isEmpty) return;
|
||||
|
||||
debugPrint(
|
||||
'[chatMessagesProvider($threadId)] reconnected — syncing ${pending.length} message(s)',
|
||||
);
|
||||
final synced = <String>[];
|
||||
for (final msg in pending) {
|
||||
try {
|
||||
await client.from('chat_messages').insert({
|
||||
'id': msg.id,
|
||||
'thread_id': msg.threadId,
|
||||
'sender_id': msg.senderId,
|
||||
'body': msg.body,
|
||||
});
|
||||
synced.add(msg.id);
|
||||
} catch (e) {
|
||||
final s = e.toString();
|
||||
if (s.contains('23505') || s.contains('duplicate key')) {
|
||||
synced.add(msg.id);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[chatMessagesProvider] failed to sync message id=${msg.id}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final current = Map<String, List<ChatMessage>>.from(
|
||||
ref.read(offlinePendingChatMessagesProvider),
|
||||
);
|
||||
final remaining =
|
||||
(current[threadId] ?? []).where((m) => !synced.contains(m.id)).toList();
|
||||
if (remaining.isEmpty) {
|
||||
current.remove(threadId);
|
||||
} else {
|
||||
current[threadId] = remaining;
|
||||
}
|
||||
ref.read(offlinePendingChatMessagesProvider.notifier).state =
|
||||
Map.unmodifiable(current);
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final chatControllerProvider = Provider<ChatController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return ChatController(client);
|
||||
return ChatController(client, ref);
|
||||
});
|
||||
|
||||
class ChatController {
|
||||
ChatController(this._client);
|
||||
ChatController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
/// Send a message to a thread. Offline: queues locally.
|
||||
Future<void> sendMessage({
|
||||
required String threadId,
|
||||
required String body,
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
await _client.from('chat_messages').insert({
|
||||
'thread_id': threadId,
|
||||
'sender_id': userId,
|
||||
'body': body,
|
||||
});
|
||||
|
||||
final id = const Uuid().v4();
|
||||
|
||||
try {
|
||||
await _client.from('chat_messages').insert({
|
||||
'id': id,
|
||||
'thread_id': threadId,
|
||||
'sender_id': userId,
|
||||
'body': body,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
|
||||
final message = ChatMessage(
|
||||
id: id,
|
||||
threadId: threadId,
|
||||
senderId: userId,
|
||||
body: body,
|
||||
createdAt: AppTime.now(),
|
||||
);
|
||||
|
||||
final ref = _ref;
|
||||
if (ref != null) {
|
||||
final current = Map<String, List<ChatMessage>>.from(
|
||||
ref.read(offlinePendingChatMessagesProvider),
|
||||
);
|
||||
current[threadId] = [...(current[threadId] ?? []), message];
|
||||
ref.read(offlinePendingChatMessagesProvider.notifier).state =
|
||||
Map.unmodifiable(current);
|
||||
}
|
||||
mirrorBatchToBrick<ChatMessage>([message], tag: 'offline_chat');
|
||||
debugPrint('[ChatController] sendMessage queued offline: $id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../brick/cache_warmer.dart';
|
||||
import '../brick/repository.dart';
|
||||
import 'stream_recovery.dart';
|
||||
|
||||
/// Current online/offline status.
|
||||
/// Defaults to true so the UI doesn't flash an offline banner on startup.
|
||||
final isOnlineProvider = StateProvider<bool>((ref) => true);
|
||||
|
||||
/// Activating this provider starts a periodic connectivity poll.
|
||||
/// Watch it from the root widget to keep it alive.
|
||||
final connectivityMonitorProvider = Provider.autoDispose<void>((ref) {
|
||||
// Wire up the global online check for StreamRecoveryWrapper so all stream
|
||||
// wrappers can suppress recovery while offline without per-instance setup.
|
||||
StreamRecoveryWrapper.setIsOnlineCallback(() => ref.read(isOnlineProvider));
|
||||
|
||||
bool previouslyOnline = true;
|
||||
|
||||
Future<void> check() async {
|
||||
final online = await ConnectivityMonitor.check();
|
||||
final wasOnline = ref.read(isOnlineProvider);
|
||||
if (online != wasOnline) {
|
||||
ref.read(isOnlineProvider.notifier).state = online;
|
||||
}
|
||||
if (previouslyOnline && !online) {
|
||||
_onDisconnected();
|
||||
}
|
||||
if (!previouslyOnline && online) {
|
||||
_onReconnected();
|
||||
}
|
||||
previouslyOnline = online;
|
||||
}
|
||||
|
||||
// Run immediately, then every 5 seconds.
|
||||
check();
|
||||
final timer = Timer.periodic(const Duration(seconds: 5), (_) => check());
|
||||
ref.onDispose(timer.cancel);
|
||||
});
|
||||
|
||||
/// Pings the app's own backend to determine online status.
|
||||
class ConnectivityMonitor {
|
||||
static Future<bool> check() async {
|
||||
try {
|
||||
final res = await http
|
||||
.head(Uri.parse('https://tasq.crmc.ph'))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
return res.statusCode < 500;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onDisconnected() {
|
||||
debugPrint('[Connectivity] Went offline — pausing Brick request queue');
|
||||
// Stop the retry loop so the queue doesn't spam network errors while offline.
|
||||
// Queued writes are preserved in SQLite and will sync when _onReconnected fires.
|
||||
AppRepository.offlineQueue?.stop();
|
||||
}
|
||||
|
||||
void _onReconnected() {
|
||||
debugPrint('[Connectivity] Back online — queued writes will sync via Brick');
|
||||
if (!AppRepository.isConfigured) return;
|
||||
|
||||
// Resume the Brick HTTP queue so queued writes (e.g. offline task creation)
|
||||
// are replayed against Supabase now that the network is available again.
|
||||
AppRepository.offlineQueue?.start();
|
||||
|
||||
// Restart any Supabase realtime streams that were suppressed while offline.
|
||||
// Streams that disconnected due to no network are NOT in an error-retry loop
|
||||
// (recovery was suppressed); this kick-starts them back to live updates.
|
||||
StreamRecoveryWrapper.notifyAllOnlineRestored();
|
||||
|
||||
// Re-warm the Brick cache so the local SQLite mirror reflects any changes
|
||||
// that happened on the server while we were offline. Fire-and-forget — it
|
||||
// runs in the background and never throws.
|
||||
final user = Supabase.instance.client.auth.currentUser;
|
||||
if (user != null) {
|
||||
CacheWarmer.warmAll(Supabase.instance.client);
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,16 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../brick/cache_helpers.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 '../utils/app_time.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'connectivity_provider.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'user_offices_provider.dart';
|
||||
@@ -60,6 +66,27 @@ final itServiceRequestQueryProvider = StateProvider<ItServiceRequestQuery>(
|
||||
(ref) => const ItServiceRequestQuery(),
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// IT Service Requests created offline (typed model — merged into live stream).
|
||||
final offlinePendingItServiceRequestsProvider =
|
||||
StateProvider<List<ItServiceRequest>>((ref) => const []);
|
||||
|
||||
/// Raw RPC params for offline-created requests — used for reconnect replay.
|
||||
final offlinePendingItServiceRequestRawProvider =
|
||||
StateProvider<List<Map<String, dynamic>>>((ref) => const []);
|
||||
|
||||
/// Field-level updates to existing requests made offline, keyed by request id.
|
||||
final offlinePendingItServiceRequestUpdatesProvider =
|
||||
StateProvider<Map<String, Map<String, dynamic>>>((ref) => const {});
|
||||
|
||||
/// Pending action-taken entries submitted offline.
|
||||
/// Each entry: {id, request_id, user_id, action_taken}
|
||||
final offlinePendingIsrActionsProvider =
|
||||
StateProvider<List<Map<String, dynamic>>>((ref) => const []);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stream providers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -71,6 +98,24 @@ final itServiceRequestsProvider = StreamProvider<List<ItServiceRequest>>((ref) {
|
||||
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
||||
final userOfficesAsync = ref.watch(userOfficesProvider);
|
||||
|
||||
final pendingNew = ref.watch(offlinePendingItServiceRequestsProvider);
|
||||
final pendingUpdates =
|
||||
ref.watch(offlinePendingItServiceRequestUpdatesProvider);
|
||||
|
||||
List<ItServiceRequest> applyPending(List<ItServiceRequest> rows) {
|
||||
final byId = {for (final r in rows) r.id: r};
|
||||
pendingUpdates.forEach((id, fields) {
|
||||
final existing = byId[id];
|
||||
if (existing == null) return;
|
||||
byId[id] = _applyUpdateFields(existing, fields);
|
||||
});
|
||||
for (final p in pendingNew) {
|
||||
byId.putIfAbsent(p.id, () => p);
|
||||
}
|
||||
return byId.values.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<ItServiceRequest>(
|
||||
stream: client
|
||||
.from('it_service_requests')
|
||||
@@ -87,12 +132,140 @@ final itServiceRequestsProvider = StreamProvider<List<ItServiceRequest>>((ref) {
|
||||
fromMap: ItServiceRequest.fromMap,
|
||||
channelName: 'it_service_requests',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<ItServiceRequest>();
|
||||
all.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return applyPending(all);
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<ItServiceRequest>(rows, tag: 'it_service_requests'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
|
||||
// Reconnect-replay: POST queued items directly, bypassing Brick's queue.
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
|
||||
// 1. Replay offline-created requests (via RPC)
|
||||
final pendingRaw = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingItServiceRequestRawProvider),
|
||||
);
|
||||
if (pendingRaw.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[itServiceRequestsProvider] reconnected — syncing ${pendingRaw.length} request(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final params in pendingRaw) {
|
||||
final id = params['p_id'] as String;
|
||||
try {
|
||||
await client.rpc(
|
||||
'insert_it_service_request_with_number',
|
||||
params: params,
|
||||
);
|
||||
syncedIds.add(id);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
syncedIds.add(id);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[itServiceRequestsProvider] failed to sync id=$id: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remainingRaw = ref
|
||||
.read(offlinePendingItServiceRequestRawProvider)
|
||||
.where((p) => !syncedIds.contains(p['p_id'] as String))
|
||||
.toList();
|
||||
ref.read(offlinePendingItServiceRequestRawProvider.notifier).state =
|
||||
List.unmodifiable(remainingRaw);
|
||||
|
||||
final remainingModels = ref
|
||||
.read(offlinePendingItServiceRequestsProvider)
|
||||
.where((r) => !syncedIds.contains(r.id))
|
||||
.toList();
|
||||
ref.read(offlinePendingItServiceRequestsProvider.notifier).state =
|
||||
List.unmodifiable(remainingModels);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Replay offline field/status updates
|
||||
final pendingUpdatesMap = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingItServiceRequestUpdatesProvider),
|
||||
);
|
||||
if (pendingUpdatesMap.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[itServiceRequestsProvider] reconnected — syncing ${pendingUpdatesMap.length} update(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final entry in pendingUpdatesMap.entries) {
|
||||
try {
|
||||
await client
|
||||
.from('it_service_requests')
|
||||
.update(entry.value)
|
||||
.eq('id', entry.key);
|
||||
syncedIds.add(entry.key);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[itServiceRequestsProvider] failed to sync update id=${entry.key}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remaining = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingItServiceRequestUpdatesProvider),
|
||||
);
|
||||
for (final id in syncedIds) {
|
||||
remaining.remove(id);
|
||||
}
|
||||
ref.read(offlinePendingItServiceRequestUpdatesProvider.notifier).state =
|
||||
remaining;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Replay pending action-taken entries
|
||||
final pendingActions = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingIsrActionsProvider),
|
||||
);
|
||||
if (pendingActions.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[itServiceRequestsProvider] reconnected — syncing ${pendingActions.length} action(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final action in pendingActions) {
|
||||
final id = action['id'] as String;
|
||||
try {
|
||||
await client
|
||||
.from('it_service_request_actions')
|
||||
.upsert(action, onConflict: 'request_id,user_id');
|
||||
syncedIds.add(id);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
syncedIds.add(id);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[itServiceRequestsProvider] failed to sync action id=$id: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remaining = ref
|
||||
.read(offlinePendingIsrActionsProvider)
|
||||
.where((a) => !syncedIds.contains(a['id'] as String))
|
||||
.toList();
|
||||
ref.read(offlinePendingIsrActionsProvider.notifier).state =
|
||||
List.unmodifiable(remaining);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) {
|
||||
var items = result.data;
|
||||
// Standard users see only requests from their offices or created by them
|
||||
var items = applyPending(result.data);
|
||||
if (profile != null && profile.role == 'standard') {
|
||||
final officeIds = (userOfficesAsync.valueOrNull ?? [])
|
||||
.where((a) => a.userId == userId)
|
||||
@@ -145,6 +318,15 @@ final itServiceRequestAssignmentsProvider =
|
||||
onStatusChanged: ref
|
||||
.read(realtimeControllerProvider)
|
||||
.handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<ItServiceRequestAssignment>();
|
||||
all.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return all;
|
||||
},
|
||||
onCacheMirror: (rows) => mirrorBatchToBrick<ItServiceRequestAssignment>(
|
||||
rows,
|
||||
tag: 'isr_assignments',
|
||||
),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
@@ -177,6 +359,19 @@ final itServiceRequestActivityLogsProvider =
|
||||
onStatusChanged: ref
|
||||
.read(realtimeControllerProvider)
|
||||
.handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<ItServiceRequestActivityLog>();
|
||||
final filtered = all
|
||||
.where((l) => l.requestId == requestId)
|
||||
.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return filtered;
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<ItServiceRequestActivityLog>(
|
||||
rows,
|
||||
tag: 'isr_activity_logs',
|
||||
),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
@@ -209,6 +404,18 @@ final itServiceRequestActionsProvider =
|
||||
onStatusChanged: ref
|
||||
.read(realtimeControllerProvider)
|
||||
.handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<ItServiceRequestAction>();
|
||||
final filtered = all
|
||||
.where((a) => a.requestId == requestId)
|
||||
.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return filtered;
|
||||
},
|
||||
onCacheMirror: (rows) => mirrorBatchToBrick<ItServiceRequestAction>(
|
||||
rows,
|
||||
tag: 'isr_actions',
|
||||
),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
@@ -222,15 +429,19 @@ final itServiceRequestActionsProvider =
|
||||
final itServiceRequestControllerProvider = Provider<ItServiceRequestController>(
|
||||
(ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return ItServiceRequestController(client);
|
||||
return ItServiceRequestController(client, ref);
|
||||
},
|
||||
);
|
||||
|
||||
class ItServiceRequestController {
|
||||
ItServiceRequestController(this._client);
|
||||
ItServiceRequestController(this._client, [this._ref]);
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
/// Creates a new IT Service Request with auto-generated number.
|
||||
///
|
||||
/// Returns `{'id': ..., 'request_number': ...}` on success. If offline,
|
||||
/// queues locally and returns `{'id': localId, 'request_number': null, 'pending': true}`.
|
||||
Future<Map<String, dynamic>> createRequest({
|
||||
required String eventName,
|
||||
required List<String> services,
|
||||
@@ -243,18 +454,22 @@ class ItServiceRequestController {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
|
||||
final offlineId = const Uuid().v4();
|
||||
final rpcParams = {
|
||||
'p_id': offlineId,
|
||||
'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,
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
params: rpcParams,
|
||||
);
|
||||
|
||||
final row = result is List
|
||||
@@ -269,7 +484,6 @@ class ItServiceRequestController {
|
||||
.eq('id', requestId);
|
||||
}
|
||||
|
||||
// Activity log
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': userId,
|
||||
@@ -278,12 +492,36 @@ class ItServiceRequestController {
|
||||
|
||||
return {'id': requestId, 'request_number': row['request_number']};
|
||||
} catch (e) {
|
||||
debugPrint('createRequest error: $e');
|
||||
rethrow;
|
||||
if (!isOfflineSaveError(e)) {
|
||||
debugPrint('createRequest error: $e');
|
||||
rethrow;
|
||||
}
|
||||
|
||||
final now = DateTime.now().toUtc();
|
||||
final local = ItServiceRequest(
|
||||
id: offlineId,
|
||||
services: services,
|
||||
eventName: eventName,
|
||||
status: status,
|
||||
outsidePremiseAllowed: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
creatorId: userId,
|
||||
officeId: officeId,
|
||||
requestedBy: requestedBy,
|
||||
requestedByUserId: requestedByUserId,
|
||||
servicesOther: servicesOther,
|
||||
);
|
||||
|
||||
_queuePendingRequest(local, rpcParams);
|
||||
debugPrint(
|
||||
'[ItServiceRequestController] createRequest queued offline: $offlineId',
|
||||
);
|
||||
return {'id': offlineId, 'request_number': null, 'pending': true};
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates IT Service Request fields.
|
||||
/// Updates IT Service Request fields. Offline: queues the field patch.
|
||||
Future<void> updateRequest({
|
||||
required String requestId,
|
||||
String? eventName,
|
||||
@@ -345,22 +583,26 @@ class ItServiceRequestController {
|
||||
|
||||
if (updates.isEmpty) return;
|
||||
|
||||
await _client
|
||||
.from('it_service_requests')
|
||||
.update(updates)
|
||||
.eq('id', requestId);
|
||||
try {
|
||||
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()},
|
||||
});
|
||||
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()},
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueRequestUpdate(requestId, updates);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update only the status of an IT Service Request.
|
||||
/// Update only the status of an IT Service Request. Offline: queues.
|
||||
Future<void> updateStatus({
|
||||
required String requestId,
|
||||
required String status,
|
||||
@@ -383,44 +625,56 @@ class ItServiceRequestController {
|
||||
}
|
||||
}
|
||||
|
||||
await _client
|
||||
.from('it_service_requests')
|
||||
.update(updates)
|
||||
.eq('id', requestId);
|
||||
try {
|
||||
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},
|
||||
});
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': userId,
|
||||
'action_type': 'status_changed',
|
||||
'meta': {'status': status},
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueRequestUpdate(requestId, updates);
|
||||
}
|
||||
}
|
||||
|
||||
/// Approve a request (admin only). Sets status to 'scheduled'.
|
||||
/// Approve a request (admin only). Offline: queues the status patch.
|
||||
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);
|
||||
final updates = <String, dynamic>{
|
||||
'status': 'scheduled',
|
||||
'approved_by': approverName,
|
||||
'approved_by_user_id': userId,
|
||||
'approved_at': DateTime.now().toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
await _client.from('it_service_request_activity_logs').insert({
|
||||
'request_id': requestId,
|
||||
'actor_id': userId,
|
||||
'action_type': 'approved',
|
||||
});
|
||||
try {
|
||||
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': 'approved',
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueRequestUpdate(requestId, updates);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Assignment management
|
||||
// Assignment management (online-only — admin/dispatcher function)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Future<void> assignStaff({
|
||||
@@ -463,7 +717,7 @@ class ItServiceRequestController {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Action Taken
|
||||
// Action Taken — offline-safe
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
Future<String> createOrUpdateAction({
|
||||
@@ -473,31 +727,44 @@ class ItServiceRequestController {
|
||||
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();
|
||||
final actionId = const Uuid().v4();
|
||||
|
||||
if (existing != null) {
|
||||
await _client
|
||||
try {
|
||||
final existing = 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;
|
||||
.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({
|
||||
'id': actionId,
|
||||
'request_id': requestId,
|
||||
'user_id': userId,
|
||||
'action_taken': actionTaken,
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
return result['id'] as String;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queuePendingAction({
|
||||
'id': actionId,
|
||||
'request_id': requestId,
|
||||
'user_id': userId,
|
||||
'action_taken': actionTaken,
|
||||
});
|
||||
return actionId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,7 +817,6 @@ class ItServiceRequestController {
|
||||
}
|
||||
|
||||
Future<void> deleteEvidence({required String evidenceId}) async {
|
||||
// Get path first
|
||||
final row = await _client
|
||||
.from('it_service_request_evidence')
|
||||
.select('file_path')
|
||||
@@ -630,7 +896,6 @@ class ItServiceRequestController {
|
||||
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')
|
||||
@@ -651,4 +916,151 @@ class ItServiceRequestController {
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Queue helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
void _queuePendingRequest(
|
||||
ItServiceRequest model,
|
||||
Map<String, dynamic> rpcParams,
|
||||
) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
|
||||
final currentModels = List<ItServiceRequest>.from(
|
||||
ref.read(offlinePendingItServiceRequestsProvider),
|
||||
);
|
||||
currentModels.add(model);
|
||||
ref.read(offlinePendingItServiceRequestsProvider.notifier).state =
|
||||
List.unmodifiable(currentModels);
|
||||
|
||||
final currentRaw = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingItServiceRequestRawProvider),
|
||||
);
|
||||
currentRaw.add(rpcParams);
|
||||
ref.read(offlinePendingItServiceRequestRawProvider.notifier).state =
|
||||
List.unmodifiable(currentRaw);
|
||||
|
||||
mirrorBatchToBrick<ItServiceRequest>([model], tag: 'offline_isr');
|
||||
}
|
||||
|
||||
void _queueRequestUpdate(String requestId, Map<String, dynamic> fields) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingItServiceRequestUpdatesProvider),
|
||||
);
|
||||
current[requestId] = {...?current[requestId], ...fields};
|
||||
ref.read(offlinePendingItServiceRequestUpdatesProvider.notifier).state =
|
||||
current;
|
||||
}
|
||||
|
||||
void _queuePendingAction(Map<String, dynamic> action) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingIsrActionsProvider),
|
||||
);
|
||||
// Replace existing entry for the same request+user if present
|
||||
final existing = current.indexWhere(
|
||||
(a) =>
|
||||
a['request_id'] == action['request_id'] &&
|
||||
a['user_id'] == action['user_id'],
|
||||
);
|
||||
if (existing >= 0) {
|
||||
current[existing] = action;
|
||||
} else {
|
||||
current.add(action);
|
||||
}
|
||||
ref.read(offlinePendingIsrActionsProvider.notifier).state =
|
||||
List.unmodifiable(current);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Field-patch helper (avoids copyWith pattern on a model with 25+ fields)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ItServiceRequest _applyUpdateFields(
|
||||
ItServiceRequest r,
|
||||
Map<String, dynamic> f,
|
||||
) {
|
||||
DateTime? parseOpt(String key) {
|
||||
if (!f.containsKey(key)) return null;
|
||||
final v = f[key];
|
||||
return v == null ? null : AppTime.parse(v as String);
|
||||
}
|
||||
|
||||
return ItServiceRequest(
|
||||
id: r.id,
|
||||
requestNumber: r.requestNumber,
|
||||
services: f.containsKey('services')
|
||||
? (f['services'] as List<dynamic>).cast<String>()
|
||||
: r.services,
|
||||
servicesOther: f.containsKey('services_other')
|
||||
? f['services_other'] as String?
|
||||
: r.servicesOther,
|
||||
eventName: (f['event_name'] as String?) ?? r.eventName,
|
||||
eventDetails: f.containsKey('event_details')
|
||||
? f['event_details'] as String?
|
||||
: r.eventDetails,
|
||||
eventDate:
|
||||
f.containsKey('event_date') ? parseOpt('event_date') : r.eventDate,
|
||||
eventEndDate: f.containsKey('event_end_date')
|
||||
? parseOpt('event_end_date')
|
||||
: r.eventEndDate,
|
||||
dryRunDate: f.containsKey('dry_run_date')
|
||||
? parseOpt('dry_run_date')
|
||||
: r.dryRunDate,
|
||||
dryRunEndDate: f.containsKey('dry_run_end_date')
|
||||
? parseOpt('dry_run_end_date')
|
||||
: r.dryRunEndDate,
|
||||
contactPerson: f.containsKey('contact_person')
|
||||
? f['contact_person'] as String?
|
||||
: r.contactPerson,
|
||||
contactNumber: f.containsKey('contact_number')
|
||||
? f['contact_number'] as String?
|
||||
: r.contactNumber,
|
||||
remarks:
|
||||
f.containsKey('remarks') ? f['remarks'] as String? : r.remarks,
|
||||
officeId:
|
||||
f.containsKey('office_id') ? f['office_id'] as String? : r.officeId,
|
||||
requestedBy: f.containsKey('requested_by')
|
||||
? f['requested_by'] as String?
|
||||
: r.requestedBy,
|
||||
requestedByUserId: f.containsKey('requested_by_user_id')
|
||||
? f['requested_by_user_id'] as String?
|
||||
: r.requestedByUserId,
|
||||
approvedBy: f.containsKey('approved_by')
|
||||
? f['approved_by'] as String?
|
||||
: r.approvedBy,
|
||||
approvedByUserId: f.containsKey('approved_by_user_id')
|
||||
? f['approved_by_user_id'] as String?
|
||||
: r.approvedByUserId,
|
||||
approvedAt: f.containsKey('approved_at')
|
||||
? parseOpt('approved_at')
|
||||
: r.approvedAt,
|
||||
status: (f['status'] as String?) ?? r.status,
|
||||
outsidePremiseAllowed:
|
||||
(f['outside_premise_allowed'] as bool?) ?? r.outsidePremiseAllowed,
|
||||
cancellationReason: f.containsKey('cancellation_reason')
|
||||
? f['cancellation_reason'] as String?
|
||||
: r.cancellationReason,
|
||||
cancelledAt: f.containsKey('cancelled_at')
|
||||
? parseOpt('cancelled_at')
|
||||
: r.cancelledAt,
|
||||
creatorId: r.creatorId,
|
||||
createdAt: r.createdAt,
|
||||
updatedAt: r.updatedAt,
|
||||
completedAt: f.containsKey('completed_at')
|
||||
? parseOpt('completed_at')
|
||||
: r.completedAt,
|
||||
dateTimeReceived: f.containsKey('date_time_received')
|
||||
? parseOpt('date_time_received')
|
||||
: r.dateTimeReceived,
|
||||
dateTimeChecked: f.containsKey('date_time_checked')
|
||||
? parseOpt('date_time_checked')
|
||||
: r.dateTimeChecked,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,42 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/leave_of_absence.dart';
|
||||
import '../utils/app_time.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'connectivity_provider.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Leaves filed offline that haven't been confirmed by Supabase yet. Merged
|
||||
/// into [leavesProvider] so the UI shows them immediately. Cleared as the
|
||||
/// realtime stream confirms each id.
|
||||
final offlinePendingLeavesProvider =
|
||||
StateProvider<List<LeaveOfAbsence>>((ref) => const []);
|
||||
|
||||
/// Status changes (approve/reject/cancel) made offline, keyed by leave id.
|
||||
/// On reconnect the listener PATCHes each entry and clears the map.
|
||||
final offlinePendingLeaveUpdatesProvider =
|
||||
StateProvider<Map<String, Map<String, dynamic>>>((ref) => const {});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// All visible leaves (own for standard, all for admin/dispatcher/it_staff).
|
||||
///
|
||||
/// Consumers should **not** treat every record as an active absence; the UI
|
||||
/// layers (dashboard, logbook) explicitly filter to `status == 'approved'` and
|
||||
/// verify the leave overlaps the current time. This prevents rejected or
|
||||
/// pending applications from inadvertently influencing schedules or status
|
||||
/// computations.
|
||||
/// verify the leave overlaps the current time.
|
||||
final leavesProvider = StreamProvider<List<LeaveOfAbsence>>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final profileAsync = ref.watch(currentProfileProvider);
|
||||
@@ -27,6 +49,40 @@ final leavesProvider = StreamProvider<List<LeaveOfAbsence>>((ref) {
|
||||
profile.role == 'dispatcher' ||
|
||||
profile.role == 'it_staff';
|
||||
|
||||
// Watch pending state — provider rebuilds when offline writes arrive so the
|
||||
// closed-over `pending*` values feed into `onOfflineData` synchronously.
|
||||
final pendingNew = ref.watch(offlinePendingLeavesProvider);
|
||||
final pendingUpdates = ref.watch(offlinePendingLeaveUpdatesProvider);
|
||||
|
||||
List<LeaveOfAbsence> applyPending(List<LeaveOfAbsence> rows) {
|
||||
final byId = {for (final r in rows) r.id: r};
|
||||
// Merge offline status updates onto existing leaves
|
||||
pendingUpdates.forEach((id, fields) {
|
||||
final existing = byId[id];
|
||||
if (existing == null) return;
|
||||
byId[id] = LeaveOfAbsence(
|
||||
id: existing.id,
|
||||
userId: existing.userId,
|
||||
leaveType: existing.leaveType,
|
||||
justification: existing.justification,
|
||||
startTime: existing.startTime,
|
||||
endTime: existing.endTime,
|
||||
status: (fields['status'] as String?) ?? existing.status,
|
||||
filedBy: existing.filedBy,
|
||||
createdAt: existing.createdAt,
|
||||
);
|
||||
});
|
||||
// Append offline-created leaves not yet on the server
|
||||
for (final p in pendingNew) {
|
||||
if (hasFullAccess || p.userId == profile.id) {
|
||||
byId.putIfAbsent(p.id, () => p);
|
||||
}
|
||||
}
|
||||
final merged = byId.values.toList()
|
||||
..sort((a, b) => b.startTime.compareTo(a.startTime));
|
||||
return merged;
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<LeaveOfAbsence>(
|
||||
stream: hasFullAccess
|
||||
? client
|
||||
@@ -50,24 +106,113 @@ final leavesProvider = StreamProvider<List<LeaveOfAbsence>>((ref) {
|
||||
fromMap: LeaveOfAbsence.fromMap,
|
||||
channelName: 'leave_of_absence',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<LeaveOfAbsence>();
|
||||
final filtered = hasFullAccess
|
||||
? all
|
||||
: all.where((l) => l.userId == profile.id).toList();
|
||||
return applyPending(filtered);
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<LeaveOfAbsence>(rows, tag: 'leave_of_absence'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
|
||||
// Reconnect-replay: directly POST queued items, bypassing Brick's queue
|
||||
// which can block on stale entries (504, deadlock, etc.).
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
|
||||
// 1. Replay offline-created leaves
|
||||
final pending = List<LeaveOfAbsence>.from(ref.read(offlinePendingLeavesProvider));
|
||||
if (pending.isNotEmpty) {
|
||||
debugPrint('[leavesProvider] reconnected — syncing ${pending.length} leave(s)');
|
||||
final synced = <String>[];
|
||||
for (final leave in pending) {
|
||||
try {
|
||||
await client.from('leave_of_absence').insert({
|
||||
'id': leave.id,
|
||||
'user_id': leave.userId,
|
||||
'leave_type': leave.leaveType,
|
||||
'justification': leave.justification,
|
||||
'start_time': leave.startTime.toIso8601String(),
|
||||
'end_time': leave.endTime.toIso8601String(),
|
||||
'status': leave.status,
|
||||
'filed_by': leave.filedBy,
|
||||
});
|
||||
synced.add(leave.id);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
synced.add(leave.id); // already in DB — drop pending entry
|
||||
} else {
|
||||
debugPrint('[leavesProvider] failed to sync leave id=${leave.id}: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = ref.read(offlinePendingLeavesProvider)
|
||||
.where((l) => !synced.contains(l.id))
|
||||
.toList();
|
||||
ref.read(offlinePendingLeavesProvider.notifier).state =
|
||||
List.unmodifiable(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Replay offline status updates
|
||||
final pendingMap = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingLeaveUpdatesProvider),
|
||||
);
|
||||
if (pendingMap.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[leavesProvider] reconnected — syncing ${pendingMap.length} status edit(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final entry in pendingMap.entries) {
|
||||
try {
|
||||
await client
|
||||
.from('leave_of_absence')
|
||||
.update(entry.value)
|
||||
.eq('id', entry.key);
|
||||
syncedIds.add(entry.key);
|
||||
} catch (e) {
|
||||
debugPrint('[leavesProvider] failed to sync edit id=${entry.key}: $e');
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remaining = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingLeaveUpdatesProvider),
|
||||
);
|
||||
for (final id in syncedIds) {
|
||||
remaining.remove(id);
|
||||
}
|
||||
ref.read(offlinePendingLeaveUpdatesProvider.notifier).state = remaining;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final leaveControllerProvider = Provider<LeaveController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return LeaveController(client);
|
||||
return LeaveController(client, ref);
|
||||
});
|
||||
|
||||
class LeaveController {
|
||||
LeaveController(this._client);
|
||||
LeaveController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
/// File a leave of absence for the current user.
|
||||
/// Caller controls auto-approval based on role policy.
|
||||
/// Offline: queues a local row + raw payload, returns silently.
|
||||
Future<void> fileLeave({
|
||||
required String leaveType,
|
||||
required String justification,
|
||||
@@ -76,43 +221,90 @@ class LeaveController {
|
||||
required bool autoApprove,
|
||||
}) async {
|
||||
final uid = _client.auth.currentUser!.id;
|
||||
final id = const Uuid().v4();
|
||||
final status = autoApprove ? 'approved' : 'pending';
|
||||
final payload = {
|
||||
'id': id,
|
||||
'user_id': uid,
|
||||
'leave_type': leaveType,
|
||||
'justification': justification,
|
||||
'start_time': startTime.toIso8601String(),
|
||||
'end_time': endTime.toIso8601String(),
|
||||
'status': autoApprove ? 'approved' : 'pending',
|
||||
'status': status,
|
||||
'filed_by': uid,
|
||||
};
|
||||
|
||||
final insertedRaw = await _client
|
||||
.from('leave_of_absence')
|
||||
.insert(payload)
|
||||
.select()
|
||||
.maybeSingle();
|
||||
final Map<String, dynamic>? inserted = insertedRaw is Map<String, dynamic>
|
||||
? insertedRaw
|
||||
: null;
|
||||
Map<String, dynamic>? inserted;
|
||||
try {
|
||||
final insertedRaw = await _client
|
||||
.from('leave_of_absence')
|
||||
.insert(payload)
|
||||
.select()
|
||||
.maybeSingle();
|
||||
inserted = insertedRaw is Map<String, dynamic> ? insertedRaw : null;
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queuePendingLeave(
|
||||
LeaveOfAbsence(
|
||||
id: id,
|
||||
userId: uid,
|
||||
leaveType: leaveType,
|
||||
justification: justification,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
status: status,
|
||||
filedBy: uid,
|
||||
createdAt: AppTime.now(),
|
||||
),
|
||||
);
|
||||
return; // Don't try to notify admins — we're offline.
|
||||
}
|
||||
|
||||
// If this was filed as pending, notify admins for approval
|
||||
final status = payload['status'] as String;
|
||||
if (status != 'pending') return;
|
||||
await _notifyAdminsOfFiling(inserted: inserted, leaveId: id, actorId: uid);
|
||||
}
|
||||
|
||||
void _queuePendingLeave(LeaveOfAbsence leave) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = List<LeaveOfAbsence>.from(
|
||||
ref.read(offlinePendingLeavesProvider),
|
||||
);
|
||||
current.add(leave);
|
||||
ref.read(offlinePendingLeavesProvider.notifier).state =
|
||||
List.unmodifiable(current);
|
||||
mirrorBatchToBrick<LeaveOfAbsence>([leave], tag: 'offline_leave');
|
||||
}
|
||||
|
||||
void _queueLeaveStatusUpdate(String leaveId, Map<String, dynamic> fields) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingLeaveUpdatesProvider),
|
||||
);
|
||||
current[leaveId] = {...?current[leaveId], ...fields};
|
||||
ref.read(offlinePendingLeaveUpdatesProvider.notifier).state = current;
|
||||
}
|
||||
|
||||
Future<void> _notifyAdminsOfFiling({
|
||||
required Map<String, dynamic>? inserted,
|
||||
required String leaveId,
|
||||
required String actorId,
|
||||
}) async {
|
||||
try {
|
||||
final adminIds = await _fetchRoleUserIds(
|
||||
roles: const ['admin'],
|
||||
excludeUserId: uid,
|
||||
excludeUserId: actorId,
|
||||
);
|
||||
if (adminIds.isEmpty) return;
|
||||
|
||||
// Resolve actor display name for nicer push text
|
||||
String actorName = 'Someone';
|
||||
try {
|
||||
final p = await _client
|
||||
.from('profiles')
|
||||
.select('full_name,display_name,name')
|
||||
.eq('id', uid)
|
||||
.eq('id', actorId)
|
||||
.maybeSingle();
|
||||
if (p != null) {
|
||||
if (p['full_name'] != null) {
|
||||
@@ -125,32 +317,24 @@ class LeaveController {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
final leaveId = (inserted ?? <String, dynamic>{})['id']?.toString() ?? '';
|
||||
final title = 'Leave Filed for Approval';
|
||||
final body = '$actorName filed a leave request that requires approval.';
|
||||
final notificationId = (inserted ?? <String, dynamic>{})['id']
|
||||
?.toString();
|
||||
final notificationId = (inserted ?? <String, dynamic>{})['id']?.toString();
|
||||
|
||||
final dataPayload = <String, dynamic>{
|
||||
'type': 'leave_filed',
|
||||
'leave_id': leaveId,
|
||||
...?(notificationId != null
|
||||
? {'notification_id': notificationId}
|
||||
: null),
|
||||
...?(notificationId != null ? {'notification_id': notificationId} : null),
|
||||
};
|
||||
|
||||
await _client
|
||||
.from('notifications')
|
||||
.insert(
|
||||
await _client.from('notifications').insert(
|
||||
adminIds
|
||||
.map(
|
||||
(userId) => {
|
||||
'user_id': userId,
|
||||
'actor_id': uid,
|
||||
'type': 'leave_filed',
|
||||
'leave_id': leaveId,
|
||||
},
|
||||
)
|
||||
.map((userId) => {
|
||||
'user_id': userId,
|
||||
'actor_id': actorId,
|
||||
'type': 'leave_filed',
|
||||
'leave_id': leaveId,
|
||||
})
|
||||
.toList(),
|
||||
);
|
||||
|
||||
@@ -175,14 +359,21 @@ class LeaveController {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
|
||||
// Update status first; then notify the requester.
|
||||
await _client
|
||||
.from('leave_of_absence')
|
||||
.update({'status': 'approved'})
|
||||
.eq('id', leaveId);
|
||||
try {
|
||||
await _client
|
||||
.from('leave_of_absence')
|
||||
.update({'status': 'approved'})
|
||||
.eq('id', leaveId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueLeaveStatusUpdate(leaveId, {'status': 'approved'});
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify requestor
|
||||
await _notifyRequester(leaveId: leaveId, actorId: userId, approved: true);
|
||||
// Notify requestor (best-effort)
|
||||
try {
|
||||
await _notifyRequester(leaveId: leaveId, actorId: userId, approved: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Reject a leave request.
|
||||
@@ -190,13 +381,20 @@ class LeaveController {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
|
||||
await _client
|
||||
.from('leave_of_absence')
|
||||
.update({'status': 'rejected'})
|
||||
.eq('id', leaveId);
|
||||
try {
|
||||
await _client
|
||||
.from('leave_of_absence')
|
||||
.update({'status': 'rejected'})
|
||||
.eq('id', leaveId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueLeaveStatusUpdate(leaveId, {'status': 'rejected'});
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify requestor
|
||||
await _notifyRequester(leaveId: leaveId, actorId: userId, approved: false);
|
||||
try {
|
||||
await _notifyRequester(leaveId: leaveId, actorId: userId, approved: false);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _notifyRequester({
|
||||
@@ -266,10 +464,15 @@ class LeaveController {
|
||||
|
||||
/// Cancel an approved leave.
|
||||
Future<void> cancelLeave(String leaveId) async {
|
||||
await _client
|
||||
.from('leave_of_absence')
|
||||
.update({'status': 'cancelled'})
|
||||
.eq('id', leaveId);
|
||||
try {
|
||||
await _client
|
||||
.from('leave_of_absence')
|
||||
.update({'status': 'cancelled'})
|
||||
.eq('id', leaveId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueLeaveStatusUpdate(leaveId, {'status': 'cancelled'});
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _fetchRoleUserIds({
|
||||
|
||||
@@ -3,20 +3,65 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../utils/device_id.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/notification_item.dart';
|
||||
import '../utils/app_time.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'connectivity_provider.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
import '../utils/app_time.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// IDs of notifications marked as read offline (optimistic update).
|
||||
/// On reconnect, batch-patched and cleared.
|
||||
final offlinePendingNotificationReadsProvider =
|
||||
StateProvider<Set<String>>((ref) => const {});
|
||||
|
||||
/// Batch read-mark operations queued offline.
|
||||
/// Each entry: {'filter_type': 'ticket'|'task', 'filter_id': ..., 'user_id': ...}
|
||||
final offlinePendingBatchNotificationReadsProvider =
|
||||
StateProvider<List<Map<String, dynamic>>>((ref) => const []);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final notificationsProvider = StreamProvider<List<NotificationItem>>((ref) {
|
||||
final userId = ref.watch(currentUserIdProvider);
|
||||
if (userId == null) {
|
||||
return const Stream.empty();
|
||||
}
|
||||
if (userId == null) return const Stream.empty();
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
final pendingReadIds = ref.watch(offlinePendingNotificationReadsProvider);
|
||||
|
||||
List<NotificationItem> applyPending(List<NotificationItem> rows) {
|
||||
if (pendingReadIds.isEmpty) return rows;
|
||||
return rows.map((n) {
|
||||
if (pendingReadIds.contains(n.id) && n.readAt == null) {
|
||||
return NotificationItem(
|
||||
id: n.id,
|
||||
userId: n.userId,
|
||||
actorId: n.actorId,
|
||||
ticketId: n.ticketId,
|
||||
taskId: n.taskId,
|
||||
leaveId: n.leaveId,
|
||||
passSlipId: n.passSlipId,
|
||||
itServiceRequestId: n.itServiceRequestId,
|
||||
announcementId: n.announcementId,
|
||||
messageId: n.messageId,
|
||||
type: n.type,
|
||||
createdAt: n.createdAt,
|
||||
readAt: AppTime.now(),
|
||||
);
|
||||
}
|
||||
return n;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<NotificationItem>(
|
||||
stream: client
|
||||
.from('notifications')
|
||||
@@ -34,10 +79,87 @@ final notificationsProvider = StreamProvider<List<NotificationItem>>((ref) {
|
||||
fromMap: NotificationItem.fromMap,
|
||||
channelName: 'notifications',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<NotificationItem>();
|
||||
final filtered = all.where((n) => n.userId == userId).toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return applyPending(filtered);
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<NotificationItem>(rows, tag: 'notifications'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
|
||||
// Reconnect-replay: batch-patch read status for queued marks.
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
|
||||
// 1. Batch-mark individual notification reads
|
||||
final pendingIds =
|
||||
Set<String>.from(ref.read(offlinePendingNotificationReadsProvider));
|
||||
if (pendingIds.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[notificationsProvider] reconnected — marking ${pendingIds.length} notification(s) read',
|
||||
);
|
||||
try {
|
||||
await client
|
||||
.from('notifications')
|
||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||
.inFilter('id', pendingIds.toList())
|
||||
.filter('read_at', 'is', null);
|
||||
ref.read(offlinePendingNotificationReadsProvider.notifier).state =
|
||||
const {};
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[notificationsProvider] failed to sync pending reads: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Replay batch read-marks for tickets and tasks
|
||||
final pendingBatch = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingBatchNotificationReadsProvider),
|
||||
);
|
||||
if (pendingBatch.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[notificationsProvider] reconnected — syncing ${pendingBatch.length} batch read(s)',
|
||||
);
|
||||
final synced = <int>[];
|
||||
for (var i = 0; i < pendingBatch.length; i++) {
|
||||
final entry = pendingBatch[i];
|
||||
final filterType = entry['filter_type'] as String;
|
||||
final filterId = entry['filter_id'] as String;
|
||||
final uid = entry['user_id'] as String;
|
||||
try {
|
||||
final query = client
|
||||
.from('notifications')
|
||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||
.eq('user_id', uid)
|
||||
.filter('read_at', 'is', null);
|
||||
if (filterType == 'ticket') {
|
||||
await query.eq('ticket_id', filterId);
|
||||
} else {
|
||||
await query.eq('task_id', filterId);
|
||||
}
|
||||
synced.add(i);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[notificationsProvider] failed to sync batch read entry $i: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = pendingBatch
|
||||
.whereIndexed((i, _) => !synced.contains(i))
|
||||
.toList();
|
||||
ref.read(offlinePendingBatchNotificationReadsProvider.notifier).state =
|
||||
List.unmodifiable(remaining);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
final unreadNotificationsCountProvider = Provider<int>((ref) {
|
||||
@@ -52,18 +174,15 @@ final notificationsControllerProvider = Provider<NotificationsController>((
|
||||
ref,
|
||||
) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return NotificationsController(client);
|
||||
return NotificationsController(client, ref);
|
||||
});
|
||||
|
||||
class NotificationsController {
|
||||
NotificationsController(this._client);
|
||||
NotificationsController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
/// Internal helper that inserts notification rows and sends pushes if
|
||||
/// [targetUserIds] is provided.
|
||||
/// Internal helper that inserts notification rows and optionally sends
|
||||
/// FCM pushes. Callers should use [createNotification] instead.
|
||||
Future<void> _createAndPush(
|
||||
List<Map<String, dynamic>> rows, {
|
||||
List<String>? targetUserIds,
|
||||
@@ -77,9 +196,6 @@ class NotificationsController {
|
||||
);
|
||||
await _client.from('notifications').insert(rows);
|
||||
|
||||
// If target user IDs are provided, invoke client-side push
|
||||
// flow by calling `sendPush`. We no longer rely on the DB trigger
|
||||
// to call the edge function.
|
||||
if (targetUserIds == null || targetUserIds.isEmpty) return;
|
||||
|
||||
debugPrint('notification rows inserted; invoking client push');
|
||||
@@ -95,11 +211,6 @@ class NotificationsController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a typed notification in the database. This method handles
|
||||
/// inserting the row(s); a PostgreSQL trigger will forward the new row to
|
||||
/// the `send_fcm` edge function, so clients do **not** directly invoke it
|
||||
/// (avoids CORS/auth problems on web). The [pushTitle]/[pushBody] values
|
||||
/// are still stored for the trigger payload.
|
||||
Future<void> createNotification({
|
||||
required List<String> userIds,
|
||||
required String type,
|
||||
@@ -132,7 +243,6 @@ class NotificationsController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience for mention-specific case; left for compatibility.
|
||||
Future<void> createMentionNotifications({
|
||||
required List<String> userIds,
|
||||
required String actorId,
|
||||
@@ -158,36 +268,55 @@ class NotificationsController {
|
||||
);
|
||||
}
|
||||
|
||||
/// Mark a single notification as read. Offline: queues locally.
|
||||
Future<void> markRead(String id) async {
|
||||
await _client
|
||||
.from('notifications')
|
||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||
.eq('id', id);
|
||||
try {
|
||||
await _client
|
||||
.from('notifications')
|
||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||
.eq('id', id);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queuePendingRead(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark all unread notifications for a ticket as read. Offline: queues.
|
||||
Future<void> markReadForTicket(String ticketId) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) return;
|
||||
await _client
|
||||
.from('notifications')
|
||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||
.eq('ticket_id', ticketId)
|
||||
.eq('user_id', userId)
|
||||
.filter('read_at', 'is', null);
|
||||
try {
|
||||
await _client
|
||||
.from('notifications')
|
||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||
.eq('ticket_id', ticketId)
|
||||
.eq('user_id', userId)
|
||||
.filter('read_at', 'is', null);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queuePendingBatchRead(
|
||||
filterType: 'ticket', filterId: ticketId, userId: userId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark all unread notifications for a task as read. Offline: queues.
|
||||
Future<void> markReadForTask(String taskId) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) return;
|
||||
await _client
|
||||
.from('notifications')
|
||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||
.eq('task_id', taskId)
|
||||
.eq('user_id', userId)
|
||||
.filter('read_at', 'is', null);
|
||||
try {
|
||||
await _client
|
||||
.from('notifications')
|
||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||
.eq('task_id', taskId)
|
||||
.eq('user_id', userId)
|
||||
.filter('read_at', 'is', null);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queuePendingBatchRead(
|
||||
filterType: 'task', filterId: taskId, userId: userId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Store or update an FCM token for the current user.
|
||||
Future<void> registerFcmToken(String token) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) return;
|
||||
@@ -196,8 +325,6 @@ class NotificationsController {
|
||||
debugPrint('registerFcmToken: device id missing; skipping');
|
||||
return;
|
||||
}
|
||||
// upsert using a unique constraint on (user_id, device_id) so a single
|
||||
// device keeps its token updated without overwriting other devices.
|
||||
final payload = {
|
||||
'user_id': userId,
|
||||
'device_id': deviceId,
|
||||
@@ -207,19 +334,16 @@ class NotificationsController {
|
||||
final res = await _client.from('fcm_tokens').upsert(payload);
|
||||
final dyn = res as dynamic;
|
||||
if (dyn.error != null) {
|
||||
// duplicate key or RLS issue - just log it
|
||||
debugPrint('registerFcmToken error: ${dyn.error?.message ?? dyn.error}');
|
||||
} else {
|
||||
debugPrint('registerFcmToken success for user=$userId token=$token');
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an FCM token (e.g. when the user logs out or uninstalls).
|
||||
Future<void> unregisterFcmToken(String token) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) return;
|
||||
final deviceId = await DeviceId.getId();
|
||||
// Prefer to delete by device_id to avoid removing other devices' tokens.
|
||||
final res = await _client
|
||||
.from('fcm_tokens')
|
||||
.delete()
|
||||
@@ -234,7 +358,6 @@ class NotificationsController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a push message via the `send_fcm` edge function.
|
||||
Future<void> sendPush({
|
||||
List<String>? tokens,
|
||||
List<String>? userIds,
|
||||
@@ -270,4 +393,52 @@ class NotificationsController {
|
||||
debugPrint('sendPush invocation error: $err');
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Queue helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
void _queuePendingRead(String id) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Set<String>.from(
|
||||
ref.read(offlinePendingNotificationReadsProvider),
|
||||
);
|
||||
current.add(id);
|
||||
ref.read(offlinePendingNotificationReadsProvider.notifier).state =
|
||||
Set.unmodifiable(current);
|
||||
}
|
||||
|
||||
void _queuePendingBatchRead({
|
||||
required String filterType,
|
||||
required String filterId,
|
||||
required String userId,
|
||||
}) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingBatchNotificationReadsProvider),
|
||||
);
|
||||
current.add({
|
||||
'filter_type': filterType,
|
||||
'filter_id': filterId,
|
||||
'user_id': userId,
|
||||
});
|
||||
ref.read(offlinePendingBatchNotificationReadsProvider.notifier).state =
|
||||
List.unmodifiable(current);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension helper for indexed iteration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extension _IndexedWhere<T> on List<T> {
|
||||
List<T> whereIndexed(bool Function(int index, T item) test) {
|
||||
final result = <T>[];
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (test(i, this[i])) result.add(this[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/pass_slip.dart';
|
||||
import '../utils/app_time.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'connectivity_provider.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Pass slips filed offline that haven't been confirmed by Supabase yet.
|
||||
final offlinePendingPassSlipsProvider =
|
||||
StateProvider<List<PassSlip>>((ref) => const []);
|
||||
|
||||
/// Status edits (approve/reject/complete) made offline, keyed by slip id.
|
||||
final offlinePendingPassSlipUpdatesProvider =
|
||||
StateProvider<Map<String, Map<String, dynamic>>>((ref) => const {});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// All visible pass slips (own for staff, all for admin/dispatcher).
|
||||
final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
@@ -18,6 +39,50 @@ final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
||||
final isAdmin = profile.role == 'admin' || profile.role == 'dispatcher';
|
||||
final hasFullAccess = isAdmin || profile.role == 'programmer';
|
||||
|
||||
final pendingNew = ref.watch(offlinePendingPassSlipsProvider);
|
||||
final pendingUpdates = ref.watch(offlinePendingPassSlipUpdatesProvider);
|
||||
|
||||
List<PassSlip> applyPending(List<PassSlip> rows) {
|
||||
final byId = {for (final r in rows) r.id: r};
|
||||
pendingUpdates.forEach((id, fields) {
|
||||
final existing = byId[id];
|
||||
if (existing == null) return;
|
||||
byId[id] = PassSlip(
|
||||
id: existing.id,
|
||||
userId: existing.userId,
|
||||
dutyScheduleId: existing.dutyScheduleId,
|
||||
reason: existing.reason,
|
||||
status: (fields['status'] as String?) ?? existing.status,
|
||||
requestedAt: existing.requestedAt,
|
||||
approvedBy: (fields['approved_by'] as String?) ?? existing.approvedBy,
|
||||
approvedAt: fields.containsKey('approved_at')
|
||||
? (fields['approved_at'] is String
|
||||
? AppTime.parse(fields['approved_at'] as String)
|
||||
: existing.approvedAt)
|
||||
: existing.approvedAt,
|
||||
slipStart: fields.containsKey('slip_start')
|
||||
? (fields['slip_start'] is String
|
||||
? AppTime.parse(fields['slip_start'] as String)
|
||||
: existing.slipStart)
|
||||
: existing.slipStart,
|
||||
slipEnd: fields.containsKey('slip_end')
|
||||
? (fields['slip_end'] is String
|
||||
? AppTime.parse(fields['slip_end'] as String)
|
||||
: existing.slipEnd)
|
||||
: existing.slipEnd,
|
||||
requestedStart: existing.requestedStart,
|
||||
);
|
||||
});
|
||||
for (final p in pendingNew) {
|
||||
if (hasFullAccess || p.userId == profile.id) {
|
||||
byId.putIfAbsent(p.id, () => p);
|
||||
}
|
||||
}
|
||||
final merged = byId.values.toList()
|
||||
..sort((a, b) => b.requestedAt.compareTo(a.requestedAt));
|
||||
return merged;
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<PassSlip>(
|
||||
stream: hasFullAccess
|
||||
? client
|
||||
@@ -41,10 +106,90 @@ final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
||||
fromMap: PassSlip.fromMap,
|
||||
channelName: 'pass_slips',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<PassSlip>();
|
||||
final filtered = hasFullAccess
|
||||
? all
|
||||
: all.where((s) => s.userId == profile.id).toList();
|
||||
return applyPending(filtered);
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<PassSlip>(rows, tag: 'pass_slips'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
|
||||
// Reconnect-replay
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
|
||||
final pending = List<PassSlip>.from(ref.read(offlinePendingPassSlipsProvider));
|
||||
if (pending.isNotEmpty) {
|
||||
debugPrint('[passSlipsProvider] reconnected — syncing ${pending.length} slip(s)');
|
||||
final synced = <String>[];
|
||||
for (final slip in pending) {
|
||||
try {
|
||||
await client.from('pass_slips').insert({
|
||||
'id': slip.id,
|
||||
'user_id': slip.userId,
|
||||
'duty_schedule_id': slip.dutyScheduleId,
|
||||
'reason': slip.reason,
|
||||
'status': slip.status,
|
||||
'requested_at': slip.requestedAt.toIso8601String(),
|
||||
if (slip.requestedStart != null)
|
||||
'requested_start': slip.requestedStart!.toIso8601String(),
|
||||
});
|
||||
synced.add(slip.id);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
synced.add(slip.id);
|
||||
} else {
|
||||
debugPrint('[passSlipsProvider] failed to sync slip id=${slip.id}: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = ref.read(offlinePendingPassSlipsProvider)
|
||||
.where((s) => !synced.contains(s.id))
|
||||
.toList();
|
||||
ref.read(offlinePendingPassSlipsProvider.notifier).state =
|
||||
List.unmodifiable(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
final pendingMap = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingPassSlipUpdatesProvider),
|
||||
);
|
||||
if (pendingMap.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[passSlipsProvider] reconnected — syncing ${pendingMap.length} status edit(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final entry in pendingMap.entries) {
|
||||
try {
|
||||
await client
|
||||
.from('pass_slips')
|
||||
.update(entry.value)
|
||||
.eq('id', entry.key);
|
||||
syncedIds.add(entry.key);
|
||||
} catch (e) {
|
||||
debugPrint('[passSlipsProvider] failed to sync edit id=${entry.key}: $e');
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remaining = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingPassSlipUpdatesProvider),
|
||||
);
|
||||
for (final id in syncedIds) {
|
||||
remaining.remove(id);
|
||||
}
|
||||
ref.read(offlinePendingPassSlipUpdatesProvider.notifier).state = remaining;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
/// Currently active pass slip for the logged-in user (approved, not completed).
|
||||
@@ -65,15 +210,42 @@ final activePassSlipsProvider = Provider<List<PassSlip>>((ref) {
|
||||
return slips.where((s) => s.isActive).toList();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final passSlipControllerProvider = Provider<PassSlipController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return PassSlipController(client);
|
||||
return PassSlipController(client, ref);
|
||||
});
|
||||
|
||||
class PassSlipController {
|
||||
PassSlipController(this._client);
|
||||
PassSlipController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
void _queuePendingSlip(PassSlip slip) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = List<PassSlip>.from(
|
||||
ref.read(offlinePendingPassSlipsProvider),
|
||||
);
|
||||
current.add(slip);
|
||||
ref.read(offlinePendingPassSlipsProvider.notifier).state =
|
||||
List.unmodifiable(current);
|
||||
mirrorBatchToBrick<PassSlip>([slip], tag: 'offline_pass_slip');
|
||||
}
|
||||
|
||||
void _queueSlipStatusUpdate(String slipId, Map<String, dynamic> fields) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingPassSlipUpdatesProvider),
|
||||
);
|
||||
current[slipId] = {...?current[slipId], ...fields};
|
||||
ref.read(offlinePendingPassSlipUpdatesProvider.notifier).state = current;
|
||||
}
|
||||
|
||||
Future<void> requestSlip({
|
||||
required String dutyScheduleId,
|
||||
@@ -82,26 +254,42 @@ class PassSlipController {
|
||||
}) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
final id = const Uuid().v4();
|
||||
final now = DateTime.now().toUtc();
|
||||
final payload = {
|
||||
'id': id,
|
||||
'user_id': userId,
|
||||
'duty_schedule_id': dutyScheduleId,
|
||||
'reason': reason,
|
||||
'status': 'pending',
|
||||
'requested_at': DateTime.now().toUtc().toIso8601String(),
|
||||
'requested_at': now.toIso8601String(),
|
||||
if (requestedStart != null)
|
||||
'requested_start': requestedStart.toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
final insertedRaw = await _client
|
||||
.from('pass_slips')
|
||||
.insert(payload)
|
||||
.select()
|
||||
.maybeSingle();
|
||||
final Map<String, dynamic>? inserted = insertedRaw is Map<String, dynamic>
|
||||
? insertedRaw
|
||||
: null;
|
||||
Map<String, dynamic>? inserted;
|
||||
try {
|
||||
final insertedRaw = await _client
|
||||
.from('pass_slips')
|
||||
.insert(payload)
|
||||
.select()
|
||||
.maybeSingle();
|
||||
inserted = insertedRaw is Map<String, dynamic> ? insertedRaw : null;
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queuePendingSlip(PassSlip(
|
||||
id: id,
|
||||
userId: userId,
|
||||
dutyScheduleId: dutyScheduleId,
|
||||
reason: reason,
|
||||
status: 'pending',
|
||||
requestedAt: AppTime.toAppTime(now),
|
||||
requestedStart: requestedStart,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify admins for approval
|
||||
// Notify admins for approval (best-effort)
|
||||
try {
|
||||
final adminIds = await _fetchRoleUserIds(
|
||||
roles: const ['admin'],
|
||||
@@ -109,7 +297,6 @@ class PassSlipController {
|
||||
);
|
||||
if (adminIds.isEmpty) return;
|
||||
|
||||
// Resolve actor display name for nice push text
|
||||
String actorName = 'Someone';
|
||||
try {
|
||||
final p = await _client
|
||||
@@ -128,32 +315,25 @@ class PassSlipController {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
final slipId = (inserted ?? <String, dynamic>{})['id']?.toString() ?? '';
|
||||
final slipId = (inserted ?? <String, dynamic>{})['id']?.toString() ?? id;
|
||||
final title = 'Pass Slip Filed for Approval';
|
||||
final body = '$actorName filed a pass slip that requires approval.';
|
||||
final notificationId = (inserted ?? <String, dynamic>{})['id']
|
||||
?.toString();
|
||||
final notificationId = (inserted ?? <String, dynamic>{})['id']?.toString();
|
||||
|
||||
final dataPayload = <String, dynamic>{
|
||||
'type': 'pass_slip_filed',
|
||||
'pass_slip_id': slipId,
|
||||
...?(notificationId != null
|
||||
? {'notification_id': notificationId}
|
||||
: null),
|
||||
...?(notificationId != null ? {'notification_id': notificationId} : null),
|
||||
};
|
||||
|
||||
await _client
|
||||
.from('notifications')
|
||||
.insert(
|
||||
await _client.from('notifications').insert(
|
||||
adminIds
|
||||
.map(
|
||||
(adminId) => {
|
||||
'user_id': adminId,
|
||||
'actor_id': userId,
|
||||
'type': 'pass_slip_filed',
|
||||
'pass_slip_id': slipId,
|
||||
},
|
||||
)
|
||||
.map((adminId) => {
|
||||
'user_id': adminId,
|
||||
'actor_id': userId,
|
||||
'type': 'pass_slip_filed',
|
||||
'pass_slip_id': slipId,
|
||||
})
|
||||
.toList(),
|
||||
);
|
||||
|
||||
@@ -169,7 +349,6 @@ class PassSlipController {
|
||||
debugPrint('pass slip send_fcm result: $res');
|
||||
} catch (e) {
|
||||
debugPrint('pass slip send_fcm error: $e');
|
||||
// Non-fatal: keep slip request working even if send_fcm fails
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,49 +356,67 @@ class PassSlipController {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
|
||||
// Determine slip start time based on requested_start
|
||||
final nowUtc = DateTime.now().toUtc();
|
||||
String slipStartIso = nowUtc.toIso8601String();
|
||||
|
||||
final row = await _client
|
||||
.from('pass_slips')
|
||||
.select('requested_start')
|
||||
.eq('id', slipId)
|
||||
.maybeSingle();
|
||||
if (row != null && row['requested_start'] != null) {
|
||||
final requestedStart = DateTime.parse(row['requested_start'] as String);
|
||||
if (requestedStart.isAfter(nowUtc)) {
|
||||
slipStartIso = requestedStart.toIso8601String();
|
||||
// Try to determine slip start time online; if offline use now.
|
||||
String slipStartIso = nowUtc.toIso8601String();
|
||||
try {
|
||||
final row = await _client
|
||||
.from('pass_slips')
|
||||
.select('requested_start')
|
||||
.eq('id', slipId)
|
||||
.maybeSingle();
|
||||
if (row != null && row['requested_start'] != null) {
|
||||
final requestedStart = DateTime.parse(row['requested_start'] as String);
|
||||
if (requestedStart.isAfter(nowUtc)) {
|
||||
slipStartIso = requestedStart.toIso8601String();
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Stick with nowUtc.
|
||||
}
|
||||
|
||||
await _client
|
||||
.from('pass_slips')
|
||||
.update({
|
||||
'status': 'approved',
|
||||
'approved_by': userId,
|
||||
'approved_at': nowUtc.toIso8601String(),
|
||||
'slip_start': slipStartIso,
|
||||
})
|
||||
.eq('id', slipId);
|
||||
final fields = {
|
||||
'status': 'approved',
|
||||
'approved_by': userId,
|
||||
'approved_at': nowUtc.toIso8601String(),
|
||||
'slip_start': slipStartIso,
|
||||
};
|
||||
|
||||
await _notifyRequester(slipId: slipId, actorId: userId, approved: true);
|
||||
try {
|
||||
await _client.from('pass_slips').update(fields).eq('id', slipId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueSlipStatusUpdate(slipId, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _notifyRequester(slipId: slipId, actorId: userId, approved: true);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> rejectSlip(String slipId) async {
|
||||
final userId = _client.auth.currentUser?.id;
|
||||
if (userId == null) throw Exception('Not authenticated');
|
||||
|
||||
await _client
|
||||
.from('pass_slips')
|
||||
.update({
|
||||
'status': 'rejected',
|
||||
'approved_by': userId,
|
||||
'approved_at': DateTime.now().toUtc().toIso8601String(),
|
||||
})
|
||||
.eq('id', slipId);
|
||||
final fields = {
|
||||
'status': 'rejected',
|
||||
'approved_by': userId,
|
||||
'approved_at': DateTime.now().toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
await _notifyRequester(slipId: slipId, actorId: userId, approved: false);
|
||||
try {
|
||||
await _client.from('pass_slips').update(fields).eq('id', slipId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueSlipStatusUpdate(slipId, fields);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _notifyRequester(slipId: slipId, actorId: userId, approved: false);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> _notifyRequester({
|
||||
@@ -288,13 +485,16 @@ class PassSlipController {
|
||||
}
|
||||
|
||||
Future<void> completeSlip(String slipId) async {
|
||||
await _client
|
||||
.from('pass_slips')
|
||||
.update({
|
||||
'status': 'completed',
|
||||
'slip_end': DateTime.now().toUtc().toIso8601String(),
|
||||
})
|
||||
.eq('id', slipId);
|
||||
final fields = {
|
||||
'status': 'completed',
|
||||
'slip_end': DateTime.now().toUtc().toIso8601String(),
|
||||
};
|
||||
try {
|
||||
await _client.from('pass_slips').update(fields).eq('id', slipId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueSlipStatusUpdate(slipId, fields);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<String>> _fetchRoleUserIds({
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/profile.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'auth_provider.dart';
|
||||
import 'connectivity_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Pending full-name updates made offline, keyed by userId.
|
||||
final offlinePendingProfileUpdatesProvider =
|
||||
StateProvider<Map<String, String>>((ref) => const {});
|
||||
|
||||
final currentUserIdProvider = Provider<String?>((ref) {
|
||||
final authState = ref.watch(authStateChangesProvider);
|
||||
// Be explicit about loading/error to avoid dynamic dispatch problems.
|
||||
@@ -37,9 +49,52 @@ final currentProfileProvider = StreamProvider<Profile?>((ref) {
|
||||
return data == null ? [] : [Profile.fromMap(data)];
|
||||
},
|
||||
fromMap: Profile.fromMap,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<Profile>();
|
||||
return all.where((p) => p.id == userId).toList();
|
||||
},
|
||||
onCacheMirror: (rows) => mirrorBatchToBrick<Profile>(
|
||||
rows.whereType<Profile>().toList(),
|
||||
tag: 'profile_current',
|
||||
),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
final pending = Map<String, String>.from(
|
||||
ref.read(offlinePendingProfileUpdatesProvider),
|
||||
);
|
||||
if (pending.isEmpty) return;
|
||||
debugPrint(
|
||||
'[currentProfileProvider] reconnected — syncing ${pending.length} profile update(s)',
|
||||
);
|
||||
final synced = <String>[];
|
||||
for (final entry in pending.entries) {
|
||||
try {
|
||||
await client
|
||||
.from('profiles')
|
||||
.update({'full_name': entry.value})
|
||||
.eq('id', entry.key);
|
||||
synced.add(entry.key);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[currentProfileProvider] failed to sync full_name for ${entry.key}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = Map<String, String>.from(
|
||||
ref.read(offlinePendingProfileUpdatesProvider),
|
||||
);
|
||||
for (final id in synced) {
|
||||
remaining.remove(id);
|
||||
}
|
||||
ref.read(offlinePendingProfileUpdatesProvider.notifier).state = remaining;
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) {
|
||||
return result.data.isEmpty ? null : result.data.first;
|
||||
});
|
||||
@@ -58,6 +113,13 @@ final profilesProvider = StreamProvider<List<Profile>>((ref) {
|
||||
return data.map(Profile.fromMap).toList();
|
||||
},
|
||||
fromMap: Profile.fromMap,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<Profile>();
|
||||
all.sort((a, b) => a.fullName.compareTo(b.fullName));
|
||||
return all;
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<Profile>(rows, tag: 'profiles'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
@@ -67,23 +129,36 @@ final profilesProvider = StreamProvider<List<Profile>>((ref) {
|
||||
/// Controller for the current user's profile (update full name / password).
|
||||
final profileControllerProvider = Provider<ProfileController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return ProfileController(client);
|
||||
return ProfileController(client, ref);
|
||||
});
|
||||
|
||||
class ProfileController {
|
||||
ProfileController(this._client);
|
||||
ProfileController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
/// Update the `profiles.full_name` for the given user id.
|
||||
/// Offline: queues the update for replay on reconnect.
|
||||
Future<void> updateFullName({
|
||||
required String userId,
|
||||
required String fullName,
|
||||
}) async {
|
||||
await _client
|
||||
.from('profiles')
|
||||
.update({'full_name': fullName})
|
||||
.eq('id', userId);
|
||||
try {
|
||||
await _client
|
||||
.from('profiles')
|
||||
.update({'full_name': fullName})
|
||||
.eq('id', userId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Map<String, String>.from(
|
||||
ref.read(offlinePendingProfileUpdatesProvider),
|
||||
);
|
||||
current[userId] = fullName;
|
||||
ref.read(offlinePendingProfileUpdatesProvider.notifier).state = current;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the current user's password (works for OAuth users too).
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'stream_recovery.dart';
|
||||
import 'supabase_provider.dart';
|
||||
|
||||
final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
|
||||
ref,
|
||||
) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
// ChangeNotifierProvider automatically disposes the notifier; no need
|
||||
// for ref.onDispose here — adding it causes a double-dispose assertion.
|
||||
return RealtimeController(client);
|
||||
return RealtimeController();
|
||||
});
|
||||
|
||||
/// Per-channel realtime controller for UI skeleton indicators.
|
||||
@@ -27,42 +21,12 @@ final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
|
||||
/// - Per-channel recovering state for pinpoint skeleton indicators
|
||||
/// - Auth token refreshes for realtime connections
|
||||
class RealtimeController extends ChangeNotifier {
|
||||
final SupabaseClient _client;
|
||||
bool _disposed = false;
|
||||
|
||||
/// Channels currently in a recovering/polling/stale state.
|
||||
final Set<String> _recoveringChannels = {};
|
||||
|
||||
StreamSubscription<AuthState>? _authSub;
|
||||
|
||||
RealtimeController(this._client) {
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() {
|
||||
try {
|
||||
_authSub = _client.auth.onAuthStateChange.listen((data) {
|
||||
final event = data.event;
|
||||
if (event == AuthChangeEvent.tokenRefreshed) {
|
||||
_ensureTokenFresh();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('RealtimeController._init error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _ensureTokenFresh() async {
|
||||
if (_disposed) return;
|
||||
try {
|
||||
final authDynamic = _client.auth as dynamic;
|
||||
if (authDynamic.refreshSession != null) {
|
||||
await authDynamic.refreshSession?.call();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('RealtimeController: token refresh failed: $e');
|
||||
}
|
||||
}
|
||||
RealtimeController();
|
||||
|
||||
// ── Per-channel status ─────────────────────────────────────────────────
|
||||
|
||||
@@ -97,13 +61,17 @@ class RealtimeController extends ChangeNotifier {
|
||||
|
||||
/// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged].
|
||||
///
|
||||
/// Routes [StreamConnectionStatus] to the appropriate mark method.
|
||||
/// Both `connected` and `polling` are treated as "recovered" because
|
||||
/// polling is a functional fallback that still delivers data — the user
|
||||
/// doesn't need to see a reconnection indicator while data flows via REST.
|
||||
/// Routes [StreamConnectionStatus] to the appropriate mark method:
|
||||
/// - `connected` — live stream; no indicator needed.
|
||||
/// - `polling` — REST fallback; data is fresh, no skeleton needed.
|
||||
/// - `offline` — device has no internet; Brick cache is active. The offline
|
||||
/// banner already communicates this state, so the reconnection skeleton
|
||||
/// should NOT show — it would be misleading while real data is on screen.
|
||||
/// - Everything else (`recovering`, `stale`, `failed`) — show skeleton.
|
||||
void handleChannelStatus(String channel, StreamConnectionStatus status) {
|
||||
if (status == StreamConnectionStatus.connected ||
|
||||
status == StreamConnectionStatus.polling) {
|
||||
status == StreamConnectionStatus.polling ||
|
||||
status == StreamConnectionStatus.offline) {
|
||||
markChannelRecovered(channel);
|
||||
} else {
|
||||
markChannelRecovering(channel);
|
||||
@@ -126,7 +94,6 @@ class RealtimeController extends ChangeNotifier {
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
_authSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/service.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
@@ -17,6 +18,13 @@ final servicesProvider = StreamProvider<List<Service>>((ref) {
|
||||
fromMap: Service.fromMap,
|
||||
channelName: 'services',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<Service>();
|
||||
all.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
return all;
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<Service>(rows, tag: 'services'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
|
||||
@@ -18,6 +18,12 @@ enum StreamConnectionStatus {
|
||||
|
||||
/// Fatal error; stream will not recover without manual intervention.
|
||||
failed,
|
||||
|
||||
/// Device is offline. The realtime subscription is paused; Brick's local
|
||||
/// SQLite cache is the active data source. No reconnection is attempted
|
||||
/// until [notifyOnlineRestored] is called. The UI should show the offline
|
||||
/// banner but NOT a skeleton/loading indicator.
|
||||
offline,
|
||||
}
|
||||
|
||||
/// Represents the result of a polling attempt.
|
||||
@@ -70,7 +76,19 @@ typedef ChannelStatusCallback =
|
||||
/// and connection status tracking. Provides graceful degradation when the
|
||||
/// realtime connection fails.
|
||||
///
|
||||
/// ## Offline behaviour
|
||||
///
|
||||
/// Recovery is **suppressed** when the device is offline (as reported by
|
||||
/// [setIsOnlineCallback]). In that state the wrapper marks the channel as
|
||||
/// `recovering` but does **not** schedule timers or fire polls — Brick's
|
||||
/// offline queue handles local data until connectivity returns.
|
||||
///
|
||||
/// When the device comes back online, call [notifyAllOnlineRestored] (done
|
||||
/// automatically by `connectivity_provider._onReconnected`) to restart every
|
||||
/// live wrapper whose stream is not already connected.
|
||||
///
|
||||
/// Error handling:
|
||||
/// - **Offline**: recovery suppressed; restarts on [notifyAllOnlineRestored].
|
||||
/// - **Timeout**: detected and handled internally with exponential backoff.
|
||||
/// - **ChannelRateLimitReached**: detected and handled with a longer minimum
|
||||
/// delay (5 s) before retrying. During recovery, a REST poll keeps data
|
||||
@@ -92,6 +110,39 @@ typedef ChannelStatusCallback =
|
||||
/// );
|
||||
/// ```
|
||||
class StreamRecoveryWrapper<T> {
|
||||
// ── Static online check ───────────────────────────────────────────────────
|
||||
|
||||
/// Returns whether the device currently has a working internet connection.
|
||||
///
|
||||
/// Defaults to `() => true` (recovery enabled) until the connectivity
|
||||
/// provider overrides it via [setIsOnlineCallback].
|
||||
static bool Function() _globalIsOnline = () => true;
|
||||
|
||||
/// Configure the global online check. Call this once from
|
||||
/// `connectivityMonitorProvider` so all wrappers suppress recovery while
|
||||
/// offline without needing per-instance wiring.
|
||||
static void setIsOnlineCallback(bool Function() fn) {
|
||||
_globalIsOnline = fn;
|
||||
}
|
||||
|
||||
// ── Static registry (WeakReference so GC can collect disposed wrappers) ──
|
||||
|
||||
static final List<WeakReference<StreamRecoveryWrapper>> _instances = [];
|
||||
|
||||
/// Notify every live wrapper that connectivity has been restored.
|
||||
///
|
||||
/// Wrappers whose stream is already [StreamConnectionStatus.connected] are
|
||||
/// skipped. All others call [retry] to restart the Supabase realtime
|
||||
/// subscription. Called by `connectivity_provider._onReconnected`.
|
||||
static void notifyAllOnlineRestored() {
|
||||
_instances.removeWhere((r) => r.target == null);
|
||||
for (final ref in List.of(_instances)) {
|
||||
ref.target?.notifyOnlineRestored();
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
final Stream<List<Map<String, dynamic>>> _realtimeStream;
|
||||
final Future<List<T>> Function() _onPollData;
|
||||
final T Function(Map<String, dynamic>) _fromMap;
|
||||
@@ -105,6 +156,17 @@ class StreamRecoveryWrapper<T> {
|
||||
/// skeleton indicators in the UI.
|
||||
final ChannelStatusCallback? _onStatusChanged;
|
||||
|
||||
/// Optional callback to fetch data from the local offline cache (e.g. Brick
|
||||
/// SQLite) when the device is offline. Called once when offline is first
|
||||
/// detected to populate the stream so the UI shows cached data instead of
|
||||
/// an empty list. If null, an empty offline result is emitted.
|
||||
final Future<List<T>> Function()? _onOfflineData;
|
||||
|
||||
/// Optional callback invoked with each batch of live data so the provider
|
||||
/// can mirror it into a local cache (e.g. Brick SQLite). Fires for every
|
||||
/// realtime emission. Errors are caught and logged; mirroring is best-effort.
|
||||
final void Function(List<T> rows)? _onCacheMirror;
|
||||
|
||||
StreamConnectionStatus _connectionStatus = StreamConnectionStatus.connected;
|
||||
int _recoveryAttempts = 0;
|
||||
Timer? _pollingTimer;
|
||||
@@ -115,6 +177,11 @@ class StreamRecoveryWrapper<T> {
|
||||
bool _disposed = false;
|
||||
bool _listening = false;
|
||||
|
||||
/// True once live data has been emitted at least once. Used to distinguish
|
||||
/// "offline at startup (never received data)" from "reconnecting after
|
||||
/// having had data" for the race-condition fix.
|
||||
bool _hasEmittedData = false;
|
||||
|
||||
StreamRecoveryWrapper({
|
||||
required Stream<List<Map<String, dynamic>>> stream,
|
||||
required Future<List<T>> Function() onPollData,
|
||||
@@ -122,11 +189,21 @@ class StreamRecoveryWrapper<T> {
|
||||
StreamRecoveryConfig config = const StreamRecoveryConfig(),
|
||||
this.channelName = 'unknown',
|
||||
ChannelStatusCallback? onStatusChanged,
|
||||
Future<List<T>> Function()? onOfflineData,
|
||||
void Function(List<T> rows)? onCacheMirror,
|
||||
}) : _realtimeStream = stream,
|
||||
_onPollData = onPollData,
|
||||
_fromMap = fromMap,
|
||||
_config = config,
|
||||
_onStatusChanged = onStatusChanged;
|
||||
_onStatusChanged = onStatusChanged,
|
||||
_onOfflineData = onOfflineData,
|
||||
_onCacheMirror = onCacheMirror {
|
||||
// Register in the static registry so [notifyAllOnlineRestored] can reach
|
||||
// this instance without requiring call-site changes.
|
||||
_instances.add(WeakReference(this));
|
||||
// Prune any dead references (disposed wrappers) to keep the list small.
|
||||
_instances.removeWhere((r) => r.target == null);
|
||||
}
|
||||
|
||||
/// The wrapped stream that emits recovery results with metadata.
|
||||
///
|
||||
@@ -161,6 +238,10 @@ class StreamRecoveryWrapper<T> {
|
||||
void _onRealtimeData(List<Map<String, dynamic>> rows) {
|
||||
if (_disposed) return;
|
||||
|
||||
// Mark that live data has been received at least once. This is used in
|
||||
// the offline race-condition check in [_onRealtimeError].
|
||||
_hasEmittedData = true;
|
||||
|
||||
// When recovering, don't reset _recoveryAttempts immediately.
|
||||
// Supabase streams emit an initial REST fetch before the realtime
|
||||
// channel is established. If the channel keeps failing, resetting
|
||||
@@ -183,9 +264,22 @@ class StreamRecoveryWrapper<T> {
|
||||
}
|
||||
|
||||
_setStatus(StreamConnectionStatus.connected);
|
||||
final converted = rows.map(_fromMap).toList();
|
||||
|
||||
// Mirror live data into the local cache so cold-launching offline still
|
||||
// shows recent state. Best-effort — never throws into the consumer.
|
||||
final mirror = _onCacheMirror;
|
||||
if (mirror != null) {
|
||||
try {
|
||||
mirror(converted);
|
||||
} catch (e) {
|
||||
debugPrint('StreamRecoveryWrapper[$channelName]: mirror failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
_emit(
|
||||
StreamRecoveryResult<T>(
|
||||
data: rows.map(_fromMap).toList(),
|
||||
data: converted,
|
||||
connectionStatus: StreamConnectionStatus.connected,
|
||||
isStale: false,
|
||||
),
|
||||
@@ -198,6 +292,37 @@ class StreamRecoveryWrapper<T> {
|
||||
// Cancel any stability timer — the connection is not stable.
|
||||
_stabilityTimer?.cancel();
|
||||
|
||||
// Determine whether this error means the device is offline.
|
||||
//
|
||||
// Two cases handled:
|
||||
// 1. Connectivity monitor already updated → _globalIsOnline() is false.
|
||||
// 2. Race condition at startup: the realtime stream fails (SocketException)
|
||||
// BEFORE ConnectivityMonitor.check() completes (it takes up to 5 s).
|
||||
// In this window _globalIsOnline() still returns the default `true`
|
||||
// but the device is actually offline. We detect this by requiring:
|
||||
// (a) the error is a raw network connectivity failure, AND
|
||||
// (b) no live data has been received yet on this wrapper.
|
||||
// If data was already flowing and then dropped, we follow normal
|
||||
// recovery so the user gets their data back when connectivity
|
||||
// restores.
|
||||
final isOffline = !_globalIsOnline() ||
|
||||
(!_hasEmittedData && _isNetworkConnectivityError(error));
|
||||
|
||||
if (isOffline) {
|
||||
debugPrint(
|
||||
'StreamRecoveryWrapper[$channelName]: offline — suppressing recovery',
|
||||
);
|
||||
_setStatus(StreamConnectionStatus.offline);
|
||||
// Emit offline data so the StreamProvider exits AsyncLoading state.
|
||||
// Without this emission the provider stays in loading indefinitely when
|
||||
// offline at startup, causing the skeleton shimmer to show even though
|
||||
// Brick's SQLite cache has data.
|
||||
if (!_hasEmittedData) {
|
||||
unawaited(_emitOfflineData());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final isRateLimit = _isRateLimitError(error);
|
||||
final isTimeout = _isTimeoutError(error);
|
||||
final tag = isRateLimit
|
||||
@@ -260,6 +385,21 @@ class StreamRecoveryWrapper<T> {
|
||||
void _onRealtimeDone() {
|
||||
if (_disposed) return;
|
||||
debugPrint('StreamRecoveryWrapper[$channelName]: stream completed');
|
||||
|
||||
// If offline, the channel closed because the network dropped. Suppress
|
||||
// the restart — [notifyOnlineRestored] will handle it when we're back.
|
||||
if (!_globalIsOnline()) {
|
||||
debugPrint(
|
||||
'StreamRecoveryWrapper[$channelName]: offline — suppressing stream restart',
|
||||
);
|
||||
_setStatus(StreamConnectionStatus.offline);
|
||||
// Emit offline data to unblock any StreamProvider still in AsyncLoading.
|
||||
if (!_hasEmittedData) {
|
||||
unawaited(_emitOfflineData());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to reconnect once if the stream closes unexpectedly.
|
||||
if (_recoveryAttempts < _config.maxRecoveryAttempts) {
|
||||
_onRealtimeError(StateError('realtime stream completed unexpectedly'));
|
||||
@@ -282,8 +422,23 @@ class StreamRecoveryWrapper<T> {
|
||||
|
||||
Future<void> _pollOnce() async {
|
||||
if (_disposed) return;
|
||||
// Don't poll when offline — it will fail and mislead the status tracker.
|
||||
if (!_globalIsOnline()) return;
|
||||
try {
|
||||
final data = await _onPollData();
|
||||
|
||||
// Mirror polled data into the local cache too.
|
||||
final mirror = _onCacheMirror;
|
||||
if (mirror != null) {
|
||||
try {
|
||||
mirror(data);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'StreamRecoveryWrapper[$channelName]: poll mirror failed: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_emit(
|
||||
StreamRecoveryResult<T>(
|
||||
data: data,
|
||||
@@ -330,6 +485,58 @@ class StreamRecoveryWrapper<T> {
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Returns true when [error] is a raw network-connectivity failure
|
||||
/// (socket closed, no route to host, DNS failure, etc.) rather than an
|
||||
/// application-level error from the server.
|
||||
///
|
||||
/// Used in the startup race-condition fix: if the realtime stream errors
|
||||
/// with a connectivity failure before the connectivity monitor has updated
|
||||
/// [_globalIsOnline], we treat it as offline so the device shows cached
|
||||
/// data instead of entering the recovery/shimmer loop.
|
||||
static bool _isNetworkConnectivityError(Object error) {
|
||||
final msg = error.toString().toLowerCase();
|
||||
return msg.contains('socketexception') ||
|
||||
msg.contains('connection refused') ||
|
||||
msg.contains('network is unreachable') ||
|
||||
msg.contains('no route to host') ||
|
||||
msg.contains('failed host lookup') ||
|
||||
msg.contains('connection reset') ||
|
||||
msg.contains('broken pipe') ||
|
||||
msg.contains('connection timed out') ||
|
||||
(msg.contains('clientexception') && msg.contains('network')) ||
|
||||
msg.contains('errno = 111') || // ECONNREFUSED
|
||||
msg.contains('errno = 101'); // ENETUNREACH
|
||||
}
|
||||
|
||||
/// Fetches data from the offline cache (via [_onOfflineData] callback when
|
||||
/// provided, otherwise an empty list) and emits it as an
|
||||
/// [StreamConnectionStatus.offline] result.
|
||||
///
|
||||
/// Called once when the device first goes offline so that consuming
|
||||
/// [StreamProvider]s exit [AsyncLoading] and the UI shows cached content
|
||||
/// (or an empty list) instead of the skeleton shimmer.
|
||||
Future<void> _emitOfflineData() async {
|
||||
if (_disposed) return;
|
||||
List<T> cached = const [];
|
||||
if (_onOfflineData != null) {
|
||||
try {
|
||||
cached = await _onOfflineData();
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'StreamRecoveryWrapper[$channelName]: offline cache read failed: $e',
|
||||
);
|
||||
cached = const [];
|
||||
}
|
||||
}
|
||||
_emit(
|
||||
StreamRecoveryResult<T>(
|
||||
data: cached,
|
||||
connectionStatus: StreamConnectionStatus.offline,
|
||||
isStale: cached.isNotEmpty,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _emit(StreamRecoveryResult<T> result) {
|
||||
if (!_disposed && _controller != null && !_controller!.isClosed) {
|
||||
_controller!.add(result);
|
||||
@@ -358,6 +565,20 @@ class StreamRecoveryWrapper<T> {
|
||||
_startRealtimeSubscription();
|
||||
}
|
||||
|
||||
/// Called by [notifyAllOnlineRestored] when connectivity is restored.
|
||||
///
|
||||
/// Only restarts the subscription if the channel is not already connected.
|
||||
/// Wrappers that were mid-backoff-timer while offline resume from scratch
|
||||
/// rather than continuing an already-stale timer.
|
||||
void notifyOnlineRestored() {
|
||||
if (_disposed) return;
|
||||
if (_connectionStatus == StreamConnectionStatus.connected) return;
|
||||
debugPrint(
|
||||
'StreamRecoveryWrapper[$channelName]: back online — restarting stream',
|
||||
);
|
||||
retry();
|
||||
}
|
||||
|
||||
/// Clean up all resources and notify the status callback that this
|
||||
/// channel is no longer active, preventing ghost entries in the
|
||||
/// [RealtimeController]'s recovering-channels set.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'announcements_provider.dart';
|
||||
import 'attendance_provider.dart';
|
||||
import 'chat_provider.dart';
|
||||
import 'it_service_request_provider.dart';
|
||||
import 'leave_provider.dart';
|
||||
import 'notifications_provider.dart';
|
||||
import 'pass_slip_provider.dart';
|
||||
import 'profile_provider.dart';
|
||||
import 'tasks_provider.dart';
|
||||
import 'tickets_provider.dart';
|
||||
import 'user_offices_provider.dart';
|
||||
|
||||
/// Total count of items waiting to sync across all modules.
|
||||
/// Used by the offline banner and reconnect overlay for dynamic messaging.
|
||||
final pendingSyncCountProvider = Provider<int>((ref) {
|
||||
// Tasks
|
||||
final tasks = ref.watch(offlinePendingTasksProvider).length;
|
||||
final taskUpdates = ref.watch(offlinePendingTaskUpdatesProvider).length;
|
||||
final messages = ref.watch(offlinePendingMessagesProvider)
|
||||
.values
|
||||
.fold<int>(0, (sum, list) => sum + list.length);
|
||||
final assignments = ref.watch(offlinePendingAssignmentsProvider).length;
|
||||
final activityLogs =
|
||||
ref.watch(offlinePendingActivityLogItemsProvider).length;
|
||||
|
||||
// Leave
|
||||
final leaves = ref.watch(offlinePendingLeavesProvider).length;
|
||||
final leaveUpdates = ref.watch(offlinePendingLeaveUpdatesProvider).length;
|
||||
|
||||
// Pass slips
|
||||
final passSlips = ref.watch(offlinePendingPassSlipsProvider).length;
|
||||
final passSlipUpdates =
|
||||
ref.watch(offlinePendingPassSlipUpdatesProvider).length;
|
||||
|
||||
// Announcements
|
||||
final announcements = ref.watch(offlinePendingAnnouncementsProvider).length;
|
||||
final announcementUpdates =
|
||||
ref.watch(offlinePendingAnnouncementUpdatesProvider).length;
|
||||
final announcementComments =
|
||||
ref.watch(offlinePendingAnnouncementCommentsProvider).length;
|
||||
|
||||
// IT Service Requests
|
||||
final itRequests =
|
||||
ref.watch(offlinePendingItServiceRequestsProvider).length;
|
||||
final itRequestUpdates =
|
||||
ref.watch(offlinePendingItServiceRequestUpdatesProvider).length;
|
||||
final isrActions = ref.watch(offlinePendingIsrActionsProvider).length;
|
||||
|
||||
// Attendance
|
||||
final checkIns = ref.watch(offlinePendingCheckInsProvider).length;
|
||||
final attendanceUpdates =
|
||||
ref.watch(offlinePendingAttendanceUpdatesProvider).length;
|
||||
|
||||
// Notifications
|
||||
final notifReads =
|
||||
ref.watch(offlinePendingNotificationReadsProvider).length;
|
||||
final notifBatchReads =
|
||||
ref.watch(offlinePendingBatchNotificationReadsProvider).length;
|
||||
|
||||
// Profile
|
||||
final profileUpdates =
|
||||
ref.watch(offlinePendingProfileUpdatesProvider).length;
|
||||
|
||||
// Tickets
|
||||
final tickets = ref.watch(offlinePendingTicketsProvider).length;
|
||||
final ticketUpdates =
|
||||
ref.watch(offlinePendingTicketUpdatesProvider).length;
|
||||
|
||||
// Offices
|
||||
final offices = ref.watch(offlinePendingOfficesProvider).length;
|
||||
|
||||
// User offices
|
||||
final officeOps = ref.watch(offlinePendingUserOfficeOpsProvider).length;
|
||||
|
||||
// Chat
|
||||
final chatMessages = ref.watch(offlinePendingChatMessagesProvider)
|
||||
.values
|
||||
.fold<int>(0, (sum, list) => sum + list.length);
|
||||
|
||||
return tasks +
|
||||
taskUpdates +
|
||||
messages +
|
||||
assignments +
|
||||
activityLogs +
|
||||
leaves +
|
||||
leaveUpdates +
|
||||
passSlips +
|
||||
passSlipUpdates +
|
||||
announcements +
|
||||
announcementUpdates +
|
||||
announcementComments +
|
||||
itRequests +
|
||||
itRequestUpdates +
|
||||
isrActions +
|
||||
checkIns +
|
||||
attendanceUpdates +
|
||||
notifReads +
|
||||
notifBatchReads +
|
||||
profileUpdates +
|
||||
tickets +
|
||||
ticketUpdates +
|
||||
offices +
|
||||
officeOps +
|
||||
chatMessages;
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,11 @@ import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../brick/repository.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import '../models/office.dart';
|
||||
import '../models/ticket.dart';
|
||||
import '../models/ticket_message.dart';
|
||||
@@ -12,11 +17,27 @@ import 'profile_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'user_offices_provider.dart';
|
||||
import 'tasks_provider.dart';
|
||||
import 'connectivity_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
|
||||
/// Pending offices created while offline.
|
||||
final offlinePendingOfficesProvider =
|
||||
StateProvider<List<Office>>((ref) => const []);
|
||||
|
||||
final officesProvider = StreamProvider<List<Office>>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final pendingOffices = ref.watch(offlinePendingOfficesProvider);
|
||||
|
||||
List<Office> applyPending(List<Office> rows) {
|
||||
if (pendingOffices.isEmpty) return rows;
|
||||
final byId = {for (final r in rows) r.id: r};
|
||||
for (final p in pendingOffices) {
|
||||
byId.putIfAbsent(p.id, () => p);
|
||||
}
|
||||
return byId.values.toList()
|
||||
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<Office>(
|
||||
stream: client.from('offices').stream(primaryKey: ['id']).order('name'),
|
||||
@@ -27,10 +48,49 @@ final officesProvider = StreamProvider<List<Office>>((ref) {
|
||||
fromMap: Office.fromMap,
|
||||
channelName: 'offices',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<Office>();
|
||||
all.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
return applyPending(all);
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<Office>(rows, tag: 'offices'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
|
||||
// Reconnect-replay for offline-created offices.
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
final pending = List<Office>.from(
|
||||
ref.read(offlinePendingOfficesProvider),
|
||||
);
|
||||
if (pending.isEmpty) return;
|
||||
final synced = <String>[];
|
||||
for (final office in pending) {
|
||||
try {
|
||||
final payload = <String, dynamic>{'id': office.id, 'name': office.name};
|
||||
if (office.serviceId != null) payload['service_id'] = office.serviceId;
|
||||
await client.from('offices').insert(payload);
|
||||
synced.add(office.id);
|
||||
} catch (e) {
|
||||
final s = e.toString();
|
||||
if (s.contains('23505') || s.contains('duplicate key')) {
|
||||
synced.add(office.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = ref
|
||||
.read(offlinePendingOfficesProvider)
|
||||
.where((o) => !synced.contains(o.id))
|
||||
.toList();
|
||||
ref.read(offlinePendingOfficesProvider.notifier).state =
|
||||
List.unmodifiable(remaining);
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
final officesOnceProvider = FutureProvider<List<Office>>((ref) async {
|
||||
@@ -70,7 +130,7 @@ final officesQueryProvider = StateProvider<OfficeQuery>(
|
||||
|
||||
final officesControllerProvider = Provider<OfficesController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return OfficesController(client);
|
||||
return OfficesController(client, ref);
|
||||
});
|
||||
|
||||
/// Ticket query parameters for server-side pagination and filtering.
|
||||
@@ -210,10 +270,90 @@ final ticketsProvider = StreamProvider<List<Ticket>>((ref) {
|
||||
fromMap: Ticket.fromMap,
|
||||
channelName: 'tickets',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () => cachedListFromBrick<Ticket>(),
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<Ticket>(rows, tag: 'tickets'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
|
||||
// Reconnect-replay: POST queued items directly, bypassing Brick's queue.
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
|
||||
// 1. Replay offline-created tickets
|
||||
final pendingRaw = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingTicketRawProvider),
|
||||
);
|
||||
if (pendingRaw.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[ticketsProvider] reconnected — syncing ${pendingRaw.length} ticket(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final row in pendingRaw) {
|
||||
final id = row['id'] as String;
|
||||
try {
|
||||
await client.from('tickets').insert(row);
|
||||
syncedIds.add(id);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
syncedIds.add(id);
|
||||
} else {
|
||||
debugPrint('[ticketsProvider] failed to sync ticket id=$id: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remainingRaw = ref
|
||||
.read(offlinePendingTicketRawProvider)
|
||||
.where((r) => !syncedIds.contains(r['id'] as String))
|
||||
.toList();
|
||||
ref.read(offlinePendingTicketRawProvider.notifier).state =
|
||||
List.unmodifiable(remainingRaw);
|
||||
final remainingModels = ref
|
||||
.read(offlinePendingTicketsProvider)
|
||||
.where((t) => !syncedIds.contains(t.id))
|
||||
.toList();
|
||||
ref.read(offlinePendingTicketsProvider.notifier).state =
|
||||
List.unmodifiable(remainingModels);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Replay offline ticket updates
|
||||
final pendingUpdates = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingTicketUpdatesProvider),
|
||||
);
|
||||
if (pendingUpdates.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[ticketsProvider] reconnected — syncing ${pendingUpdates.length} ticket update(s)',
|
||||
);
|
||||
final syncedIds = <String>[];
|
||||
for (final entry in pendingUpdates.entries) {
|
||||
try {
|
||||
await client
|
||||
.from('tickets')
|
||||
.update(entry.value)
|
||||
.eq('id', entry.key);
|
||||
syncedIds.add(entry.key);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ticketsProvider] failed to sync update id=${entry.key}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (syncedIds.isNotEmpty) {
|
||||
final remaining = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingTicketUpdatesProvider),
|
||||
);
|
||||
for (final id in syncedIds) {
|
||||
remaining.remove(id);
|
||||
}
|
||||
ref.read(offlinePendingTicketUpdatesProvider.notifier).state = remaining;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var lastResultHash = '';
|
||||
Timer? debounceTimer;
|
||||
// broadcast() so Riverpod and any other listener can both receive events.
|
||||
@@ -239,6 +379,35 @@ final ticketsProvider = StreamProvider<List<Ticket>>((ref) {
|
||||
// prevents a duplicate rebuild if both arrive with identical data.
|
||||
unawaited(
|
||||
Future(() async {
|
||||
// When offline, seed from Brick local SQLite cache instead of network.
|
||||
if (!ref.read(isOnlineProvider) && AppRepository.isConfigured) {
|
||||
try {
|
||||
final cached = await AppRepository.instance.get<Ticket>(
|
||||
policy: OfflineFirstGetPolicy.localOnly,
|
||||
);
|
||||
debugPrint('[ticketsProvider] offline seed: ${cached.length} tickets from local cache');
|
||||
final payload = _buildTicketPayload(
|
||||
tickets: cached,
|
||||
isGlobal: isGlobal,
|
||||
allowedOfficeIds: allowedOfficeIds,
|
||||
query: query,
|
||||
);
|
||||
final processed = await compute(_processTicketsInIsolate, payload);
|
||||
final tickets = (processed as List<dynamic>)
|
||||
.cast<Map<String, dynamic>>()
|
||||
.map(Ticket.fromMap)
|
||||
.toList();
|
||||
final hash = tickets.fold('', (h, t) => '$h${t.id}');
|
||||
if (!controller.isClosed && hash != lastResultHash) {
|
||||
lastResultHash = hash;
|
||||
controller.add(tickets);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ticketsProvider] offline seed error: $e');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Seed fetch: order newest-first and limit to 200 rows to reduce
|
||||
// initial payload. The realtime stream delivers subsequent changes.
|
||||
@@ -506,35 +675,91 @@ final taskMessagesProvider = StreamProvider.family<List<TicketMessage>, String>(
|
||||
},
|
||||
);
|
||||
|
||||
/// Tickets created offline that haven't synced to Supabase yet.
|
||||
/// Merged into the live stream in the UI — cleared entry-by-entry as the
|
||||
/// realtime stream delivers each synced UUID.
|
||||
final offlinePendingTicketsProvider =
|
||||
StateProvider<List<Ticket>>((ref) => const []);
|
||||
|
||||
/// Raw INSERT payloads for offline-created tickets — used for reconnect replay.
|
||||
final offlinePendingTicketRawProvider =
|
||||
StateProvider<List<Map<String, dynamic>>>((ref) => const []);
|
||||
|
||||
/// Field-level updates to existing tickets made offline, keyed by ticket id.
|
||||
final offlinePendingTicketUpdatesProvider =
|
||||
StateProvider<Map<String, Map<String, dynamic>>>((ref) => const {});
|
||||
|
||||
final ticketsControllerProvider = Provider<TicketsController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return TicketsController(client);
|
||||
return TicketsController(client, ref);
|
||||
});
|
||||
|
||||
class TicketsController {
|
||||
TicketsController(this._client);
|
||||
TicketsController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
Future<void> createTicket({
|
||||
/// Creates a ticket. Returns the new [Ticket] so callers can add it to
|
||||
/// [offlinePendingTicketsProvider] when offline.
|
||||
Future<Ticket?> createTicket({
|
||||
required String subject,
|
||||
required String description,
|
||||
required String officeId,
|
||||
}) async {
|
||||
final actorId = _client.auth.currentUser?.id;
|
||||
final data = await _client
|
||||
.from('tickets')
|
||||
.insert({
|
||||
// Generate UUID before the network call so Brick's offline queue captures
|
||||
// the same UUID in the queued request — preventing a UUID mismatch on replay.
|
||||
final id = const Uuid().v4();
|
||||
try {
|
||||
final data = await _client
|
||||
.from('tickets')
|
||||
.insert({
|
||||
'id': id,
|
||||
'subject': subject,
|
||||
'description': description,
|
||||
'office_id': officeId,
|
||||
'creator_id': actorId,
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
final ticketId = data['id'] as String?;
|
||||
if (ticketId == null) return null;
|
||||
unawaited(_notifyCreated(ticketId: ticketId, actorId: actorId));
|
||||
return null; // online path — ticket will arrive via realtime stream
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
final ticket = Ticket(
|
||||
id: id,
|
||||
subject: subject,
|
||||
description: description,
|
||||
officeId: officeId,
|
||||
creatorId: actorId ?? '',
|
||||
status: 'pending',
|
||||
createdAt: DateTime.now().toUtc(),
|
||||
respondedAt: null,
|
||||
promotedAt: null,
|
||||
closedAt: null,
|
||||
);
|
||||
// Queue for reconnect replay and persist locally for cold-launch.
|
||||
final ref = _ref;
|
||||
if (ref != null) {
|
||||
final currentRaw = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingTicketRawProvider),
|
||||
);
|
||||
currentRaw.add({
|
||||
'id': id,
|
||||
'subject': subject,
|
||||
'description': description,
|
||||
'office_id': officeId,
|
||||
'creator_id': _client.auth.currentUser?.id,
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
final ticketId = data['id'] as String?;
|
||||
if (ticketId == null) return;
|
||||
unawaited(_notifyCreated(ticketId: ticketId, actorId: actorId));
|
||||
'creator_id': actorId,
|
||||
});
|
||||
ref.read(offlinePendingTicketRawProvider.notifier).state =
|
||||
List.unmodifiable(currentRaw);
|
||||
}
|
||||
mirrorBatchToBrick<Ticket>([ticket], tag: 'offline_ticket');
|
||||
return ticket;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _notifyCreated({
|
||||
@@ -661,27 +886,66 @@ class TicketsController {
|
||||
required String? ticketId,
|
||||
required String content,
|
||||
}) async {
|
||||
final senderId = _client.auth.currentUser?.id;
|
||||
final payload = <String, dynamic>{
|
||||
'task_id': taskId,
|
||||
'content': content,
|
||||
'sender_id': _client.auth.currentUser?.id,
|
||||
'sender_id': senderId,
|
||||
};
|
||||
if (ticketId != null) {
|
||||
payload['ticket_id'] = ticketId;
|
||||
}
|
||||
final data = await _client
|
||||
.from('ticket_messages')
|
||||
.insert(payload)
|
||||
.select()
|
||||
.single();
|
||||
return TicketMessage.fromMap(data);
|
||||
try {
|
||||
final data = await _client
|
||||
.from('ticket_messages')
|
||||
.insert(payload)
|
||||
.select()
|
||||
.single();
|
||||
return TicketMessage.fromMap(data);
|
||||
} catch (e) {
|
||||
if (isOfflineSaveError(e)) {
|
||||
// Store message locally so it appears in the UI immediately and is
|
||||
// replayed by tasksProvider's reconnect listener when back online.
|
||||
final ref = _ref;
|
||||
if (ref != null) {
|
||||
final createdAt = DateTime.now().toUtc().toIso8601String();
|
||||
final msgPayload = Map<String, dynamic>.from(payload)
|
||||
..['created_at'] = createdAt;
|
||||
final current = Map<String, List<Map<String, dynamic>>>.from(
|
||||
ref.read(offlinePendingMessagesProvider),
|
||||
);
|
||||
current[taskId] = [...(current[taskId] ?? []), msgPayload];
|
||||
ref.read(offlinePendingMessagesProvider.notifier).state = current;
|
||||
// Return a synthetic TicketMessage so the caller's mention logic
|
||||
// still has a message object to work with.
|
||||
return TicketMessage(
|
||||
id: -DateTime.now().millisecondsSinceEpoch,
|
||||
ticketId: ticketId,
|
||||
taskId: taskId,
|
||||
senderId: senderId,
|
||||
content: content,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTicketStatus({
|
||||
required String ticketId,
|
||||
required String status,
|
||||
}) async {
|
||||
await _client.from('tickets').update({'status': status}).eq('id', ticketId);
|
||||
try {
|
||||
await _client
|
||||
.from('tickets')
|
||||
.update({'status': status})
|
||||
.eq('id', ticketId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueTicketUpdate(ticketId, {'status': status});
|
||||
return;
|
||||
}
|
||||
|
||||
// If ticket is promoted, create a linked Task (only once) — the
|
||||
// TasksController.createTask already runs auto-assignment on creation.
|
||||
@@ -718,6 +982,7 @@ class TicketsController {
|
||||
}
|
||||
|
||||
/// Update editable ticket fields such as subject, description, and office.
|
||||
/// Offline: queues the field patch.
|
||||
Future<void> updateTicket({
|
||||
required String ticketId,
|
||||
String? subject,
|
||||
@@ -730,29 +995,59 @@ class TicketsController {
|
||||
if (officeId != null) payload['office_id'] = officeId;
|
||||
if (payload.isEmpty) return;
|
||||
|
||||
await _client.from('tickets').update(payload).eq('id', ticketId);
|
||||
|
||||
// record an activity row for edit operations (best-effort)
|
||||
try {
|
||||
final actorId = _client.auth.currentUser?.id;
|
||||
await _client.from('ticket_messages').insert({
|
||||
'ticket_id': ticketId,
|
||||
'sender_id': actorId,
|
||||
'content': 'Ticket updated',
|
||||
});
|
||||
} catch (_) {}
|
||||
await _client.from('tickets').update(payload).eq('id', ticketId);
|
||||
// record an activity row for edit operations (best-effort)
|
||||
try {
|
||||
final actorId = _client.auth.currentUser?.id;
|
||||
await _client.from('ticket_messages').insert({
|
||||
'ticket_id': ticketId,
|
||||
'sender_id': actorId,
|
||||
'content': 'Ticket updated',
|
||||
});
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueTicketUpdate(ticketId, payload);
|
||||
}
|
||||
}
|
||||
|
||||
void _queueTicketUpdate(String ticketId, Map<String, dynamic> fields) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Map<String, Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingTicketUpdatesProvider),
|
||||
);
|
||||
current[ticketId] = {...?current[ticketId], ...fields};
|
||||
ref.read(offlinePendingTicketUpdatesProvider.notifier).state = current;
|
||||
}
|
||||
}
|
||||
|
||||
class OfficesController {
|
||||
OfficesController(this._client);
|
||||
OfficesController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
Future<void> createOffice({required String name, String? serviceId}) async {
|
||||
final payload = {'name': name};
|
||||
final id = const Uuid().v4();
|
||||
final payload = <String, dynamic>{'id': id, 'name': name};
|
||||
if (serviceId != null) payload['service_id'] = serviceId;
|
||||
await _client.from('offices').insert(payload);
|
||||
try {
|
||||
await _client.from('offices').insert(payload);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final office = Office(id: id, name: name, serviceId: serviceId);
|
||||
final current = List<Office>.from(
|
||||
ref.read(offlinePendingOfficesProvider),
|
||||
);
|
||||
current.add(office);
|
||||
ref.read(offlinePendingOfficesProvider.notifier).state =
|
||||
List.unmodifiable(current);
|
||||
mirrorBatchToBrick<Office>([office], tag: 'offline_offices');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateOffice({
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import 'connectivity_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
|
||||
class TypingIndicatorState {
|
||||
@@ -37,24 +38,47 @@ final typingIndicatorProvider =
|
||||
String
|
||||
>((ref, ticketId) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
final controller = TypingIndicatorController(client, ticketId);
|
||||
final controller = TypingIndicatorController(
|
||||
client,
|
||||
ticketId,
|
||||
isOnline: () => ref.read(isOnlineProvider),
|
||||
);
|
||||
|
||||
// Pause the realtime channel when the device goes offline so Supabase's
|
||||
// internal reconnection loop stops firing WebSocket errors.
|
||||
// Resume (re-subscribe) when connectivity returns.
|
||||
ref.listen<bool>(isOnlineProvider, (previous, next) {
|
||||
if (next == false) {
|
||||
controller.pauseForOffline();
|
||||
} else if (previous != true && next == true) {
|
||||
// previous is null on first call after a provider rebuild — treat
|
||||
// null and false both as "was offline" to ensure the channel resumes.
|
||||
controller.resumeFromOffline();
|
||||
}
|
||||
});
|
||||
|
||||
return controller;
|
||||
});
|
||||
|
||||
class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
||||
TypingIndicatorController(this._client, this._ticketId)
|
||||
: super(
|
||||
const TypingIndicatorState(
|
||||
userIds: {},
|
||||
channelStatus: 'init',
|
||||
lastPayload: {},
|
||||
),
|
||||
) {
|
||||
TypingIndicatorController(
|
||||
this._client,
|
||||
this._ticketId, {
|
||||
bool Function()? isOnline,
|
||||
}) : _isOnline = isOnline ?? (() => true),
|
||||
super(
|
||||
const TypingIndicatorState(
|
||||
userIds: {},
|
||||
channelStatus: 'init',
|
||||
lastPayload: {},
|
||||
),
|
||||
) {
|
||||
_initChannel();
|
||||
}
|
||||
|
||||
final SupabaseClient _client;
|
||||
final String _ticketId;
|
||||
final bool Function() _isOnline;
|
||||
RealtimeChannel? _channel;
|
||||
Timer? _typingTimer;
|
||||
final Map<String, Timer> _remoteTimeouts = {};
|
||||
@@ -94,7 +118,9 @@ class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
||||
try {
|
||||
if (_disposed || !mounted) return;
|
||||
state = state.copyWith(channelStatus: status.name);
|
||||
if (error != null) {
|
||||
if (error != null && _isOnline()) {
|
||||
// Only log when online — offline WebSocket errors are expected and
|
||||
// handled by pauseForOffline() which unsubscribes the channel.
|
||||
debugPrint('TypingIndicatorController: subscribe error: $error');
|
||||
}
|
||||
} catch (e, st) {
|
||||
@@ -235,6 +261,31 @@ class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Called when the device goes offline.
|
||||
///
|
||||
/// Unsubscribes the Supabase channel so its internal WebSocket reconnection
|
||||
/// loop stops firing errors. The channel object is retained so
|
||||
/// [resumeFromOffline] can cleanly re-subscribe on the same ticket.
|
||||
void pauseForOffline() {
|
||||
if (_disposed) return;
|
||||
debugPrint('TypingIndicatorController[$_ticketId]: offline — unsubscribing');
|
||||
_channel?.unsubscribe();
|
||||
}
|
||||
|
||||
/// Called when the device comes back online.
|
||||
///
|
||||
/// Tears down the stale channel and opens a fresh subscription so broadcast
|
||||
/// events are received again without any missed-message gaps.
|
||||
void resumeFromOffline() {
|
||||
if (_disposed) return;
|
||||
debugPrint(
|
||||
'TypingIndicatorController[$_ticketId]: back online — resubscribing',
|
||||
);
|
||||
_channel?.unsubscribe();
|
||||
_channel = null;
|
||||
_initChannel();
|
||||
}
|
||||
|
||||
// Exposed for tests only: simulate a remote typing broadcast.
|
||||
@visibleForTesting
|
||||
void debugSimulateRemoteTyping(String userId, {bool stop = false}) {
|
||||
|
||||
@@ -1,63 +1,186 @@
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../brick/cache_warmer.dart';
|
||||
import '../models/user_office.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'connectivity_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
import 'realtime_controller.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Queued assign/remove operations made offline.
|
||||
/// Each entry: {'op': 'assign'|'remove', 'user_id': ..., 'office_id': ...}
|
||||
final offlinePendingUserOfficeOpsProvider =
|
||||
StateProvider<List<Map<String, dynamic>>>((ref) => const []);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final userOfficesProvider = StreamProvider<List<UserOffice>>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
|
||||
final pendingOps = ref.watch(offlinePendingUserOfficeOpsProvider);
|
||||
|
||||
List<UserOffice> applyPending(List<UserOffice> rows) {
|
||||
if (pendingOps.isEmpty) return rows;
|
||||
var result = List<UserOffice>.from(rows);
|
||||
for (final op in pendingOps) {
|
||||
final userId = op['user_id'] as String;
|
||||
final officeId = op['office_id'] as String;
|
||||
if (op['op'] == 'assign') {
|
||||
final exists = result.any(
|
||||
(r) => r.userId == userId && r.officeId == officeId,
|
||||
);
|
||||
if (!exists) result.add(UserOffice(userId: userId, officeId: officeId));
|
||||
} else if (op['op'] == 'remove') {
|
||||
result.removeWhere(
|
||||
(r) => r.userId == userId && r.officeId == officeId,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
final wrapper = StreamRecoveryWrapper<UserOffice>(
|
||||
stream: client
|
||||
.from('user_offices')
|
||||
.stream(primaryKey: ['user_id', 'office_id'])
|
||||
.order('created_at'),
|
||||
onPollData: () async {
|
||||
final data = await client
|
||||
.from('user_offices')
|
||||
.select()
|
||||
.order('created_at');
|
||||
final data = await client.from('user_offices').select().order('created_at');
|
||||
return data.map(UserOffice.fromMap).toList();
|
||||
},
|
||||
fromMap: UserOffice.fromMap,
|
||||
channelName: 'user_offices',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final rows = await CacheWarmer.readCachedUserOffices();
|
||||
return applyPending(rows.map(UserOffice.fromMap).toList());
|
||||
},
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
return wrapper.stream.map((result) => result.data);
|
||||
|
||||
// Reconnect-replay
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
final pending = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingUserOfficeOpsProvider),
|
||||
);
|
||||
if (pending.isEmpty) return;
|
||||
debugPrint(
|
||||
'[userOfficesProvider] reconnected — syncing ${pending.length} office op(s)',
|
||||
);
|
||||
final synced = <int>[];
|
||||
for (var i = 0; i < pending.length; i++) {
|
||||
final op = pending[i];
|
||||
final userId = op['user_id'] as String;
|
||||
final officeId = op['office_id'] as String;
|
||||
try {
|
||||
if (op['op'] == 'assign') {
|
||||
await client.from('user_offices').insert({
|
||||
'user_id': userId,
|
||||
'office_id': officeId,
|
||||
});
|
||||
} else {
|
||||
await client
|
||||
.from('user_offices')
|
||||
.delete()
|
||||
.eq('user_id', userId)
|
||||
.eq('office_id', officeId);
|
||||
}
|
||||
synced.add(i);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
if (msg.contains('23505') || msg.contains('duplicate key')) {
|
||||
synced.add(i);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[userOfficesProvider] failed to sync op $i: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = <Map<String, dynamic>>[];
|
||||
for (var i = 0; i < pending.length; i++) {
|
||||
if (!synced.contains(i)) remaining.add(pending[i]);
|
||||
}
|
||||
ref.read(offlinePendingUserOfficeOpsProvider.notifier).state =
|
||||
List.unmodifiable(remaining);
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) => applyPending(result.data));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
final userOfficesControllerProvider = Provider<UserOfficesController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return UserOfficesController(client);
|
||||
return UserOfficesController(client, ref);
|
||||
});
|
||||
|
||||
class UserOfficesController {
|
||||
UserOfficesController(this._client);
|
||||
UserOfficesController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
/// Assign a user to an office. Offline: queues the operation.
|
||||
Future<void> assignUserOffice({
|
||||
required String userId,
|
||||
required String officeId,
|
||||
}) async {
|
||||
await _client.from('user_offices').insert({
|
||||
'user_id': userId,
|
||||
'office_id': officeId,
|
||||
});
|
||||
try {
|
||||
await _client.from('user_offices').insert({
|
||||
'user_id': userId,
|
||||
'office_id': officeId,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueOp('assign', userId, officeId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a user from an office. Offline: queues the operation.
|
||||
Future<void> removeUserOffice({
|
||||
required String userId,
|
||||
required String officeId,
|
||||
}) async {
|
||||
await _client
|
||||
.from('user_offices')
|
||||
.delete()
|
||||
.eq('user_id', userId)
|
||||
.eq('office_id', officeId);
|
||||
try {
|
||||
await _client
|
||||
.from('user_offices')
|
||||
.delete()
|
||||
.eq('user_id', userId)
|
||||
.eq('office_id', officeId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
_queueOp('remove', userId, officeId);
|
||||
}
|
||||
}
|
||||
|
||||
void _queueOp(String op, String userId, String officeId) {
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = List<Map<String, dynamic>>.from(
|
||||
ref.read(offlinePendingUserOfficeOpsProvider),
|
||||
);
|
||||
// Dedupe: collapse redundant assign/remove pairs for the same user+office
|
||||
current.removeWhere(
|
||||
(e) => e['user_id'] == userId && e['office_id'] == officeId,
|
||||
);
|
||||
current.add({'op': op, 'user_id': userId, 'office_id': officeId});
|
||||
ref.read(offlinePendingUserOfficeOpsProvider.notifier).state =
|
||||
List.unmodifiable(current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/duty_schedule.dart';
|
||||
import '../models/swap_request.dart';
|
||||
@@ -52,6 +53,13 @@ final dutySchedulesProvider = StreamProvider<List<DutySchedule>>((ref) {
|
||||
fromMap: DutySchedule.fromMap,
|
||||
channelName: 'duty_schedules',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<DutySchedule>();
|
||||
all.sort((a, b) => a.startTime.compareTo(b.startTime));
|
||||
return all;
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<DutySchedule>(rows, tag: 'duty_schedules'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
@@ -150,6 +158,20 @@ final swapRequestsProvider = StreamProvider<List<SwapRequest>>((ref) {
|
||||
fromMap: SwapRequest.fromMap,
|
||||
channelName: 'swap_requests',
|
||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<SwapRequest>();
|
||||
final filtered = isAdmin
|
||||
? all
|
||||
: all
|
||||
.where(
|
||||
(r) => r.requesterId == profileId || r.recipientId == profileId,
|
||||
)
|
||||
.toList();
|
||||
filtered.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return filtered;
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<SwapRequest>(rows, tag: 'swap_requests'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
|
||||
@@ -258,30 +258,48 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
|
||||
return;
|
||||
}
|
||||
setState(() => saving = true);
|
||||
final controller = ref.read(
|
||||
officesControllerProvider,
|
||||
);
|
||||
if (office == null) {
|
||||
await controller.createOffice(
|
||||
name: name,
|
||||
serviceId: selectedServiceId,
|
||||
);
|
||||
} else {
|
||||
await controller.updateOffice(
|
||||
id: office.id,
|
||||
name: name,
|
||||
serviceId: selectedServiceId,
|
||||
);
|
||||
}
|
||||
ref.invalidate(officesProvider);
|
||||
if (context.mounted) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
showSuccessSnackBar(
|
||||
context,
|
||||
office == null
|
||||
? 'Office "$name" has been created successfully.'
|
||||
: 'Office "$name" has been updated successfully.',
|
||||
try {
|
||||
final controller = ref.read(
|
||||
officesControllerProvider,
|
||||
);
|
||||
if (office == null) {
|
||||
await controller.createOffice(
|
||||
name: name,
|
||||
serviceId: selectedServiceId,
|
||||
);
|
||||
} else {
|
||||
await controller.updateOffice(
|
||||
id: office.id,
|
||||
name: name,
|
||||
serviceId: selectedServiceId,
|
||||
);
|
||||
}
|
||||
ref.invalidate(officesProvider);
|
||||
if (context.mounted) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
showSuccessSnackBar(
|
||||
context,
|
||||
office == null
|
||||
? 'Office "$name" has been created successfully.'
|
||||
: 'Office "$name" has been updated successfully.',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!dialogContext.mounted) return;
|
||||
if (isOfflineSaveError(e)) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
showSuccessSnackBarGlobal(
|
||||
office == null
|
||||
? 'Office "$name" saved offline — will sync when connected.'
|
||||
: 'Office "$name" update saved offline — will sync when connected.',
|
||||
);
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed to save: $e');
|
||||
}
|
||||
} finally {
|
||||
if (dialogContext.mounted) {
|
||||
setState(() => saving = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: saving
|
||||
|
||||
@@ -57,9 +57,12 @@ class _AnnouncementCommentsSectionState
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to post comment: $e')),
|
||||
);
|
||||
if (isOfflineSaveError(e)) {
|
||||
_controller.clear();
|
||||
showSuccessSnackBar(context, 'Comment saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed to post comment: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _sending = false);
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../widgets/app_page_header.dart';
|
||||
import '../../widgets/m3_card.dart';
|
||||
import '../../widgets/profile_avatar.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
import '../../widgets/sync_pending_badge.dart';
|
||||
import 'announcement_comments_section.dart';
|
||||
import 'create_announcement_dialog.dart';
|
||||
|
||||
@@ -317,6 +318,14 @@ class _AnnouncementCardState extends ConsumerState<_AnnouncementCard> {
|
||||
ref.watch(announcementCommentsProvider(widget.announcement.id));
|
||||
final commentCount = commentsAsync.valueOrNull?.length ?? 0;
|
||||
|
||||
// Pending-sync state
|
||||
final pendingNew = ref
|
||||
.watch(offlinePendingAnnouncementsProvider)
|
||||
.any((a) => a.id == widget.announcement.id);
|
||||
final pendingUpdates = ref.watch(offlinePendingAnnouncementUpdatesProvider);
|
||||
final isAnnouncementPending =
|
||||
pendingNew || pendingUpdates.containsKey(widget.announcement.id);
|
||||
|
||||
// Rebuild UI when cooldown is active.
|
||||
if (_inCooldown) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
@@ -428,9 +437,21 @@ class _AnnouncementCardState extends ConsumerState<_AnnouncementCard> {
|
||||
// Title + Body
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Text(
|
||||
widget.announcement.title,
|
||||
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.announcement.title,
|
||||
style: tt.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
if (isAnnouncementPending) ...[
|
||||
const SizedBox(width: 8),
|
||||
SyncPendingBadge(isPending: true, compact: true),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
|
||||
@@ -269,7 +269,14 @@ class _CreateAnnouncementContentState
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) showErrorSnackBar(context, 'Failed to save: $e');
|
||||
if (mounted) {
|
||||
if (isOfflineSaveError(e)) {
|
||||
Navigator.of(context).pop();
|
||||
showSuccessSnackBarGlobal('Announcement saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed to save: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
@@ -537,7 +544,14 @@ class _BannerSettingsContentState
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
showSuccessSnackBarGlobal('Banner settings saved.');
|
||||
} catch (e) {
|
||||
if (mounted) showErrorSnackBar(context, 'Failed to save: $e');
|
||||
if (mounted) {
|
||||
if (isOfflineSaveError(e)) {
|
||||
Navigator.of(context).pop();
|
||||
showSuccessSnackBarGlobal('Banner settings saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed to save: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import '../../widgets/gemini_button.dart';
|
||||
import '../../widgets/multi_select_picker.dart';
|
||||
import '../../widgets/app_page_header.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
import '../../widgets/sync_pending_badge.dart';
|
||||
|
||||
class AttendanceScreen extends ConsumerStatefulWidget {
|
||||
const AttendanceScreen({super.key});
|
||||
@@ -362,6 +363,7 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
|
||||
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
||||
final schedulesAsync = ref.watch(dutySchedulesProvider);
|
||||
final logsAsync = ref.watch(attendanceLogsProvider);
|
||||
final pendingCheckIns = ref.watch(offlinePendingCheckInsProvider);
|
||||
final allowTracking = profile?.allowTracking ?? false;
|
||||
// local state for optimistic switch update. We only trust `_trackingLocal`
|
||||
// while a save is in flight – after that the server-side value (`allowTracking`)
|
||||
@@ -433,8 +435,12 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
|
||||
// (Note: this override should not prevent the overtime check-in from being shown.)
|
||||
|
||||
// Show overtime check-in when the user has no schedule today, or their last
|
||||
// scheduled shift has already ended.
|
||||
// scheduled shift has already ended. Guard with hasValue so the card never
|
||||
// flashes during the initial stream loading phase (valueOrNull returns []
|
||||
// while loading, making hasScheduleToday false prematurely).
|
||||
final showOvertimeCard =
|
||||
schedulesAsync.hasValue &&
|
||||
logsAsync.hasValue &&
|
||||
(activeOvertimeLog.isEmpty && _overtimeLogId == null) &&
|
||||
activeLog.isEmpty &&
|
||||
(!hasScheduleToday || hasScheduleEnded);
|
||||
@@ -786,6 +792,15 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (pendingCheckIns.any(
|
||||
(l) => l.dutyScheduleId == schedule.id,
|
||||
)) ...[
|
||||
SyncPendingBadge(
|
||||
isPending: true,
|
||||
compact: true,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
_statusChip(context, statusLabel),
|
||||
],
|
||||
),
|
||||
@@ -4228,7 +4243,11 @@ class _MyScheduleTabState extends ConsumerState<_MyScheduleTab> {
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
if (!alreadyProcessed) {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Response saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Refresh the swap list — the new provider fires an immediate REST poll
|
||||
@@ -4872,6 +4891,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
final slipsAsync = ref.watch(passSlipsProvider);
|
||||
final profilesAsync = ref.watch(profilesProvider);
|
||||
final activeSlip = ref.watch(activePassSlipProvider);
|
||||
final pendingSlips = ref.watch(offlinePendingPassSlipsProvider);
|
||||
final isAdmin = profile?.role == 'admin' || profile?.role == 'dispatcher';
|
||||
|
||||
final Map<String, Profile> profileById = {
|
||||
@@ -4985,6 +5005,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
slip,
|
||||
profileById,
|
||||
showActions: true,
|
||||
isPending: pendingSlips.any((p) => p.id == slip.id),
|
||||
),
|
||||
),
|
||||
if (slips.where((s) => s.status == 'pending').isEmpty)
|
||||
@@ -5017,6 +5038,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
slip,
|
||||
profileById,
|
||||
showActions: false,
|
||||
isPending: pendingSlips.any((p) => p.id == slip.id),
|
||||
),
|
||||
),
|
||||
if (slips.isEmpty)
|
||||
@@ -5044,6 +5066,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
PassSlip slip,
|
||||
Map<String, Profile> profileById, {
|
||||
required bool showActions,
|
||||
bool isPending = false,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
@@ -5073,6 +5096,10 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Text(name, style: theme.textTheme.titleSmall)),
|
||||
if (isPending) ...[
|
||||
SyncPendingBadge(isPending: true, compact: true),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
@@ -5151,7 +5178,11 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Approval saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
@@ -5167,7 +5198,11 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Rejection saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
@@ -5183,7 +5218,11 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Completion saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
@@ -5212,6 +5251,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
||||
final leavesAsync = ref.watch(leavesProvider);
|
||||
final profilesAsync = ref.watch(profilesProvider);
|
||||
final pendingLeavesList = ref.watch(offlinePendingLeavesProvider);
|
||||
|
||||
if (profile == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -5259,6 +5299,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
leave,
|
||||
profileById,
|
||||
showApproval: true,
|
||||
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -5292,6 +5333,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
leave,
|
||||
profileById,
|
||||
showApproval: false,
|
||||
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5314,6 +5356,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
leave,
|
||||
profileById,
|
||||
showApproval: false,
|
||||
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||
),
|
||||
),
|
||||
if (leaves
|
||||
@@ -5343,6 +5386,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
LeaveOfAbsence leave,
|
||||
Map<String, Profile> profileById, {
|
||||
required bool showApproval,
|
||||
bool isPending = false,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
@@ -5372,6 +5416,10 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Text(name, style: theme.textTheme.titleSmall)),
|
||||
if (isPending) ...[
|
||||
SyncPendingBadge(isPending: true, compact: true),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
@@ -5473,7 +5521,11 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Approval saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
@@ -5491,7 +5543,11 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Rejection saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
@@ -5509,7 +5565,11 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Cancellation saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
@@ -5618,7 +5678,14 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
|
||||
widget.onSubmitted();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) showErrorSnackBar(context, 'Failed: $e');
|
||||
if (!mounted) return;
|
||||
if (isOfflineSaveError(e)) {
|
||||
Navigator.of(context).pop();
|
||||
showSuccessSnackBarGlobal('Pass slip request saved offline — will sync when connected.');
|
||||
widget.onSubmitted();
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed: $e');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
@@ -5893,7 +5960,12 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> {
|
||||
widget.onSubmitted();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
if (!mounted) return;
|
||||
if (isOfflineSaveError(e)) {
|
||||
Navigator.of(context).pop();
|
||||
showSuccessSnackBarGlobal('Leave request saved offline — will sync when connected.');
|
||||
widget.onSubmitted();
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Failed to file leave: $e');
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -1335,7 +1335,7 @@ class _ItServiceRequestDetailScreenState
|
||||
_finishSave(success: true);
|
||||
} catch (e) {
|
||||
debugPrint('Save field error: $e');
|
||||
_finishSave(success: false);
|
||||
_finishSave(success: isOfflineSaveError(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1350,7 +1350,7 @@ class _ItServiceRequestDetailScreenState
|
||||
_finishSave(success: true);
|
||||
} catch (e) {
|
||||
debugPrint('Save datetime error: $e');
|
||||
_finishSave(success: false);
|
||||
_finishSave(success: isOfflineSaveError(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1485,7 +1485,13 @@ class _ItServiceRequestDetailScreenState
|
||||
|
||||
if (mounted) showSuccessSnackBar(context, 'Text improved successfully');
|
||||
} catch (e) {
|
||||
if (mounted) showErrorSnackBar(context, 'Error: $e');
|
||||
if (mounted) {
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Error: $e');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
@@ -1525,7 +1531,13 @@ class _ItServiceRequestDetailScreenState
|
||||
|
||||
if (mounted) showSuccessSnackBar(context, 'Request approved');
|
||||
} catch (e) {
|
||||
if (mounted) showErrorSnackBar(context, 'Error: $e');
|
||||
if (mounted) {
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Approval saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1630,7 +1642,13 @@ class _ItServiceRequestDetailScreenState
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) showErrorSnackBar(context, 'Error: $e');
|
||||
if (mounted) {
|
||||
if (isOfflineSaveError(e)) {
|
||||
showSuccessSnackBar(context, 'Status change saved offline — will sync when connected.');
|
||||
} else {
|
||||
showErrorSnackBar(context, 'Error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../models/it_service_request.dart';
|
||||
import '../../models/it_service_request_assignment.dart';
|
||||
import '../../models/office.dart';
|
||||
import '../../models/profile.dart';
|
||||
import '../../brick/cache_helpers.dart';
|
||||
import '../../providers/it_service_request_provider.dart';
|
||||
import '../../providers/profile_provider.dart';
|
||||
import '../../providers/realtime_controller.dart';
|
||||
@@ -23,6 +24,7 @@ import '../../widgets/app_page_header.dart';
|
||||
import '../../widgets/app_state_view.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
import '../../widgets/status_pill.dart';
|
||||
import '../../widgets/sync_pending_badge.dart';
|
||||
|
||||
class ItServiceRequestsListScreen extends ConsumerStatefulWidget {
|
||||
const ItServiceRequestsListScreen({super.key});
|
||||
@@ -67,6 +69,26 @@ class _ItServiceRequestsListScreenState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final requestsAsync = ref.watch(itServiceRequestsProvider);
|
||||
final offlinePending = ref.watch(offlinePendingItServiceRequestsProvider);
|
||||
final pendingUpdates =
|
||||
ref.watch(offlinePendingItServiceRequestUpdatesProvider);
|
||||
final isrPendingIds = {
|
||||
...offlinePending.map((r) => r.id),
|
||||
...pendingUpdates.keys,
|
||||
};
|
||||
|
||||
ref.listen<AsyncValue<List<ItServiceRequest>>>(itServiceRequestsProvider, (_, next) {
|
||||
final live = next.valueOrNull;
|
||||
if (live == null || live.isEmpty) return;
|
||||
final pending = ref.read(offlinePendingItServiceRequestsProvider);
|
||||
if (pending.isEmpty) return;
|
||||
final liveIds = live.map((r) => r.id).toSet();
|
||||
final stillPending = pending.where((r) => !liveIds.contains(r.id)).toList();
|
||||
if (stillPending.length != pending.length) {
|
||||
ref.read(offlinePendingItServiceRequestsProvider.notifier).state = stillPending;
|
||||
}
|
||||
});
|
||||
|
||||
final profileAsync = ref.watch(currentProfileProvider);
|
||||
final profilesAsync = ref.watch(profilesProvider);
|
||||
final officesAsync = ref.watch(officesProvider);
|
||||
@@ -131,8 +153,12 @@ class _ItServiceRequestsListScreenState
|
||||
],
|
||||
);
|
||||
}
|
||||
final allRequests =
|
||||
final liveRequests =
|
||||
requestsAsync.valueOrNull ?? <ItServiceRequest>[];
|
||||
final liveIds = liveRequests.map((r) => r.id).toSet();
|
||||
final pendingOnly =
|
||||
offlinePending.where((r) => !liveIds.contains(r.id)).toList();
|
||||
final allRequests = [...pendingOnly, ...liveRequests];
|
||||
if (allRequests.isEmpty) {
|
||||
return const AppEmptyView(
|
||||
icon: Icons.miscellaneous_services_outlined,
|
||||
@@ -277,12 +303,14 @@ class _ItServiceRequestsListScreenState
|
||||
officeById: officeById,
|
||||
profileById: profileById,
|
||||
assignments: assignments,
|
||||
pendingIds: isrPendingIds,
|
||||
),
|
||||
_RequestList(
|
||||
requests: _applyFilters(allRequests),
|
||||
officeById: officeById,
|
||||
profileById: profileById,
|
||||
assignments: assignments,
|
||||
pendingIds: isrPendingIds,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -291,6 +319,7 @@ class _ItServiceRequestsListScreenState
|
||||
officeById: officeById,
|
||||
profileById: profileById,
|
||||
assignments: assignments,
|
||||
pendingIds: isrPendingIds,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -427,12 +456,31 @@ class _ItServiceRequestsListScreenState
|
||||
requestedByUserId: profile?.id,
|
||||
status: (profile?.role == 'standard') ? 'pending_approval' : 'draft',
|
||||
);
|
||||
if (context.mounted) {
|
||||
final isOffline = data['pending'] == true;
|
||||
if (isOffline) {
|
||||
final localId = data['id'] as String;
|
||||
final allCached = await cachedListFromBrick<ItServiceRequest>();
|
||||
final local = allCached.where((r) => r.id == localId).firstOrNull;
|
||||
if (local != null && context.mounted) {
|
||||
final current = ref.read(offlinePendingItServiceRequestsProvider);
|
||||
ref.read(offlinePendingItServiceRequestsProvider.notifier).state = [
|
||||
local,
|
||||
...current,
|
||||
];
|
||||
}
|
||||
if (context.mounted) {
|
||||
showSuccessSnackBar(
|
||||
context,
|
||||
'Request saved offline — will sync when connected.',
|
||||
);
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
showSuccessSnackBar(context, 'Request created');
|
||||
context.go('/it-service-requests/${data['id']}');
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) showErrorSnackBar(context, 'Error: $e');
|
||||
if (!context.mounted) return;
|
||||
showErrorSnackBar(context, 'Error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,12 +648,14 @@ class _RequestList extends StatelessWidget {
|
||||
required this.officeById,
|
||||
required this.profileById,
|
||||
required this.assignments,
|
||||
required this.pendingIds,
|
||||
});
|
||||
|
||||
final List<ItServiceRequest> requests;
|
||||
final Map<String, Office> officeById;
|
||||
final Map<String, Profile> profileById;
|
||||
final List<ItServiceRequestAssignment> assignments;
|
||||
final Set<String> pendingIds;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -631,6 +681,7 @@ class _RequestList extends StatelessWidget {
|
||||
request: request,
|
||||
officeName: office,
|
||||
assignedStaff: assignedStaff,
|
||||
isPending: pendingIds.contains(request.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -643,11 +694,13 @@ class _RequestTile extends StatelessWidget {
|
||||
required this.request,
|
||||
this.officeName,
|
||||
required this.assignedStaff,
|
||||
this.isPending = false,
|
||||
});
|
||||
|
||||
final ItServiceRequest request;
|
||||
final String? officeName;
|
||||
final List<String> assignedStaff;
|
||||
final bool isPending;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -669,11 +722,22 @@ class _RequestTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
request.eventName,
|
||||
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
request.eventName,
|
||||
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (isPending) ...[
|
||||
const SizedBox(width: 8),
|
||||
SyncPendingBadge(isPending: true, compact: true),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (request.services.isNotEmpty)
|
||||
|
||||
@@ -423,10 +423,15 @@ class _ItJobTileState extends ConsumerState<_ItJobTile> {
|
||||
// The realtime stream may arrive with a slight delay; clearing here
|
||||
// causes a visible revert flash before the stream catches up.
|
||||
} catch (e) {
|
||||
// Revert optimistic state on failure only
|
||||
if (mounted) {
|
||||
setState(() => _optimisticChecked = !val);
|
||||
showErrorSnackBar(context, 'Failed to update: $e');
|
||||
if (isOfflineSaveError(e)) {
|
||||
// Brick queued the write — keep optimistic state, just notify user
|
||||
showSuccessSnackBar(context, 'Saved offline — will sync when connected.');
|
||||
} else {
|
||||
// Revert optimistic state on real failure
|
||||
setState(() => _optimisticChecked = !val);
|
||||
showErrorSnackBar(context, 'Failed to update: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user