Offline Support
This commit is contained in:
@@ -35,6 +35,24 @@
|
|||||||
},
|
},
|
||||||
"createdAt": "2026-04-08T22:52:50.281Z",
|
"createdAt": "2026-04-08T22:52:50.281Z",
|
||||||
"updatedAt": "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"
|
"version": "3.0.0"
|
||||||
|
|||||||
@@ -51,6 +51,12 @@
|
|||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/hook-handler.cjs route",
|
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/hook-handler.cjs route",
|
||||||
"timeout": 10000
|
"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",
|
"type": "command",
|
||||||
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/auto-memory-hook.mjs import",
|
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/auto-memory-hook.mjs import",
|
||||||
"timeout": 8000
|
"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",
|
"type": "command",
|
||||||
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/auto-memory-hook.mjs sync",
|
"command": "cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/auto-memory-hook.mjs sync",
|
||||||
"timeout": 10000
|
"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/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/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
|
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 'routing/app_router.dart';
|
||||||
import 'models/profile.dart';
|
import 'models/profile.dart';
|
||||||
|
import 'providers/connectivity_provider.dart';
|
||||||
import 'providers/profile_provider.dart';
|
import 'providers/profile_provider.dart';
|
||||||
import 'services/background_location_service.dart';
|
import 'services/background_location_service.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
import 'utils/snackbar.dart';
|
import 'utils/snackbar.dart';
|
||||||
import 'widgets/ios_install_prompt.dart';
|
import 'widgets/ios_install_prompt.dart';
|
||||||
|
import 'widgets/offline_banner.dart';
|
||||||
|
|
||||||
class TasqApp extends ConsumerWidget {
|
class TasqApp extends ConsumerWidget {
|
||||||
const TasqApp({super.key});
|
const TasqApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
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);
|
final router = ref.watch(appRouterProvider);
|
||||||
|
|
||||||
// Ensure background service is running if the profile indicates tracking.
|
// Ensure background service is running if the profile indicates tracking.
|
||||||
@@ -48,8 +56,9 @@ class TasqApp extends ConsumerWidget {
|
|||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
FlutterQuillLocalizations.delegate,
|
FlutterQuillLocalizations.delegate,
|
||||||
],
|
],
|
||||||
builder: (context, child) =>
|
builder: (context, child) => OfflineBanner(
|
||||||
IosInstallPrompt(child: child ?? const SizedBox.shrink()),
|
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';
|
import 'firebase_options.dart';
|
||||||
// removed unused imports
|
// removed unused imports
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
|
import 'brick/cache_warmer.dart';
|
||||||
|
import 'brick/repository.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
import 'providers/notifications_provider.dart';
|
import 'providers/notifications_provider.dart';
|
||||||
import 'providers/notification_navigation_provider.dart';
|
import 'providers/notification_navigation_provider.dart';
|
||||||
@@ -276,10 +278,16 @@ Future<void> main() async {
|
|||||||
return;
|
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 {
|
try {
|
||||||
await Supabase.initialize(
|
await Supabase.initialize(
|
||||||
url: supabaseUrl,
|
url: supabaseUrl,
|
||||||
anonKey: supabaseAnonKey,
|
anonKey: supabaseAnonKey,
|
||||||
|
httpClient: brickHttpClient,
|
||||||
).timeout(const Duration(seconds: 20));
|
).timeout(const Duration(seconds: 20));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Supabase init failed or timed out: $e');
|
debugPrint('Supabase init failed or timed out: $e');
|
||||||
@@ -287,6 +295,16 @@ Future<void> main() async {
|
|||||||
return;
|
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)
|
// Initialize background location service (flutter_background_service)
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
try {
|
try {
|
||||||
@@ -302,6 +320,17 @@ Future<void> main() async {
|
|||||||
// Run this after runApp so startup is not blocked by network/token ops.
|
// Run this after runApp so startup is not blocked by network/token ops.
|
||||||
final supaClient = Supabase.instance.client;
|
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
|
// listen for auth changes to register/unregister token accordingly
|
||||||
supaClient.auth.onAuthStateChange.listen((data) async {
|
supaClient.auth.onAuthStateChange.listen((data) async {
|
||||||
final event = data.event;
|
final event = data.event;
|
||||||
@@ -347,6 +376,16 @@ Future<void> main() async {
|
|||||||
return;
|
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;
|
String? token;
|
||||||
try {
|
try {
|
||||||
token = await FirebaseMessaging.instance.getToken();
|
token = await FirebaseMessaging.instance.getToken();
|
||||||
|
|||||||
@@ -1,111 +1 @@
|
|||||||
import '../utils/app_time.dart';
|
export 'announcement.model.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?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'announcement_comment.model.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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'attendance_log.model.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?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'chat_message.model.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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'duty_schedule.model.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?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'it_service_request.model.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'it_service_request_action.model.dart';
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'it_service_request_activity_log.model.dart';
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'it_service_request_assignment.model.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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'leave_of_absence.model.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'notification_item.model.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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
export 'office.model.dart';
|
||||||
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?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'pass_slip.model.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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
export 'profile.model.dart';
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
export 'service.model.dart';
|
||||||
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? ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'swap_request.model.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?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'task.model.dart';
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'task_activity_log.model.dart';
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
export 'team.model.dart';
|
||||||
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?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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';
|
export 'ticket.model.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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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.dart';
|
||||||
import '../models/announcement_comment.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 'notifications_provider.dart';
|
||||||
import 'profile_provider.dart';
|
import 'profile_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'realtime_controller.dart';
|
import 'realtime_controller.dart';
|
||||||
import '../utils/app_time.dart';
|
|
||||||
|
|
||||||
/// Maximum rows fetched per poll. Announcements are lower-volume than tasks,
|
/// Maximum rows fetched per poll.
|
||||||
/// so 100 is sufficient; adjust if orgs routinely exceed this.
|
|
||||||
const int kAnnouncementsPageLimit = 100;
|
const int kAnnouncementsPageLimit = 100;
|
||||||
|
|
||||||
/// Thrown when an announcement or comment is saved successfully but
|
/// Thrown when an announcement or comment is saved successfully but
|
||||||
@@ -24,6 +27,26 @@ class AnnouncementNotificationException implements Exception {
|
|||||||
String toString() => message;
|
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).
|
/// Streams all announcements visible to the current user (RLS filtered).
|
||||||
final announcementsProvider = StreamProvider<List<Announcement>>((ref) {
|
final announcementsProvider = StreamProvider<List<Announcement>>((ref) {
|
||||||
final userId = ref.watch(currentUserIdProvider);
|
final userId = ref.watch(currentUserIdProvider);
|
||||||
@@ -31,6 +54,53 @@ final announcementsProvider = StreamProvider<List<Announcement>>((ref) {
|
|||||||
|
|
||||||
final client = ref.watch(supabaseClientProvider);
|
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>(
|
final wrapper = StreamRecoveryWrapper<Announcement>(
|
||||||
stream: client
|
stream: client
|
||||||
.from('announcements')
|
.from('announcements')
|
||||||
@@ -47,10 +117,138 @@ final announcementsProvider = StreamProvider<List<Announcement>>((ref) {
|
|||||||
fromMap: Announcement.fromMap,
|
fromMap: Announcement.fromMap,
|
||||||
channelName: 'announcements',
|
channelName: 'announcements',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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.
|
/// Streams comments for a specific announcement.
|
||||||
@@ -61,6 +259,20 @@ final announcementCommentsProvider =
|
|||||||
) {
|
) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
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>(
|
final wrapper = StreamRecoveryWrapper<AnnouncementComment>(
|
||||||
stream: client
|
stream: client
|
||||||
.from('announcement_comments')
|
.from('announcement_comments')
|
||||||
@@ -78,37 +290,51 @@ final announcementCommentsProvider =
|
|||||||
fromMap: AnnouncementComment.fromMap,
|
fromMap: AnnouncementComment.fromMap,
|
||||||
channelName: 'announcement_comments_$announcementId',
|
channelName: 'announcement_comments_$announcementId',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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.
|
/// 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 activeBannerAnnouncementsProvider = Provider<List<Announcement>>((ref) {
|
||||||
final all = ref.watch(announcementsProvider).valueOrNull ?? [];
|
final all = ref.watch(announcementsProvider).valueOrNull ?? [];
|
||||||
return all.where((a) => !a.isTemplate && a.isBannerActive).toList();
|
return all.where((a) => !a.isTemplate && a.isBannerActive).toList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Controller
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
final announcementsControllerProvider =
|
final announcementsControllerProvider =
|
||||||
Provider<AnnouncementsController>((ref) {
|
Provider<AnnouncementsController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
final notifCtrl = ref.watch(notificationsControllerProvider);
|
final notifCtrl = ref.watch(notificationsControllerProvider);
|
||||||
return AnnouncementsController(client, notifCtrl);
|
return AnnouncementsController(client, notifCtrl, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class AnnouncementsController {
|
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
|
// _client is declared dynamic to allow test doubles.
|
||||||
// subset of methods used by this class. In production it is a SupabaseClient.
|
|
||||||
// ignore: avoid_dynamic_calls
|
// ignore: avoid_dynamic_calls
|
||||||
final dynamic _client;
|
final dynamic _client;
|
||||||
final dynamic _notifCtrl;
|
final dynamic _notifCtrl;
|
||||||
|
final Ref? _ref;
|
||||||
|
|
||||||
/// Create a new announcement and send push notifications to target users.
|
/// Create a new announcement and send push notifications to target users.
|
||||||
|
/// Offline: queues a local row and returns silently.
|
||||||
Future<void> createAnnouncement({
|
Future<void> createAnnouncement({
|
||||||
required String title,
|
required String title,
|
||||||
required String body,
|
required String body,
|
||||||
@@ -120,10 +346,14 @@ class AnnouncementsController {
|
|||||||
DateTime? bannerHideAt,
|
DateTime? bannerHideAt,
|
||||||
int? pushIntervalMinutes,
|
int? pushIntervalMinutes,
|
||||||
}) async {
|
}) async {
|
||||||
final authorId = _client.auth.currentUser?.id;
|
final authorId = _client.auth.currentUser?.id as String?;
|
||||||
if (authorId == null) return;
|
if (authorId == null) return;
|
||||||
|
|
||||||
|
final id = const Uuid().v4();
|
||||||
|
final now = AppTime.now();
|
||||||
|
|
||||||
final row = {
|
final row = {
|
||||||
|
'id': id,
|
||||||
'author_id': authorId,
|
'author_id': authorId,
|
||||||
'title': title,
|
'title': title,
|
||||||
'body': body,
|
'body': body,
|
||||||
@@ -136,23 +366,39 @@ class AnnouncementsController {
|
|||||||
'push_interval_minutes': pushIntervalMinutes,
|
'push_interval_minutes': pushIntervalMinutes,
|
||||||
};
|
};
|
||||||
|
|
||||||
final result = await _client
|
String? announcementId;
|
||||||
.from('announcements')
|
try {
|
||||||
.insert(row)
|
final result = await _client
|
||||||
.select('id')
|
.from('announcements')
|
||||||
.single();
|
.insert(row)
|
||||||
final announcementId = result['id'] as String;
|
.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;
|
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;
|
if (bannerEnabled && pushIntervalMinutes != null) return;
|
||||||
|
|
||||||
// Query users whose role matches visible_roles, excluding the author
|
|
||||||
try {
|
try {
|
||||||
final profiles = await _client
|
final profiles = await _client
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
@@ -160,9 +406,8 @@ class AnnouncementsController {
|
|||||||
.inFilter('role', visibleRoles);
|
.inFilter('role', visibleRoles);
|
||||||
final userIds = (profiles as List)
|
final userIds = (profiles as List)
|
||||||
.map((p) => p['id'] as String)
|
.map((p) => p['id'] as String)
|
||||||
.where((id) => id != authorId)
|
.where((uid) => uid != authorId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (userIds.isEmpty) return;
|
if (userIds.isEmpty) return;
|
||||||
|
|
||||||
await _notifCtrl.createNotification(
|
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({
|
Future<void> updateAnnouncement({
|
||||||
required String id,
|
required String id,
|
||||||
required String title,
|
required String title,
|
||||||
@@ -221,11 +466,16 @@ class AnnouncementsController {
|
|||||||
} else if (clearPushInterval) {
|
} else if (clearPushInterval) {
|
||||||
payload['push_interval_minutes'] = null;
|
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.
|
/// Update only the banner settings. Offline: queues the field patch.
|
||||||
/// Intended for the "Manage Banner" popup available to the poster and admins.
|
|
||||||
Future<void> updateBannerSettings({
|
Future<void> updateBannerSettings({
|
||||||
required String id,
|
required String id,
|
||||||
required bool bannerEnabled,
|
required bool bannerEnabled,
|
||||||
@@ -233,25 +483,37 @@ class AnnouncementsController {
|
|||||||
DateTime? bannerHideAt,
|
DateTime? bannerHideAt,
|
||||||
int? pushIntervalMinutes,
|
int? pushIntervalMinutes,
|
||||||
}) async {
|
}) async {
|
||||||
await _client.from('announcements').update({
|
final payload = <String, dynamic>{
|
||||||
'banner_enabled': bannerEnabled,
|
'banner_enabled': bannerEnabled,
|
||||||
'banner_show_at': bannerShowAt?.toUtc().toIso8601String(),
|
'banner_show_at': bannerShowAt?.toUtc().toIso8601String(),
|
||||||
'banner_hide_at': bannerHideAt?.toUtc().toIso8601String(),
|
'banner_hide_at': bannerHideAt?.toUtc().toIso8601String(),
|
||||||
'push_interval_minutes': pushIntervalMinutes,
|
'push_interval_minutes': pushIntervalMinutes,
|
||||||
'updated_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Immediately stops a banner by setting [banner_hide_at] to now.
|
/// Immediately stops a banner by setting banner_hide_at to now.
|
||||||
/// Usable by the poster or an admin.
|
/// Offline: queues the field patch.
|
||||||
Future<void> dismissBanner(String id) async {
|
Future<void> dismissBanner(String id) async {
|
||||||
await _client.from('announcements').update({
|
final payload = <String, dynamic>{
|
||||||
'banner_hide_at': AppTime.nowUtc().toIso8601String(),
|
'banner_hide_at': AppTime.nowUtc().toIso8601String(),
|
||||||
'updated_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 {
|
Future<void> deleteAnnouncement(String id) async {
|
||||||
await _client.from('announcements').delete().eq('id', id);
|
await _client.from('announcements').delete().eq('id', id);
|
||||||
}
|
}
|
||||||
@@ -264,27 +526,41 @@ class AnnouncementsController {
|
|||||||
.eq('id', templateId)
|
.eq('id', templateId)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
if (data == null) return null;
|
if (data == null) return null;
|
||||||
return Announcement.fromMap(data);
|
return Announcement.fromMap(data as Map<String, dynamic>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a comment to an announcement.
|
/// Add a comment. Offline: queues the comment locally.
|
||||||
/// Notifies the announcement author and all previous commenters.
|
|
||||||
Future<void> addComment({
|
Future<void> addComment({
|
||||||
required String announcementId,
|
required String announcementId,
|
||||||
required String body,
|
required String body,
|
||||||
}) async {
|
}) async {
|
||||||
final authorId = _client.auth.currentUser?.id;
|
final authorId = _client.auth.currentUser?.id as String?;
|
||||||
if (authorId == null) return;
|
if (authorId == null) return;
|
||||||
|
|
||||||
await _client.from('announcement_comments').insert({
|
final id = const Uuid().v4();
|
||||||
'announcement_id': announcementId,
|
|
||||||
'author_id': authorId,
|
try {
|
||||||
'body': body,
|
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 {
|
try {
|
||||||
// Get the announcement author and title
|
|
||||||
final announcement = await _client
|
final announcement = await _client
|
||||||
.from('announcements')
|
.from('announcements')
|
||||||
.select('author_id, title')
|
.select('author_id, title')
|
||||||
@@ -296,23 +572,18 @@ class AnnouncementsController {
|
|||||||
final announcementTitle =
|
final announcementTitle =
|
||||||
announcement['title'] as String? ?? 'an announcement';
|
announcement['title'] as String? ?? 'an announcement';
|
||||||
|
|
||||||
// Get all unique commenters on this announcement
|
|
||||||
final comments = await _client
|
final comments = await _client
|
||||||
.from('announcement_comments')
|
.from('announcement_comments')
|
||||||
.select('author_id')
|
.select('author_id')
|
||||||
.eq('announcement_id', announcementId);
|
.eq('announcement_id', announcementId);
|
||||||
final commenterIds = (comments as List)
|
final commenterIds =
|
||||||
.map((c) => c['author_id'] as String)
|
(comments as List).map((c) => c['author_id'] as String).toSet();
|
||||||
.toSet();
|
|
||||||
|
|
||||||
// Combine author + commenters, exclude self
|
|
||||||
final notifyIds = <String>{announcementAuthorId, ...commenterIds}
|
final notifyIds = <String>{announcementAuthorId, ...commenterIds}
|
||||||
.where((id) => id != authorId)
|
.where((uid) => uid != authorId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (notifyIds.isEmpty) return;
|
if (notifyIds.isEmpty) return;
|
||||||
|
|
||||||
// Fetch commenter's display name for a human-readable push body
|
|
||||||
final commenterData = await _client
|
final commenterData = await _client
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('full_name')
|
.select('full_name')
|
||||||
@@ -340,10 +611,9 @@ class AnnouncementsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Re-sends push notifications for an existing announcement.
|
/// Re-sends push notifications for an existing announcement.
|
||||||
/// Intended for use by admins or the announcement author.
|
|
||||||
Future<void> resendAnnouncementNotification(
|
Future<void> resendAnnouncementNotification(
|
||||||
Announcement announcement) async {
|
Announcement announcement) async {
|
||||||
final actorId = _client.auth.currentUser?.id;
|
final actorId = _client.auth.currentUser?.id as String?;
|
||||||
if (actorId == null) return;
|
if (actorId == null) return;
|
||||||
if (announcement.visibleRoles.isEmpty) return;
|
if (announcement.visibleRoles.isEmpty) return;
|
||||||
|
|
||||||
@@ -354,9 +624,8 @@ class AnnouncementsController {
|
|||||||
.inFilter('role', announcement.visibleRoles);
|
.inFilter('role', announcement.visibleRoles);
|
||||||
final userIds = (profiles as List)
|
final userIds = (profiles as List)
|
||||||
.map((p) => p['id'] as String)
|
.map((p) => p['id'] as String)
|
||||||
.where((id) => id != actorId)
|
.where((uid) => uid != actorId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (userIds.isEmpty) return;
|
if (userIds.isEmpty) return;
|
||||||
|
|
||||||
await _notifCtrl.createNotification(
|
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 {
|
Future<void> deleteComment(String id) async {
|
||||||
await _client.from('announcement_comments').delete().eq('id', id);
|
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 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show debugPrint;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../brick/cache_helpers.dart';
|
||||||
import '../models/attendance_log.dart';
|
import '../models/attendance_log.dart';
|
||||||
import '../utils/app_time.dart';
|
import '../utils/app_time.dart';
|
||||||
|
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||||
|
import 'connectivity_provider.dart';
|
||||||
import 'profile_provider.dart';
|
import 'profile_provider.dart';
|
||||||
import 'reports_provider.dart';
|
import 'reports_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'realtime_controller.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 attendanceDateRangeProvider = StateProvider<ReportDateRange>((ref) {
|
||||||
final now = AppTime.now();
|
final now = AppTime.now();
|
||||||
final today = DateTime(now.year, now.month, now.day);
|
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.
|
/// Filter for logbook users (multi-select). If empty, all users are shown.
|
||||||
final attendanceUserFilterProvider = StateProvider<List<String>>((ref) => []);
|
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).
|
/// All visible attendance logs (own for standard, all for admin/dispatcher/it_staff).
|
||||||
final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
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(
|
final profileId = ref.watch(
|
||||||
currentProfileProvider.select((p) => p.valueOrNull?.id),
|
currentProfileProvider.select((p) => p.valueOrNull?.id),
|
||||||
);
|
);
|
||||||
@@ -45,6 +70,56 @@ final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
|||||||
profileRole == 'dispatcher' ||
|
profileRole == 'dispatcher' ||
|
||||||
profileRole == 'it_staff';
|
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>(
|
final wrapper = StreamRecoveryWrapper<AttendanceLog>(
|
||||||
stream: hasFullAccess
|
stream: hasFullAccess
|
||||||
? client
|
? client
|
||||||
@@ -68,55 +143,225 @@ final attendanceLogsProvider = StreamProvider<List<AttendanceLog>>((ref) {
|
|||||||
fromMap: AttendanceLog.fromMap,
|
fromMap: AttendanceLog.fromMap,
|
||||||
channelName: 'attendance_logs',
|
channelName: 'attendance_logs',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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 attendanceControllerProvider = Provider<AttendanceController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return AttendanceController(client);
|
return AttendanceController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class AttendanceController {
|
class AttendanceController {
|
||||||
AttendanceController(this._client);
|
AttendanceController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
final SupabaseClient _client;
|
||||||
|
final Ref? _ref;
|
||||||
|
|
||||||
/// Check in to a duty schedule. Returns the attendance log ID.
|
/// Check in to a duty schedule. Returns the attendance log ID.
|
||||||
|
/// Offline: queues locally via StateProvider, returns local UUID.
|
||||||
Future<String?> checkIn({
|
Future<String?> checkIn({
|
||||||
required String dutyScheduleId,
|
required String dutyScheduleId,
|
||||||
required double lat,
|
required double lat,
|
||||||
required double lng,
|
required double lng,
|
||||||
|
String shiftType = 'normal',
|
||||||
}) async {
|
}) async {
|
||||||
final data = await _client.rpc(
|
try {
|
||||||
'attendance_check_in',
|
final data = await _client.rpc(
|
||||||
params: {'p_duty_id': dutyScheduleId, 'p_lat': lat, 'p_lng': lng},
|
'attendance_check_in',
|
||||||
);
|
params: {'p_duty_id': dutyScheduleId, 'p_lat': lat, 'p_lng': lng},
|
||||||
return data as String?;
|
);
|
||||||
|
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({
|
Future<void> checkOut({
|
||||||
required String attendanceId,
|
required String attendanceId,
|
||||||
required double lat,
|
required double lat,
|
||||||
required double lng,
|
required double lng,
|
||||||
String? justification,
|
String? justification,
|
||||||
}) async {
|
}) async {
|
||||||
await _client.rpc(
|
try {
|
||||||
'attendance_check_out',
|
await _client.rpc(
|
||||||
params: {
|
'attendance_check_out',
|
||||||
'p_attendance_id': attendanceId,
|
params: {
|
||||||
'p_lat': lat,
|
'p_attendance_id': attendanceId,
|
||||||
'p_lng': lng,
|
'p_lat': lat,
|
||||||
'p_justification': justification,
|
'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).
|
/// Overtime check-in (online-only — requires server-side schedule creation).
|
||||||
/// Creates an overtime duty schedule + attendance log in one RPC call.
|
|
||||||
Future<String?> overtimeCheckIn({
|
Future<String?> overtimeCheckIn({
|
||||||
required double lat,
|
required double lat,
|
||||||
required double lng,
|
required double lng,
|
||||||
@@ -130,41 +375,99 @@ class AttendanceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Upload a verification selfie and update the attendance log.
|
/// Upload a verification selfie and update the attendance log.
|
||||||
|
/// Offline: queues the status update only (photo bytes are not persisted).
|
||||||
Future<void> uploadVerification({
|
Future<void> uploadVerification({
|
||||||
required String attendanceId,
|
required String attendanceId,
|
||||||
required Uint8List bytes,
|
required Uint8List bytes,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String status, // 'verified', 'unverified'
|
required String status,
|
||||||
bool isCheckOut = false,
|
bool isCheckOut = false,
|
||||||
}) async {
|
}) async {
|
||||||
final userId = _client.auth.currentUser!.id;
|
final userId = _client.auth.currentUser!.id;
|
||||||
final ext = fileName.split('.').last.toLowerCase();
|
final ext = fileName.split('.').last.toLowerCase();
|
||||||
final prefix = isCheckOut ? 'checkout' : 'checkin';
|
final prefix = isCheckOut ? 'checkout' : 'checkin';
|
||||||
final path = '$userId/${prefix}_$attendanceId.$ext';
|
final path = '$userId/${prefix}_$attendanceId.$ext';
|
||||||
await _client.storage
|
|
||||||
.from('attendance-verification')
|
try {
|
||||||
.uploadBinary(
|
await _client.storage
|
||||||
path,
|
.from('attendance-verification')
|
||||||
bytes,
|
.uploadBinary(
|
||||||
fileOptions: const FileOptions(upsert: true),
|
path,
|
||||||
);
|
bytes,
|
||||||
final url = _client.storage
|
fileOptions: const FileOptions(upsert: true),
|
||||||
.from('attendance-verification')
|
);
|
||||||
.getPublicUrl(path);
|
final url = _client.storage
|
||||||
final column = isCheckOut
|
.from('attendance-verification')
|
||||||
? 'check_out_verification_photo_url'
|
.getPublicUrl(path);
|
||||||
: 'check_in_verification_photo_url';
|
final column = isCheckOut
|
||||||
await _client
|
? 'check_out_verification_photo_url'
|
||||||
.from('attendance_logs')
|
: 'check_in_verification_photo_url';
|
||||||
.update({'verification_status': status, column: url})
|
await _client
|
||||||
.eq('id', attendanceId);
|
.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 {
|
Future<void> skipVerification(String attendanceId) async {
|
||||||
await _client
|
try {
|
||||||
.from('attendance_logs')
|
await _client
|
||||||
.update({'verification_status': 'skipped'})
|
.from('attendance_logs')
|
||||||
.eq('id', attendanceId);
|
.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:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../brick/cache_helpers.dart';
|
||||||
import '../models/chat_message.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 'supabase_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'realtime_controller.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.
|
/// Real-time chat messages for a swap request thread.
|
||||||
final chatMessagesProvider = StreamProvider.family<List<ChatMessage>, String>((
|
final chatMessagesProvider = StreamProvider.family<List<ChatMessage>, String>((
|
||||||
ref,
|
ref,
|
||||||
@@ -13,6 +32,19 @@ final chatMessagesProvider = StreamProvider.family<List<ChatMessage>, String>((
|
|||||||
) {
|
) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
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>(
|
final wrapper = StreamRecoveryWrapper<ChatMessage>(
|
||||||
stream: client
|
stream: client
|
||||||
.from('chat_messages')
|
.from('chat_messages')
|
||||||
@@ -30,32 +62,126 @@ final chatMessagesProvider = StreamProvider.family<List<ChatMessage>, String>((
|
|||||||
fromMap: ChatMessage.fromMap,
|
fromMap: ChatMessage.fromMap,
|
||||||
channelName: 'chat_messages_$threadId',
|
channelName: 'chat_messages_$threadId',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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 chatControllerProvider = Provider<ChatController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return ChatController(client);
|
return ChatController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class ChatController {
|
class ChatController {
|
||||||
ChatController(this._client);
|
ChatController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
final SupabaseClient _client;
|
||||||
|
final Ref? _ref;
|
||||||
|
|
||||||
|
/// Send a message to a thread. Offline: queues locally.
|
||||||
Future<void> sendMessage({
|
Future<void> sendMessage({
|
||||||
required String threadId,
|
required String threadId,
|
||||||
required String body,
|
required String body,
|
||||||
}) async {
|
}) async {
|
||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) throw Exception('Not authenticated');
|
if (userId == null) throw Exception('Not authenticated');
|
||||||
await _client.from('chat_messages').insert({
|
|
||||||
'thread_id': threadId,
|
final id = const Uuid().v4();
|
||||||
'sender_id': userId,
|
|
||||||
'body': body,
|
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:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:flutter/material.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.dart';
|
||||||
import '../models/it_service_request_assignment.dart';
|
import '../models/it_service_request_assignment.dart';
|
||||||
import '../models/it_service_request_activity_log.dart';
|
import '../models/it_service_request_activity_log.dart';
|
||||||
import '../models/it_service_request_action.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 'profile_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'user_offices_provider.dart';
|
import 'user_offices_provider.dart';
|
||||||
@@ -60,6 +66,27 @@ final itServiceRequestQueryProvider = StateProvider<ItServiceRequestQuery>(
|
|||||||
(ref) => const 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
|
// Stream providers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -71,6 +98,24 @@ final itServiceRequestsProvider = StreamProvider<List<ItServiceRequest>>((ref) {
|
|||||||
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
||||||
final userOfficesAsync = ref.watch(userOfficesProvider);
|
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>(
|
final wrapper = StreamRecoveryWrapper<ItServiceRequest>(
|
||||||
stream: client
|
stream: client
|
||||||
.from('it_service_requests')
|
.from('it_service_requests')
|
||||||
@@ -87,12 +132,140 @@ final itServiceRequestsProvider = StreamProvider<List<ItServiceRequest>>((ref) {
|
|||||||
fromMap: ItServiceRequest.fromMap,
|
fromMap: ItServiceRequest.fromMap,
|
||||||
channelName: 'it_service_requests',
|
channelName: 'it_service_requests',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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) {
|
return wrapper.stream.map((result) {
|
||||||
var items = result.data;
|
var items = applyPending(result.data);
|
||||||
// Standard users see only requests from their offices or created by them
|
|
||||||
if (profile != null && profile.role == 'standard') {
|
if (profile != null && profile.role == 'standard') {
|
||||||
final officeIds = (userOfficesAsync.valueOrNull ?? [])
|
final officeIds = (userOfficesAsync.valueOrNull ?? [])
|
||||||
.where((a) => a.userId == userId)
|
.where((a) => a.userId == userId)
|
||||||
@@ -145,6 +318,15 @@ final itServiceRequestAssignmentsProvider =
|
|||||||
onStatusChanged: ref
|
onStatusChanged: ref
|
||||||
.read(realtimeControllerProvider)
|
.read(realtimeControllerProvider)
|
||||||
.handleChannelStatus,
|
.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);
|
ref.onDispose(wrapper.dispose);
|
||||||
@@ -177,6 +359,19 @@ final itServiceRequestActivityLogsProvider =
|
|||||||
onStatusChanged: ref
|
onStatusChanged: ref
|
||||||
.read(realtimeControllerProvider)
|
.read(realtimeControllerProvider)
|
||||||
.handleChannelStatus,
|
.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);
|
ref.onDispose(wrapper.dispose);
|
||||||
@@ -209,6 +404,18 @@ final itServiceRequestActionsProvider =
|
|||||||
onStatusChanged: ref
|
onStatusChanged: ref
|
||||||
.read(realtimeControllerProvider)
|
.read(realtimeControllerProvider)
|
||||||
.handleChannelStatus,
|
.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);
|
ref.onDispose(wrapper.dispose);
|
||||||
@@ -222,15 +429,19 @@ final itServiceRequestActionsProvider =
|
|||||||
final itServiceRequestControllerProvider = Provider<ItServiceRequestController>(
|
final itServiceRequestControllerProvider = Provider<ItServiceRequestController>(
|
||||||
(ref) {
|
(ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return ItServiceRequestController(client);
|
return ItServiceRequestController(client, ref);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
class ItServiceRequestController {
|
class ItServiceRequestController {
|
||||||
ItServiceRequestController(this._client);
|
ItServiceRequestController(this._client, [this._ref]);
|
||||||
final SupabaseClient _client;
|
final SupabaseClient _client;
|
||||||
|
final Ref? _ref;
|
||||||
|
|
||||||
/// Creates a new IT Service Request with auto-generated number.
|
/// 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({
|
Future<Map<String, dynamic>> createRequest({
|
||||||
required String eventName,
|
required String eventName,
|
||||||
required List<String> services,
|
required List<String> services,
|
||||||
@@ -243,18 +454,22 @@ class ItServiceRequestController {
|
|||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) throw Exception('Not authenticated');
|
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 {
|
try {
|
||||||
final result = await _client.rpc(
|
final result = await _client.rpc(
|
||||||
'insert_it_service_request_with_number',
|
'insert_it_service_request_with_number',
|
||||||
params: {
|
params: rpcParams,
|
||||||
'p_event_name': eventName,
|
|
||||||
'p_services': services,
|
|
||||||
'p_creator_id': userId,
|
|
||||||
'p_office_id': officeId,
|
|
||||||
'p_requested_by': requestedBy,
|
|
||||||
'p_requested_by_user_id': requestedByUserId,
|
|
||||||
'p_status': status,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final row = result is List
|
final row = result is List
|
||||||
@@ -269,7 +484,6 @@ class ItServiceRequestController {
|
|||||||
.eq('id', requestId);
|
.eq('id', requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activity log
|
|
||||||
await _client.from('it_service_request_activity_logs').insert({
|
await _client.from('it_service_request_activity_logs').insert({
|
||||||
'request_id': requestId,
|
'request_id': requestId,
|
||||||
'actor_id': userId,
|
'actor_id': userId,
|
||||||
@@ -278,12 +492,36 @@ class ItServiceRequestController {
|
|||||||
|
|
||||||
return {'id': requestId, 'request_number': row['request_number']};
|
return {'id': requestId, 'request_number': row['request_number']};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('createRequest error: $e');
|
if (!isOfflineSaveError(e)) {
|
||||||
rethrow;
|
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({
|
Future<void> updateRequest({
|
||||||
required String requestId,
|
required String requestId,
|
||||||
String? eventName,
|
String? eventName,
|
||||||
@@ -345,22 +583,26 @@ class ItServiceRequestController {
|
|||||||
|
|
||||||
if (updates.isEmpty) return;
|
if (updates.isEmpty) return;
|
||||||
|
|
||||||
await _client
|
try {
|
||||||
.from('it_service_requests')
|
await _client
|
||||||
.update(updates)
|
.from('it_service_requests')
|
||||||
.eq('id', requestId);
|
.update(updates)
|
||||||
|
.eq('id', requestId);
|
||||||
|
|
||||||
// Log updated fields
|
final userId = _client.auth.currentUser?.id;
|
||||||
final userId = _client.auth.currentUser?.id;
|
await _client.from('it_service_request_activity_logs').insert({
|
||||||
await _client.from('it_service_request_activity_logs').insert({
|
'request_id': requestId,
|
||||||
'request_id': requestId,
|
'actor_id': userId,
|
||||||
'actor_id': userId,
|
'action_type': 'updated',
|
||||||
'action_type': 'updated',
|
'meta': {'fields': updates.keys.toList()},
|
||||||
'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({
|
Future<void> updateStatus({
|
||||||
required String requestId,
|
required String requestId,
|
||||||
required String status,
|
required String status,
|
||||||
@@ -383,44 +625,56 @@ class ItServiceRequestController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _client
|
try {
|
||||||
.from('it_service_requests')
|
await _client
|
||||||
.update(updates)
|
.from('it_service_requests')
|
||||||
.eq('id', requestId);
|
.update(updates)
|
||||||
|
.eq('id', requestId);
|
||||||
|
|
||||||
await _client.from('it_service_request_activity_logs').insert({
|
await _client.from('it_service_request_activity_logs').insert({
|
||||||
'request_id': requestId,
|
'request_id': requestId,
|
||||||
'actor_id': userId,
|
'actor_id': userId,
|
||||||
'action_type': 'status_changed',
|
'action_type': 'status_changed',
|
||||||
'meta': {'status': status},
|
'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({
|
Future<void> approveRequest({
|
||||||
required String requestId,
|
required String requestId,
|
||||||
required String approverName,
|
required String approverName,
|
||||||
}) async {
|
}) async {
|
||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
await _client
|
final updates = <String, dynamic>{
|
||||||
.from('it_service_requests')
|
'status': 'scheduled',
|
||||||
.update({
|
'approved_by': approverName,
|
||||||
'status': 'scheduled',
|
'approved_by_user_id': userId,
|
||||||
'approved_by': approverName,
|
'approved_at': DateTime.now().toUtc().toIso8601String(),
|
||||||
'approved_by_user_id': userId,
|
};
|
||||||
'approved_at': DateTime.now().toUtc().toIso8601String(),
|
|
||||||
})
|
|
||||||
.eq('id', requestId);
|
|
||||||
|
|
||||||
await _client.from('it_service_request_activity_logs').insert({
|
try {
|
||||||
'request_id': requestId,
|
await _client
|
||||||
'actor_id': userId,
|
.from('it_service_requests')
|
||||||
'action_type': 'approved',
|
.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({
|
Future<void> assignStaff({
|
||||||
@@ -463,7 +717,7 @@ class ItServiceRequestController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Action Taken
|
// Action Taken — offline-safe
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
Future<String> createOrUpdateAction({
|
Future<String> createOrUpdateAction({
|
||||||
@@ -473,31 +727,44 @@ class ItServiceRequestController {
|
|||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) throw Exception('Not authenticated');
|
if (userId == null) throw Exception('Not authenticated');
|
||||||
|
|
||||||
// Check if action already exists for this user
|
final actionId = const Uuid().v4();
|
||||||
final existing = await _client
|
|
||||||
.from('it_service_request_actions')
|
|
||||||
.select('id')
|
|
||||||
.eq('request_id', requestId)
|
|
||||||
.eq('user_id', userId)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (existing != null) {
|
try {
|
||||||
await _client
|
final existing = await _client
|
||||||
.from('it_service_request_actions')
|
.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')
|
.select('id')
|
||||||
.single();
|
.eq('request_id', requestId)
|
||||||
return result['id'] as String;
|
.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 {
|
Future<void> deleteEvidence({required String evidenceId}) async {
|
||||||
// Get path first
|
|
||||||
final row = await _client
|
final row = await _client
|
||||||
.from('it_service_request_evidence')
|
.from('it_service_request_evidence')
|
||||||
.select('file_path')
|
.select('file_path')
|
||||||
@@ -630,7 +896,6 @@ class ItServiceRequestController {
|
|||||||
await _client.storage.from('it_service_attachments').remove([path]);
|
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 {
|
Future<List<Map<String, dynamic>>> listEvidence(String requestId) async {
|
||||||
final rows = await _client
|
final rows = await _client
|
||||||
.from('it_service_request_evidence')
|
.from('it_service_request_evidence')
|
||||||
@@ -651,4 +916,151 @@ class ItServiceRequestController {
|
|||||||
)
|
)
|
||||||
.toList();
|
.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/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.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 '../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 'profile_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'realtime_controller.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).
|
/// All visible leaves (own for standard, all for admin/dispatcher/it_staff).
|
||||||
///
|
///
|
||||||
/// Consumers should **not** treat every record as an active absence; the UI
|
/// Consumers should **not** treat every record as an active absence; the UI
|
||||||
/// layers (dashboard, logbook) explicitly filter to `status == 'approved'` and
|
/// layers (dashboard, logbook) explicitly filter to `status == 'approved'` and
|
||||||
/// verify the leave overlaps the current time. This prevents rejected or
|
/// verify the leave overlaps the current time.
|
||||||
/// pending applications from inadvertently influencing schedules or status
|
|
||||||
/// computations.
|
|
||||||
final leavesProvider = StreamProvider<List<LeaveOfAbsence>>((ref) {
|
final leavesProvider = StreamProvider<List<LeaveOfAbsence>>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
final profileAsync = ref.watch(currentProfileProvider);
|
final profileAsync = ref.watch(currentProfileProvider);
|
||||||
@@ -27,6 +49,40 @@ final leavesProvider = StreamProvider<List<LeaveOfAbsence>>((ref) {
|
|||||||
profile.role == 'dispatcher' ||
|
profile.role == 'dispatcher' ||
|
||||||
profile.role == 'it_staff';
|
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>(
|
final wrapper = StreamRecoveryWrapper<LeaveOfAbsence>(
|
||||||
stream: hasFullAccess
|
stream: hasFullAccess
|
||||||
? client
|
? client
|
||||||
@@ -50,24 +106,113 @@ final leavesProvider = StreamProvider<List<LeaveOfAbsence>>((ref) {
|
|||||||
fromMap: LeaveOfAbsence.fromMap,
|
fromMap: LeaveOfAbsence.fromMap,
|
||||||
channelName: 'leave_of_absence',
|
channelName: 'leave_of_absence',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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 leaveControllerProvider = Provider<LeaveController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return LeaveController(client);
|
return LeaveController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class LeaveController {
|
class LeaveController {
|
||||||
LeaveController(this._client);
|
LeaveController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
final SupabaseClient _client;
|
||||||
|
final Ref? _ref;
|
||||||
|
|
||||||
/// File a leave of absence for the current user.
|
/// File a leave of absence for the current user.
|
||||||
/// Caller controls auto-approval based on role policy.
|
/// Caller controls auto-approval based on role policy.
|
||||||
|
/// Offline: queues a local row + raw payload, returns silently.
|
||||||
Future<void> fileLeave({
|
Future<void> fileLeave({
|
||||||
required String leaveType,
|
required String leaveType,
|
||||||
required String justification,
|
required String justification,
|
||||||
@@ -76,43 +221,90 @@ class LeaveController {
|
|||||||
required bool autoApprove,
|
required bool autoApprove,
|
||||||
}) async {
|
}) async {
|
||||||
final uid = _client.auth.currentUser!.id;
|
final uid = _client.auth.currentUser!.id;
|
||||||
|
final id = const Uuid().v4();
|
||||||
|
final status = autoApprove ? 'approved' : 'pending';
|
||||||
final payload = {
|
final payload = {
|
||||||
|
'id': id,
|
||||||
'user_id': uid,
|
'user_id': uid,
|
||||||
'leave_type': leaveType,
|
'leave_type': leaveType,
|
||||||
'justification': justification,
|
'justification': justification,
|
||||||
'start_time': startTime.toIso8601String(),
|
'start_time': startTime.toIso8601String(),
|
||||||
'end_time': endTime.toIso8601String(),
|
'end_time': endTime.toIso8601String(),
|
||||||
'status': autoApprove ? 'approved' : 'pending',
|
'status': status,
|
||||||
'filed_by': uid,
|
'filed_by': uid,
|
||||||
};
|
};
|
||||||
|
|
||||||
final insertedRaw = await _client
|
Map<String, dynamic>? inserted;
|
||||||
.from('leave_of_absence')
|
try {
|
||||||
.insert(payload)
|
final insertedRaw = await _client
|
||||||
.select()
|
.from('leave_of_absence')
|
||||||
.maybeSingle();
|
.insert(payload)
|
||||||
final Map<String, dynamic>? inserted = insertedRaw is Map<String, dynamic>
|
.select()
|
||||||
? insertedRaw
|
.maybeSingle();
|
||||||
: null;
|
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
|
// If this was filed as pending, notify admins for approval
|
||||||
final status = payload['status'] as String;
|
|
||||||
if (status != 'pending') return;
|
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 {
|
try {
|
||||||
final adminIds = await _fetchRoleUserIds(
|
final adminIds = await _fetchRoleUserIds(
|
||||||
roles: const ['admin'],
|
roles: const ['admin'],
|
||||||
excludeUserId: uid,
|
excludeUserId: actorId,
|
||||||
);
|
);
|
||||||
if (adminIds.isEmpty) return;
|
if (adminIds.isEmpty) return;
|
||||||
|
|
||||||
// Resolve actor display name for nicer push text
|
|
||||||
String actorName = 'Someone';
|
String actorName = 'Someone';
|
||||||
try {
|
try {
|
||||||
final p = await _client
|
final p = await _client
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('full_name,display_name,name')
|
.select('full_name,display_name,name')
|
||||||
.eq('id', uid)
|
.eq('id', actorId)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
if (p != null) {
|
if (p != null) {
|
||||||
if (p['full_name'] != null) {
|
if (p['full_name'] != null) {
|
||||||
@@ -125,32 +317,24 @@ class LeaveController {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
final leaveId = (inserted ?? <String, dynamic>{})['id']?.toString() ?? '';
|
|
||||||
final title = 'Leave Filed for Approval';
|
final title = 'Leave Filed for Approval';
|
||||||
final body = '$actorName filed a leave request that requires approval.';
|
final body = '$actorName filed a leave request that requires approval.';
|
||||||
final notificationId = (inserted ?? <String, dynamic>{})['id']
|
final notificationId = (inserted ?? <String, dynamic>{})['id']?.toString();
|
||||||
?.toString();
|
|
||||||
|
|
||||||
final dataPayload = <String, dynamic>{
|
final dataPayload = <String, dynamic>{
|
||||||
'type': 'leave_filed',
|
'type': 'leave_filed',
|
||||||
'leave_id': leaveId,
|
'leave_id': leaveId,
|
||||||
...?(notificationId != null
|
...?(notificationId != null ? {'notification_id': notificationId} : null),
|
||||||
? {'notification_id': notificationId}
|
|
||||||
: null),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await _client
|
await _client.from('notifications').insert(
|
||||||
.from('notifications')
|
|
||||||
.insert(
|
|
||||||
adminIds
|
adminIds
|
||||||
.map(
|
.map((userId) => {
|
||||||
(userId) => {
|
'user_id': userId,
|
||||||
'user_id': userId,
|
'actor_id': actorId,
|
||||||
'actor_id': uid,
|
'type': 'leave_filed',
|
||||||
'type': 'leave_filed',
|
'leave_id': leaveId,
|
||||||
'leave_id': leaveId,
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -175,14 +359,21 @@ class LeaveController {
|
|||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) throw Exception('Not authenticated');
|
if (userId == null) throw Exception('Not authenticated');
|
||||||
|
|
||||||
// Update status first; then notify the requester.
|
try {
|
||||||
await _client
|
await _client
|
||||||
.from('leave_of_absence')
|
.from('leave_of_absence')
|
||||||
.update({'status': 'approved'})
|
.update({'status': 'approved'})
|
||||||
.eq('id', leaveId);
|
.eq('id', leaveId);
|
||||||
|
} catch (e) {
|
||||||
|
if (!isOfflineSaveError(e)) rethrow;
|
||||||
|
_queueLeaveStatusUpdate(leaveId, {'status': 'approved'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Notify requestor
|
// Notify requestor (best-effort)
|
||||||
await _notifyRequester(leaveId: leaveId, actorId: userId, approved: true);
|
try {
|
||||||
|
await _notifyRequester(leaveId: leaveId, actorId: userId, approved: true);
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reject a leave request.
|
/// Reject a leave request.
|
||||||
@@ -190,13 +381,20 @@ class LeaveController {
|
|||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) throw Exception('Not authenticated');
|
if (userId == null) throw Exception('Not authenticated');
|
||||||
|
|
||||||
await _client
|
try {
|
||||||
.from('leave_of_absence')
|
await _client
|
||||||
.update({'status': 'rejected'})
|
.from('leave_of_absence')
|
||||||
.eq('id', leaveId);
|
.update({'status': 'rejected'})
|
||||||
|
.eq('id', leaveId);
|
||||||
|
} catch (e) {
|
||||||
|
if (!isOfflineSaveError(e)) rethrow;
|
||||||
|
_queueLeaveStatusUpdate(leaveId, {'status': 'rejected'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Notify requestor
|
try {
|
||||||
await _notifyRequester(leaveId: leaveId, actorId: userId, approved: false);
|
await _notifyRequester(leaveId: leaveId, actorId: userId, approved: false);
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _notifyRequester({
|
Future<void> _notifyRequester({
|
||||||
@@ -266,10 +464,15 @@ class LeaveController {
|
|||||||
|
|
||||||
/// Cancel an approved leave.
|
/// Cancel an approved leave.
|
||||||
Future<void> cancelLeave(String leaveId) async {
|
Future<void> cancelLeave(String leaveId) async {
|
||||||
await _client
|
try {
|
||||||
.from('leave_of_absence')
|
await _client
|
||||||
.update({'status': 'cancelled'})
|
.from('leave_of_absence')
|
||||||
.eq('id', leaveId);
|
.update({'status': 'cancelled'})
|
||||||
|
.eq('id', leaveId);
|
||||||
|
} catch (e) {
|
||||||
|
if (!isOfflineSaveError(e)) rethrow;
|
||||||
|
_queueLeaveStatusUpdate(leaveId, {'status': 'cancelled'});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> _fetchRoleUserIds({
|
Future<List<String>> _fetchRoleUserIds({
|
||||||
|
|||||||
@@ -3,20 +3,65 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import '../utils/device_id.dart';
|
import '../utils/device_id.dart';
|
||||||
|
|
||||||
|
import '../brick/cache_helpers.dart';
|
||||||
import '../models/notification_item.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 'profile_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'realtime_controller.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 notificationsProvider = StreamProvider<List<NotificationItem>>((ref) {
|
||||||
final userId = ref.watch(currentUserIdProvider);
|
final userId = ref.watch(currentUserIdProvider);
|
||||||
if (userId == null) {
|
if (userId == null) return const Stream.empty();
|
||||||
return const Stream.empty();
|
|
||||||
}
|
|
||||||
final client = ref.watch(supabaseClientProvider);
|
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>(
|
final wrapper = StreamRecoveryWrapper<NotificationItem>(
|
||||||
stream: client
|
stream: client
|
||||||
.from('notifications')
|
.from('notifications')
|
||||||
@@ -34,10 +79,87 @@ final notificationsProvider = StreamProvider<List<NotificationItem>>((ref) {
|
|||||||
fromMap: NotificationItem.fromMap,
|
fromMap: NotificationItem.fromMap,
|
||||||
channelName: 'notifications',
|
channelName: 'notifications',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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) {
|
final unreadNotificationsCountProvider = Provider<int>((ref) {
|
||||||
@@ -52,18 +174,15 @@ final notificationsControllerProvider = Provider<NotificationsController>((
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return NotificationsController(client);
|
return NotificationsController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class NotificationsController {
|
class NotificationsController {
|
||||||
NotificationsController(this._client);
|
NotificationsController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
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(
|
Future<void> _createAndPush(
|
||||||
List<Map<String, dynamic>> rows, {
|
List<Map<String, dynamic>> rows, {
|
||||||
List<String>? targetUserIds,
|
List<String>? targetUserIds,
|
||||||
@@ -77,9 +196,6 @@ class NotificationsController {
|
|||||||
);
|
);
|
||||||
await _client.from('notifications').insert(rows);
|
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;
|
if (targetUserIds == null || targetUserIds.isEmpty) return;
|
||||||
|
|
||||||
debugPrint('notification rows inserted; invoking client push');
|
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({
|
Future<void> createNotification({
|
||||||
required List<String> userIds,
|
required List<String> userIds,
|
||||||
required String type,
|
required String type,
|
||||||
@@ -132,7 +243,6 @@ class NotificationsController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience for mention-specific case; left for compatibility.
|
|
||||||
Future<void> createMentionNotifications({
|
Future<void> createMentionNotifications({
|
||||||
required List<String> userIds,
|
required List<String> userIds,
|
||||||
required String actorId,
|
required String actorId,
|
||||||
@@ -158,36 +268,55 @@ class NotificationsController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark a single notification as read. Offline: queues locally.
|
||||||
Future<void> markRead(String id) async {
|
Future<void> markRead(String id) async {
|
||||||
await _client
|
try {
|
||||||
.from('notifications')
|
await _client
|
||||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
.from('notifications')
|
||||||
.eq('id', id);
|
.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 {
|
Future<void> markReadForTicket(String ticketId) async {
|
||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) return;
|
if (userId == null) return;
|
||||||
await _client
|
try {
|
||||||
.from('notifications')
|
await _client
|
||||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
.from('notifications')
|
||||||
.eq('ticket_id', ticketId)
|
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||||
.eq('user_id', userId)
|
.eq('ticket_id', ticketId)
|
||||||
.filter('read_at', 'is', null);
|
.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 {
|
Future<void> markReadForTask(String taskId) async {
|
||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) return;
|
if (userId == null) return;
|
||||||
await _client
|
try {
|
||||||
.from('notifications')
|
await _client
|
||||||
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
.from('notifications')
|
||||||
.eq('task_id', taskId)
|
.update({'read_at': AppTime.nowUtc().toIso8601String()})
|
||||||
.eq('user_id', userId)
|
.eq('task_id', taskId)
|
||||||
.filter('read_at', 'is', null);
|
.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 {
|
Future<void> registerFcmToken(String token) async {
|
||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) return;
|
if (userId == null) return;
|
||||||
@@ -196,8 +325,6 @@ class NotificationsController {
|
|||||||
debugPrint('registerFcmToken: device id missing; skipping');
|
debugPrint('registerFcmToken: device id missing; skipping');
|
||||||
return;
|
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 = {
|
final payload = {
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'device_id': deviceId,
|
'device_id': deviceId,
|
||||||
@@ -207,19 +334,16 @@ class NotificationsController {
|
|||||||
final res = await _client.from('fcm_tokens').upsert(payload);
|
final res = await _client.from('fcm_tokens').upsert(payload);
|
||||||
final dyn = res as dynamic;
|
final dyn = res as dynamic;
|
||||||
if (dyn.error != null) {
|
if (dyn.error != null) {
|
||||||
// duplicate key or RLS issue - just log it
|
|
||||||
debugPrint('registerFcmToken error: ${dyn.error?.message ?? dyn.error}');
|
debugPrint('registerFcmToken error: ${dyn.error?.message ?? dyn.error}');
|
||||||
} else {
|
} else {
|
||||||
debugPrint('registerFcmToken success for user=$userId token=$token');
|
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 {
|
Future<void> unregisterFcmToken(String token) async {
|
||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) return;
|
if (userId == null) return;
|
||||||
final deviceId = await DeviceId.getId();
|
final deviceId = await DeviceId.getId();
|
||||||
// Prefer to delete by device_id to avoid removing other devices' tokens.
|
|
||||||
final res = await _client
|
final res = await _client
|
||||||
.from('fcm_tokens')
|
.from('fcm_tokens')
|
||||||
.delete()
|
.delete()
|
||||||
@@ -234,7 +358,6 @@ class NotificationsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a push message via the `send_fcm` edge function.
|
|
||||||
Future<void> sendPush({
|
Future<void> sendPush({
|
||||||
List<String>? tokens,
|
List<String>? tokens,
|
||||||
List<String>? userIds,
|
List<String>? userIds,
|
||||||
@@ -270,4 +393,52 @@ class NotificationsController {
|
|||||||
debugPrint('sendPush invocation error: $err');
|
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/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../brick/cache_helpers.dart';
|
||||||
import '../models/pass_slip.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 'profile_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'realtime_controller.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).
|
/// All visible pass slips (own for staff, all for admin/dispatcher).
|
||||||
final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
@@ -18,6 +39,50 @@ final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
|||||||
final isAdmin = profile.role == 'admin' || profile.role == 'dispatcher';
|
final isAdmin = profile.role == 'admin' || profile.role == 'dispatcher';
|
||||||
final hasFullAccess = isAdmin || profile.role == 'programmer';
|
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>(
|
final wrapper = StreamRecoveryWrapper<PassSlip>(
|
||||||
stream: hasFullAccess
|
stream: hasFullAccess
|
||||||
? client
|
? client
|
||||||
@@ -41,10 +106,90 @@ final passSlipsProvider = StreamProvider<List<PassSlip>>((ref) {
|
|||||||
fromMap: PassSlip.fromMap,
|
fromMap: PassSlip.fromMap,
|
||||||
channelName: 'pass_slips',
|
channelName: 'pass_slips',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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).
|
/// 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();
|
return slips.where((s) => s.isActive).toList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Controller
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
final passSlipControllerProvider = Provider<PassSlipController>((ref) {
|
final passSlipControllerProvider = Provider<PassSlipController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return PassSlipController(client);
|
return PassSlipController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class PassSlipController {
|
class PassSlipController {
|
||||||
PassSlipController(this._client);
|
PassSlipController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
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({
|
Future<void> requestSlip({
|
||||||
required String dutyScheduleId,
|
required String dutyScheduleId,
|
||||||
@@ -82,26 +254,42 @@ class PassSlipController {
|
|||||||
}) async {
|
}) async {
|
||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) throw Exception('Not authenticated');
|
if (userId == null) throw Exception('Not authenticated');
|
||||||
|
final id = const Uuid().v4();
|
||||||
|
final now = DateTime.now().toUtc();
|
||||||
final payload = {
|
final payload = {
|
||||||
|
'id': id,
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
'duty_schedule_id': dutyScheduleId,
|
'duty_schedule_id': dutyScheduleId,
|
||||||
'reason': reason,
|
'reason': reason,
|
||||||
'status': 'pending',
|
'status': 'pending',
|
||||||
'requested_at': DateTime.now().toUtc().toIso8601String(),
|
'requested_at': now.toIso8601String(),
|
||||||
if (requestedStart != null)
|
if (requestedStart != null)
|
||||||
'requested_start': requestedStart.toUtc().toIso8601String(),
|
'requested_start': requestedStart.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
final insertedRaw = await _client
|
Map<String, dynamic>? inserted;
|
||||||
.from('pass_slips')
|
try {
|
||||||
.insert(payload)
|
final insertedRaw = await _client
|
||||||
.select()
|
.from('pass_slips')
|
||||||
.maybeSingle();
|
.insert(payload)
|
||||||
final Map<String, dynamic>? inserted = insertedRaw is Map<String, dynamic>
|
.select()
|
||||||
? insertedRaw
|
.maybeSingle();
|
||||||
: null;
|
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 {
|
try {
|
||||||
final adminIds = await _fetchRoleUserIds(
|
final adminIds = await _fetchRoleUserIds(
|
||||||
roles: const ['admin'],
|
roles: const ['admin'],
|
||||||
@@ -109,7 +297,6 @@ class PassSlipController {
|
|||||||
);
|
);
|
||||||
if (adminIds.isEmpty) return;
|
if (adminIds.isEmpty) return;
|
||||||
|
|
||||||
// Resolve actor display name for nice push text
|
|
||||||
String actorName = 'Someone';
|
String actorName = 'Someone';
|
||||||
try {
|
try {
|
||||||
final p = await _client
|
final p = await _client
|
||||||
@@ -128,32 +315,25 @@ class PassSlipController {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
final slipId = (inserted ?? <String, dynamic>{})['id']?.toString() ?? '';
|
final slipId = (inserted ?? <String, dynamic>{})['id']?.toString() ?? id;
|
||||||
final title = 'Pass Slip Filed for Approval';
|
final title = 'Pass Slip Filed for Approval';
|
||||||
final body = '$actorName filed a pass slip that requires approval.';
|
final body = '$actorName filed a pass slip that requires approval.';
|
||||||
final notificationId = (inserted ?? <String, dynamic>{})['id']
|
final notificationId = (inserted ?? <String, dynamic>{})['id']?.toString();
|
||||||
?.toString();
|
|
||||||
|
|
||||||
final dataPayload = <String, dynamic>{
|
final dataPayload = <String, dynamic>{
|
||||||
'type': 'pass_slip_filed',
|
'type': 'pass_slip_filed',
|
||||||
'pass_slip_id': slipId,
|
'pass_slip_id': slipId,
|
||||||
...?(notificationId != null
|
...?(notificationId != null ? {'notification_id': notificationId} : null),
|
||||||
? {'notification_id': notificationId}
|
|
||||||
: null),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await _client
|
await _client.from('notifications').insert(
|
||||||
.from('notifications')
|
|
||||||
.insert(
|
|
||||||
adminIds
|
adminIds
|
||||||
.map(
|
.map((adminId) => {
|
||||||
(adminId) => {
|
'user_id': adminId,
|
||||||
'user_id': adminId,
|
'actor_id': userId,
|
||||||
'actor_id': userId,
|
'type': 'pass_slip_filed',
|
||||||
'type': 'pass_slip_filed',
|
'pass_slip_id': slipId,
|
||||||
'pass_slip_id': slipId,
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -169,7 +349,6 @@ class PassSlipController {
|
|||||||
debugPrint('pass slip send_fcm result: $res');
|
debugPrint('pass slip send_fcm result: $res');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('pass slip send_fcm error: $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;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) throw Exception('Not authenticated');
|
if (userId == null) throw Exception('Not authenticated');
|
||||||
|
|
||||||
// Determine slip start time based on requested_start
|
|
||||||
final nowUtc = DateTime.now().toUtc();
|
final nowUtc = DateTime.now().toUtc();
|
||||||
String slipStartIso = nowUtc.toIso8601String();
|
|
||||||
|
|
||||||
final row = await _client
|
// Try to determine slip start time online; if offline use now.
|
||||||
.from('pass_slips')
|
String slipStartIso = nowUtc.toIso8601String();
|
||||||
.select('requested_start')
|
try {
|
||||||
.eq('id', slipId)
|
final row = await _client
|
||||||
.maybeSingle();
|
.from('pass_slips')
|
||||||
if (row != null && row['requested_start'] != null) {
|
.select('requested_start')
|
||||||
final requestedStart = DateTime.parse(row['requested_start'] as String);
|
.eq('id', slipId)
|
||||||
if (requestedStart.isAfter(nowUtc)) {
|
.maybeSingle();
|
||||||
slipStartIso = requestedStart.toIso8601String();
|
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
|
final fields = {
|
||||||
.from('pass_slips')
|
'status': 'approved',
|
||||||
.update({
|
'approved_by': userId,
|
||||||
'status': 'approved',
|
'approved_at': nowUtc.toIso8601String(),
|
||||||
'approved_by': userId,
|
'slip_start': slipStartIso,
|
||||||
'approved_at': nowUtc.toIso8601String(),
|
};
|
||||||
'slip_start': slipStartIso,
|
|
||||||
})
|
|
||||||
.eq('id', slipId);
|
|
||||||
|
|
||||||
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 {
|
Future<void> rejectSlip(String slipId) async {
|
||||||
final userId = _client.auth.currentUser?.id;
|
final userId = _client.auth.currentUser?.id;
|
||||||
if (userId == null) throw Exception('Not authenticated');
|
if (userId == null) throw Exception('Not authenticated');
|
||||||
|
|
||||||
await _client
|
final fields = {
|
||||||
.from('pass_slips')
|
'status': 'rejected',
|
||||||
.update({
|
'approved_by': userId,
|
||||||
'status': 'rejected',
|
'approved_at': DateTime.now().toUtc().toIso8601String(),
|
||||||
'approved_by': userId,
|
};
|
||||||
'approved_at': DateTime.now().toUtc().toIso8601String(),
|
|
||||||
})
|
|
||||||
.eq('id', slipId);
|
|
||||||
|
|
||||||
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({
|
Future<void> _notifyRequester({
|
||||||
@@ -288,13 +485,16 @@ class PassSlipController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> completeSlip(String slipId) async {
|
Future<void> completeSlip(String slipId) async {
|
||||||
await _client
|
final fields = {
|
||||||
.from('pass_slips')
|
'status': 'completed',
|
||||||
.update({
|
'slip_end': DateTime.now().toUtc().toIso8601String(),
|
||||||
'status': 'completed',
|
};
|
||||||
'slip_end': DateTime.now().toUtc().toIso8601String(),
|
try {
|
||||||
})
|
await _client.from('pass_slips').update(fields).eq('id', slipId);
|
||||||
.eq('id', slipId);
|
} catch (e) {
|
||||||
|
if (!isOfflineSaveError(e)) rethrow;
|
||||||
|
_queueSlipStatusUpdate(slipId, fields);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> _fetchRoleUserIds({
|
Future<List<String>> _fetchRoleUserIds({
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show debugPrint;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import '../brick/cache_helpers.dart';
|
||||||
import '../models/profile.dart';
|
import '../models/profile.dart';
|
||||||
|
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||||
import 'auth_provider.dart';
|
import 'auth_provider.dart';
|
||||||
|
import 'connectivity_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'stream_recovery.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 currentUserIdProvider = Provider<String?>((ref) {
|
||||||
final authState = ref.watch(authStateChangesProvider);
|
final authState = ref.watch(authStateChangesProvider);
|
||||||
// Be explicit about loading/error to avoid dynamic dispatch problems.
|
// 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)];
|
return data == null ? [] : [Profile.fromMap(data)];
|
||||||
},
|
},
|
||||||
fromMap: Profile.fromMap,
|
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.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 wrapper.stream.map((result) {
|
||||||
return result.data.isEmpty ? null : result.data.first;
|
return result.data.isEmpty ? null : result.data.first;
|
||||||
});
|
});
|
||||||
@@ -58,6 +113,13 @@ final profilesProvider = StreamProvider<List<Profile>>((ref) {
|
|||||||
return data.map(Profile.fromMap).toList();
|
return data.map(Profile.fromMap).toList();
|
||||||
},
|
},
|
||||||
fromMap: Profile.fromMap,
|
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);
|
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).
|
/// Controller for the current user's profile (update full name / password).
|
||||||
final profileControllerProvider = Provider<ProfileController>((ref) {
|
final profileControllerProvider = Provider<ProfileController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return ProfileController(client);
|
return ProfileController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class ProfileController {
|
class ProfileController {
|
||||||
ProfileController(this._client);
|
ProfileController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
final SupabaseClient _client;
|
||||||
|
final Ref? _ref;
|
||||||
|
|
||||||
/// Update the `profiles.full_name` for the given user id.
|
/// Update the `profiles.full_name` for the given user id.
|
||||||
|
/// Offline: queues the update for replay on reconnect.
|
||||||
Future<void> updateFullName({
|
Future<void> updateFullName({
|
||||||
required String userId,
|
required String userId,
|
||||||
required String fullName,
|
required String fullName,
|
||||||
}) async {
|
}) async {
|
||||||
await _client
|
try {
|
||||||
.from('profiles')
|
await _client
|
||||||
.update({'full_name': fullName})
|
.from('profiles')
|
||||||
.eq('id', userId);
|
.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).
|
/// 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/foundation.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
||||||
|
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'supabase_provider.dart';
|
|
||||||
|
|
||||||
final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
|
final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
|
||||||
// ChangeNotifierProvider automatically disposes the notifier; no need
|
// ChangeNotifierProvider automatically disposes the notifier; no need
|
||||||
// for ref.onDispose here — adding it causes a double-dispose assertion.
|
// for ref.onDispose here — adding it causes a double-dispose assertion.
|
||||||
return RealtimeController(client);
|
return RealtimeController();
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Per-channel realtime controller for UI skeleton indicators.
|
/// Per-channel realtime controller for UI skeleton indicators.
|
||||||
@@ -27,42 +21,12 @@ final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
|
|||||||
/// - Per-channel recovering state for pinpoint skeleton indicators
|
/// - Per-channel recovering state for pinpoint skeleton indicators
|
||||||
/// - Auth token refreshes for realtime connections
|
/// - Auth token refreshes for realtime connections
|
||||||
class RealtimeController extends ChangeNotifier {
|
class RealtimeController extends ChangeNotifier {
|
||||||
final SupabaseClient _client;
|
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
||||||
/// Channels currently in a recovering/polling/stale state.
|
/// Channels currently in a recovering/polling/stale state.
|
||||||
final Set<String> _recoveringChannels = {};
|
final Set<String> _recoveringChannels = {};
|
||||||
|
|
||||||
StreamSubscription<AuthState>? _authSub;
|
RealtimeController();
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Per-channel status ─────────────────────────────────────────────────
|
// ── Per-channel status ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -97,13 +61,17 @@ class RealtimeController extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged].
|
/// Convenience callback suitable for [StreamRecoveryWrapper.onStatusChanged].
|
||||||
///
|
///
|
||||||
/// Routes [StreamConnectionStatus] to the appropriate mark method.
|
/// Routes [StreamConnectionStatus] to the appropriate mark method:
|
||||||
/// Both `connected` and `polling` are treated as "recovered" because
|
/// - `connected` — live stream; no indicator needed.
|
||||||
/// polling is a functional fallback that still delivers data — the user
|
/// - `polling` — REST fallback; data is fresh, no skeleton needed.
|
||||||
/// doesn't need to see a reconnection indicator while data flows via REST.
|
/// - `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) {
|
void handleChannelStatus(String channel, StreamConnectionStatus status) {
|
||||||
if (status == StreamConnectionStatus.connected ||
|
if (status == StreamConnectionStatus.connected ||
|
||||||
status == StreamConnectionStatus.polling) {
|
status == StreamConnectionStatus.polling ||
|
||||||
|
status == StreamConnectionStatus.offline) {
|
||||||
markChannelRecovered(channel);
|
markChannelRecovered(channel);
|
||||||
} else {
|
} else {
|
||||||
markChannelRecovering(channel);
|
markChannelRecovering(channel);
|
||||||
@@ -126,7 +94,6 @@ class RealtimeController extends ChangeNotifier {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_authSub?.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../brick/cache_helpers.dart';
|
||||||
import '../models/service.dart';
|
import '../models/service.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
@@ -17,6 +18,13 @@ final servicesProvider = StreamProvider<List<Service>>((ref) {
|
|||||||
fromMap: Service.fromMap,
|
fromMap: Service.fromMap,
|
||||||
channelName: 'services',
|
channelName: 'services',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
ref.onDispose(wrapper.dispose);
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ enum StreamConnectionStatus {
|
|||||||
|
|
||||||
/// Fatal error; stream will not recover without manual intervention.
|
/// Fatal error; stream will not recover without manual intervention.
|
||||||
failed,
|
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.
|
/// Represents the result of a polling attempt.
|
||||||
@@ -70,7 +76,19 @@ typedef ChannelStatusCallback =
|
|||||||
/// and connection status tracking. Provides graceful degradation when the
|
/// and connection status tracking. Provides graceful degradation when the
|
||||||
/// realtime connection fails.
|
/// 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:
|
/// Error handling:
|
||||||
|
/// - **Offline**: recovery suppressed; restarts on [notifyAllOnlineRestored].
|
||||||
/// - **Timeout**: detected and handled internally with exponential backoff.
|
/// - **Timeout**: detected and handled internally with exponential backoff.
|
||||||
/// - **ChannelRateLimitReached**: detected and handled with a longer minimum
|
/// - **ChannelRateLimitReached**: detected and handled with a longer minimum
|
||||||
/// delay (5 s) before retrying. During recovery, a REST poll keeps data
|
/// delay (5 s) before retrying. During recovery, a REST poll keeps data
|
||||||
@@ -92,6 +110,39 @@ typedef ChannelStatusCallback =
|
|||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
class StreamRecoveryWrapper<T> {
|
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 Stream<List<Map<String, dynamic>>> _realtimeStream;
|
||||||
final Future<List<T>> Function() _onPollData;
|
final Future<List<T>> Function() _onPollData;
|
||||||
final T Function(Map<String, dynamic>) _fromMap;
|
final T Function(Map<String, dynamic>) _fromMap;
|
||||||
@@ -105,6 +156,17 @@ class StreamRecoveryWrapper<T> {
|
|||||||
/// skeleton indicators in the UI.
|
/// skeleton indicators in the UI.
|
||||||
final ChannelStatusCallback? _onStatusChanged;
|
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;
|
StreamConnectionStatus _connectionStatus = StreamConnectionStatus.connected;
|
||||||
int _recoveryAttempts = 0;
|
int _recoveryAttempts = 0;
|
||||||
Timer? _pollingTimer;
|
Timer? _pollingTimer;
|
||||||
@@ -115,6 +177,11 @@ class StreamRecoveryWrapper<T> {
|
|||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
bool _listening = 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({
|
StreamRecoveryWrapper({
|
||||||
required Stream<List<Map<String, dynamic>>> stream,
|
required Stream<List<Map<String, dynamic>>> stream,
|
||||||
required Future<List<T>> Function() onPollData,
|
required Future<List<T>> Function() onPollData,
|
||||||
@@ -122,11 +189,21 @@ class StreamRecoveryWrapper<T> {
|
|||||||
StreamRecoveryConfig config = const StreamRecoveryConfig(),
|
StreamRecoveryConfig config = const StreamRecoveryConfig(),
|
||||||
this.channelName = 'unknown',
|
this.channelName = 'unknown',
|
||||||
ChannelStatusCallback? onStatusChanged,
|
ChannelStatusCallback? onStatusChanged,
|
||||||
|
Future<List<T>> Function()? onOfflineData,
|
||||||
|
void Function(List<T> rows)? onCacheMirror,
|
||||||
}) : _realtimeStream = stream,
|
}) : _realtimeStream = stream,
|
||||||
_onPollData = onPollData,
|
_onPollData = onPollData,
|
||||||
_fromMap = fromMap,
|
_fromMap = fromMap,
|
||||||
_config = config,
|
_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.
|
/// The wrapped stream that emits recovery results with metadata.
|
||||||
///
|
///
|
||||||
@@ -161,6 +238,10 @@ class StreamRecoveryWrapper<T> {
|
|||||||
void _onRealtimeData(List<Map<String, dynamic>> rows) {
|
void _onRealtimeData(List<Map<String, dynamic>> rows) {
|
||||||
if (_disposed) return;
|
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.
|
// When recovering, don't reset _recoveryAttempts immediately.
|
||||||
// Supabase streams emit an initial REST fetch before the realtime
|
// Supabase streams emit an initial REST fetch before the realtime
|
||||||
// channel is established. If the channel keeps failing, resetting
|
// channel is established. If the channel keeps failing, resetting
|
||||||
@@ -183,9 +264,22 @@ class StreamRecoveryWrapper<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setStatus(StreamConnectionStatus.connected);
|
_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(
|
_emit(
|
||||||
StreamRecoveryResult<T>(
|
StreamRecoveryResult<T>(
|
||||||
data: rows.map(_fromMap).toList(),
|
data: converted,
|
||||||
connectionStatus: StreamConnectionStatus.connected,
|
connectionStatus: StreamConnectionStatus.connected,
|
||||||
isStale: false,
|
isStale: false,
|
||||||
),
|
),
|
||||||
@@ -198,6 +292,37 @@ class StreamRecoveryWrapper<T> {
|
|||||||
// Cancel any stability timer — the connection is not stable.
|
// Cancel any stability timer — the connection is not stable.
|
||||||
_stabilityTimer?.cancel();
|
_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 isRateLimit = _isRateLimitError(error);
|
||||||
final isTimeout = _isTimeoutError(error);
|
final isTimeout = _isTimeoutError(error);
|
||||||
final tag = isRateLimit
|
final tag = isRateLimit
|
||||||
@@ -260,6 +385,21 @@ class StreamRecoveryWrapper<T> {
|
|||||||
void _onRealtimeDone() {
|
void _onRealtimeDone() {
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
debugPrint('StreamRecoveryWrapper[$channelName]: stream completed');
|
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.
|
// Attempt to reconnect once if the stream closes unexpectedly.
|
||||||
if (_recoveryAttempts < _config.maxRecoveryAttempts) {
|
if (_recoveryAttempts < _config.maxRecoveryAttempts) {
|
||||||
_onRealtimeError(StateError('realtime stream completed unexpectedly'));
|
_onRealtimeError(StateError('realtime stream completed unexpectedly'));
|
||||||
@@ -282,8 +422,23 @@ class StreamRecoveryWrapper<T> {
|
|||||||
|
|
||||||
Future<void> _pollOnce() async {
|
Future<void> _pollOnce() async {
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
// Don't poll when offline — it will fail and mislead the status tracker.
|
||||||
|
if (!_globalIsOnline()) return;
|
||||||
try {
|
try {
|
||||||
final data = await _onPollData();
|
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(
|
_emit(
|
||||||
StreamRecoveryResult<T>(
|
StreamRecoveryResult<T>(
|
||||||
data: data,
|
data: data,
|
||||||
@@ -330,6 +485,58 @@ class StreamRecoveryWrapper<T> {
|
|||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────
|
// ── 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) {
|
void _emit(StreamRecoveryResult<T> result) {
|
||||||
if (!_disposed && _controller != null && !_controller!.isClosed) {
|
if (!_disposed && _controller != null && !_controller!.isClosed) {
|
||||||
_controller!.add(result);
|
_controller!.add(result);
|
||||||
@@ -358,6 +565,20 @@ class StreamRecoveryWrapper<T> {
|
|||||||
_startRealtimeSubscription();
|
_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
|
/// Clean up all resources and notify the status callback that this
|
||||||
/// channel is no longer active, preventing ghost entries in the
|
/// channel is no longer active, preventing ghost entries in the
|
||||||
/// [RealtimeController]'s recovering-channels set.
|
/// [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/material.dart';
|
||||||
import 'package:flutter/foundation.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/office.dart';
|
||||||
import '../models/ticket.dart';
|
import '../models/ticket.dart';
|
||||||
import '../models/ticket_message.dart';
|
import '../models/ticket_message.dart';
|
||||||
@@ -12,11 +17,27 @@ import 'profile_provider.dart';
|
|||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'user_offices_provider.dart';
|
import 'user_offices_provider.dart';
|
||||||
import 'tasks_provider.dart';
|
import 'tasks_provider.dart';
|
||||||
|
import 'connectivity_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'realtime_controller.dart';
|
import 'realtime_controller.dart';
|
||||||
|
|
||||||
|
/// Pending offices created while offline.
|
||||||
|
final offlinePendingOfficesProvider =
|
||||||
|
StateProvider<List<Office>>((ref) => const []);
|
||||||
|
|
||||||
final officesProvider = StreamProvider<List<Office>>((ref) {
|
final officesProvider = StreamProvider<List<Office>>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
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>(
|
final wrapper = StreamRecoveryWrapper<Office>(
|
||||||
stream: client.from('offices').stream(primaryKey: ['id']).order('name'),
|
stream: client.from('offices').stream(primaryKey: ['id']).order('name'),
|
||||||
@@ -27,10 +48,49 @@ final officesProvider = StreamProvider<List<Office>>((ref) {
|
|||||||
fromMap: Office.fromMap,
|
fromMap: Office.fromMap,
|
||||||
channelName: 'offices',
|
channelName: 'offices',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
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 {
|
final officesOnceProvider = FutureProvider<List<Office>>((ref) async {
|
||||||
@@ -70,7 +130,7 @@ final officesQueryProvider = StateProvider<OfficeQuery>(
|
|||||||
|
|
||||||
final officesControllerProvider = Provider<OfficesController>((ref) {
|
final officesControllerProvider = Provider<OfficesController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return OfficesController(client);
|
return OfficesController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Ticket query parameters for server-side pagination and filtering.
|
/// Ticket query parameters for server-side pagination and filtering.
|
||||||
@@ -210,10 +270,90 @@ final ticketsProvider = StreamProvider<List<Ticket>>((ref) {
|
|||||||
fromMap: Ticket.fromMap,
|
fromMap: Ticket.fromMap,
|
||||||
channelName: 'tickets',
|
channelName: 'tickets',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||||
|
onOfflineData: () => cachedListFromBrick<Ticket>(),
|
||||||
|
onCacheMirror: (rows) =>
|
||||||
|
mirrorBatchToBrick<Ticket>(rows, tag: 'tickets'),
|
||||||
);
|
);
|
||||||
|
|
||||||
ref.onDispose(wrapper.dispose);
|
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 = '';
|
var lastResultHash = '';
|
||||||
Timer? debounceTimer;
|
Timer? debounceTimer;
|
||||||
// broadcast() so Riverpod and any other listener can both receive events.
|
// 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.
|
// prevents a duplicate rebuild if both arrive with identical data.
|
||||||
unawaited(
|
unawaited(
|
||||||
Future(() async {
|
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 {
|
try {
|
||||||
// Seed fetch: order newest-first and limit to 200 rows to reduce
|
// Seed fetch: order newest-first and limit to 200 rows to reduce
|
||||||
// initial payload. The realtime stream delivers subsequent changes.
|
// 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 ticketsControllerProvider = Provider<TicketsController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return TicketsController(client);
|
return TicketsController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class TicketsController {
|
class TicketsController {
|
||||||
TicketsController(this._client);
|
TicketsController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
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 subject,
|
||||||
required String description,
|
required String description,
|
||||||
required String officeId,
|
required String officeId,
|
||||||
}) async {
|
}) async {
|
||||||
final actorId = _client.auth.currentUser?.id;
|
final actorId = _client.auth.currentUser?.id;
|
||||||
final data = await _client
|
// Generate UUID before the network call so Brick's offline queue captures
|
||||||
.from('tickets')
|
// the same UUID in the queued request — preventing a UUID mismatch on replay.
|
||||||
.insert({
|
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,
|
'subject': subject,
|
||||||
'description': description,
|
'description': description,
|
||||||
'office_id': officeId,
|
'office_id': officeId,
|
||||||
'creator_id': _client.auth.currentUser?.id,
|
'creator_id': actorId,
|
||||||
})
|
});
|
||||||
.select('id')
|
ref.read(offlinePendingTicketRawProvider.notifier).state =
|
||||||
.single();
|
List.unmodifiable(currentRaw);
|
||||||
final ticketId = data['id'] as String?;
|
}
|
||||||
if (ticketId == null) return;
|
mirrorBatchToBrick<Ticket>([ticket], tag: 'offline_ticket');
|
||||||
unawaited(_notifyCreated(ticketId: ticketId, actorId: actorId));
|
return ticket;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _notifyCreated({
|
Future<void> _notifyCreated({
|
||||||
@@ -661,27 +886,66 @@ class TicketsController {
|
|||||||
required String? ticketId,
|
required String? ticketId,
|
||||||
required String content,
|
required String content,
|
||||||
}) async {
|
}) async {
|
||||||
|
final senderId = _client.auth.currentUser?.id;
|
||||||
final payload = <String, dynamic>{
|
final payload = <String, dynamic>{
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'content': content,
|
'content': content,
|
||||||
'sender_id': _client.auth.currentUser?.id,
|
'sender_id': senderId,
|
||||||
};
|
};
|
||||||
if (ticketId != null) {
|
if (ticketId != null) {
|
||||||
payload['ticket_id'] = ticketId;
|
payload['ticket_id'] = ticketId;
|
||||||
}
|
}
|
||||||
final data = await _client
|
try {
|
||||||
.from('ticket_messages')
|
final data = await _client
|
||||||
.insert(payload)
|
.from('ticket_messages')
|
||||||
.select()
|
.insert(payload)
|
||||||
.single();
|
.select()
|
||||||
return TicketMessage.fromMap(data);
|
.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({
|
Future<void> updateTicketStatus({
|
||||||
required String ticketId,
|
required String ticketId,
|
||||||
required String status,
|
required String status,
|
||||||
}) async {
|
}) 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
|
// If ticket is promoted, create a linked Task (only once) — the
|
||||||
// TasksController.createTask already runs auto-assignment on creation.
|
// TasksController.createTask already runs auto-assignment on creation.
|
||||||
@@ -718,6 +982,7 @@ class TicketsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update editable ticket fields such as subject, description, and office.
|
/// Update editable ticket fields such as subject, description, and office.
|
||||||
|
/// Offline: queues the field patch.
|
||||||
Future<void> updateTicket({
|
Future<void> updateTicket({
|
||||||
required String ticketId,
|
required String ticketId,
|
||||||
String? subject,
|
String? subject,
|
||||||
@@ -730,29 +995,59 @@ class TicketsController {
|
|||||||
if (officeId != null) payload['office_id'] = officeId;
|
if (officeId != null) payload['office_id'] = officeId;
|
||||||
if (payload.isEmpty) return;
|
if (payload.isEmpty) return;
|
||||||
|
|
||||||
await _client.from('tickets').update(payload).eq('id', ticketId);
|
|
||||||
|
|
||||||
// record an activity row for edit operations (best-effort)
|
|
||||||
try {
|
try {
|
||||||
final actorId = _client.auth.currentUser?.id;
|
await _client.from('tickets').update(payload).eq('id', ticketId);
|
||||||
await _client.from('ticket_messages').insert({
|
// record an activity row for edit operations (best-effort)
|
||||||
'ticket_id': ticketId,
|
try {
|
||||||
'sender_id': actorId,
|
final actorId = _client.auth.currentUser?.id;
|
||||||
'content': 'Ticket updated',
|
await _client.from('ticket_messages').insert({
|
||||||
});
|
'ticket_id': ticketId,
|
||||||
} catch (_) {}
|
'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 {
|
class OfficesController {
|
||||||
OfficesController(this._client);
|
OfficesController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
final SupabaseClient _client;
|
||||||
|
final Ref? _ref;
|
||||||
|
|
||||||
Future<void> createOffice({required String name, String? serviceId}) async {
|
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;
|
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({
|
Future<void> updateOffice({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import 'connectivity_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
|
|
||||||
class TypingIndicatorState {
|
class TypingIndicatorState {
|
||||||
@@ -37,24 +38,47 @@ final typingIndicatorProvider =
|
|||||||
String
|
String
|
||||||
>((ref, ticketId) {
|
>((ref, ticketId) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
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;
|
return controller;
|
||||||
});
|
});
|
||||||
|
|
||||||
class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
||||||
TypingIndicatorController(this._client, this._ticketId)
|
TypingIndicatorController(
|
||||||
: super(
|
this._client,
|
||||||
const TypingIndicatorState(
|
this._ticketId, {
|
||||||
userIds: {},
|
bool Function()? isOnline,
|
||||||
channelStatus: 'init',
|
}) : _isOnline = isOnline ?? (() => true),
|
||||||
lastPayload: {},
|
super(
|
||||||
),
|
const TypingIndicatorState(
|
||||||
) {
|
userIds: {},
|
||||||
|
channelStatus: 'init',
|
||||||
|
lastPayload: {},
|
||||||
|
),
|
||||||
|
) {
|
||||||
_initChannel();
|
_initChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
final SupabaseClient _client;
|
final SupabaseClient _client;
|
||||||
final String _ticketId;
|
final String _ticketId;
|
||||||
|
final bool Function() _isOnline;
|
||||||
RealtimeChannel? _channel;
|
RealtimeChannel? _channel;
|
||||||
Timer? _typingTimer;
|
Timer? _typingTimer;
|
||||||
final Map<String, Timer> _remoteTimeouts = {};
|
final Map<String, Timer> _remoteTimeouts = {};
|
||||||
@@ -94,7 +118,9 @@ class TypingIndicatorController extends StateNotifier<TypingIndicatorState> {
|
|||||||
try {
|
try {
|
||||||
if (_disposed || !mounted) return;
|
if (_disposed || !mounted) return;
|
||||||
state = state.copyWith(channelStatus: status.name);
|
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');
|
debugPrint('TypingIndicatorController: subscribe error: $error');
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} 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.
|
// Exposed for tests only: simulate a remote typing broadcast.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
void debugSimulateRemoteTyping(String userId, {bool stop = false}) {
|
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:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import '../brick/cache_warmer.dart';
|
||||||
import '../models/user_office.dart';
|
import '../models/user_office.dart';
|
||||||
|
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||||
|
import 'connectivity_provider.dart';
|
||||||
import 'supabase_provider.dart';
|
import 'supabase_provider.dart';
|
||||||
import 'stream_recovery.dart';
|
import 'stream_recovery.dart';
|
||||||
import 'realtime_controller.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 userOfficesProvider = StreamProvider<List<UserOffice>>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
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>(
|
final wrapper = StreamRecoveryWrapper<UserOffice>(
|
||||||
stream: client
|
stream: client
|
||||||
.from('user_offices')
|
.from('user_offices')
|
||||||
.stream(primaryKey: ['user_id', 'office_id'])
|
.stream(primaryKey: ['user_id', 'office_id'])
|
||||||
.order('created_at'),
|
.order('created_at'),
|
||||||
onPollData: () async {
|
onPollData: () async {
|
||||||
final data = await client
|
final data = await client.from('user_offices').select().order('created_at');
|
||||||
.from('user_offices')
|
|
||||||
.select()
|
|
||||||
.order('created_at');
|
|
||||||
return data.map(UserOffice.fromMap).toList();
|
return data.map(UserOffice.fromMap).toList();
|
||||||
},
|
},
|
||||||
fromMap: UserOffice.fromMap,
|
fromMap: UserOffice.fromMap,
|
||||||
channelName: 'user_offices',
|
channelName: 'user_offices',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
||||||
|
onOfflineData: () async {
|
||||||
|
final rows = await CacheWarmer.readCachedUserOffices();
|
||||||
|
return applyPending(rows.map(UserOffice.fromMap).toList());
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ref.onDispose(wrapper.dispose);
|
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 userOfficesControllerProvider = Provider<UserOfficesController>((ref) {
|
||||||
final client = ref.watch(supabaseClientProvider);
|
final client = ref.watch(supabaseClientProvider);
|
||||||
return UserOfficesController(client);
|
return UserOfficesController(client, ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
class UserOfficesController {
|
class UserOfficesController {
|
||||||
UserOfficesController(this._client);
|
UserOfficesController(this._client, [this._ref]);
|
||||||
|
|
||||||
final SupabaseClient _client;
|
final SupabaseClient _client;
|
||||||
|
final Ref? _ref;
|
||||||
|
|
||||||
|
/// Assign a user to an office. Offline: queues the operation.
|
||||||
Future<void> assignUserOffice({
|
Future<void> assignUserOffice({
|
||||||
required String userId,
|
required String userId,
|
||||||
required String officeId,
|
required String officeId,
|
||||||
}) async {
|
}) async {
|
||||||
await _client.from('user_offices').insert({
|
try {
|
||||||
'user_id': userId,
|
await _client.from('user_offices').insert({
|
||||||
'office_id': officeId,
|
'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({
|
Future<void> removeUserOffice({
|
||||||
required String userId,
|
required String userId,
|
||||||
required String officeId,
|
required String officeId,
|
||||||
}) async {
|
}) async {
|
||||||
await _client
|
try {
|
||||||
.from('user_offices')
|
await _client
|
||||||
.delete()
|
.from('user_offices')
|
||||||
.eq('user_id', userId)
|
.delete()
|
||||||
.eq('office_id', officeId);
|
.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:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import '../brick/cache_helpers.dart';
|
||||||
import '../models/app_settings.dart';
|
import '../models/app_settings.dart';
|
||||||
import '../models/duty_schedule.dart';
|
import '../models/duty_schedule.dart';
|
||||||
import '../models/swap_request.dart';
|
import '../models/swap_request.dart';
|
||||||
@@ -52,6 +53,13 @@ final dutySchedulesProvider = StreamProvider<List<DutySchedule>>((ref) {
|
|||||||
fromMap: DutySchedule.fromMap,
|
fromMap: DutySchedule.fromMap,
|
||||||
channelName: 'duty_schedules',
|
channelName: 'duty_schedules',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
ref.onDispose(wrapper.dispose);
|
||||||
@@ -150,6 +158,20 @@ final swapRequestsProvider = StreamProvider<List<SwapRequest>>((ref) {
|
|||||||
fromMap: SwapRequest.fromMap,
|
fromMap: SwapRequest.fromMap,
|
||||||
channelName: 'swap_requests',
|
channelName: 'swap_requests',
|
||||||
onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus,
|
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);
|
ref.onDispose(wrapper.dispose);
|
||||||
|
|||||||
@@ -258,30 +258,48 @@ class _OfficesScreenState extends ConsumerState<OfficesScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() => saving = true);
|
setState(() => saving = true);
|
||||||
final controller = ref.read(
|
try {
|
||||||
officesControllerProvider,
|
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.',
|
|
||||||
);
|
);
|
||||||
|
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
|
child: saving
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ class _AnnouncementCommentsSectionState
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (isOfflineSaveError(e)) {
|
||||||
SnackBar(content: Text('Failed to post comment: $e')),
|
_controller.clear();
|
||||||
);
|
showSuccessSnackBar(context, 'Comment saved offline — will sync when connected.');
|
||||||
|
} else {
|
||||||
|
showErrorSnackBar(context, 'Failed to post comment: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _sending = false);
|
if (mounted) setState(() => _sending = false);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import '../../widgets/app_page_header.dart';
|
|||||||
import '../../widgets/m3_card.dart';
|
import '../../widgets/m3_card.dart';
|
||||||
import '../../widgets/profile_avatar.dart';
|
import '../../widgets/profile_avatar.dart';
|
||||||
import '../../widgets/responsive_body.dart';
|
import '../../widgets/responsive_body.dart';
|
||||||
|
import '../../widgets/sync_pending_badge.dart';
|
||||||
import 'announcement_comments_section.dart';
|
import 'announcement_comments_section.dart';
|
||||||
import 'create_announcement_dialog.dart';
|
import 'create_announcement_dialog.dart';
|
||||||
|
|
||||||
@@ -317,6 +318,14 @@ class _AnnouncementCardState extends ConsumerState<_AnnouncementCard> {
|
|||||||
ref.watch(announcementCommentsProvider(widget.announcement.id));
|
ref.watch(announcementCommentsProvider(widget.announcement.id));
|
||||||
final commentCount = commentsAsync.valueOrNull?.length ?? 0;
|
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.
|
// Rebuild UI when cooldown is active.
|
||||||
if (_inCooldown) {
|
if (_inCooldown) {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
@@ -428,9 +437,21 @@ class _AnnouncementCardState extends ConsumerState<_AnnouncementCard> {
|
|||||||
// Title + Body
|
// Title + Body
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
child: Text(
|
child: Row(
|
||||||
widget.announcement.title,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
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(
|
Padding(
|
||||||
|
|||||||
@@ -269,7 +269,14 @@ class _CreateAnnouncementContentState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
}
|
}
|
||||||
@@ -537,7 +544,14 @@ class _BannerSettingsContentState
|
|||||||
if (mounted) Navigator.of(context).pop();
|
if (mounted) Navigator.of(context).pop();
|
||||||
showSuccessSnackBarGlobal('Banner settings saved.');
|
showSuccessSnackBarGlobal('Banner settings saved.');
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import '../../widgets/gemini_button.dart';
|
|||||||
import '../../widgets/multi_select_picker.dart';
|
import '../../widgets/multi_select_picker.dart';
|
||||||
import '../../widgets/app_page_header.dart';
|
import '../../widgets/app_page_header.dart';
|
||||||
import '../../widgets/responsive_body.dart';
|
import '../../widgets/responsive_body.dart';
|
||||||
|
import '../../widgets/sync_pending_badge.dart';
|
||||||
|
|
||||||
class AttendanceScreen extends ConsumerStatefulWidget {
|
class AttendanceScreen extends ConsumerStatefulWidget {
|
||||||
const AttendanceScreen({super.key});
|
const AttendanceScreen({super.key});
|
||||||
@@ -362,6 +363,7 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
|
|||||||
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
||||||
final schedulesAsync = ref.watch(dutySchedulesProvider);
|
final schedulesAsync = ref.watch(dutySchedulesProvider);
|
||||||
final logsAsync = ref.watch(attendanceLogsProvider);
|
final logsAsync = ref.watch(attendanceLogsProvider);
|
||||||
|
final pendingCheckIns = ref.watch(offlinePendingCheckInsProvider);
|
||||||
final allowTracking = profile?.allowTracking ?? false;
|
final allowTracking = profile?.allowTracking ?? false;
|
||||||
// local state for optimistic switch update. We only trust `_trackingLocal`
|
// local state for optimistic switch update. We only trust `_trackingLocal`
|
||||||
// while a save is in flight – after that the server-side value (`allowTracking`)
|
// 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.)
|
// (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
|
// 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 =
|
final showOvertimeCard =
|
||||||
|
schedulesAsync.hasValue &&
|
||||||
|
logsAsync.hasValue &&
|
||||||
(activeOvertimeLog.isEmpty && _overtimeLogId == null) &&
|
(activeOvertimeLog.isEmpty && _overtimeLogId == null) &&
|
||||||
activeLog.isEmpty &&
|
activeLog.isEmpty &&
|
||||||
(!hasScheduleToday || hasScheduleEnded);
|
(!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),
|
_statusChip(context, statusLabel),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -4228,7 +4243,11 @@ class _MyScheduleTabState extends ConsumerState<_MyScheduleTab> {
|
|||||||
}
|
}
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (!alreadyProcessed) {
|
if (!alreadyProcessed) {
|
||||||
showErrorSnackBar(context, 'Failed: $e');
|
if (isOfflineSaveError(e)) {
|
||||||
|
showSuccessSnackBar(context, 'Response saved offline — will sync when connected.');
|
||||||
|
} else {
|
||||||
|
showErrorSnackBar(context, 'Failed: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Refresh the swap list — the new provider fires an immediate REST poll
|
// 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 slipsAsync = ref.watch(passSlipsProvider);
|
||||||
final profilesAsync = ref.watch(profilesProvider);
|
final profilesAsync = ref.watch(profilesProvider);
|
||||||
final activeSlip = ref.watch(activePassSlipProvider);
|
final activeSlip = ref.watch(activePassSlipProvider);
|
||||||
|
final pendingSlips = ref.watch(offlinePendingPassSlipsProvider);
|
||||||
final isAdmin = profile?.role == 'admin' || profile?.role == 'dispatcher';
|
final isAdmin = profile?.role == 'admin' || profile?.role == 'dispatcher';
|
||||||
|
|
||||||
final Map<String, Profile> profileById = {
|
final Map<String, Profile> profileById = {
|
||||||
@@ -4985,6 +5005,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
|||||||
slip,
|
slip,
|
||||||
profileById,
|
profileById,
|
||||||
showActions: true,
|
showActions: true,
|
||||||
|
isPending: pendingSlips.any((p) => p.id == slip.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (slips.where((s) => s.status == 'pending').isEmpty)
|
if (slips.where((s) => s.status == 'pending').isEmpty)
|
||||||
@@ -5017,6 +5038,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
|||||||
slip,
|
slip,
|
||||||
profileById,
|
profileById,
|
||||||
showActions: false,
|
showActions: false,
|
||||||
|
isPending: pendingSlips.any((p) => p.id == slip.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (slips.isEmpty)
|
if (slips.isEmpty)
|
||||||
@@ -5044,6 +5066,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
|||||||
PassSlip slip,
|
PassSlip slip,
|
||||||
Map<String, Profile> profileById, {
|
Map<String, Profile> profileById, {
|
||||||
required bool showActions,
|
required bool showActions,
|
||||||
|
bool isPending = false,
|
||||||
}) {
|
}) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colors = theme.colorScheme;
|
final colors = theme.colorScheme;
|
||||||
@@ -5073,6 +5096,10 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(name, style: theme.textTheme.titleSmall)),
|
Expanded(child: Text(name, style: theme.textTheme.titleSmall)),
|
||||||
|
if (isPending) ...[
|
||||||
|
SyncPendingBadge(isPending: true, compact: true),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10,
|
horizontal: 10,
|
||||||
@@ -5151,7 +5178,11 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'Failed: $e');
|
if (isOfflineSaveError(e)) {
|
||||||
|
showSuccessSnackBar(context, 'Approval saved offline — will sync when connected.');
|
||||||
|
} else {
|
||||||
|
showErrorSnackBar(context, 'Failed: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
@@ -5167,7 +5198,11 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'Failed: $e');
|
if (isOfflineSaveError(e)) {
|
||||||
|
showSuccessSnackBar(context, 'Rejection saved offline — will sync when connected.');
|
||||||
|
} else {
|
||||||
|
showErrorSnackBar(context, 'Failed: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
@@ -5183,7 +5218,11 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'Failed: $e');
|
if (isOfflineSaveError(e)) {
|
||||||
|
showSuccessSnackBar(context, 'Completion saved offline — will sync when connected.');
|
||||||
|
} else {
|
||||||
|
showErrorSnackBar(context, 'Failed: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
@@ -5212,6 +5251,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
final profile = ref.watch(currentProfileProvider).valueOrNull;
|
||||||
final leavesAsync = ref.watch(leavesProvider);
|
final leavesAsync = ref.watch(leavesProvider);
|
||||||
final profilesAsync = ref.watch(profilesProvider);
|
final profilesAsync = ref.watch(profilesProvider);
|
||||||
|
final pendingLeavesList = ref.watch(offlinePendingLeavesProvider);
|
||||||
|
|
||||||
if (profile == null) {
|
if (profile == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@@ -5259,6 +5299,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
leave,
|
leave,
|
||||||
profileById,
|
profileById,
|
||||||
showApproval: true,
|
showApproval: true,
|
||||||
|
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -5292,6 +5333,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
leave,
|
leave,
|
||||||
profileById,
|
profileById,
|
||||||
showApproval: false,
|
showApproval: false,
|
||||||
|
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -5314,6 +5356,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
leave,
|
leave,
|
||||||
profileById,
|
profileById,
|
||||||
showApproval: false,
|
showApproval: false,
|
||||||
|
isPending: pendingLeavesList.any((l) => l.id == leave.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (leaves
|
if (leaves
|
||||||
@@ -5343,6 +5386,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
LeaveOfAbsence leave,
|
LeaveOfAbsence leave,
|
||||||
Map<String, Profile> profileById, {
|
Map<String, Profile> profileById, {
|
||||||
required bool showApproval,
|
required bool showApproval,
|
||||||
|
bool isPending = false,
|
||||||
}) {
|
}) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colors = theme.colorScheme;
|
final colors = theme.colorScheme;
|
||||||
@@ -5372,6 +5416,10 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(name, style: theme.textTheme.titleSmall)),
|
Expanded(child: Text(name, style: theme.textTheme.titleSmall)),
|
||||||
|
if (isPending) ...[
|
||||||
|
SyncPendingBadge(isPending: true, compact: true),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10,
|
horizontal: 10,
|
||||||
@@ -5473,7 +5521,11 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'Failed: $e');
|
if (isOfflineSaveError(e)) {
|
||||||
|
showSuccessSnackBar(context, 'Approval saved offline — will sync when connected.');
|
||||||
|
} else {
|
||||||
|
showErrorSnackBar(context, 'Failed: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
@@ -5491,7 +5543,11 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'Failed: $e');
|
if (isOfflineSaveError(e)) {
|
||||||
|
showSuccessSnackBar(context, 'Rejection saved offline — will sync when connected.');
|
||||||
|
} else {
|
||||||
|
showErrorSnackBar(context, 'Failed: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
@@ -5509,7 +5565,11 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
showErrorSnackBar(context, 'Failed: $e');
|
if (isOfflineSaveError(e)) {
|
||||||
|
showSuccessSnackBar(context, 'Cancellation saved offline — will sync when connected.');
|
||||||
|
} else {
|
||||||
|
showErrorSnackBar(context, 'Failed: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
@@ -5618,7 +5678,14 @@ class _PassSlipDialogState extends ConsumerState<_PassSlipDialog> {
|
|||||||
widget.onSubmitted();
|
widget.onSubmitted();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
if (mounted) setState(() => _submitting = false);
|
if (mounted) setState(() => _submitting = false);
|
||||||
}
|
}
|
||||||
@@ -5893,7 +5960,12 @@ class _FileLeaveDialogState extends ConsumerState<_FileLeaveDialog> {
|
|||||||
widget.onSubmitted();
|
widget.onSubmitted();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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');
|
showErrorSnackBar(context, 'Failed to file leave: $e');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1335,7 +1335,7 @@ class _ItServiceRequestDetailScreenState
|
|||||||
_finishSave(success: true);
|
_finishSave(success: true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Save field error: $e');
|
debugPrint('Save field error: $e');
|
||||||
_finishSave(success: false);
|
_finishSave(success: isOfflineSaveError(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1350,7 +1350,7 @@ class _ItServiceRequestDetailScreenState
|
|||||||
_finishSave(success: true);
|
_finishSave(success: true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Save datetime error: $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');
|
if (mounted) showSuccessSnackBar(context, 'Text improved successfully');
|
||||||
} catch (e) {
|
} 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 {
|
} finally {
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
}
|
}
|
||||||
@@ -1525,7 +1531,13 @@ class _ItServiceRequestDetailScreenState
|
|||||||
|
|
||||||
if (mounted) showSuccessSnackBar(context, 'Request approved');
|
if (mounted) showSuccessSnackBar(context, 'Request approved');
|
||||||
} catch (e) {
|
} 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) {
|
} 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/it_service_request_assignment.dart';
|
||||||
import '../../models/office.dart';
|
import '../../models/office.dart';
|
||||||
import '../../models/profile.dart';
|
import '../../models/profile.dart';
|
||||||
|
import '../../brick/cache_helpers.dart';
|
||||||
import '../../providers/it_service_request_provider.dart';
|
import '../../providers/it_service_request_provider.dart';
|
||||||
import '../../providers/profile_provider.dart';
|
import '../../providers/profile_provider.dart';
|
||||||
import '../../providers/realtime_controller.dart';
|
import '../../providers/realtime_controller.dart';
|
||||||
@@ -23,6 +24,7 @@ import '../../widgets/app_page_header.dart';
|
|||||||
import '../../widgets/app_state_view.dart';
|
import '../../widgets/app_state_view.dart';
|
||||||
import '../../widgets/responsive_body.dart';
|
import '../../widgets/responsive_body.dart';
|
||||||
import '../../widgets/status_pill.dart';
|
import '../../widgets/status_pill.dart';
|
||||||
|
import '../../widgets/sync_pending_badge.dart';
|
||||||
|
|
||||||
class ItServiceRequestsListScreen extends ConsumerStatefulWidget {
|
class ItServiceRequestsListScreen extends ConsumerStatefulWidget {
|
||||||
const ItServiceRequestsListScreen({super.key});
|
const ItServiceRequestsListScreen({super.key});
|
||||||
@@ -67,6 +69,26 @@ class _ItServiceRequestsListScreenState
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final requestsAsync = ref.watch(itServiceRequestsProvider);
|
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 profileAsync = ref.watch(currentProfileProvider);
|
||||||
final profilesAsync = ref.watch(profilesProvider);
|
final profilesAsync = ref.watch(profilesProvider);
|
||||||
final officesAsync = ref.watch(officesProvider);
|
final officesAsync = ref.watch(officesProvider);
|
||||||
@@ -131,8 +153,12 @@ class _ItServiceRequestsListScreenState
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final allRequests =
|
final liveRequests =
|
||||||
requestsAsync.valueOrNull ?? <ItServiceRequest>[];
|
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) {
|
if (allRequests.isEmpty) {
|
||||||
return const AppEmptyView(
|
return const AppEmptyView(
|
||||||
icon: Icons.miscellaneous_services_outlined,
|
icon: Icons.miscellaneous_services_outlined,
|
||||||
@@ -277,12 +303,14 @@ class _ItServiceRequestsListScreenState
|
|||||||
officeById: officeById,
|
officeById: officeById,
|
||||||
profileById: profileById,
|
profileById: profileById,
|
||||||
assignments: assignments,
|
assignments: assignments,
|
||||||
|
pendingIds: isrPendingIds,
|
||||||
),
|
),
|
||||||
_RequestList(
|
_RequestList(
|
||||||
requests: _applyFilters(allRequests),
|
requests: _applyFilters(allRequests),
|
||||||
officeById: officeById,
|
officeById: officeById,
|
||||||
profileById: profileById,
|
profileById: profileById,
|
||||||
assignments: assignments,
|
assignments: assignments,
|
||||||
|
pendingIds: isrPendingIds,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -291,6 +319,7 @@ class _ItServiceRequestsListScreenState
|
|||||||
officeById: officeById,
|
officeById: officeById,
|
||||||
profileById: profileById,
|
profileById: profileById,
|
||||||
assignments: assignments,
|
assignments: assignments,
|
||||||
|
pendingIds: isrPendingIds,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -427,12 +456,31 @@ class _ItServiceRequestsListScreenState
|
|||||||
requestedByUserId: profile?.id,
|
requestedByUserId: profile?.id,
|
||||||
status: (profile?.role == 'standard') ? 'pending_approval' : 'draft',
|
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');
|
showSuccessSnackBar(context, 'Request created');
|
||||||
context.go('/it-service-requests/${data['id']}');
|
context.go('/it-service-requests/${data['id']}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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.officeById,
|
||||||
required this.profileById,
|
required this.profileById,
|
||||||
required this.assignments,
|
required this.assignments,
|
||||||
|
required this.pendingIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<ItServiceRequest> requests;
|
final List<ItServiceRequest> requests;
|
||||||
final Map<String, Office> officeById;
|
final Map<String, Office> officeById;
|
||||||
final Map<String, Profile> profileById;
|
final Map<String, Profile> profileById;
|
||||||
final List<ItServiceRequestAssignment> assignments;
|
final List<ItServiceRequestAssignment> assignments;
|
||||||
|
final Set<String> pendingIds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -631,6 +681,7 @@ class _RequestList extends StatelessWidget {
|
|||||||
request: request,
|
request: request,
|
||||||
officeName: office,
|
officeName: office,
|
||||||
assignedStaff: assignedStaff,
|
assignedStaff: assignedStaff,
|
||||||
|
isPending: pendingIds.contains(request.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -643,11 +694,13 @@ class _RequestTile extends StatelessWidget {
|
|||||||
required this.request,
|
required this.request,
|
||||||
this.officeName,
|
this.officeName,
|
||||||
required this.assignedStaff,
|
required this.assignedStaff,
|
||||||
|
this.isPending = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ItServiceRequest request;
|
final ItServiceRequest request;
|
||||||
final String? officeName;
|
final String? officeName;
|
||||||
final List<String> assignedStaff;
|
final List<String> assignedStaff;
|
||||||
|
final bool isPending;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -669,11 +722,22 @@ class _RequestTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Row(
|
||||||
request.eventName,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
children: [
|
||||||
maxLines: 2,
|
Expanded(
|
||||||
overflow: TextOverflow.ellipsis,
|
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),
|
const SizedBox(height: 4),
|
||||||
if (request.services.isNotEmpty)
|
if (request.services.isNotEmpty)
|
||||||
|
|||||||
@@ -423,10 +423,15 @@ class _ItJobTileState extends ConsumerState<_ItJobTile> {
|
|||||||
// The realtime stream may arrive with a slight delay; clearing here
|
// The realtime stream may arrive with a slight delay; clearing here
|
||||||
// causes a visible revert flash before the stream catches up.
|
// causes a visible revert flash before the stream catches up.
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Revert optimistic state on failure only
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _optimisticChecked = !val);
|
if (isOfflineSaveError(e)) {
|
||||||
showErrorSnackBar(context, 'Failed to update: $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