feat: Update pubspec.yaml with new dependencies and local assets
- Updated description format in pubspec.yaml - Added multiple utility packages for enhanced functionality - Included local assets and fonts for better UI design - Updated widget test to reference the new app structure - Registered new plugins in generated_plugin_registrant.cc for Windows - Updated generated_plugins.cmake to include new plugins - Modified Supabase config to allow additional redirect URLs
|
@ -0,0 +1,42 @@
|
|||
# Update these with your Supabase details from your project settings > API
|
||||
# https://app.supabase.com/project/_/settings/api
|
||||
|
||||
# Supabase Production URL
|
||||
# SUPABASE_URL=https://bhfzrlgxqkbkjepvqeva.supabase.co
|
||||
# SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJoZnpybGd4cWtia2plcHZxZXZhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MTU2ODUsImV4cCI6MjA2MjA5MTY4NX0.qDe8QNOON5ra6-JSQ-mhBEXdRFxoQGPPifBpB_-5FrU
|
||||
|
||||
# # Supabase Service Role Secret Key
|
||||
# SERVICE_ROLE_SECRET="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNwcGVqcm9leW9uc3F4dWxpbmFqIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTczOTM2MTEyNiwiZXhwIjoyMDU0OTM3MTI2fQ.iYIVeUChLIcC7NRaeJ6dViI9JiUZSMUKufFsDTfAkjA"
|
||||
# SUPABASE_STORAGE_URL="https://cppejroeyonsqxulinaj.supabase.co/storage/v1/object/public"
|
||||
|
||||
# Connect to Supabase via connection pooling
|
||||
# DATABASE_URL="postgresql://postgres.bhfzrlgxqkbkjepvqeva:TA-SIGAP2024@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true"
|
||||
|
||||
# Direct connection to the database. Used for migrations
|
||||
# DIRECT_URL="postgresql://postgres.bhfzrlgxqkbkjepvqeva:TA-SIGAP2024@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres"
|
||||
|
||||
# Supabase Local URL
|
||||
SUPABASE_URL=http://host.docker.internal:54321
|
||||
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
|
||||
|
||||
SERVICE_ROLE_SECRET=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
|
||||
|
||||
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
|
||||
DIRECT_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
|
||||
|
||||
# RESEND_API_KEY_TES="re_WtXdegYe_Ey3yiShKfZZtjCyY1agkEaSi"
|
||||
RESEND_API_KEY="re_4EyTgztQ_3istGdHQFeSTQsLBtH6oAdub"
|
||||
SEND_EMAIL_HOOK_SECRET="jeroAB/CXdS721OiHV0Ac0yRcxO7eNihgjblH62xMhLBNc6OwK3DQnkbrHjTlSw5anml2onNTolG3SzZ"
|
||||
|
||||
# db connection string
|
||||
# Connect to Supabase via connection pooling with Supavisor.
|
||||
|
||||
# Direct connection to the database. Used for migrations.
|
||||
# DIRECT_URL="postgresql://prisma.cppejroeyonsqxulinaj:prisma@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres"
|
||||
|
||||
DENO_ENV=development
|
||||
|
||||
MAPBOX_ACCESS_TOKEN="pk.eyJ1IjoidmVyZ2lsZ29vZDEiLCJhIjoiY205b254eGltMGJ5dzJqb2F4cGpsZXlpNSJ9.zxmnSQxuc5NBwiFpsTGJCg"
|
||||
MAPBOX_TILESET_ID="vergilgood1.cm9x176pl09k11ope7hzkij0r-06afz"
|
||||
NODE_ENV=development
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"denoland.vscode-deno"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||
},
|
||||
"deno.enablePaths": [
|
||||
"supabase/functions"
|
||||
],
|
||||
"deno.lint": true,
|
||||
"deno.unstable": [
|
||||
"bare-node-builtins",
|
||||
"byonm",
|
||||
"sloppy-imports",
|
||||
"unsafe-proto",
|
||||
"webgpu",
|
||||
"broadcast-channel",
|
||||
"worker-options",
|
||||
"cron",
|
||||
"kv",
|
||||
"ffi",
|
||||
"fs",
|
||||
"http",
|
||||
"net"
|
||||
],
|
||||
"files.autoSave": "afterDelay"
|
||||
}
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
analyzer:
|
||||
errors:
|
||||
unused_element: ignore
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="sigap"
|
||||
<!-- Required to fetch data from the internet. -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- ... -->
|
||||
<application android:label="sigap"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
<activity android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
|
@ -16,19 +17,43 @@
|
|||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<meta-data android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- Add this intent-filter for Deep Links -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="signin" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="signup" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="forgotpassword" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="resetpassword" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="verifyemail" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="verifyphone" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="verifyemailotp" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="verifyphoneotp" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="verifyemailchange" />
|
||||
<data android:scheme="io.supabase.flutterquickstart"
|
||||
android:host="verifyphonechange" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
<meta-data android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
|
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 2.0 MiB |
After Width: | Height: | Size: 2.7 MiB |
After Width: | Height: | Size: 12 MiB |
After Width: | Height: | Size: 369 KiB |
After Width: | Height: | Size: 247 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 8.4 KiB |
|
@ -45,5 +45,18 @@
|
|||
<true />
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true />
|
||||
<!-- Add this array for Deep Links -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>io.supabase.flutterquickstart</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<!-- ... other tags -->
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:get/get_navigation/src/root/get_material_app.dart';
|
||||
import 'package:sigap/src/cores/bindings/general_bindings.dart';
|
||||
import 'package:sigap/src/cores/routes/app_pages.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/text_strings.dart';
|
||||
import 'package:sigap/src/utils/theme/theme.dart';
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetMaterialApp(
|
||||
title: TTexts.appName,
|
||||
themeMode: ThemeMode.system,
|
||||
theme: TAppTheme.lightTheme,
|
||||
darkTheme: TAppTheme.darkTheme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
initialBinding: GeneralBindings(),
|
||||
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
||||
supportedLocales: const [Locale('id', '')],
|
||||
getPages: AppPages.routes,
|
||||
home: const Scaffold(
|
||||
backgroundColor: TColors.primary,
|
||||
body: Center(child: CircularProgressIndicator(color: TColors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,122 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:sigap/app.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
Future<void> main() async {
|
||||
// Ensure that the Flutter binding is initialized before calling any Flutter
|
||||
final WidgetsBinding widgetBinding =
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
// -- GetX Local Storage
|
||||
await GetStorage.init();
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
// -- Await splash until other item load
|
||||
FlutterNativeSplash.preserve(widgetsBinding: widgetBinding);
|
||||
|
||||
// Initialize the authentication repository with Supabase
|
||||
await Supabase.initialize(
|
||||
url: dotenv.env['SUPABASE_URL']!,
|
||||
anonKey: dotenv.env['SUPABASE_ANON_KEY']!,
|
||||
authOptions: const FlutterAuthClientOptions(
|
||||
authFlowType: AuthFlowType.pkce,
|
||||
// detectSessionInUri: true,
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
realtimeClientOptions: RealtimeClientOptions(
|
||||
logLevel: RealtimeLogLevel.info,
|
||||
),
|
||||
storageOptions: const StorageClientOptions(retryAttempts: 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Text('You have pushed the button this many times:'),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||
);
|
||||
}
|
||||
|
||||
// Add this to your dependencies initialization:
|
||||
await Get.putAsync(() => SupabaseService().init());
|
||||
|
||||
runApp(const App());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/controllers/forgot_password_controller.dart';
|
||||
import 'package:sigap/src/features/auth/controllers/signin_controller.dart';
|
||||
import 'package:sigap/src/features/auth/controllers/signup_controller.dart';
|
||||
import 'package:sigap/src/features/onboarding/controllers/choose_role_controller.dart';
|
||||
import 'package:sigap/src/features/onboarding/controllers/onboarding_controller.dart';
|
||||
|
||||
// Onboarding controller bindings
|
||||
class OnboardingControllerBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<OnboardingController>(() => OnboardingController());
|
||||
}
|
||||
}
|
||||
|
||||
class ChooseRoleControllerBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ChooseRoleController>(() => ChooseRoleController());
|
||||
}
|
||||
}
|
||||
|
||||
// Auth controller bindings
|
||||
class SignInControllerBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<SignInController>(() => SignInController());
|
||||
}
|
||||
}
|
||||
|
||||
class SignUpControllerBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<SignUpController>(() => SignUpController());
|
||||
}
|
||||
}
|
||||
|
||||
class ForgotPasswordControllerBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ForgotPasswordController>(() => ForgotPasswordController());
|
||||
}
|
||||
}
|
||||
|
||||
// Main controller bindings
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||
|
||||
class GeneralBindings extends Bindings {
|
||||
Logger? get logger => Logger();
|
||||
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.put(NetworkManager());
|
||||
Get.put(logger);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,430 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/screens/onboarding/onboarding_screen.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
||||
import 'package:sigap/src/utils/exceptions/platform_exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class AuthenticationRepository extends GetxController {
|
||||
static AuthenticationRepository get instance => Get.find();
|
||||
|
||||
// Variable
|
||||
final storage = GetStorage();
|
||||
final _supabase = SupabaseService.instance.client;
|
||||
|
||||
// Getters that use the Supabase service
|
||||
User? get authUser => SupabaseService.instance.currentUser;
|
||||
String? get currentUserId => SupabaseService.instance.currentUserId;
|
||||
// get isSessionExpired => authUser?.isExpired;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
FlutterNativeSplash.remove();
|
||||
screenRedirect();
|
||||
// storage.remove('TEMP_ROLE');
|
||||
}
|
||||
|
||||
screenRedirect() async {
|
||||
final user = _supabase.auth.currentUser;
|
||||
if (user != null) {
|
||||
// local storage
|
||||
storage.writeIfNull('isFirstTime', true);
|
||||
// check if user is already logged in
|
||||
storage.read('isFirstTime') != true
|
||||
? Get.offAll(() => const SignInScreen())
|
||||
: Get.offAll(() => const OnboardingScreen());
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- Email and Password Sign In -----------------
|
||||
Future<AuthResponse> signInWithEmailPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _supabase.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
return response;
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [SESSION] - CHECK SESSION
|
||||
Future<Map<String, dynamic>?> getSession() async {
|
||||
try {
|
||||
final session = _supabase.auth.currentSession;
|
||||
if (session != null) {
|
||||
return {'user': session.user, 'session': session};
|
||||
}
|
||||
return null;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [Email Verification] - EMAIL VERIFICATION
|
||||
Future<void> sendEmailVerification() async {
|
||||
try {
|
||||
await _supabase.auth.resend(
|
||||
type: OtpType.signup,
|
||||
email: _supabase.auth.currentUser!.email,
|
||||
);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [Email Reset Password ] - RESET PASSWORD
|
||||
Future<void> sendOtpResetPassword(String email) async {
|
||||
try {
|
||||
await _supabase.auth.resetPasswordForEmail(email);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// Compare OTP
|
||||
Future<AuthResponse> verifyOtp(String otp) async {
|
||||
try {
|
||||
final AuthResponse res = await _supabase.auth.verifyOTP(
|
||||
type: OtpType.signup,
|
||||
token: otp,
|
||||
);
|
||||
|
||||
final Session? session = res.session;
|
||||
final User? user = res.user;
|
||||
|
||||
if (session == null && user == null) {
|
||||
throw TExceptions('Failed to verify OTP. Please try again.');
|
||||
}
|
||||
|
||||
return res;
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// Update password after reset
|
||||
Future<UserResponse> updatePasswordAfterReset(String newPassword) async {
|
||||
try {
|
||||
final response = await _supabase.auth.updateUser(
|
||||
UserAttributes(password: newPassword),
|
||||
);
|
||||
return response;
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> changePassword(
|
||||
String currentPassword,
|
||||
String newPassword,
|
||||
String email,
|
||||
) async {
|
||||
try {
|
||||
// Reauthenticate user
|
||||
await _supabase.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: currentPassword,
|
||||
);
|
||||
|
||||
// Update password
|
||||
await _supabase.auth.updateUser(UserAttributes(password: newPassword));
|
||||
|
||||
// Password changed successfully
|
||||
return 'Password changed successfully.';
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [Email Verification] - CREATE NEW VERIFICATION USER EMAIL
|
||||
Future<void> resendEmailVerification(String email) async {
|
||||
try {
|
||||
await _supabase.auth.resend(type: OtpType.signup, email: email);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- Social Sign In -----------------
|
||||
// [GoogleAuthentication] - GOOGLE
|
||||
Future<bool> signInWithGoogle() async {
|
||||
try {
|
||||
return await _supabase.auth.signInWithOAuth(
|
||||
OAuthProvider.google,
|
||||
authScreenLaunchMode: LaunchMode.inAppWebView,
|
||||
);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [FacebookAuthentication] - FACEBOOK
|
||||
Future<bool> signInWithFacebook() async {
|
||||
try {
|
||||
return await _supabase.auth.signInWithOAuth(
|
||||
OAuthProvider.facebook,
|
||||
authScreenLaunchMode: LaunchMode.inAppWebView,
|
||||
);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [AppleAuthentication] - APPLE
|
||||
Future<bool> signInWithApple() async {
|
||||
try {
|
||||
return await _supabase.auth.signInWithOAuth(
|
||||
OAuthProvider.apple,
|
||||
authScreenLaunchMode: LaunchMode.inAppWebView,
|
||||
);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [GithubAuthentication] - GITHUB
|
||||
Future<bool> signInWithGithub() async {
|
||||
try {
|
||||
return await _supabase.auth.signInWithOAuth(
|
||||
OAuthProvider.github,
|
||||
authScreenLaunchMode: LaunchMode.inAppWebView,
|
||||
);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [TwitterAuthentication] - TWITTER
|
||||
Future<bool> signInWithTwitter() async {
|
||||
try {
|
||||
return await _supabase.auth.signInWithOAuth(
|
||||
OAuthProvider.twitter,
|
||||
authScreenLaunchMode: LaunchMode.inAppWebView,
|
||||
);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// [Email AUTH] - SIGN UP with role selection
|
||||
Future<AuthResponse> signUpWithCredential(
|
||||
String email,
|
||||
String password, {
|
||||
Map<String, dynamic>? userMetadata,
|
||||
bool isOfficer = false,
|
||||
Map<String, dynamic>? officerData,
|
||||
}) async {
|
||||
try {
|
||||
// Prepare the complete user metadata
|
||||
final metadata = userMetadata ?? {};
|
||||
|
||||
// Add officer flag and data if needed
|
||||
metadata['is_officer'] = isOfficer;
|
||||
if (isOfficer && officerData != null) {
|
||||
metadata['officer_data'] = officerData;
|
||||
}
|
||||
|
||||
final authResponse = await _supabase.auth.signUp(
|
||||
email: email,
|
||||
password: password,
|
||||
data: metadata,
|
||||
);
|
||||
|
||||
final Session? session = authResponse.session;
|
||||
final User? user = authResponse.user;
|
||||
|
||||
if (session == null && user == null) {
|
||||
throw TExceptions('Failed to sign up. Please try again.');
|
||||
}
|
||||
|
||||
return authResponse;
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- Logout -----------------
|
||||
// [Sign Out] - SIGN OUT
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _supabase.auth.signOut();
|
||||
Get.offAll(() => const SignInScreen());
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- Delete Account -----------------
|
||||
Future<void> deleteAccount() async {
|
||||
try {
|
||||
final userId = _supabase.auth.currentUser!.id;
|
||||
await _supabase.rpc('delete_user', params: {'user_id': userId});
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates a user's profile with the officer status and metadata
|
||||
Future<UserResponse> updateUserRole({
|
||||
required bool isOfficer,
|
||||
Map<String, dynamic>? officerData,
|
||||
}) async {
|
||||
try {
|
||||
// Prepare metadata with the officer flag
|
||||
final userMetadata = <String, dynamic>{'is_officer': isOfficer};
|
||||
|
||||
// Add officer data if provided
|
||||
if (isOfficer && officerData != null) {
|
||||
userMetadata['officer_data'] = officerData;
|
||||
}
|
||||
|
||||
// Update the user metadata which will trigger the role change function
|
||||
final response = await _supabase.auth.updateUser(
|
||||
UserAttributes(data: userMetadata),
|
||||
);
|
||||
return response;
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/repositories/auth/auth_repositories.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
||||
import 'package:sigap/src/utils/exceptions/platform_exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class OfficerRepository extends GetxController {
|
||||
static OfficerRepository get instance => Get.find();
|
||||
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
// Get current user ID
|
||||
String? get currentUserId => _supabase.auth.currentUser?.id;
|
||||
|
||||
// Get officer data
|
||||
Future<OfficerModel> getOfficerData() async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final officerData =
|
||||
await _supabase
|
||||
.from('officers')
|
||||
.select(
|
||||
'*, roles(*), units:unit_id(*), patrol_units:patrol_unit_id(*)',
|
||||
)
|
||||
.eq('id', currentUserId!)
|
||||
.single();
|
||||
|
||||
return OfficerModel.fromJson(officerData);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch officer data: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Update officer profile
|
||||
Future<OfficerModel> updateOfficerProfile(OfficerModel officer) async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final updatedOfficerData = {
|
||||
'name': officer.name,
|
||||
'nrp': officer.nrp,
|
||||
'rank': officer.rank,
|
||||
'position': officer.position,
|
||||
'phone': officer.phone,
|
||||
'email': officer.email,
|
||||
'avatar': officer.avatar,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
await _supabase
|
||||
.from('officers')
|
||||
.update(updatedOfficerData)
|
||||
.eq('id', currentUserId!);
|
||||
|
||||
// Also update the user's metadata to keep it in sync
|
||||
final currentMetadata = _supabase.auth.currentUser?.userMetadata ?? {};
|
||||
final officerDataInMetadata = currentMetadata['officer_data'] ?? {};
|
||||
|
||||
final updatedOfficerMetadata = {
|
||||
...officerDataInMetadata,
|
||||
'name': officer.name,
|
||||
'nrp': officer.nrp,
|
||||
'rank': officer.rank,
|
||||
'position': officer.position,
|
||||
'phone': officer.phone,
|
||||
};
|
||||
|
||||
await _supabase.auth.updateUser(
|
||||
UserAttributes(
|
||||
data: {...currentMetadata, 'officer_data': updatedOfficerMetadata},
|
||||
),
|
||||
);
|
||||
|
||||
return await getOfficerData();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} catch (e) {
|
||||
throw 'Failed to update officer profile: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Get officer by ID
|
||||
Future<OfficerModel> getOfficerById(String officerId) async {
|
||||
try {
|
||||
final officerData =
|
||||
await _supabase
|
||||
.from('officers')
|
||||
.select(
|
||||
'*, roles(*), units:unit_id(*), patrol_units:patrol_unit_id(*)',
|
||||
)
|
||||
.eq('id', officerId)
|
||||
.single();
|
||||
|
||||
return OfficerModel.fromJson(officerData);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch officer data: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Convert regular user to officer
|
||||
Future<void> convertToOfficer(OfficerModel officer) async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final authRepository = Get.put(AuthenticationRepository());
|
||||
|
||||
// Prepare officer data
|
||||
final officerData = {
|
||||
'unit_id': officer.unitId,
|
||||
'nrp': officer.nrp,
|
||||
'name': officer.name,
|
||||
'rank': officer.rank ?? '',
|
||||
'position': officer.position ?? '',
|
||||
'phone': officer.phone ?? '',
|
||||
'email': officer.email ?? '',
|
||||
};
|
||||
|
||||
// Update user metadata with officer flag and data
|
||||
await authRepository.updateUserRole(
|
||||
isOfficer: true,
|
||||
officerData: officerData,
|
||||
);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} catch (e) {
|
||||
throw 'Failed to convert to officer: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Convert officer to regular user
|
||||
Future<void> convertToRegularUser() async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final authRepository = Get.put(AuthenticationRepository());
|
||||
|
||||
// Update user metadata to remove officer flag
|
||||
await authRepository.updateUserRole(isOfficer: false);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} catch (e) {
|
||||
throw 'Failed to convert to regular user: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/personalization/models/profile_model.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
||||
import 'package:sigap/src/utils/exceptions/platform_exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class ProfileRepository extends GetxController {
|
||||
static ProfileRepository get instance => Get.find();
|
||||
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
// Get current user ID
|
||||
String? get currentUserId => _supabase.auth.currentUser?.id;
|
||||
|
||||
// Get user profile data
|
||||
Future<ProfileModel> getProfileData() async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final profileData =
|
||||
await _supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('user_id', currentUserId!)
|
||||
.single();
|
||||
|
||||
return ProfileModel.fromJson(profileData);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch profile data: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Update avatar
|
||||
Future<String> uploadAvatar(String filePath) async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final fileName =
|
||||
'${currentUserId}_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final storageResponse = await _supabase.storage
|
||||
.from('avatars')
|
||||
.upload(fileName, File(filePath));
|
||||
|
||||
final avatarUrl = _supabase.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl(fileName);
|
||||
|
||||
// Update the profile with the new avatar URL
|
||||
await _supabase
|
||||
.from('profiles')
|
||||
.update({'avatar': avatarUrl})
|
||||
.eq('user_id', currentUserId!);
|
||||
|
||||
return avatarUrl;
|
||||
} on StorageException catch (e) {
|
||||
throw 'Storage error: ${e.message}';
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
throw 'Failed to upload avatar: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile information
|
||||
Future<ProfileModel> updateProfile(ProfileModel profile) async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final updatedProfileData = profile.toJson();
|
||||
|
||||
await _supabase
|
||||
.from('profiles')
|
||||
.update(updatedProfileData)
|
||||
.eq('user_id', currentUserId!);
|
||||
|
||||
return await getProfileData();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
throw 'Failed to update profile: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Get profile by user ID
|
||||
Future<ProfileModel> getProfileByUserId(String userId) async {
|
||||
try {
|
||||
final profileData =
|
||||
await _supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
return ProfileModel.fromJson(profileData);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch profile data: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/personalization/models/roles_model.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class RolesRepository extends GetxController {
|
||||
static RolesRepository get instance => Get.find();
|
||||
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
// Get all roles
|
||||
Future<List<RoleModel>> getAllRoles() async {
|
||||
try {
|
||||
final roles = await _supabase.from('roles').select().order('name');
|
||||
|
||||
return (roles as List).map((role) => RoleModel.fromJson(role)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch roles: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Get role by ID
|
||||
Future<RoleModel> getRoleById(String roleId) async {
|
||||
try {
|
||||
final role =
|
||||
await _supabase.from('roles').select().eq('id', roleId).single();
|
||||
|
||||
return RoleModel.fromJson(role);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch role data: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Get role by name
|
||||
Future<RoleModel> getRoleByName(String roleName) async {
|
||||
try {
|
||||
final role =
|
||||
await _supabase.from('roles').select().eq('name', roleName).single();
|
||||
|
||||
return RoleModel.fromJson(role);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch role data: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:get/get.dart';
|
||||
|
||||
class UnitsRepository extends GetxController {
|
||||
static UnitsRepository get instance => Get.find();
|
||||
|
||||
// Add your methods and properties here
|
||||
// For example, you can add methods to fetch units, create units, etc.
|
||||
|
||||
// Example method to fetch all units
|
||||
Future<List<UnitModel>> getAllUnits() async {
|
||||
try {
|
||||
final response = await _supabase.from('units').select();
|
||||
return (response as List)
|
||||
.map((unit) => UnitModel.fromJson(unit))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch units: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
||||
import 'package:sigap/src/utils/exceptions/platform_exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class UserRepository extends GetxController {
|
||||
static UserRepository get instance => Get.find();
|
||||
|
||||
final _supabase = SupabaseService.instance.client;
|
||||
|
||||
// Get current user ID
|
||||
String? get currentUserId => SupabaseService.instance.currentUserId;
|
||||
|
||||
// Check if user is an officer
|
||||
Future<bool> isCurrentUserOfficer() async {
|
||||
try {
|
||||
if (currentUserId == null) return false;
|
||||
|
||||
final user = _supabase.auth.currentUser;
|
||||
if (user == null) return false;
|
||||
|
||||
// Check if officer flag is present in user metadata
|
||||
final metadata = user.userMetadata;
|
||||
if (metadata != null && metadata.containsKey('is_officer')) {
|
||||
return metadata['is_officer'] == true;
|
||||
}
|
||||
|
||||
// If no flag in metadata, check if user exists in officers table
|
||||
final officerData =
|
||||
await _supabase
|
||||
.from('officers')
|
||||
.select()
|
||||
.eq('id', currentUserId!)
|
||||
.single();
|
||||
|
||||
return officerData != null;
|
||||
} on PostgrestException catch (error) {
|
||||
// PGRST116 means no rows returned - not an officer
|
||||
if (error.code == 'PGRST116') return false;
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
// Default to not an officer on error
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get user data for regular users
|
||||
Future<UserModel> getUserData() async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final userData =
|
||||
await _supabase
|
||||
.from('users')
|
||||
.select('*, profiles(*), role:roles(*)')
|
||||
.eq('id', currentUserId!)
|
||||
.single();
|
||||
|
||||
return UserModel.fromJson(userData);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch user data: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Update user profile data
|
||||
Future<void> updateUserProfile({
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? bio,
|
||||
String? address,
|
||||
String? birthDate,
|
||||
String? avatar,
|
||||
String? username,
|
||||
String? phone,
|
||||
}) async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
// First update user profile
|
||||
final Map<String, dynamic> profileData = {};
|
||||
|
||||
if (firstName != null) profileData['first_name'] = firstName;
|
||||
if (lastName != null) profileData['last_name'] = lastName;
|
||||
if (bio != null) profileData['bio'] = bio;
|
||||
if (address != null) profileData['address'] = address;
|
||||
if (birthDate != null) profileData['birth_date'] = birthDate;
|
||||
if (avatar != null) profileData['avatar'] = avatar;
|
||||
if (username != null) profileData['username'] = username;
|
||||
|
||||
if (profileData.isNotEmpty) {
|
||||
await _supabase
|
||||
.from('profiles')
|
||||
.update(profileData)
|
||||
.eq('user_id', currentUserId!);
|
||||
}
|
||||
|
||||
// Then update user table if needed
|
||||
if (phone != null) {
|
||||
await _supabase
|
||||
.from('users')
|
||||
.update({'phone': phone})
|
||||
.eq('id', currentUserId!);
|
||||
|
||||
// Also update auth user phone
|
||||
await _supabase.auth.updateUser(UserAttributes(phone: phone));
|
||||
}
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
throw const TFormatException();
|
||||
} on PlatformException catch (e) {
|
||||
throw TPlatformException(e.code).message;
|
||||
} catch (e) {
|
||||
throw 'Failed to update profile: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
|
||||
// Get user by ID
|
||||
Future<UserModel> getUserById(String userId) async {
|
||||
try {
|
||||
final userData =
|
||||
await _supabase
|
||||
.from('users')
|
||||
.select('*, profiles(*), role:roles(*)')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
return UserModel.fromJson(userData);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} catch (e) {
|
||||
throw 'Failed to fetch user data: ${e.toString()}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/bindings/controller_bindings.dart';
|
||||
import 'package:sigap/src/features/auth/screens/forgot-password/forgot_password.dart';
|
||||
import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart';
|
||||
import 'package:sigap/src/features/auth/screens/signup/signup_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/screens/choose-role/choose_role_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/screens/onboarding/onboarding_screen.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
|
||||
class AppPages {
|
||||
static const initialPage = AppRoutes.panicButton;
|
||||
|
||||
static final routes = [
|
||||
// Onboarding
|
||||
GetPage(
|
||||
name: AppRoutes.onboarding,
|
||||
page: () => const OnboardingScreen(),
|
||||
binding: OnboardingControllerBinding(),
|
||||
),
|
||||
|
||||
GetPage(
|
||||
name: AppRoutes.chooseRole,
|
||||
page: () => const ChooseRoleScreen(),
|
||||
binding: ChooseRoleControllerBinding(),
|
||||
),
|
||||
|
||||
// Auth
|
||||
GetPage(
|
||||
name: AppRoutes.signIn,
|
||||
page: () => const SignInScreen(),
|
||||
binding: SignInControllerBinding(),
|
||||
),
|
||||
|
||||
GetPage(
|
||||
name: AppRoutes.signUp,
|
||||
page: () => const SignUpScreen(),
|
||||
binding: SignUpControllerBinding(),
|
||||
),
|
||||
|
||||
GetPage(
|
||||
name: AppRoutes.forgotPassword,
|
||||
page: () => const ForgotPasswordScreen(),
|
||||
binding: ForgotPasswordControllerBinding(),
|
||||
),
|
||||
|
||||
// Main pages
|
||||
// GetPage(name: AppRoutes.explore, page: () => const ExploreScreen()),
|
||||
// GetPage(name: AppRoutes.map, page: () => const MapScreen()),
|
||||
// GetPage(name: AppRoutes.panicButton, page: () => const PanicButtonScreen()),
|
||||
// GetPage(name: AppRoutes.communityWatch, page: () => const CommunityWatchScreen()),
|
||||
// GetPage(name: AppRoutes.dailyOps, page: () => const DailyOpsScreen()),
|
||||
|
||||
// Personalization
|
||||
// GetPage(name: AppRoutes.profile, page: () => const ProfileScreen()),
|
||||
// GetPage(name: AppRoutes.settings, page: () => const SettingsScreen()),
|
||||
];
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class SupabaseService extends GetxService {
|
||||
static SupabaseService get instance => Get.find<SupabaseService>();
|
||||
|
||||
final _client = Supabase.instance.client;
|
||||
|
||||
/// Get Supabase client instance
|
||||
SupabaseClient get client => _client;
|
||||
|
||||
/// Get current authenticated user
|
||||
User? get currentUser => _client.auth.currentUser;
|
||||
|
||||
/// Get current user ID, if authenticated
|
||||
String? get currentUserId => _client.auth.currentUser?.id;
|
||||
|
||||
/// Check if user is authenticated
|
||||
bool get isAuthenticated => currentUser != null;
|
||||
|
||||
/// Check if session is expired
|
||||
// bool get isSessionExpired => currentUser?.isExpired ?? true;
|
||||
|
||||
/// Check if current user is an officer based on metadata
|
||||
bool get isOfficer {
|
||||
final metadata = currentUser?.userMetadata;
|
||||
if (metadata != null && metadata.containsKey('is_officer')) {
|
||||
return metadata['is_officer'] == true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Initialize Supabase service
|
||||
Future<SupabaseService> init() async {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
extension UserMetadataExtension on SupabaseService {
|
||||
/// Update the current user's metadata
|
||||
Future<void> updateUserMetadata(Map<String, dynamic> metadata) async {
|
||||
if (!isAuthenticated) {
|
||||
throw Exception('User is not authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
// Get existing metadata to merge with new values
|
||||
final existingMetadata = currentUser?.userMetadata ?? {};
|
||||
final mergedMetadata = {...existingMetadata, ...metadata};
|
||||
|
||||
// Update user metadata
|
||||
await client.auth.updateUser(UserAttributes(data: mergedMetadata));
|
||||
} on AuthException catch (e) {
|
||||
throw Exception('Failed to update user metadata: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('An unexpected error occurred: $e');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/repositories/auth/auth_repositories.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
class EmailVerificationController extends GetxController {
|
||||
// OTP text controllers
|
||||
final List<TextEditingController> otpControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
|
||||
// Focus nodes for OTP fields
|
||||
final List<FocusNode> focusNodes = List.generate(6, (_) => FocusNode());
|
||||
|
||||
// Observable variables
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxBool isVerified = false.obs;
|
||||
final RxBool isResendEnabled = false.obs;
|
||||
final RxInt resendCountdown = 60.obs;
|
||||
final RxString verificationError = ''.obs;
|
||||
|
||||
// Timer for resend countdown
|
||||
Timer? _countdownTimer;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
startResendTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_countdownTimer?.cancel();
|
||||
for (var controller in otpControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (var node in focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Start the resend timer
|
||||
void startResendTimer() {
|
||||
resendCountdown.value = 60;
|
||||
isResendEnabled.value = false;
|
||||
|
||||
_countdownTimer?.cancel();
|
||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (resendCountdown.value > 0) {
|
||||
resendCountdown.value--;
|
||||
} else {
|
||||
isResendEnabled.value = true;
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle OTP input change
|
||||
void onOtpChanged(String value, int index) {
|
||||
if (value.length == 1) {
|
||||
// Move to next field
|
||||
if (index < otpControllers.length - 1) {
|
||||
focusNodes[index + 1].requestFocus();
|
||||
} else {
|
||||
// Last field filled, hide keyboard
|
||||
focusNodes[index].unfocus();
|
||||
// Verify OTP automatically when all fields are filled
|
||||
verifyOtp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the complete OTP
|
||||
String getOtp() {
|
||||
return otpControllers.map((controller) => controller.text).join();
|
||||
}
|
||||
|
||||
// Verify OTP
|
||||
Future<void> verifyOtp() async {
|
||||
final otp = getOtp();
|
||||
|
||||
// Check if OTP is complete
|
||||
if (otp.length != otpControllers.length) {
|
||||
verificationError.value = 'Please enter the complete verification code';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
verificationError.value = '';
|
||||
|
||||
// Simulate API call
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
// TODO: Implement actual OTP verification
|
||||
// For demo, we'll consider "1234" as valid OTP
|
||||
if (otp == "1234") {
|
||||
isVerified.value = true;
|
||||
|
||||
// Navigate to role selection after successful verification
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
Get.offNamed(AppRoutes.chooseRole);
|
||||
} else {
|
||||
verificationError.value =
|
||||
'Invalid verification code. Please try again.';
|
||||
}
|
||||
} catch (e) {
|
||||
verificationError.value = 'Verification failed: ${e.toString()}';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Resend verification code
|
||||
Future<void> resendCode(String email) async {
|
||||
if (!isResendEnabled.value) return;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
await AuthenticationRepository.instance.resendEmailVerification(email);
|
||||
|
||||
// Reset the timer
|
||||
startResendTimer();
|
||||
|
||||
// Clear fields
|
||||
for (var controller in otpControllers) {
|
||||
controller.clear();
|
||||
}
|
||||
|
||||
// Focus on first field
|
||||
focusNodes[0].requestFocus();
|
||||
|
||||
// Show success message
|
||||
TLoaders.successSnackBar(
|
||||
title: 'Code Resent',
|
||||
message: 'A new verification code has been sent to your email.',
|
||||
);
|
||||
} catch (e) {
|
||||
verificationError.value = 'Resend failed: ${e.toString()}';
|
||||
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Resend Failed',
|
||||
message: 'Failed to resend the verification code. Please try again.',
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Go back to sign in
|
||||
void goToSignIn() {
|
||||
Get.offAllNamed(AppRoutes.signIn);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
||||
class ForgotPasswordController extends GetxController {
|
||||
// Form key for validation
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
// Text controllers
|
||||
final emailController = TextEditingController();
|
||||
|
||||
// Observable variables
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxString emailError = ''.obs;
|
||||
final RxBool isEmailSent = false.obs;
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
emailController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Validate email
|
||||
String? validateEmail(String? value) {
|
||||
final error = TValidators.validateEmail(value);
|
||||
emailError.value = error ?? '';
|
||||
return error;
|
||||
}
|
||||
|
||||
// Reset password method
|
||||
Future<void> resetPassword() async {
|
||||
// Clear previous errors
|
||||
emailError.value = '';
|
||||
|
||||
// Validate form
|
||||
if (formKey.currentState?.validate() != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Simulate API call
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
// TODO: Implement actual password reset logic
|
||||
// This would typically involve calling an authentication service
|
||||
|
||||
// Show success message
|
||||
isEmailSent.value = true;
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Failed to send reset email: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Go back to sign in
|
||||
void goBack() {
|
||||
Get.back();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:sigap/src/cores/repositories/auth/auth_repositories.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
class SignInController extends GetxController {
|
||||
final rememberMe = false.obs;
|
||||
final isPasswordVisible = false.obs;
|
||||
|
||||
final localStorage = GetStorage();
|
||||
|
||||
final email = TextEditingController();
|
||||
final password = TextEditingController();
|
||||
|
||||
final emailError = ''.obs;
|
||||
final passwordError = ''.obs;
|
||||
|
||||
final isLoading = false.obs;
|
||||
|
||||
GlobalKey<FormState> signinFormKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
// Check if remember me is checked
|
||||
if (localStorage.read('REMEMBER_ME_CHECK') == true) {
|
||||
email.text = localStorage.read('REMEMBER_ME_EMAIL');
|
||||
password.text = localStorage.read('REMEMBER_ME_PASSWORD');
|
||||
}
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
// Toggle password visibility
|
||||
void togglePasswordVisibility() {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
}
|
||||
|
||||
// Sign in method
|
||||
Future<void> credentialsSignIn() async {
|
||||
try {
|
||||
// Start loading
|
||||
// TFullScreenLoader.openLoadingDialog(
|
||||
// 'Logging you in....',
|
||||
// TImages.amongUsLoading,
|
||||
// );
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
// Check connection
|
||||
final isConected = await NetworkManager.instance.isConnected();
|
||||
if (!isConected) {
|
||||
// TFullScreenLoader.stopLoading();
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Form validation
|
||||
if (!signinFormKey.currentState!.validate()) {
|
||||
// TFullScreenLoader.stopLoading();
|
||||
emailError.value = '';
|
||||
passwordError.value = '';
|
||||
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Store data if remember me is checked
|
||||
if (rememberMe.value) {
|
||||
localStorage.write('REMEMBER_ME_EMAIL', email.text);
|
||||
localStorage.write('REMEMBER_ME_PASSWORD', password.text);
|
||||
localStorage.write('REMEMBER_ME_CHECK', rememberMe.value);
|
||||
}
|
||||
|
||||
// Login with credentials
|
||||
await AuthenticationRepository.instance.signInWithEmailPassword(
|
||||
email: email.text.trim(),
|
||||
password: password.text.trim(),
|
||||
);
|
||||
|
||||
// Stop loading
|
||||
// TFullScreenLoader.stopLoading();
|
||||
isLoading.value = false;
|
||||
|
||||
// Refresh user data
|
||||
// final updatedUser = await UserRepository.instance.getUserDetailById();
|
||||
// UserController.instance.user.value = updatedUser;
|
||||
|
||||
// Redirect to home screen
|
||||
AuthenticationRepository.instance.screenRedirect();
|
||||
} catch (e) {
|
||||
// Remove loading
|
||||
// TFullScreenLoader.stopLoading();
|
||||
isLoading.value = false;
|
||||
|
||||
TLoaders.errorSnackBar(title: 'Oh snap', message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// -- Google Sign In Authentication
|
||||
Future<void> googleSignIn() async {
|
||||
try {
|
||||
// Start loading
|
||||
// TFullScreenLoader.openLoadingDialog(
|
||||
// 'Logging you in....',
|
||||
// TImages.amongUsLoading,
|
||||
// );
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
// Check connection
|
||||
final isConected = await NetworkManager.instance.isConnected();
|
||||
if (!isConected) {
|
||||
// TFullScreenLoader.stopLoading();
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Form validation
|
||||
if (!signinFormKey.currentState!.validate()) {
|
||||
// TFullScreenLoader.stopLoading();
|
||||
emailError.value = '';
|
||||
passwordError.value = '';
|
||||
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Login with Google and save user data in Firebase Authtentication
|
||||
await AuthenticationRepository.instance.signInWithGoogle();
|
||||
|
||||
// final role = localStorage.read('TEMP_ROLE');
|
||||
|
||||
// Logger().w(['User Data: $user']);
|
||||
|
||||
// Stop loading
|
||||
// TFullScreenLoader.stopLoading();
|
||||
isLoading.value = false;
|
||||
|
||||
// Redirect to home screen
|
||||
AuthenticationRepository.instance.screenRedirect();
|
||||
} catch (e) {
|
||||
// Remove loading
|
||||
// TFullScreenLoader.stopLoading();
|
||||
isLoading.value = false;
|
||||
|
||||
TLoaders.errorSnackBar(title: 'Oh snap', message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to sign up screen
|
||||
void goToSignUp() {
|
||||
Get.toNamed(AppRoutes.signUp);
|
||||
}
|
||||
|
||||
// Navigate to forgot password screen
|
||||
void goToForgotPassword() {
|
||||
Get.toNamed(AppRoutes.forgotPassword);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:sigap/src/cores/repositories/auth/auth_repositories.dart';
|
||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||
import 'package:sigap/src/utils/popups/full_screen_loader.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
class SignUpController extends GetxController {
|
||||
static SignUpController get instance => Get.find();
|
||||
|
||||
// Variable
|
||||
final storage = GetStorage();
|
||||
|
||||
final hidePassword = true.obs;
|
||||
final hideConfirmPassword = true.obs;
|
||||
final privacyPolicy = false.obs;
|
||||
final isOfficer = false.obs; // Add flag for officer registration
|
||||
|
||||
final email = TextEditingController();
|
||||
final firstName = TextEditingController();
|
||||
final lastName = TextEditingController();
|
||||
final username = TextEditingController();
|
||||
|
||||
final phoneNumber = TextEditingController();
|
||||
final password = TextEditingController();
|
||||
final confirmPassword = TextEditingController();
|
||||
|
||||
// Officer specific fields
|
||||
final nrp = TextEditingController();
|
||||
final rank = TextEditingController();
|
||||
final position = TextEditingController();
|
||||
final unitId =
|
||||
TextEditingController(); // This should be selected from a dropdown
|
||||
|
||||
// State variables
|
||||
final emailError = ''.obs;
|
||||
final firstNameError = ''.obs;
|
||||
final lastNameError = ''.obs;
|
||||
final usernameError = ''.obs;
|
||||
final phoneNumberError = ''.obs;
|
||||
final passwordError = ''.obs;
|
||||
final confirmPasswordError = ''.obs;
|
||||
|
||||
final isPasswordVisible = false.obs;
|
||||
final isConfirmPasswordVisible = false.obs;
|
||||
final isPrivacyPolicyAccepted = false.obs;
|
||||
|
||||
final isOfficerMode = false.obs;
|
||||
final isOfficerError = ''.obs;
|
||||
final isUnitIdError = ''.obs;
|
||||
final isNrpError = ''.obs;
|
||||
final isRankError = ''.obs;
|
||||
final isPositionError = ''.obs;
|
||||
|
||||
final isLoading = false.obs;
|
||||
|
||||
GlobalKey<FormState> signupFormKey = GlobalKey<FormState>();
|
||||
|
||||
// Sign Up Function
|
||||
void signUp() async {
|
||||
Logger().i('SignUp process started');
|
||||
try {
|
||||
// Start loading
|
||||
Logger().i('Opening loading dialog');
|
||||
TFullScreenLoader.openLoadingDialog(
|
||||
'Processing your information....',
|
||||
TImages.amongUsLoading,
|
||||
);
|
||||
|
||||
// Check connection
|
||||
Logger().i('Checking network connection');
|
||||
final isConnected = await NetworkManager.instance.isConnected();
|
||||
if (!isConnected) {
|
||||
Logger().w('No internet connection');
|
||||
TFullScreenLoader.stopLoading();
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'No Internet Connection',
|
||||
message: 'Please check your internet connection and try again.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Form validation
|
||||
Logger().i('Validating form');
|
||||
if (!signupFormKey.currentState!.validate()) {
|
||||
Logger().w('Form validation failed');
|
||||
TFullScreenLoader.stopLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user agreed to the terms and conditions
|
||||
Logger().i('Checking privacy policy acceptance');
|
||||
if (!privacyPolicy.value) {
|
||||
Logger().w('Privacy policy not accepted');
|
||||
TFullScreenLoader.stopLoading();
|
||||
|
||||
TLoaders.warningSnackBar(
|
||||
title: 'Accept Privacy Policy',
|
||||
message:
|
||||
'In order to create account, you must have to read and accept the privacy policy & terms of use.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare user metadata based on user type (officer or viewer)
|
||||
Map<String, dynamic> userMetadata = {
|
||||
'first_name': firstName.text.trim(),
|
||||
'last_name': lastName.text.trim(),
|
||||
'phone': phoneNumber.text.trim(),
|
||||
'is_officer': isOfficer.value,
|
||||
};
|
||||
|
||||
// Add officer-specific data if registering as officer
|
||||
if (isOfficer.value) {
|
||||
if (unitId.text.isEmpty) {
|
||||
Logger().w('Unit ID is required for officer registration');
|
||||
TFullScreenLoader.stopLoading();
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Missing Information',
|
||||
message: 'Unit ID is required for officer registration.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
userMetadata['officer_data'] = {
|
||||
'unit_id': unitId.text.trim(),
|
||||
'nrp': nrp.text.trim(),
|
||||
'name': "${firstName.text.trim()} ${lastName.text.trim()}",
|
||||
'rank': rank.text.trim(),
|
||||
'position': position.text.trim(),
|
||||
'phone': phoneNumber.text.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// Register user with Supabase Auth
|
||||
Logger().i('Registering user with Supabase Auth');
|
||||
final authResponse = await AuthenticationRepository.instance
|
||||
.signUpWithCredential(
|
||||
email.text.trim(),
|
||||
password.text.trim(),
|
||||
userMetadata: userMetadata,
|
||||
);
|
||||
|
||||
// Store email for verification screen
|
||||
storage.write('CURRENT_USER_EMAIL', email.text.trim());
|
||||
|
||||
// Remove loading
|
||||
Logger().i('Stopping loading dialog');
|
||||
TFullScreenLoader.stopLoading();
|
||||
|
||||
// Show success message
|
||||
Logger().i('Showing success message');
|
||||
TLoaders.successSnackBar(
|
||||
title: 'Congratulations',
|
||||
message: 'Your account has been created! Verify email to continue.',
|
||||
);
|
||||
|
||||
// Move to verification screen
|
||||
Logger().i('Navigating to VerifyEmailScreen');
|
||||
// Get.to(() => VerifyEmailScreen(email: email.text.trim()));
|
||||
} catch (e) {
|
||||
// Remove loading
|
||||
Logger().e('Error occurred: $e');
|
||||
TFullScreenLoader.stopLoading();
|
||||
|
||||
// Show error to the user
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Registration Failed',
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle officer registration mode
|
||||
void toggleOfficerMode(bool value) {
|
||||
isOfficer.value = value;
|
||||
update();
|
||||
}
|
||||
|
||||
// Navigate to sign in screen
|
||||
void goToSignIn() {
|
||||
Get.back();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,418 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
||||
class StepFormController extends GetxController {
|
||||
static StepFormController get to => Get.find();
|
||||
|
||||
// Role information
|
||||
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
|
||||
|
||||
// Current step index
|
||||
final RxInt currentStep = 0.obs;
|
||||
|
||||
// Form keys for each step (dynamic based on role)
|
||||
late List<GlobalKey<FormState>> stepFormKeys;
|
||||
|
||||
// Common information (for all roles)
|
||||
final nameController = TextEditingController();
|
||||
final phoneController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
|
||||
// Viewer-specific fields
|
||||
final emergencyNameController = TextEditingController();
|
||||
final emergencyPhoneController = TextEditingController();
|
||||
final relationshipController = TextEditingController();
|
||||
|
||||
// Officer-specific fields
|
||||
final nrpController = TextEditingController();
|
||||
final rankController = TextEditingController();
|
||||
final positionController = TextEditingController();
|
||||
final unitIdController = TextEditingController();
|
||||
|
||||
// Error states - Common
|
||||
final RxString nameError = ''.obs;
|
||||
final RxString phoneError = ''.obs;
|
||||
final RxString addressError = ''.obs;
|
||||
|
||||
// Error states - Viewer
|
||||
final RxString emergencyNameError = ''.obs;
|
||||
final RxString emergencyPhoneError = ''.obs;
|
||||
final RxString relationshipError = ''.obs;
|
||||
|
||||
// Error states - Officer
|
||||
final RxString nrpError = ''.obs;
|
||||
final RxString rankError = ''.obs;
|
||||
final RxString positionError = ''.obs;
|
||||
final RxString unitIdError = ''.obs;
|
||||
|
||||
// Loading state
|
||||
final RxBool isLoading = false.obs;
|
||||
|
||||
// Available units for officer role
|
||||
final RxList<Map<String, dynamic>> availableUnits =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Get role from arguments
|
||||
final arguments = Get.arguments;
|
||||
if (arguments != null && arguments['role'] != null) {
|
||||
selectedRole.value = arguments['role'] as RoleModel;
|
||||
_initializeBasedOnRole();
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'No role selected. Please go back and select a role.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedRole.value?.isOfficer == true) {
|
||||
_fetchAvailableUnits();
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeBasedOnRole() {
|
||||
if (selectedRole.value?.isOfficer == true) {
|
||||
stepFormKeys = List.generate(3, (_) => GlobalKey<FormState>());
|
||||
} else {
|
||||
// Viewer role or default
|
||||
stepFormKeys = List.generate(2, (_) => GlobalKey<FormState>());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchAvailableUnits() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
// Here we would fetch units from repository
|
||||
// For now we'll use dummy data
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
availableUnits.value = [
|
||||
{"id": "unit1", "name": "Polres Jember"},
|
||||
{"id": "unit2", "name": "Polsek Sumbersari"},
|
||||
{"id": "unit3", "name": "Polsek Kaliwates"},
|
||||
];
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Failed to load units: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// Dispose all controllers
|
||||
nameController.dispose();
|
||||
phoneController.dispose();
|
||||
addressController.dispose();
|
||||
|
||||
emergencyNameController.dispose();
|
||||
emergencyPhoneController.dispose();
|
||||
relationshipController.dispose();
|
||||
|
||||
nrpController.dispose();
|
||||
rankController.dispose();
|
||||
positionController.dispose();
|
||||
unitIdController.dispose();
|
||||
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Validate current step
|
||||
bool validateCurrentStep() {
|
||||
clearErrors(); // Clear previous errors
|
||||
|
||||
final currentFormKey = stepFormKeys[currentStep.value];
|
||||
if (currentFormKey.currentState?.validate() ?? false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If validation failed using the form's built-in validation,
|
||||
// we may want to perform additional validation and set error messages
|
||||
if (currentStep.value == 0) {
|
||||
validatePersonalInfo();
|
||||
} else if (currentStep.value == 1) {
|
||||
if (selectedRole.value?.isOfficer == true) {
|
||||
validateOfficerInfo();
|
||||
} else {
|
||||
validateEmergencyContact();
|
||||
}
|
||||
} else if (currentStep.value == 2 &&
|
||||
selectedRole.value?.isOfficer == true) {
|
||||
validateOfficerAdditionalInfo();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void clearErrors() {
|
||||
// Clear common errors
|
||||
nameError.value = '';
|
||||
phoneError.value = '';
|
||||
addressError.value = '';
|
||||
|
||||
// Clear viewer-specific errors
|
||||
emergencyNameError.value = '';
|
||||
emergencyPhoneError.value = '';
|
||||
relationshipError.value = '';
|
||||
|
||||
// Clear officer-specific errors
|
||||
nrpError.value = '';
|
||||
rankError.value = '';
|
||||
positionError.value = '';
|
||||
unitIdError.value = '';
|
||||
}
|
||||
|
||||
bool validatePersonalInfo() {
|
||||
bool isValid = true;
|
||||
|
||||
final nameValidation = TValidators.validateUserInput(
|
||||
'Full name',
|
||||
nameController.text,
|
||||
100,
|
||||
);
|
||||
if (nameValidation != null) {
|
||||
nameError.value = nameValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
final phoneValidation = TValidators.validatePhoneNumber(
|
||||
phoneController.text,
|
||||
);
|
||||
if (phoneValidation != null) {
|
||||
phoneError.value = phoneValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
final addressValidation = TValidators.validateUserInput(
|
||||
'Address',
|
||||
addressController.text,
|
||||
255,
|
||||
);
|
||||
if (addressValidation != null) {
|
||||
addressError.value = addressValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
bool validateEmergencyContact() {
|
||||
bool isValid = true;
|
||||
|
||||
final nameValidation = TValidators.validateUserInput(
|
||||
'Emergency contact name',
|
||||
emergencyNameController.text,
|
||||
100,
|
||||
);
|
||||
if (nameValidation != null) {
|
||||
emergencyNameError.value = nameValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
final phoneValidation = TValidators.validatePhoneNumber(
|
||||
emergencyPhoneController.text,
|
||||
);
|
||||
if (phoneValidation != null) {
|
||||
emergencyPhoneError.value = phoneValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
final relationshipValidation = TValidators.validateUserInput(
|
||||
'Relationship',
|
||||
relationshipController.text,
|
||||
50,
|
||||
);
|
||||
if (relationshipValidation != null) {
|
||||
relationshipError.value = relationshipValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
bool validateOfficerInfo() {
|
||||
bool isValid = true;
|
||||
|
||||
final nrpValidation = TValidators.validateUserInput(
|
||||
'NRP',
|
||||
nrpController.text,
|
||||
50,
|
||||
);
|
||||
if (nrpValidation != null) {
|
||||
nrpError.value = nrpValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
final rankValidation = TValidators.validateUserInput(
|
||||
'Rank',
|
||||
rankController.text,
|
||||
50,
|
||||
);
|
||||
if (rankValidation != null) {
|
||||
rankError.value = rankValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
bool validateOfficerAdditionalInfo() {
|
||||
bool isValid = true;
|
||||
|
||||
final positionValidation = TValidators.validateUserInput(
|
||||
'Position',
|
||||
positionController.text,
|
||||
100,
|
||||
);
|
||||
if (positionValidation != null) {
|
||||
positionError.value = positionValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (unitIdController.text.isEmpty) {
|
||||
unitIdError.value = 'Please select a unit';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// Go to next step
|
||||
void nextStep() {
|
||||
if (!validateCurrentStep()) return;
|
||||
|
||||
if (currentStep.value < stepFormKeys.length - 1) {
|
||||
currentStep.value++;
|
||||
} else {
|
||||
submitForm();
|
||||
}
|
||||
}
|
||||
|
||||
// Go to previous step
|
||||
void previousStep() {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--;
|
||||
}
|
||||
}
|
||||
|
||||
// Go to specific step
|
||||
void goToStep(int step) {
|
||||
if (step >= 0 && step < stepFormKeys.length) {
|
||||
// Only allow going to a step if all previous steps are valid
|
||||
bool canProceed = true;
|
||||
for (int i = 0; i < step; i++) {
|
||||
clearErrors(); // Clear errors before validating
|
||||
final formKey = stepFormKeys[i];
|
||||
if (!(formKey.currentState?.validate() ?? false)) {
|
||||
canProceed = false;
|
||||
currentStep.value = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (canProceed) {
|
||||
currentStep.value = step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the complete form
|
||||
Future<void> submitForm() async {
|
||||
// Validate all steps
|
||||
bool isValid = true;
|
||||
for (int i = 0; i < stepFormKeys.length; i++) {
|
||||
final formKey = stepFormKeys[i];
|
||||
if (!(formKey.currentState?.validate() ?? false)) {
|
||||
isValid = false;
|
||||
currentStep.value = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Prepare data based on role
|
||||
final Map<String, dynamic> userData = {
|
||||
'name': nameController.text,
|
||||
'phone': phoneController.text,
|
||||
'address': addressController.text,
|
||||
};
|
||||
|
||||
if (selectedRole.value?.isOfficer == true) {
|
||||
// Officer role
|
||||
final officerData = {
|
||||
'nrp': nrpController.text,
|
||||
'rank': rankController.text,
|
||||
'position': positionController.text,
|
||||
'unit_id': unitIdController.text,
|
||||
};
|
||||
|
||||
// Update auth user with officer flag and data
|
||||
await SupabaseService.instance.updateUserMetadata({
|
||||
'is_officer': true,
|
||||
'officer_data': {
|
||||
...officerData,
|
||||
'name': userData['name'],
|
||||
'phone': userData['phone'],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Viewer role
|
||||
final emergencyContact = {
|
||||
'name': emergencyNameController.text,
|
||||
'phone': emergencyPhoneController.text,
|
||||
'relationship': relationshipController.text,
|
||||
};
|
||||
|
||||
// Update auth user with viewer role
|
||||
await SupabaseService.instance.updateUserMetadata({
|
||||
'is_officer': false,
|
||||
'emergency_contact': emergencyContact,
|
||||
'address': userData['address'],
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate to success screen
|
||||
Get.toNamed(
|
||||
AppRoutes.stateScreen,
|
||||
arguments: {
|
||||
'type': 'success',
|
||||
'title': 'Profile Completed',
|
||||
'message': 'Your profile information has been successfully saved.',
|
||||
'buttonText': 'Continue',
|
||||
'onButtonPressed': () => Get.offAllNamed(AppRoutes.explore),
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
Get.toNamed(
|
||||
AppRoutes.stateScreen,
|
||||
arguments: {
|
||||
'type': 'error',
|
||||
'title': 'Submission Failed',
|
||||
'message': 'There was an error saving your profile: ${e.toString()}',
|
||||
'buttonText': 'Try Again',
|
||||
'onButtonPressed': () => Get.back(),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/controllers/email_verification_controller.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_header.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/otp_input_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class EmailVerificationScreen extends StatelessWidget {
|
||||
const EmailVerificationScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<EmailVerificationController>();
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: TColors.textPrimary),
|
||||
onPressed: controller.goToSignIn,
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Obx(
|
||||
() =>
|
||||
controller.isVerified.value
|
||||
? _buildVerifiedView()
|
||||
: _buildVerificationForm(controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerificationForm(EmailVerificationController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Header
|
||||
const AuthHeader(
|
||||
title: 'Email Verification',
|
||||
subtitle: 'Enter the 4-digit code sent to your email',
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// OTP input fields
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(
|
||||
4,
|
||||
(index) => OtpInputField(
|
||||
controller: controller.otpControllers[index],
|
||||
focusNode: controller.focusNodes[index],
|
||||
autoFocus: index == 0,
|
||||
onChanged: (value) => controller.onOtpChanged(value, index),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Error message
|
||||
Obx(
|
||||
() =>
|
||||
controller.verificationError.value.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
controller.verificationError.value,
|
||||
style: TextStyle(color: TColors.error, fontSize: 14),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Verify button
|
||||
Obx(
|
||||
() => AuthButton(
|
||||
text: 'Verify',
|
||||
onPressed: controller.verifyOtp,
|
||||
isLoading: controller.isLoading.value,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Resend code
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Didn\'t receive the code?',
|
||||
style: TextStyle(color: TColors.textSecondary),
|
||||
),
|
||||
Obx(
|
||||
() => TextButton(
|
||||
onPressed:
|
||||
controller.isResendEnabled.value
|
||||
? controller.resendCode
|
||||
: null,
|
||||
child: Text(
|
||||
controller.isResendEnabled.value
|
||||
? 'Resend'
|
||||
: 'Resend in ${controller.resendCountdown.value}s',
|
||||
style: TextStyle(
|
||||
color:
|
||||
controller.isResendEnabled.value
|
||||
? TColors.primary
|
||||
: TColors.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerifiedView() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Success icon
|
||||
Icon(Icons.check_circle_outline, size: 80, color: TColors.success),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Success message
|
||||
Text(
|
||||
'Email Verified',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
'Your email has been successfully verified. Please wait while we redirect you.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16, color: TColors.textSecondary),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Loading indicator
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(TColors.primary),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/controllers/forgot_password_controller.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_header.dart';
|
||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class ForgotPasswordScreen extends StatelessWidget {
|
||||
const ForgotPasswordScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<ForgotPasswordController>();
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: TColors.textPrimary),
|
||||
onPressed: controller.goBack,
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Obx(
|
||||
() => Form(
|
||||
key: controller.formKey,
|
||||
child:
|
||||
controller.isEmailSent.value
|
||||
? _buildSuccessView(controller)
|
||||
: _buildFormView(controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormView(ForgotPasswordController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
const AuthHeader(
|
||||
title: 'Forgot Password',
|
||||
subtitle: 'Enter your email to reset your password',
|
||||
),
|
||||
|
||||
// Email field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Email',
|
||||
controller: controller.emailController,
|
||||
validator: controller.validateEmail,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
errorText: controller.emailError.value,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Reset password button
|
||||
Obx(
|
||||
() => AuthButton(
|
||||
text: 'Reset Password',
|
||||
onPressed: controller.resetPassword,
|
||||
isLoading: controller.isLoading.value,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessView(ForgotPasswordController controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Success icon
|
||||
Icon(Icons.check_circle_outline, size: 80, color: TColors.success),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Success message
|
||||
Text(
|
||||
'Email Sent',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
'We have sent a password recovery instructions to your email.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16, color: TColors.textSecondary),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Back to sign in button
|
||||
AuthButton(text: 'Back to Sign In', onPressed: controller.goBack),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/controllers/signin_controller.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_divider.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_header.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/password_field.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/social_button.dart';
|
||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
||||
class SignInScreen extends StatelessWidget {
|
||||
const SignInScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<SignInController>();
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: controller.signinFormKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Header
|
||||
const AuthHeader(
|
||||
title: 'Welcome Back',
|
||||
subtitle: 'Sign in to your account to continue',
|
||||
),
|
||||
|
||||
// Email field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Email',
|
||||
controller: controller.email,
|
||||
validator: TValidators.validateEmail,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
errorText: controller.emailError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Password field
|
||||
Obx(
|
||||
() => PasswordField(
|
||||
label: 'Password',
|
||||
controller: controller.password,
|
||||
validator: TValidators.validatePassword,
|
||||
isVisible: controller.isPasswordVisible,
|
||||
errorText: controller.passwordError.value,
|
||||
onToggleVisibility: controller.togglePasswordVisibility,
|
||||
),
|
||||
),
|
||||
|
||||
// Forgot password
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: controller.goToForgotPassword,
|
||||
child: Text(
|
||||
'Forgot Password?',
|
||||
style: TextStyle(
|
||||
color: TColors.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Sign in button
|
||||
Obx(
|
||||
() => AuthButton(
|
||||
text: 'Sign In',
|
||||
onPressed: controller.credentialsSignIn,
|
||||
isLoading: controller.isLoading.value,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Or divider
|
||||
const AuthDivider(text: 'OR'),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Social sign in buttons
|
||||
SocialButton(
|
||||
text: 'Continue with Google',
|
||||
icon: Icons.g_mobiledata,
|
||||
onPressed: () => controller.googleSignIn(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Don't have an account
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Don\'t have an account?',
|
||||
style: TextStyle(color: TColors.textSecondary),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: controller.goToSignUp,
|
||||
child: Text(
|
||||
'Sign Up',
|
||||
style: TextStyle(
|
||||
color: TColors.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/controllers/signup_controller.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_divider.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_header.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/password_field.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/social_button.dart';
|
||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class SignUpScreen extends StatelessWidget {
|
||||
const SignUpScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<SignUpController>();
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: TColors.textPrimary),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
const AuthHeader(
|
||||
title: 'Create Account',
|
||||
subtitle: 'Sign up to get started with the app',
|
||||
),
|
||||
|
||||
// Name field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Full Name',
|
||||
controller: controller.nameController,
|
||||
validator: controller.validateName,
|
||||
errorText: controller.nameError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Email field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Email',
|
||||
controller: controller.emailController,
|
||||
validator: controller.validateEmail,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
errorText: controller.emailError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Password field
|
||||
Obx(
|
||||
() => PasswordField(
|
||||
label: 'Password',
|
||||
controller: controller.passwordController,
|
||||
validator: controller.validatePassword,
|
||||
isVisible: controller.isPasswordVisible,
|
||||
errorText: controller.passwordError.value,
|
||||
onToggleVisibility: controller.togglePasswordVisibility,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Confirm password field
|
||||
Obx(
|
||||
() => PasswordField(
|
||||
label: 'Confirm Password',
|
||||
controller: controller.confirmPasswordController,
|
||||
validator: controller.validateConfirmPassword,
|
||||
isVisible: controller.isConfirmPasswordVisible,
|
||||
errorText: controller.confirmPasswordError.value,
|
||||
onToggleVisibility:
|
||||
controller.toggleConfirmPasswordVisibility,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Sign up button
|
||||
Obx(
|
||||
() => AuthButton(
|
||||
text: 'Sign Up',
|
||||
onPressed: controller.signUp,
|
||||
isLoading: controller.isLoading.value,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Or divider
|
||||
const AuthDivider(text: 'OR'),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Social sign up button
|
||||
SocialButton(
|
||||
text: 'Continue with Google',
|
||||
icon: Icons.g_mobiledata,
|
||||
onPressed: () {
|
||||
// TODO: Implement Google sign up
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Already have an account
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Already have an account?',
|
||||
style: TextStyle(color: TColors.textSecondary),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: controller.goToSignIn,
|
||||
child: Text(
|
||||
'Sign In',
|
||||
style: TextStyle(
|
||||
color: TColors.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,402 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/controllers/step_form_controller.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/shared/widgets/dropdown/custom_dropdown.dart';
|
||||
import 'package:sigap/src/shared/widgets/indicators/step_indicator.dart';
|
||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
||||
class StepFormScreen extends StatelessWidget {
|
||||
const StepFormScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<StepFormController>();
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'Complete Your ${controller.selectedRole.value?.name ?? ""} Profile',
|
||||
style: TextStyle(
|
||||
color: TColors.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: TColors.textPrimary),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Obx(() {
|
||||
// Show loading while initializing
|
||||
if (controller.selectedRole.value == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Step indicator
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Obx(
|
||||
() => StepIndicator(
|
||||
currentStep: controller.currentStep.value,
|
||||
totalSteps: controller.stepFormKeys.length,
|
||||
stepTitles: _getStepTitles(controller.selectedRole.value!),
|
||||
onStepTapped: controller.goToStep,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Step content
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Obx(() {
|
||||
return _buildStepContent(controller);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Navigation buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Back button
|
||||
Obx(
|
||||
() =>
|
||||
controller.currentStep.value > 0
|
||||
? Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: AuthButton(
|
||||
text: 'Previous',
|
||||
onPressed: controller.previousStep,
|
||||
isPrimary: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
|
||||
// Next/Submit button
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: controller.currentStep.value > 0 ? 8.0 : 0.0,
|
||||
),
|
||||
child: Obx(
|
||||
() => AuthButton(
|
||||
text:
|
||||
controller.currentStep.value ==
|
||||
controller.stepFormKeys.length - 1
|
||||
? 'Submit'
|
||||
: 'Next',
|
||||
onPressed: controller.nextStep,
|
||||
isLoading: controller.isLoading.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _getStepTitles(RoleModel role) {
|
||||
if (role.isOfficer) {
|
||||
return ['Personal', 'Officer Info', 'Unit Info'];
|
||||
} else {
|
||||
return ['Personal', 'Emergency'];
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStepContent(StepFormController controller) {
|
||||
final isOfficer = controller.selectedRole.value?.isOfficer ?? false;
|
||||
|
||||
switch (controller.currentStep.value) {
|
||||
case 0:
|
||||
return _buildPersonalInfoStep(controller);
|
||||
case 1:
|
||||
return isOfficer
|
||||
? _buildOfficerInfoStep(controller)
|
||||
: _buildEmergencyContactStep(controller);
|
||||
case 2:
|
||||
// This step only exists for officers
|
||||
if (isOfficer) {
|
||||
return _buildOfficerAdditionalInfoStep(controller);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPersonalInfoStep(StepFormController controller) {
|
||||
return Form(
|
||||
key: controller.stepFormKeys[0],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Personal Information',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Please provide your personal details',
|
||||
style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Name field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Full Name',
|
||||
controller: controller.nameController,
|
||||
validator:
|
||||
(value) =>
|
||||
TValidators.validateUserInput('Full name', value, 100),
|
||||
errorText: controller.nameError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Phone field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Phone Number',
|
||||
controller: controller.phoneController,
|
||||
validator: TValidators.validatePhoneNumber,
|
||||
errorText: controller.phoneError.value,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Address field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Address',
|
||||
controller: controller.addressController,
|
||||
validator:
|
||||
(value) =>
|
||||
TValidators.validateUserInput('Address', value, 255),
|
||||
errorText: controller.addressError.value,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmergencyContactStep(StepFormController controller) {
|
||||
return Form(
|
||||
key: controller.stepFormKeys[1],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Emergency Contact',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Please provide emergency contact details',
|
||||
style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Emergency contact name field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Contact Name',
|
||||
controller: controller.emergencyNameController,
|
||||
validator:
|
||||
(value) => TValidators.validateUserInput(
|
||||
'Emergency contact name',
|
||||
value,
|
||||
100,
|
||||
),
|
||||
errorText: controller.emergencyNameError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Emergency contact phone field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Contact Phone',
|
||||
controller: controller.emergencyPhoneController,
|
||||
validator: TValidators.validatePhoneNumber,
|
||||
errorText: controller.emergencyPhoneError.value,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Relationship field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Relationship',
|
||||
controller: controller.relationshipController,
|
||||
validator:
|
||||
(value) =>
|
||||
TValidators.validateUserInput('Relationship', value, 50),
|
||||
errorText: controller.relationshipError.value,
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOfficerInfoStep(StepFormController controller) {
|
||||
return Form(
|
||||
key: controller.stepFormKeys[1],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Officer Information',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Please provide your officer details',
|
||||
style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// NRP field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'NRP',
|
||||
controller: controller.nrpController,
|
||||
validator: TValidators.validateNRP,
|
||||
errorText: controller.nrpError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Rank field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Rank',
|
||||
controller: controller.rankController,
|
||||
validator: TValidators.validateRank,
|
||||
errorText: controller.rankError.value,
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOfficerAdditionalInfoStep(StepFormController controller) {
|
||||
return Form(
|
||||
key: controller.stepFormKeys[2],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Unit Information',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Please provide your unit details',
|
||||
style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Position field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Position',
|
||||
controller: controller.positionController,
|
||||
validator: TValidators.validatePosition,
|
||||
errorText: controller.positionError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
||||
// Unit Dropdown
|
||||
Obx(
|
||||
() => CustomDropdown(
|
||||
label: 'Unit',
|
||||
items:
|
||||
controller.availableUnits
|
||||
.map(
|
||||
(unit) => DropdownMenuItem(
|
||||
value: unit['id'],
|
||||
child: Text(unit['name']),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value:
|
||||
controller.unitIdController.text.isEmpty
|
||||
? null
|
||||
: controller.unitIdController.text,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
controller.unitIdController.text = value.toString();
|
||||
}
|
||||
},
|
||||
validator: TValidators.validateUnitId,
|
||||
errorText: controller.unitIdError.value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
|
||||
class AuthButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback onPressed;
|
||||
final bool isLoading;
|
||||
final bool isPrimary;
|
||||
|
||||
const AuthButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onPressed,
|
||||
this.isLoading = false,
|
||||
this.isPrimary = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child:
|
||||
isPrimary
|
||||
? ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TColors.primary,
|
||||
foregroundColor: TColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 0,
|
||||
disabledBackgroundColor: TColors.primary.withOpacity(0.6),
|
||||
),
|
||||
child:
|
||||
isLoading
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
TColors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
)
|
||||
: OutlinedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: TColors.primary,
|
||||
side: BorderSide(color: TColors.primary),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
disabledForegroundColor: TColors.primary.withOpacity(0.6),
|
||||
),
|
||||
child:
|
||||
isLoading
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
TColors.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class AuthDivider extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const AuthDivider({super.key, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: TColors.borderPrimary, thickness: 1)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(color: TColors.textSecondary, fontSize: 14),
|
||||
),
|
||||
),
|
||||
Expanded(child: Divider(color: TColors.borderPrimary, thickness: 1)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class AuthHeader extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
||||
const AuthHeader({super.key, required this.title, required this.subtitle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(fontSize: 16, color: TColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class OtpInputField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final FocusNode focusNode;
|
||||
final Function(String) onChanged;
|
||||
final bool autoFocus;
|
||||
|
||||
const OtpInputField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.focusNode,
|
||||
required this.onChanged,
|
||||
this.autoFocus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
autofocus: autoFocus,
|
||||
onChanged: onChanged,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
inputFormatters: [
|
||||
LengthLimitingTextInputFormatter(1),
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
filled: true,
|
||||
fillColor: TColors.lightContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: TColors.primary, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
import '../../../../shared/widgets/text/custom_text_field.dart';
|
||||
|
||||
class PasswordField extends StatelessWidget {
|
||||
final String label;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?)? validator;
|
||||
final RxBool isVisible;
|
||||
final String? errorText;
|
||||
final TextInputAction textInputAction;
|
||||
final Function()? onToggleVisibility;
|
||||
|
||||
const PasswordField({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.controller,
|
||||
this.validator,
|
||||
required this.isVisible,
|
||||
this.errorText,
|
||||
this.textInputAction = TextInputAction.done,
|
||||
this.onToggleVisibility,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => CustomTextField(
|
||||
label: label,
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
obscureText: !isVisible.value,
|
||||
errorText: errorText,
|
||||
textInputAction: textInputAction,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
isVisible.value ? Icons.visibility_off : Icons.visibility,
|
||||
color: TColors.textSecondary,
|
||||
),
|
||||
onPressed: onToggleVisibility,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class SocialButton extends StatelessWidget {
|
||||
final String text;
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const SocialButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, color: TColors.textPrimary),
|
||||
label: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: TColors.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: TColors.textPrimary,
|
||||
side: BorderSide(color: TColors.borderPrimary),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/repositories/roles/roles_repository.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
class ChooseRoleController extends GetxController {
|
||||
static ChooseRoleController get instance => Get.find();
|
||||
|
||||
// Repositories
|
||||
final _rolesRepository = Get.find<RolesRepository>();
|
||||
|
||||
// List of available roles
|
||||
final RxList<RoleModel> roles = <RoleModel>[].obs;
|
||||
|
||||
// Selected role
|
||||
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
|
||||
|
||||
// Loading state
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxBool isOfficer = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
// Fetch available roles from repository
|
||||
Future<void> fetchRoles() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Get all roles from repository
|
||||
final allRoles = await _rolesRepository.getAllRoles();
|
||||
|
||||
// Filter roles to only include viewer and officer
|
||||
roles.value =
|
||||
allRoles
|
||||
.where(
|
||||
(role) =>
|
||||
role.name.toLowerCase() == 'viewer' ||
|
||||
role.name.toLowerCase() == 'officer',
|
||||
)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Error',
|
||||
message: 'Failed to load available roles. Please try again.',
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Select a role
|
||||
void selectRole(RoleModel role) {
|
||||
selectedRole.value = role;
|
||||
}
|
||||
|
||||
// Continue with selected role
|
||||
Future<void> continueWithRole() async {
|
||||
if (selectedRole.value == null) {
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Error',
|
||||
message: 'Please select a role to continue.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Check if the selected role is officer
|
||||
if (selectedRole.value!.name.toLowerCase() == 'officer') {
|
||||
isOfficer.value = true;
|
||||
} else {
|
||||
isOfficer.value = false;
|
||||
}
|
||||
|
||||
// Navigate to sign up screen with selected role
|
||||
Get.offNamed(
|
||||
AppRoutes.signUp,
|
||||
arguments: {'role': selectedRole.value, 'isOfficer': isOfficer.value},
|
||||
);
|
||||
} catch (e) {
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Error',
|
||||
message:
|
||||
'An error occurred while selecting the role. Please try again.',
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/onboarding/datas/onboarding_data.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
|
||||
class OnboardingController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
// Singleton instance
|
||||
static OnboardingController get instance => Get.find();
|
||||
|
||||
// Observable variables
|
||||
final RxInt currentIndex = 0.obs;
|
||||
final PageController pageController = PageController(initialPage: 0);
|
||||
|
||||
// Animation controllers
|
||||
late AnimationController animationController;
|
||||
late Animation<double> fadeAnimation;
|
||||
late Animation<Offset> slideAnimation;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Initialize animation controllers
|
||||
animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: animationController, curve: Curves.easeIn),
|
||||
);
|
||||
|
||||
slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.2),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(parent: animationController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
pageController.dispose();
|
||||
animationController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Method to handle page changes
|
||||
void onPageChanged(int index) {
|
||||
currentIndex.value = index;
|
||||
animationController.reset();
|
||||
animationController.forward();
|
||||
}
|
||||
|
||||
// Method to go to next page
|
||||
void nextPage() {
|
||||
if (currentIndex.value == contents.length - 1) {
|
||||
navigateToWelcomeScreen();
|
||||
} else {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to skip to welcome screen
|
||||
void skipToWelcomeScreen() {
|
||||
navigateToWelcomeScreen();
|
||||
}
|
||||
|
||||
// Method to navigate to welcome screen
|
||||
void navigateToWelcomeScreen() {
|
||||
Get.offAllNamed(AppRoutes.welcome);
|
||||
}
|
||||
|
||||
void getStarted() {
|
||||
Get.offAllNamed(AppRoutes.chooseRole);
|
||||
}
|
||||
|
||||
void goToSignIn() {
|
||||
Get.offAllNamed(AppRoutes.signIn);
|
||||
}
|
||||
|
||||
void goToSignUp() {
|
||||
Get.offAllNamed(AppRoutes.signUp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:sigap/src/features/onboarding/models/onboarding_model.dart';
|
||||
|
||||
final List<OnboardingContent> contents = [
|
||||
OnboardingContent(
|
||||
title: 'Crime Mapping & Analysis',
|
||||
description:
|
||||
'Visualize crime levels across Jember Regency with advanced K-means clustering based on population density, unemployment, and crime incidents.',
|
||||
image: 'assets/images/onboarding1.png',
|
||||
),
|
||||
OnboardingContent(
|
||||
title: 'Emergency Panic Button',
|
||||
description:
|
||||
'One-click emergency alert with automatic geolocation transmission to connect directly with police dispatch in critical situations.',
|
||||
image: 'assets/images/onboarding2.png',
|
||||
),
|
||||
OnboardingContent(
|
||||
title: 'Data-Driven Insights',
|
||||
description:
|
||||
'Track crime trends from 2020 to 2024 with interactive maps and comprehensive reports for better decision making.',
|
||||
image: 'assets/images/onboarding3.png',
|
||||
),
|
||||
];
|
|
@ -0,0 +1,12 @@
|
|||
class OnboardingContent {
|
||||
final String title;
|
||||
final String description;
|
||||
final String image;
|
||||
|
||||
OnboardingContent({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.image,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/features/auth/screens/widgets/auth_header.dart';
|
||||
import 'package:sigap/src/features/onboarding/controllers/choose_role_controller.dart';
|
||||
import 'package:sigap/src/features/onboarding/screens/choose-role/widgets/role_card.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class ChooseRoleScreen extends StatelessWidget {
|
||||
const ChooseRoleScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<ChooseRoleController>();
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
'Choose Role',
|
||||
style: TextStyle(
|
||||
color: TColors.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
const AuthHeader(
|
||||
title: 'Select Your Role',
|
||||
subtitle: 'Choose the role that best describes your position',
|
||||
),
|
||||
|
||||
// Role list
|
||||
Expanded(
|
||||
child: Obx(
|
||||
() => ListView.builder(
|
||||
itemCount: controller.roles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final role = controller.roles[index];
|
||||
return Obx(
|
||||
() => RoleCard(
|
||||
role: role,
|
||||
isSelected:
|
||||
controller.selectedRole.value?.id == role.id,
|
||||
onTap: () => controller.selectRole(role),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Continue button
|
||||
Obx(
|
||||
() => AuthButton(
|
||||
text: 'Continue',
|
||||
onPressed: controller.continueWithRole,
|
||||
isLoading: controller.isLoading.value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class RoleCard extends StatelessWidget {
|
||||
final RoleModel role;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const RoleCard({
|
||||
super.key,
|
||||
required this.role,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? TColors.primary.withOpacity(0.1)
|
||||
: TColors.lightContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? TColors.primary : TColors.borderPrimary,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? TColors.primary : TColors.secondary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
role.iconData,
|
||||
color: isSelected ? TColors.white : TColors.primary,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
role.name,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
role.description!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: TColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isSelected ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: isSelected ? TColors.primary : TColors.textSecondary,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/onboarding/datas/onboarding_data.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
import '../../controllers/onboarding_controller.dart';
|
||||
|
||||
class OnboardingScreen extends StatelessWidget {
|
||||
const OnboardingScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<OnboardingController>();
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
// Get screen dimensions for responsive design
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isSmallScreen = size.height < 700;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Skip button
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextButton(
|
||||
onPressed: controller.skipToWelcomeScreen,
|
||||
child: Text(
|
||||
'Skip',
|
||||
style: TextStyle(
|
||||
color: TColors.textSecondary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Page view for slides
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: PageView.builder(
|
||||
controller: controller.pageController,
|
||||
itemCount: contents.length,
|
||||
onPageChanged: controller.onPageChanged,
|
||||
itemBuilder: (_, i) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Animated image
|
||||
Expanded(
|
||||
flex: isSmallScreen ? 3 : 4,
|
||||
child: FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: Image.asset(
|
||||
contents[i].image,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: isSmallScreen ? 16 : 32),
|
||||
|
||||
// Animated title
|
||||
FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: Text(
|
||||
contents[i].title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isSmallScreen ? 22 : 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: isSmallScreen ? 12 : 20),
|
||||
|
||||
// Animated description
|
||||
FadeTransition(
|
||||
opacity: controller.fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: controller.slideAnimation,
|
||||
child: Text(
|
||||
contents[i].description,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isSmallScreen ? 14 : 16,
|
||||
color: TColors.textSecondary,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Indicator and buttons
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
children: [
|
||||
// Dot indicators
|
||||
Obx(
|
||||
() => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
contents.length,
|
||||
(index) =>
|
||||
buildDot(index, controller.currentIndex.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Next or Get Started button
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: Obx(
|
||||
() => ElevatedButton(
|
||||
onPressed: controller.nextPage,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TColors.primary,
|
||||
foregroundColor: TColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Text(
|
||||
controller.currentIndex.value == contents.length - 1
|
||||
? 'Get Started'
|
||||
: 'Next',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Animated dot indicator
|
||||
Widget buildDot(int index, int currentIndex) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
height: 8,
|
||||
width: currentIndex == index ? 24 : 8,
|
||||
decoration: BoxDecoration(
|
||||
color: currentIndex == index ? TColors.primary : TColors.grey,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/onboarding/controllers/onboarding_controller.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class WelcomeScreen extends StatelessWidget {
|
||||
const WelcomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.find<OnboardingController>();
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isSmallScreen = size.height < 700;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
Hero(
|
||||
tag: 'app_logo',
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: TColors.primary.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.location_on,
|
||||
size: 50,
|
||||
color: TColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 24 : 40),
|
||||
Text(
|
||||
'Welcome to GIS Crime Cluster',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isSmallScreen ? 24 : 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 12 : 16),
|
||||
Text(
|
||||
'Visualize crime patterns and enhance safety across Jember Regency with our data-driven GIS application',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isSmallScreen ? 14 : 16,
|
||||
color: TColors.textSecondary,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: controller.getStarted,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TColors.primary,
|
||||
foregroundColor: TColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'Get Started',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? 24 : 32),
|
||||
Text(
|
||||
'By continuing, you agree to our Terms of Service and Privacy Policy',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12, color: TColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export 'users_model.dart';
|
||||
export 'officers_model.dart';
|
||||
export 'profile_model.dart';
|
||||
export 'roles_model.dart';
|
|
@ -0,0 +1,159 @@
|
|||
import 'package:sigap/src/features/personalization/models/roles_model.dart';
|
||||
|
||||
class OfficerModel {
|
||||
final String id;
|
||||
final String unitId;
|
||||
final String roleId;
|
||||
final String? patrolUnitId;
|
||||
final String nrp;
|
||||
final String name;
|
||||
final String? rank;
|
||||
final String? position;
|
||||
final String? phone;
|
||||
final String? email;
|
||||
final String? avatar;
|
||||
final DateTime? validUntil;
|
||||
final String? qrCode;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final RoleModel? role;
|
||||
|
||||
OfficerModel({
|
||||
required this.id,
|
||||
required this.unitId,
|
||||
required this.roleId,
|
||||
this.patrolUnitId,
|
||||
required this.nrp,
|
||||
required this.name,
|
||||
this.rank,
|
||||
this.position,
|
||||
this.phone,
|
||||
this.email,
|
||||
this.avatar,
|
||||
this.validUntil,
|
||||
this.qrCode,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.role,
|
||||
});
|
||||
|
||||
// Create an OfficerModel instance from a JSON object
|
||||
factory OfficerModel.fromJson(Map<String, dynamic> json) {
|
||||
return OfficerModel(
|
||||
id: json['id'] as String,
|
||||
unitId: json['unit_id'] as String,
|
||||
roleId: json['role_id'] as String,
|
||||
patrolUnitId: json['patrol_unit_id'] as String?,
|
||||
nrp: json['nrp'] as String,
|
||||
name: json['name'] as String,
|
||||
rank: json['rank'] as String?,
|
||||
position: json['position'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
email: json['email'] as String?,
|
||||
avatar: json['avatar'] as String?,
|
||||
validUntil:
|
||||
json['valid_until'] != null
|
||||
? DateTime.parse(json['valid_until'] as String)
|
||||
: null,
|
||||
qrCode: json['qr_code'] as String?,
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: null,
|
||||
role:
|
||||
json['roles'] != null
|
||||
? RoleModel.fromJson(json['roles'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert an OfficerModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'unit_id': unitId,
|
||||
'role_id': roleId,
|
||||
'patrol_unit_id': patrolUnitId,
|
||||
'nrp': nrp,
|
||||
'name': name,
|
||||
'rank': rank,
|
||||
'position': position,
|
||||
'phone': phone,
|
||||
'email': email,
|
||||
'avatar': avatar,
|
||||
'valid_until': validUntil?.toIso8601String(),
|
||||
'qr_code': qrCode,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
if (role != null) 'roles': role!.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the OfficerModel with updated fields
|
||||
OfficerModel copyWith({
|
||||
String? id,
|
||||
String? unitId,
|
||||
String? roleId,
|
||||
String? patrolUnitId,
|
||||
String? nrp,
|
||||
String? name,
|
||||
String? rank,
|
||||
String? position,
|
||||
String? phone,
|
||||
String? email,
|
||||
String? avatar,
|
||||
DateTime? validUntil,
|
||||
String? qrCode,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
RoleModel? role,
|
||||
}) {
|
||||
return OfficerModel(
|
||||
id: id ?? this.id,
|
||||
unitId: unitId ?? this.unitId,
|
||||
roleId: roleId ?? this.roleId,
|
||||
patrolUnitId: patrolUnitId ?? this.patrolUnitId,
|
||||
nrp: nrp ?? this.nrp,
|
||||
name: name ?? this.name,
|
||||
rank: rank ?? this.rank,
|
||||
position: position ?? this.position,
|
||||
phone: phone ?? this.phone,
|
||||
email: email ?? this.email,
|
||||
avatar: avatar ?? this.avatar,
|
||||
validUntil: validUntil ?? this.validUntil,
|
||||
qrCode: qrCode ?? this.qrCode,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
role: role ?? this.role,
|
||||
);
|
||||
}
|
||||
|
||||
// Create an OfficerModel from a User metadata
|
||||
factory OfficerModel.fromUserMetadata(
|
||||
String userId,
|
||||
Map<String, dynamic> metadata,
|
||||
) {
|
||||
final officerData = metadata['officer_data'] ?? {};
|
||||
|
||||
return OfficerModel(
|
||||
id: userId,
|
||||
unitId: officerData['unit_id'] ?? '',
|
||||
roleId: '', // This would need to be fetched or defined elsewhere
|
||||
nrp: officerData['nrp'] ?? '',
|
||||
name: officerData['name'] ?? '',
|
||||
rank: officerData['rank'],
|
||||
position: officerData['position'],
|
||||
phone: officerData['phone'],
|
||||
email: metadata['email'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'OfficerModel(id: $id, name: $name, nrp: $nrp)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
class ProfileModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String? avatar;
|
||||
final String? username;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? bio;
|
||||
final Map<String, dynamic>? address;
|
||||
final DateTime? birthDate;
|
||||
|
||||
ProfileModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
this.avatar,
|
||||
this.username,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.bio,
|
||||
this.address,
|
||||
this.birthDate,
|
||||
});
|
||||
|
||||
// Create a ProfileModel instance from a JSON object
|
||||
factory ProfileModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProfileModel(
|
||||
id: json['id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
avatar: json['avatar'] as String?,
|
||||
username: json['username'] as String?,
|
||||
firstName: json['first_name'] as String?,
|
||||
lastName: json['last_name'] as String?,
|
||||
bio: json['bio'] as String?,
|
||||
address:
|
||||
json['address'] != null
|
||||
? Map<String, dynamic>.from(json['address'] as Map)
|
||||
: null,
|
||||
birthDate:
|
||||
json['birth_date'] != null
|
||||
? DateTime.parse(json['birth_date'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a ProfileModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'user_id': userId,
|
||||
'avatar': avatar,
|
||||
'username': username,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'bio': bio,
|
||||
'address': address,
|
||||
'birth_date': birthDate?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the ProfileModel with updated fields
|
||||
ProfileModel copyWith({
|
||||
String? id,
|
||||
String? userId,
|
||||
String? avatar,
|
||||
String? username,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? bio,
|
||||
Map<String, dynamic>? address,
|
||||
DateTime? birthDate,
|
||||
}) {
|
||||
return ProfileModel(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
avatar: avatar ?? this.avatar,
|
||||
username: username ?? this.username,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
bio: bio ?? this.bio,
|
||||
address: address ?? this.address,
|
||||
birthDate: birthDate ?? this.birthDate,
|
||||
);
|
||||
}
|
||||
|
||||
// Get full name from first name and last name
|
||||
String? get fullName {
|
||||
if (firstName == null && lastName == null) return null;
|
||||
return [firstName, lastName].where((x) => x != null).join(' ');
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProfileModel(id: $id, username: $username, fullName: $fullName)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
|
||||
class RoleModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? icon;
|
||||
final String? description;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
RoleModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.icon,
|
||||
this.description,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
// Create a RoleModel instance from a JSON object
|
||||
factory RoleModel.fromJson(Map<String, dynamic> json) {
|
||||
return RoleModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
icon: json['icon'] as String?,
|
||||
description: json['description'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a RoleModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'icon': icon,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the RoleModel with updated fields
|
||||
RoleModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? icon,
|
||||
String? description,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return RoleModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
icon: icon ?? this.icon,
|
||||
description: description ?? this.description,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the role is an officer role
|
||||
bool get isOfficer => name.toLowerCase() == 'officer';
|
||||
|
||||
// Check if the role is a viewer role
|
||||
bool get isViewer => name.toLowerCase() == 'viewer';
|
||||
|
||||
// Convert the instance method to a static method for easier access
|
||||
static IconData iconRole(String? iconName) {
|
||||
if (iconName != null) {
|
||||
// Map icon string to IconData
|
||||
switch (iconName) {
|
||||
case 'user':
|
||||
return Iconsax.user;
|
||||
case 'eye':
|
||||
return CupertinoIcons.eye;
|
||||
// Add more mappings as needed
|
||||
default:
|
||||
return Iconsax.user; // fallback
|
||||
}
|
||||
}
|
||||
return Iconsax.user; // default icon
|
||||
}
|
||||
|
||||
// Keep the instance getter for convenience
|
||||
IconData get iconData {
|
||||
if (icon != null) {
|
||||
return RoleModel.iconRole(icon);
|
||||
} else if (isOfficer) {
|
||||
return Iconsax.user;
|
||||
} else if (isViewer) {
|
||||
return CupertinoIcons.person;
|
||||
}
|
||||
return Iconsax.user; // default icon
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RoleModel(id: $id, name: $name)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
import 'package:sigap/src/features/personalization/models/profile_model.dart';
|
||||
import 'package:sigap/src/features/personalization/models/roles_model.dart';
|
||||
|
||||
class UserModel {
|
||||
final String id;
|
||||
final String rolesId;
|
||||
final String email;
|
||||
final String? phone;
|
||||
final String? encryptedPassword;
|
||||
final DateTime? invitedAt;
|
||||
final DateTime? confirmedAt;
|
||||
final DateTime? emailConfirmedAt;
|
||||
final DateTime? recoverySentAt;
|
||||
final DateTime? lastSignInAt;
|
||||
final Map<String, dynamic>? appMetadata;
|
||||
final Map<String, dynamic>? userMetadata;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? bannedUntil;
|
||||
final bool isAnonymous;
|
||||
final ProfileModel? profile;
|
||||
final RoleModel? role;
|
||||
|
||||
UserModel({
|
||||
required this.id,
|
||||
required this.rolesId,
|
||||
required this.email,
|
||||
this.phone,
|
||||
this.encryptedPassword,
|
||||
this.invitedAt,
|
||||
this.confirmedAt,
|
||||
this.emailConfirmedAt,
|
||||
this.recoverySentAt,
|
||||
this.lastSignInAt,
|
||||
this.appMetadata,
|
||||
this.userMetadata,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.bannedUntil,
|
||||
required this.isAnonymous,
|
||||
this.profile,
|
||||
this.role,
|
||||
});
|
||||
|
||||
// Create a UserModel instance from a JSON object
|
||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserModel(
|
||||
id: json['id'] as String,
|
||||
rolesId: json['roles_id'] as String,
|
||||
email: json['email'] as String,
|
||||
phone: json['phone'] as String?,
|
||||
encryptedPassword: json['encrypted_password'] as String?,
|
||||
invitedAt:
|
||||
json['invited_at'] != null
|
||||
? DateTime.parse(json['invited_at'] as String)
|
||||
: null,
|
||||
confirmedAt:
|
||||
json['confirmed_at'] != null
|
||||
? DateTime.parse(json['confirmed_at'] as String)
|
||||
: null,
|
||||
emailConfirmedAt:
|
||||
json['email_confirmed_at'] != null
|
||||
? DateTime.parse(json['email_confirmed_at'] as String)
|
||||
: null,
|
||||
recoverySentAt:
|
||||
json['recovery_sent_at'] != null
|
||||
? DateTime.parse(json['recovery_sent_at'] as String)
|
||||
: null,
|
||||
lastSignInAt:
|
||||
json['last_sign_in_at'] != null
|
||||
? DateTime.parse(json['last_sign_in_at'] as String)
|
||||
: null,
|
||||
appMetadata: json['app_metadata'] as Map<String, dynamic>?,
|
||||
userMetadata: json['user_metadata'] as Map<String, dynamic>?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
bannedUntil:
|
||||
json['banned_until'] != null
|
||||
? DateTime.parse(json['banned_until'] as String)
|
||||
: null,
|
||||
isAnonymous: json['is_anonymous'] as bool? ?? false,
|
||||
profile:
|
||||
json['profiles'] != null
|
||||
? ProfileModel.fromJson(json['profiles'] as Map<String, dynamic>)
|
||||
: null,
|
||||
role:
|
||||
json['role'] != null
|
||||
? RoleModel.fromJson(json['role'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a UserModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'roles_id': rolesId,
|
||||
'email': email,
|
||||
'phone': phone,
|
||||
'encrypted_password': encryptedPassword,
|
||||
'invited_at': invitedAt?.toIso8601String(),
|
||||
'confirmed_at': confirmedAt?.toIso8601String(),
|
||||
'email_confirmed_at': emailConfirmedAt?.toIso8601String(),
|
||||
'recovery_sent_at': recoverySentAt?.toIso8601String(),
|
||||
'last_sign_in_at': lastSignInAt?.toIso8601String(),
|
||||
'app_metadata': appMetadata,
|
||||
'user_metadata': userMetadata,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'banned_until': bannedUntil?.toIso8601String(),
|
||||
'is_anonymous': isAnonymous,
|
||||
if (profile != null) 'profiles': profile!.toJson(),
|
||||
if (role != null) 'role': role!.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the UserModel with updated fields
|
||||
UserModel copyWith({
|
||||
String? id,
|
||||
String? rolesId,
|
||||
String? email,
|
||||
String? phone,
|
||||
String? encryptedPassword,
|
||||
DateTime? invitedAt,
|
||||
DateTime? confirmedAt,
|
||||
DateTime? emailConfirmedAt,
|
||||
DateTime? recoverySentAt,
|
||||
DateTime? lastSignInAt,
|
||||
Map<String, dynamic>? appMetadata,
|
||||
Map<String, dynamic>? userMetadata,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DateTime? bannedUntil,
|
||||
bool? isAnonymous,
|
||||
ProfileModel? profile,
|
||||
RoleModel? role,
|
||||
}) {
|
||||
return UserModel(
|
||||
id: id ?? this.id,
|
||||
rolesId: rolesId ?? this.rolesId,
|
||||
email: email ?? this.email,
|
||||
phone: phone ?? this.phone,
|
||||
encryptedPassword: encryptedPassword ?? this.encryptedPassword,
|
||||
invitedAt: invitedAt ?? this.invitedAt,
|
||||
confirmedAt: confirmedAt ?? this.confirmedAt,
|
||||
emailConfirmedAt: emailConfirmedAt ?? this.emailConfirmedAt,
|
||||
recoverySentAt: recoverySentAt ?? this.recoverySentAt,
|
||||
lastSignInAt: lastSignInAt ?? this.lastSignInAt,
|
||||
appMetadata: appMetadata ?? this.appMetadata,
|
||||
userMetadata: userMetadata ?? this.userMetadata,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
bannedUntil: bannedUntil ?? this.bannedUntil,
|
||||
isAnonymous: isAnonymous ?? this.isAnonymous,
|
||||
profile: profile ?? this.profile,
|
||||
role: role ?? this.role,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user is an officer based on metadata
|
||||
bool get isOfficer {
|
||||
if (userMetadata != null && userMetadata!.containsKey('is_officer')) {
|
||||
return userMetadata!['is_officer'] == true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserModel(id: $id, email: $email)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
class CommonController extends GetxController {
|
||||
static CommonController get instance => Get.find<CommonController>();
|
||||
|
||||
void showConfirmationDialog({
|
||||
required String title,
|
||||
required String message,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
Get.defaultDialog(
|
||||
title: title,
|
||||
titleStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
middleText: message,
|
||||
middleTextStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
backgroundColor: TColors.white,
|
||||
radius: 16,
|
||||
actions: [
|
||||
TextButton(onPressed: () => Get.back(), child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onConfirm();
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('Confirm'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void showDeleteDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
alignment: Alignment.center,
|
||||
backgroundColor: TColors.white,
|
||||
title: Text(
|
||||
'Delete post from denta koas app ?',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: Text(
|
||||
'Post cannot be recovered once deleted. Are you sure you want to delete this post?',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
actions: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
// Handle delete action
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
overlayColor: TColors.darkGrey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.cardRadiusSm),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Delete',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall!.apply(color: TColors.error),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
overlayColor: TColors.darkGrey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.cardRadiusSm),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showOptions(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Iconsax.edit),
|
||||
title: const Text('Edit'),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Iconsax.close_circle),
|
||||
title: const Text('Close'),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Iconsax.trash, color: TColors.error),
|
||||
title: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(color: TColors.error),
|
||||
),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
showDeleteDialog(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void likeButton() {
|
||||
var isLiked = false.obs;
|
||||
|
||||
void toggleLike() {
|
||||
isLiked.value = !isLiked.value;
|
||||
}
|
||||
|
||||
Obx(
|
||||
() => IconButton(
|
||||
icon: Icon(
|
||||
Iconsax.heart,
|
||||
color: isLiked.value ? TColors.error : TColors.grey,
|
||||
),
|
||||
onPressed: toggleLike,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
class DSpacingStyle {
|
||||
static const EdgeInsetsGeometry paddingWithAppBarHeight = EdgeInsets.only(
|
||||
top: TSizes.appBarHeight,
|
||||
left: TSizes.defaultSpace,
|
||||
right: TSizes.defaultSpace,
|
||||
bottom: TSizes.defaultSpace,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:iconsax/iconsax.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/device/device_utility.dart';
|
||||
|
||||
class DAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final Widget? avatar;
|
||||
final Widget? title;
|
||||
final IconData? leadingIcon;
|
||||
final List<Widget>? actions;
|
||||
final VoidCallback? leadingOnPressed;
|
||||
final VoidCallback? onBack;
|
||||
final bool showBackArrow;
|
||||
final bool centerTitle;
|
||||
final bool showActions;
|
||||
|
||||
const DAppBar({
|
||||
super.key,
|
||||
this.avatar,
|
||||
this.title,
|
||||
this.leadingIcon,
|
||||
this.actions,
|
||||
this.leadingOnPressed,
|
||||
this.showBackArrow = false,
|
||||
this.centerTitle = false,
|
||||
this.onBack,
|
||||
this.showActions = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: TSizes.defaultSpace),
|
||||
child: AppBar(
|
||||
centerTitle: centerTitle,
|
||||
automaticallyImplyLeading: false,
|
||||
leading:
|
||||
showBackArrow
|
||||
? IconButton(
|
||||
onPressed: onBack ?? () => Get.back(),
|
||||
icon: const Icon(Icons.chevron_left),
|
||||
)
|
||||
: leadingIcon != null
|
||||
? IconButton(
|
||||
onPressed: leadingOnPressed,
|
||||
icon: Icon(leadingIcon),
|
||||
)
|
||||
: null,
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (avatar != null) ...[
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Colors.transparent,
|
||||
child: avatar,
|
||||
),
|
||||
const SizedBox(width: 8), // Spasi antara avatar dan title
|
||||
],
|
||||
if (title != null) title!, // Title jika ada
|
||||
],
|
||||
),
|
||||
actions:
|
||||
showActions
|
||||
? actions ?? []
|
||||
: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Iconsax.notification),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Iconsax.search_normal),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(TDeviceUtils.getAppBarHeight());
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/device/device_utility.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
||||
const TabBarApp({
|
||||
super.key,
|
||||
required this.tabs,
|
||||
this.controller,
|
||||
this.isScrollable = false,
|
||||
this.padding,
|
||||
this.indicatorColor = TColors.primary,
|
||||
this.automaticIndicatorColorAdjustment = true,
|
||||
this.indicatorWeight = 2.0,
|
||||
this.indicatorPadding = EdgeInsets.zero,
|
||||
this.indicator,
|
||||
this.indicatorSize,
|
||||
this.dividerColor,
|
||||
this.dividerHeight,
|
||||
this.labelColor = TColors.primary,
|
||||
this.labelStyle,
|
||||
this.labelPadding,
|
||||
this.unselectedLabelColor = TColors.darkGrey,
|
||||
this.unselectedLabelStyle,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.overlayColor,
|
||||
this.mouseCursor,
|
||||
this.enableFeedback,
|
||||
this.onTap,
|
||||
this.physics,
|
||||
this.splashFactory,
|
||||
this.splashBorderRadius,
|
||||
this.tabAlignment = TabAlignment.fill,
|
||||
this.textScaler,
|
||||
this.indicatorAnimation,
|
||||
});
|
||||
|
||||
final List<Widget> tabs;
|
||||
final TabController? controller;
|
||||
final bool isScrollable;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final Color? indicatorColor;
|
||||
final bool automaticIndicatorColorAdjustment;
|
||||
final double indicatorWeight;
|
||||
final EdgeInsetsGeometry indicatorPadding;
|
||||
final Decoration? indicator;
|
||||
final TabBarIndicatorSize? indicatorSize;
|
||||
final Color? dividerColor;
|
||||
final double? dividerHeight;
|
||||
final Color? labelColor;
|
||||
final TextStyle? labelStyle;
|
||||
final EdgeInsetsGeometry? labelPadding;
|
||||
final Color? unselectedLabelColor;
|
||||
final TextStyle? unselectedLabelStyle;
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
final WidgetStateProperty<Color?>? overlayColor;
|
||||
final MouseCursor? mouseCursor;
|
||||
final bool? enableFeedback;
|
||||
final void Function(int)? onTap;
|
||||
final ScrollPhysics? physics;
|
||||
final InteractiveInkFeatureFactory? splashFactory;
|
||||
final BorderRadius? splashBorderRadius;
|
||||
final TabAlignment? tabAlignment;
|
||||
final TextScaler? textScaler;
|
||||
final TabIndicatorAnimation? indicatorAnimation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = THelperFunctions.isDarkMode(context);
|
||||
return Material(
|
||||
color: dark ? TColors.black : TColors.white,
|
||||
child: TabBar(
|
||||
tabs: tabs,
|
||||
controller: controller,
|
||||
isScrollable: isScrollable,
|
||||
padding: padding,
|
||||
indicatorColor: indicatorColor,
|
||||
automaticIndicatorColorAdjustment: automaticIndicatorColorAdjustment,
|
||||
indicatorWeight: indicatorWeight,
|
||||
indicatorPadding: indicatorPadding,
|
||||
indicator: indicator,
|
||||
indicatorSize: indicatorSize,
|
||||
dividerColor: dividerColor,
|
||||
dividerHeight: dividerHeight,
|
||||
labelColor: labelColor,
|
||||
labelStyle: labelStyle,
|
||||
labelPadding: labelPadding,
|
||||
unselectedLabelColor: unselectedLabelColor,
|
||||
unselectedLabelStyle: unselectedLabelStyle,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
mouseCursor: mouseCursor,
|
||||
enableFeedback: enableFeedback,
|
||||
onTap: onTap,
|
||||
physics: physics,
|
||||
splashFactory: splashFactory,
|
||||
tabAlignment: tabAlignment,
|
||||
textScaler: textScaler,
|
||||
indicatorAnimation: indicatorAnimation,
|
||||
overlayColor: WidgetStateProperty.all(Colors.blue.shade50),
|
||||
splashBorderRadius: splashBorderRadius,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(TDeviceUtils.getAppBarHeight());
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
class LikeButton extends StatefulWidget {
|
||||
final String postId;
|
||||
|
||||
const LikeButton({super.key, required this.postId});
|
||||
|
||||
@override
|
||||
LikeButtonState createState() => LikeButtonState();
|
||||
}
|
||||
|
||||
class LikeButtonState extends State<LikeButton> {
|
||||
bool isLiked = false; // Status awal tombol like
|
||||
|
||||
void toggleLike() async {
|
||||
bool newIsLiked = !isLiked; // Ubah status like sementara
|
||||
|
||||
try {
|
||||
// Panggil API untuk like atau unlike
|
||||
|
||||
// Jika berhasil, baru ubah status like
|
||||
setState(() {
|
||||
isLiked = newIsLiked;
|
||||
});
|
||||
} catch (e) {
|
||||
TLoaders.errorSnackBar(title: 'Error', message: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
isLiked ? CupertinoIcons.heart_fill : CupertinoIcons.heart,
|
||||
color: isLiked ? TColors.primary : Colors.grey,
|
||||
),
|
||||
onPressed: toggleLike,
|
||||
);
|
||||
}
|
||||
}
|