Initial commit: Proyek Mobile

This commit is contained in:
jouel88 2026-03-19 21:19:02 +07:00
commit 798c049de4
228 changed files with 21047 additions and 0 deletions

53
.gitignore vendored Normal file
View File

@ -0,0 +1,53 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Environment variables
.env
.env*
# Firebase configuration (if you want to keep them private)
android/app/google-services.json
ios/Runner/GoogleService-Info.plist

45
.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "9f455d2486bcb28cad87b062475f42edc959f636"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
- platform: android
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
- platform: ios
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
- platform: linux
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
- platform: macos
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
- platform: web
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
- platform: windows
create_revision: 9f455d2486bcb28cad87b062475f42edc959f636
base_revision: 9f455d2486bcb28cad87b062475f42edc959f636
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,52 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.mpg_mobile"
// UBAH 1: Hardcode ke 34 agar support ML Kit terbaru
compileSdk = 36
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// Pastikan applicationId ini sesuai dengan yang Anda inginkan
applicationId = "com.example.mpg_mobile"
// UBAH 2: Ubah minSdk ke 23 (Android 6.0) minimal untuk AI
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
// UBAH 3: Aktifkan MultiDex karena library AI sangat besar
multiDexEnabled = true
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
// UBAH 4: Tambahkan blok dependencies ini manual di paling bawah
dependencies {
implementation("androidx.multidex:multidex:2.0.1")
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,55 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:label="mpg_mobile"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_GOOGLE_MAPS_API_KEY_HERE"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
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"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</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"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.mpg_mobile
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

BIN
android/build_log.txt Normal file

Binary file not shown.

BIN
android/build_result.txt Normal file

Binary file not shown.

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx3G -XX:MaxMetaspaceSize=1G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

View File

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

3
devtools_options.yaml Normal file
View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mpgMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mpgMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mpgMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mpgMobile.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mpgMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.mpgMobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,16 @@
import Flutter
import UIKit
import GoogleMaps
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR_GOOGLE_MAPS_API_KEY_HERE")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

57
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Aplikasi ini membutuhkan akses lokasi untuk fitur absensi dan pemetaan.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Aplikasi ini membutuhkan akses lokasi untuk fitur absensi dan pemetaan.</string>
<key>NSCameraUsageDescription</key>
<string>Aplikasi ini membutuhkan akses kamera untuk mengambil foto profil.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Aplikasi ini membutuhkan akses galeri untuk mengunggah foto profil.</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Mpg Mobile</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>mpg_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@ -0,0 +1,25 @@
import 'package:hive_flutter/hive_flutter.dart';
class CacheManager {
static const String _authBox = 'auth_cache';
static const String _settingsBox = 'settings_cache';
static Future<void> init() async {
await Hive.initFlutter();
await Hive.openBox(_authBox);
await Hive.openBox(_settingsBox);
}
static Box get authBox => Hive.box(_authBox);
static Box get settingsBox => Hive.box(_settingsBox);
static Future<void> saveData(String boxName, String key, dynamic value) async {
final box = Hive.box(boxName);
await box.put(key, value);
}
static dynamic getData(String boxName, String key) {
final box = Hive.box(boxName);
return box.get(key);
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../services/api_config_service.dart';
class ApiUrl {
static String? _cachedBaseUrl;
static String? _cachedImageBaseUrl;
static Future<String> getBaseUrl() async {
_cachedBaseUrl = await ApiConfigService.getApiUrl();
return _cachedBaseUrl!;
}
static Future<String> getImageBaseUrl() async {
final baseUrl = await getBaseUrl();
_cachedImageBaseUrl = ApiConfigService.getStorageUrl(baseUrl);
return _cachedImageBaseUrl!;
}
static String get baseUrl => dotenv.env['API_BASE_URL'] ?? _cachedBaseUrl ?? 'http://192.168.1.3:8000/api';
static String get imageBaseUrl => _cachedImageBaseUrl ?? baseUrl.replaceAll('/api', '/storage/');
static Future<void> initialize() async {
await getBaseUrl();
await getImageBaseUrl();
}
static Future<void> reload() async {
_cachedBaseUrl = null;
_cachedImageBaseUrl = null;
await initialize();
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class ErrorHandler {
static final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
static void showError(String message, {int durationSeconds = 3}) {
scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red.shade600,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: durationSeconds),
action: SnackBarAction(
label: 'Tutup',
textColor: Colors.white,
onPressed: () {
scaffoldMessengerKey.currentState?.hideCurrentSnackBar();
},
),
),
);
}
static void showSuccess(String message, {int durationSeconds = 2}) {
scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green.shade600,
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: durationSeconds),
),
);
}
}

View File

@ -0,0 +1,102 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class ApiConfigService {
static const String _apiUrlKey = 'api_base_url';
static const String _selectedPresetKey = 'selected_preset';
static const Map<String, String> presets = {
'current_ip': 'http://192.168.1.7:8000/api',
'hostname': 'http://LAPTOP-I0SUKSKL:8000/api',
'emulator': 'http://10.0.2.2:8000/api',
'custom': '',
};
static Future<String> getApiUrl() async {
final prefs = await SharedPreferences.getInstance();
final savedUrl = prefs.getString(_apiUrlKey);
if (savedUrl != null && savedUrl.isNotEmpty) {
return savedUrl;
}
final selectedPreset = prefs.getString(_selectedPresetKey);
if (selectedPreset != null && presets.containsKey(selectedPreset)) {
final presetUrl = presets[selectedPreset];
if (presetUrl != null && presetUrl.isNotEmpty) {
return presetUrl;
}
}
final envUrl = dotenv.env['API_BASE_URL'];
if (envUrl != null && envUrl.isNotEmpty) {
return envUrl;
}
return presets['hostname']!;
}
static Future<void> setCustomUrl(String url) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_apiUrlKey, url);
await prefs.setString(_selectedPresetKey, 'custom');
}
static Future<void> setPreset(String presetKey) async {
if (!presets.containsKey(presetKey)) {
throw Exception('Invalid preset: $presetKey');
}
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_selectedPresetKey, presetKey);
final presetUrl = presets[presetKey];
if (presetUrl != null && presetUrl.isNotEmpty) {
await prefs.setString(_apiUrlKey, presetUrl);
}
}
static Future<String> getCurrentPreset() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_selectedPresetKey) ?? 'hostname';
}
static Future<String?> getSavedUrl() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_apiUrlKey);
}
static Future<void> clearConfig() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_apiUrlKey);
await prefs.remove(_selectedPresetKey);
}
static Future<bool> testConnection(String url) async {
try {
final uri = Uri.parse(url);
return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https');
} catch (e) {
return false;
}
}
static String getStorageUrl(String apiUrl) {
return apiUrl.replaceAll('/api', '/storage/');
}
static String getPresetDisplayName(String presetKey) {
switch (presetKey) {
case 'current_ip':
return 'IP Laptop Saat Ini - Recommended';
case 'hostname':
return 'Hostname (LAPTOP-I0SUKSKL)';
case 'emulator':
return 'Android Emulator (10.0.2.2)';
case 'custom':
return 'Custom URL';
default:
return presetKey;
}
}
}

View File

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class ShimmerLoading extends StatelessWidget {
final double width;
final double height;
final ShapeBorder shapeBorder;
const ShimmerLoading.rectangular({
super.key,
this.width = double.infinity,
required this.height,
}) : shapeBorder = const RoundedRectangleBorder();
const ShimmerLoading.circular({
super.key,
required this.width,
required this.height,
this.shapeBorder = const CircleBorder(),
});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
loop: 0,
child: Container(
width: width,
height: height,
decoration: ShapeDecoration(
color: Colors.grey[400]!,
shape: shapeBorder,
),
),
);
}
}

207
lib/core/theme.dart Normal file
View File

@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
static const Color primaryDark = Color(0xFF0F172A);
static const Color primaryOrange = Color(0xFFE65100);
static const Color primaryBlue = Color(0xFF1E3A8A);
static const Color secondaryBlue = Color(0xFF3B82F6);
static const Color bgWhite = Color(0xFFFFFFFF);
static const Color bgLight = Color(0xFFF8FAFC);
static const Color bgCard = Color(0xFFFFFFFF);
static const Color bgInput = Color(0xFFF1F5F9);
static const Color textPrimary = Color(0xFF0F172A);
static const Color textSecondary = Color(0xFF475569);
static const Color textTertiary = Color(0xFF94A3B8);
static const Color statusGreen = Color(0xFF10B981);
static const Color statusYellow = Color(0xFFF59E0B);
static const Color statusRed = Color(0xFFEF4444);
static const Color statusOrange = Color(0xFFFF5722);
static const Color glassWhite10 = Color(0x1AFFFFFF);
static const Color glassWhite20 = Color(0x33FFFFFF);
static const Color glassWhite50 = Color(0x80FFFFFF);
static const Color glassWhite70 = Color(0xB3FFFFFF);
static const Color glassDark10 = Color(0x1A000000);
static const Color glassDark40 = Color(0x66000000);
static const Color badgeCutiBg = Color(0xFFFEE2E2);
static const Color badgeCutiText = Color(0xFFDC2626);
static const Color badgeSakitBg = Color(0xFFFFEDD5);
static const Color badgeSakitText = Color(0xFFEA580C);
static const Color badgeIzinBg = Color(0xFFDBEAFE);
static const Color badgeIzinText = Color(0xFF2563EB);
static const Color badgeLemburBg = Color(0xFFF3E8FF);
static const Color badgeLemburText = Color(0xFF9333EA);
static const double spacingXs = 4.0;
static const double spacingSm = 8.0;
static const double spacingMd = 14.0;
static const double spacingLg = 20.0;
static const double spacingXl = 28.0;
static const double spacingXxl = 40.0;
static const double radiusSm = 12.0;
static const double radiusMd = 16.0;
static const double radiusLg = 20.0;
static const double radiusXl = 24.0;
static const double radiusFull = 9999.0;
static List<BoxShadow> shadowSm = [
BoxShadow(
color: primaryDark.withOpacity(0.04),
blurRadius: 16,
spreadRadius: 0,
offset: const Offset(0, 4),
),
];
static List<BoxShadow> shadowMd = [
BoxShadow(
color: primaryDark.withOpacity(0.06),
blurRadius: 24,
spreadRadius: -2,
offset: const Offset(0, 12),
),
BoxShadow(
color: primaryDark.withOpacity(0.03),
blurRadius: 8,
spreadRadius: -1,
offset: const Offset(0, 4),
),
];
static List<BoxShadow> shadowLg = [
BoxShadow(
color: primaryDark.withOpacity(0.08),
blurRadius: 40,
spreadRadius: -4,
offset: const Offset(0, 20),
),
];
static List<BoxShadow> glowPrimary = [
BoxShadow(
color: primaryDark.withOpacity(0.3),
blurRadius: 20,
spreadRadius: -4,
offset: const Offset(0, 8),
),
];
static List<BoxShadow> glowBlue = [
BoxShadow(
color: primaryBlue.withOpacity(0.4),
blurRadius: 24,
spreadRadius: -4,
offset: const Offset(0, 8),
),
];
static List<BoxShadow> glowGreen = [
BoxShadow(
color: statusGreen.withOpacity(0.4),
blurRadius: 24,
spreadRadius: -4,
offset: const Offset(0, 8),
),
];
static List<BoxShadow> glowRed = [
BoxShadow(
color: statusRed.withOpacity(0.4),
blurRadius: 24,
spreadRadius: -4,
offset: const Offset(0, 8),
),
];
static List<BoxShadow> glowOrange = [
BoxShadow(
color: statusOrange.withOpacity(0.4),
blurRadius: 24,
spreadRadius: -4,
offset: const Offset(0, 8),
),
];
static TextStyle get heading1 => GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w800,
letterSpacing: -1.0,
color: textPrimary,
);
static TextStyle get heading2 => GoogleFonts.inter(
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
color: textPrimary,
);
static TextStyle get heading3 => GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
color: textPrimary,
);
static TextStyle get bodyLarge => GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w500,
color: textPrimary,
);
static TextStyle get bodyMedium => GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w500,
color: textPrimary,
);
static TextStyle get bodySmall => GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: textSecondary,
);
static TextStyle get labelLarge => GoogleFonts.inter(
fontSize: 13,
fontWeight: FontWeight.w600,
color: textPrimary,
);
static TextStyle get labelMedium => GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: textPrimary,
);
static ThemeData get lightTheme {
return ThemeData(
primaryColor: primaryDark,
scaffoldBackgroundColor: bgWhite,
colorScheme: const ColorScheme.light(
primary: primaryDark,
secondary: primaryOrange,
surface: bgWhite,
error: statusRed,
),
textTheme: TextTheme(
displayLarge: heading1,
displayMedium: heading2,
displaySmall: heading3,
bodyLarge: bodyLarge,
bodyMedium: bodyMedium,
bodySmall: bodySmall,
labelLarge: labelLarge,
),
useMaterial3: true,
);
}
}

View File

@ -0,0 +1,60 @@
import 'dart:ui';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
class CameraUtils {
static InputImage inputImageFromCameraImage({
required CameraImage image,
required CameraDescription camera,
required InputImageRotation rotation,
}) {
final allBytes = WriteBuffer();
for (final Plane plane in image.planes) {
allBytes.putUint8List(plane.bytes);
}
final bytes = allBytes.done().buffer.asUint8List();
final Size imageSize = Size(image.width.toDouble(), image.height.toDouble());
final format = InputImageFormatUtils.fromRawValue(image.format.raw) ?? InputImageFormat.nv21;
final inputImageMetadata = InputImageMetadata(
size: imageSize,
rotation: rotation,
format: format,
bytesPerRow: image.planes[0].bytesPerRow,
);
return InputImage.fromBytes(bytes: bytes, metadata: inputImageMetadata);
}
static InputImageRotation rotationIntToImageRotation(int rotation) {
switch (rotation) {
case 90:
return InputImageRotation.rotation90deg;
case 180:
return InputImageRotation.rotation180deg;
case 270:
return InputImageRotation.rotation270deg;
default:
return InputImageRotation.rotation0deg;
}
}
}
class InputImageFormatUtils {
static InputImageFormat? fromRawValue(int rawValue) {
switch (rawValue) {
case 17:
return InputImageFormat.nv21;
case 35:
return InputImageFormat.yuv420;
case 842094169:
return InputImageFormat.yv12;
default:
return null;
}
}
}

114
lib/main.dart Normal file
View File

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'core/theme.dart';
import 'core/error_handler.dart';
import 'providers/auth_provider.dart';
import 'providers/home_provider.dart';
import 'providers/attendance_provider.dart';
import 'providers/pengajuan_provider.dart';
import 'providers/calendar_provider.dart';
import 'providers/face_provider.dart';
import 'providers/poin_provider.dart';
import 'providers/signature_provider.dart';
import 'providers/surat_izin_provider.dart';
import 'providers/notification_provider.dart';
import 'screens/splash_screen.dart';
import 'screens/auth/login_screen.dart';
import 'screens/main_screen.dart';
import 'screens/presensi/presensi_screen.dart';
import 'screens/schedule/schedule_screen.dart';
import 'screens/pengajuan/pengajuan_screen.dart';
import 'screens/pengajuan/forms/cuti_form_screen.dart';
import 'screens/pengajuan/forms/sakit_form_screen.dart';
import 'screens/pengajuan/forms/izin_form_screen.dart';
import 'screens/pengajuan/forms/lembur_form_screen.dart';
import 'screens/onboarding/face_enrollment_screen.dart';
import 'screens/poin/point_usage_screen.dart';
import 'screens/poin/poin_history_screen.dart';
import 'screens/notification/notification_screen.dart';
import 'screens/salary/salary_estimator_screen.dart';
import 'screens/api_settings_screen.dart';
import 'screens/profile/face_test_screen.dart';
import 'screens/profile/signature_screen.dart';
import 'screens/profile/edit_profile_screen.dart';
import 'screens/documents/surat_izin_screen.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'core/cache_manager.dart';
import 'core/constants/api_url.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env");
await CacheManager.init();
await ApiUrl.initialize();
await initializeDateFormatting('id_ID', null);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => HomeProvider()),
ChangeNotifierProvider(create: (_) => AttendanceProvider()),
ChangeNotifierProvider(create: (_) => PengajuanProvider()),
ChangeNotifierProvider(create: (_) => CalendarProvider()),
ChangeNotifierProvider(create: (_) => FaceProvider()),
ChangeNotifierProvider(create: (_) => PoinProvider()),
ChangeNotifierProvider(create: (_) => SignatureProvider()),
ChangeNotifierProvider(create: (_) => SuratIzinProvider()),
ChangeNotifierProvider(create: (_) => NotificationProvider()),
],
child: MaterialApp(
title: 'MPG HRIS',
scaffoldMessengerKey: ErrorHandler.scaffoldMessengerKey,
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme.copyWith(
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
),
),
initialRoute: '/',
routes: {
'/': (context) => const SplashScreen(),
'/login': (context) => const LoginScreen(),
'/home': (context) => const MainScreen(),
'/presensi': (context) => const PresensiScreen(),
'/schedule': (context) => const ScheduleScreen(),
'/pengajuan': (context) => const PengajuanScreen(),
'/pengajuan/cuti': (context) => const CutiFormScreen(),
'/pengajuan/sakit': (context) => const SakitFormScreen(),
'/pengajuan/izin': (context) => const IzinFormScreen(),
'/pengajuan/lembur': (context) => const LemburFormScreen(),
'/onboarding/face-enrollment': (context) => const FaceEnrollmentScreen(),
'/poin/usage': (context) => const PointUsageScreen(),
'/poin/history': (context) => const PoinHistoryScreen(),
'/notification': (context) => const NotificationScreen(),
'/salary/estimator': (context) => const SalaryEstimatorScreen(),
'/settings/api': (context) => const ApiSettingsScreen(),
'/profile/face-test': (context) => const FaceTestScreen(),
'/profile/signature': (context) => const SignatureScreen(),
'/profile/edit': (context) => const EditProfileScreen(),
'/surat-izin': (context) => const SuratIzinScreen(),
},
),
);
}
}

View File

@ -0,0 +1,25 @@
class AnnouncementModel {
final String title;
final String description;
final String date;
final String jabatan;
final String? avatarUrl;
AnnouncementModel({
required this.title,
required this.description,
required this.date,
required this.jabatan,
this.avatarUrl,
});
factory AnnouncementModel.fromJson(Map<String, dynamic> json) {
return AnnouncementModel(
title: json['title']?.toString() ?? '',
description: json['description']?.toString() ?? '',
date: json['tanggal']?.toString() ?? '',
jabatan: json['jabatan']?.toString() ?? '',
avatarUrl: json['avatar_url']?.toString(),
);
}
}

View File

@ -0,0 +1,16 @@
class DivisionModel {
final int idDivisi;
final String namaDivisi;
DivisionModel({
required this.idDivisi,
required this.namaDivisi,
});
factory DivisionModel.fromJson(Map<String, dynamic> json) {
return DivisionModel(
idDivisi: json['id_divisi'],
namaDivisi: json['nama_divisi'],
);
}
}

View File

@ -0,0 +1,16 @@
class JabatanModel {
final int idJabatan;
final String namaJabatan;
JabatanModel({
required this.idJabatan,
required this.namaJabatan,
});
factory JabatanModel.fromJson(Map<String, dynamic> json) {
return JabatanModel(
idJabatan: json['id_jabatan'],
namaJabatan: json['nama_jabatan'],
);
}
}

View File

@ -0,0 +1,63 @@
class LemburModel {
final String idLembur;
final int idUser;
final DateTime tanggalLembur;
final String jamMulai;
final String jamSelesai;
final int durasiMenit;
final String keterangan;
final int idStatus;
final String? statusLabel;
final DateTime tanggalDiajukan;
LemburModel({
required this.idLembur,
required this.idUser,
required this.tanggalLembur,
required this.jamMulai,
required this.jamSelesai,
required this.durasiMenit,
required this.keterangan,
required this.idStatus,
this.statusLabel,
required this.tanggalDiajukan,
});
factory LemburModel.fromJson(Map<String, dynamic> json) {
return LemburModel(
idLembur: json['id_lembur'].toString(),
idUser: json['id_user'],
tanggalLembur: DateTime.parse(json['tanggal_lembur']),
jamMulai: json['jam_mulai'],
jamSelesai: json['jam_selesai'],
durasiMenit: json['durasi_menit'] is int ? json['durasi_menit'] : int.parse(json['durasi_menit'].toString()),
keterangan: json['keterangan'] ?? '',
idStatus: json['id_status'],
statusLabel: _getStatusLabel(json['id_status']),
tanggalDiajukan: DateTime.parse(json['tanggal_diajukan']),
);
}
static String _getStatusLabel(int id) {
switch (id) {
case 1: return 'Pending';
case 2: return 'Approved';
case 3: return 'Rejected';
default: return 'Unknown';
}
}
Map<String, dynamic> toJson() {
return {
'id_lembur': idLembur,
'id_user': idUser,
'tanggal_lembur': tanggalLembur.toIso8601String(),
'jam_mulai': jamMulai,
'jam_selesai': jamSelesai,
'durasi_menit': durasiMenit,
'keterangan': keterangan,
'id_status': idStatus,
'tanggal_diajukan': tanggalDiajukan.toIso8601String(),
};
}
}

View File

@ -0,0 +1,83 @@
class PengajuanModel {
final String id;
final String jenis;
final String tanggalPengajuan;
final String tanggalMulai;
final String tanggalSelesai;
final int totalHari;
final String keterangan;
final String status;
final String? approvedAt;
final String? jamMulai;
final String? jamSelesai;
PengajuanModel({
required this.id,
required this.jenis,
required this.tanggalPengajuan,
required this.tanggalMulai,
required this.tanggalSelesai,
required this.totalHari,
required this.keterangan,
required this.status,
this.approvedAt,
this.jamMulai,
this.jamSelesai,
});
factory PengajuanModel.fromJson(Map<String, dynamic> json) {
return PengajuanModel(
id: json['id_izin'].toString(),
jenis: json['jenis_izin'] != null
? json['jenis_izin']['nama_izin']
: 'N/A',
tanggalPengajuan: json['created_at'] != null
? json['created_at'].toString().split('T')[0]
: '',
tanggalMulai: json['tanggal_mulai'],
tanggalSelesai: json['tanggal_selesai'],
totalHari: _calculateDays(json['tanggal_mulai'], json['tanggal_selesai']),
keterangan: json['alasan'] ?? '',
status: _mapStatus(json['id_status']),
approvedAt: json['approved_at'],
);
}
factory PengajuanModel.fromLemburJson(Map<String, dynamic> json) {
return PengajuanModel(
id: json['id_lembur'].toString(),
jenis: 'Lembur',
tanggalPengajuan: json['created_at'] != null
? json['created_at'].toString().split('T')[0]
: '',
tanggalMulai: json['tanggal_lembur'] ?? '',
tanggalSelesai: json['tanggal_lembur'] ?? '',
totalHari: 1,
keterangan: json['keterangan'] ?? '',
status: _mapStatus(json['id_status']),
approvedAt: json['approved_at'],
jamMulai: json['jam_mulai'],
jamSelesai: json['jam_selesai'],
);
}
static int _calculateDays(String? start, String? end) {
if (start == null || end == null) return 0;
try {
final startDate = DateTime.parse(start);
final endDate = DateTime.parse(end);
return endDate.difference(startDate).inDays + 1;
} catch (_) {
return 0;
}
}
static String _mapStatus(dynamic statusId) {
if (statusId == null) return 'pending';
final id = int.tryParse(statusId.toString());
if (id == 1) return 'pending';
if (id == 2) return 'approved';
if (id == 3) return 'rejected';
return 'pending';
}
}

View File

@ -0,0 +1,128 @@
class PresensiModel {
final String tanggal;
final String? jamMasuk;
final String? jamPulang;
final String shift;
final String lokasi;
final String? statusJadwal;
final bool isAdjusted;
final String? adjustmentNote;
final bool sudahAbsenMasuk;
final bool sudahAbsenPulang;
final String? statusMasuk;
final String? statusPulang;
final String? jadwalJamMasuk;
final String? jadwalJamPulang;
final String? kantorNama;
final double? kantorLat;
final double? kantorLon;
final double? kantorRadius;
PresensiModel({
required this.tanggal,
this.jamMasuk,
this.jamPulang,
required this.shift,
required this.lokasi,
this.statusJadwal,
this.isAdjusted = false,
this.adjustmentNote,
this.sudahAbsenMasuk = false,
this.sudahAbsenPulang = false,
this.statusMasuk,
this.statusPulang,
this.jadwalJamMasuk,
this.jadwalJamPulang,
this.kantorNama,
this.kantorLat,
this.kantorLon,
this.kantorRadius,
});
factory PresensiModel.fromJson(Map<String, dynamic> json) {
return PresensiModel(
tanggal: json['tanggal'],
jamMasuk: json['jam_masuk'],
jamPulang: json['jam_pulang'],
shift: json['shift'],
lokasi: json['lokasi'],
statusJadwal: json['status_jadwal'],
isAdjusted: json['is_adjusted'] ?? false,
adjustmentNote: json['note'],
sudahAbsenMasuk: json['sudah_absen_masuk'] ?? false,
sudahAbsenPulang: json['sudah_absen_pulang'] ?? false,
statusMasuk: json['status_masuk'],
statusPulang: json['status_pulang'],
jadwalJamMasuk: json['jadwal_jam_masuk'],
jadwalJamPulang: json['jadwal_jam_pulang'],
kantorNama: json['kantor_nama']?.toString(),
kantorLat: (json['kantor_lat'] is num) ? (json['kantor_lat'] as num).toDouble() : null,
kantorLon: (json['kantor_lon'] is num) ? (json['kantor_lon'] as num).toDouble() : null,
kantorRadius: (json['kantor_radius'] is num) ? (json['kantor_radius'] as num).toDouble() : null,
);
}
}
class PresensiHistoryModel {
final int? idPresensi;
final String tanggal;
final String? tanggalRaw;
final String? jamMasuk;
final String? jamPulang;
final String totalJam;
final String? shift;
final String? statusMasuk;
final String? statusPulang;
final String? waktuTerlambat;
final String? keteranganLuarRadius;
final bool verifikasiWajah;
final int? statusValidasi;
final String? alasanPenolakan;
PresensiHistoryModel({
this.idPresensi,
required this.tanggal,
this.tanggalRaw,
this.jamMasuk,
this.jamPulang,
required this.totalJam,
this.shift,
this.statusMasuk,
this.statusPulang,
this.waktuTerlambat,
this.keteranganLuarRadius,
this.verifikasiWajah = false,
this.statusValidasi,
this.alasanPenolakan,
});
factory PresensiHistoryModel.fromJson(Map<String, dynamic> json) {
String hitungTotalJam = '-';
if (json['jam_masuk'] != null && json['jam_pulang'] != null) {
try {
final masuk = DateTime.parse("2000-01-01 ${json['jam_masuk']}");
final pulang = DateTime.parse("2000-01-01 ${json['jam_pulang']}");
final selisih = pulang.difference(masuk);
hitungTotalJam = '${selisih.inHours}j ${selisih.inMinutes.remainder(60)}m';
} catch (e) {
}
}
return PresensiHistoryModel(
idPresensi: json['id_presensi'],
tanggal: json['tanggal'] ?? '',
tanggalRaw: json['tanggal_raw'],
jamMasuk: json['jam_masuk'],
jamPulang: json['jam_pulang'],
totalJam: json['total_jam'] ?? hitungTotalJam,
shift: json['shift'],
statusMasuk: json['status_masuk'],
statusPulang: json['status_pulang'],
waktuTerlambat: json['waktu_terlambat'],
keteranganLuarRadius: json['keterangan_luar_radius'],
verifikasiWajah: json['verifikasi_wajah'] ?? false,
statusValidasi: json['status_validasi'],
alasanPenolakan: json['alasan_penolakan'],
);
}
}

View File

@ -0,0 +1,50 @@
class ScheduleModel {
final String tanggal;
final String hari;
final bool isHariKerja;
final bool isHariLibur;
final String? keteranganLibur;
final String shiftNama;
final String jamMasuk;
final String jamPulang;
final String? statusPoin;
final String? statusPresensi;
final String warnaKalender;
ScheduleModel({
required this.tanggal,
required this.hari,
this.isHariKerja = false,
this.isHariLibur = false,
this.keteranganLibur,
this.shiftNama = '-',
this.jamMasuk = '-',
this.jamPulang = '-',
this.statusPoin,
this.statusPresensi,
this.warnaKalender = '#E0E0E0',
});
factory ScheduleModel.fromJson(Map<String, dynamic> json) {
bool parseBool(dynamic value) {
if (value == true) return true;
if (value == 1) return true;
if (value == '1') return true;
return false;
}
return ScheduleModel(
tanggal: json['tanggal']?.toString() ?? '',
hari: json['hari']?.toString() ?? '',
isHariKerja: parseBool(json['is_hari_kerja']),
isHariLibur: parseBool(json['is_hari_libur']),
keteranganLibur: json['keterangan_libur']?.toString(),
shiftNama: json['shift_nama']?.toString() ?? '-',
jamMasuk: json['jam_masuk']?.toString() ?? json['jam_mulai']?.toString() ?? '-',
jamPulang: json['jam_pulang']?.toString() ?? json['jam_selesai']?.toString() ?? '-',
statusPoin: json['status_poin']?.toString(),
statusPresensi: json['status_presensi']?.toString(),
warnaKalender: json['warna_kalender']?.toString() ?? '#E0E0E0',
);
}
}

View File

@ -0,0 +1,25 @@
class ShiftModel {
final int idShift;
final String namaShift;
final String jamVideo;
final String jamMulai;
final String jamSelesai;
ShiftModel({
required this.idShift,
required this.namaShift,
required this.jamVideo,
required this.jamMulai,
required this.jamSelesai,
});
factory ShiftModel.fromJson(Map<String, dynamic> json) {
return ShiftModel(
idShift: json['id_shift'],
namaShift: json['nama_shift'],
jamVideo: json['jam_masuk'] ?? '',
jamMulai: json['jam_masuk'],
jamSelesai: json['jam_pulang'],
);
}
}

View File

@ -0,0 +1,112 @@
class ApprovalModel {
final int tahap;
final String tahapLabel;
final String status;
final String approverNama;
final String approverJabatan;
final String? ttdApprover;
final String? catatan;
final String? createdAt;
ApprovalModel({
required this.tahap,
required this.tahapLabel,
required this.status,
required this.approverNama,
required this.approverJabatan,
this.ttdApprover,
this.catatan,
this.createdAt,
});
factory ApprovalModel.fromJson(Map<String, dynamic> json) {
return ApprovalModel(
tahap: json['tahap'] is int ? json['tahap'] : int.tryParse(json['tahap']?.toString() ?? '0') ?? 0,
tahapLabel: json['tahap_label']?.toString() ?? '',
status: json['status']?.toString() ?? '',
approverNama: json['approver_nama']?.toString() ?? 'N/A',
approverJabatan: json['approver_jabatan']?.toString() ?? 'N/A',
ttdApprover: json['ttd_approver']?.toString(),
catatan: json['catatan']?.toString(),
createdAt: json['created_at']?.toString(),
);
}
}
class SuratIzinModel {
final String id;
final String nomorSurat;
final String statusSurat;
final String jenisIzin;
final String? ttdPengaju;
final List<ApprovalModel> approvals;
final String? createdAt;
final String? isiSurat;
final String? pengajuNama;
final String? pengajuNik;
final String? pengajuJabatan;
final String? pengajuDivisi;
final String? tanggalMulai;
final String? tanggalSelesai;
final String? alasan;
SuratIzinModel({
required this.id,
required this.nomorSurat,
required this.statusSurat,
required this.jenisIzin,
this.ttdPengaju,
required this.approvals,
this.createdAt,
this.isiSurat,
this.pengajuNama,
this.pengajuNik,
this.pengajuJabatan,
this.pengajuDivisi,
this.tanggalMulai,
this.tanggalSelesai,
this.alasan,
});
factory SuratIzinModel.fromJson(Map<String, dynamic> json) {
List<ApprovalModel> approvalsList = [];
if (json['approvals'] != null && json['approvals'] is List) {
approvalsList = (json['approvals'] as List)
.map((a) => ApprovalModel.fromJson(a))
.toList();
}
return SuratIzinModel(
id: json['id_surat']?.toString() ?? '',
nomorSurat: json['nomor_surat']?.toString() ?? '',
statusSurat: json['status_surat']?.toString() ?? '',
jenisIzin: json['jenis_izin']?.toString() ?? 'N/A',
ttdPengaju: json['ttd_pengaju']?.toString(),
approvals: approvalsList,
createdAt: json['created_at']?.toString(),
isiSurat: json['isi_surat']?.toString(),
pengajuNama: json['pengaju'] != null ? json['pengaju']['nama']?.toString() : null,
pengajuNik: json['pengaju'] != null ? json['pengaju']['nik']?.toString() : null,
pengajuJabatan: json['pengaju'] != null ? json['pengaju']['jabatan']?.toString() : null,
pengajuDivisi: json['pengaju'] != null ? json['pengaju']['divisi']?.toString() : null,
tanggalMulai: json['pengajuan'] != null ? json['pengajuan']['tanggal_mulai']?.toString() : null,
tanggalSelesai: json['pengajuan'] != null ? json['pengajuan']['tanggal_selesai']?.toString() : null,
alasan: json['pengajuan'] != null ? json['pengajuan']['alasan']?.toString() : null,
);
}
String get statusLabel {
switch (statusSurat) {
case 'menunggu_manajer': return 'Menunggu Manajer';
case 'menunggu_hrd': return 'Menunggu HRD';
case 'disetujui': return 'Disetujui';
case 'ditolak': return 'Ditolak';
default: return statusSurat;
}
}
bool get isApproved => statusSurat == 'disetujui';
bool get isRejected => statusSurat == 'ditolak';
bool get isPending => statusSurat.startsWith('menunggu');
}

View File

@ -0,0 +1,22 @@
class TandaTanganModel {
final String id;
final String fileTtd;
final bool isActive;
final String? createdAt;
TandaTanganModel({
required this.id,
required this.fileTtd,
required this.isActive,
this.createdAt,
});
factory TandaTanganModel.fromJson(Map<String, dynamic> json) {
return TandaTanganModel(
id: json['id_tanda_tangan']?.toString() ?? '',
fileTtd: json['file_ttd']?.toString() ?? '',
isActive: json['is_active'] == true || json['is_active'] == 1,
createdAt: json['created_at']?.toString(),
);
}
}

127
lib/models/user_model.dart Normal file
View File

@ -0,0 +1,127 @@
import '../core/constants/api_url.dart';
class UserModel {
final int id;
final String namaLengkap;
final String email;
final String? foto;
final String jabatan;
final String divisi;
final String? kantor;
final String? nik;
final String? noTelp;
final String? alamat;
final String? tglBergabung;
final int sisaCuti;
final String statusAktif;
final List<RoleData> roles;
UserModel({
required this.id,
required this.namaLengkap,
required this.email,
this.foto,
required this.jabatan,
required this.divisi,
this.kantor,
this.nik,
this.noTelp,
this.alamat,
this.tglBergabung,
this.sisaCuti = 0,
this.statusAktif = 'Aktif',
this.roles = const [],
});
factory UserModel.fromJson(Map<String, dynamic> json) {
String? divisiName;
String? jabatanName;
String? kantorName;
if (json['divisi'] != null) {
divisiName = json['divisi'] is Map
? json['divisi']['nama_divisi']
: json['divisi'].toString();
}
if (json['jabatan'] != null) {
jabatanName = json['jabatan'] is Map
? json['jabatan']['nama_jabatan']
: json['jabatan'].toString();
}
if (json['kantor'] != null) {
kantorName = json['kantor'] is Map
? json['kantor']['nama_kantor']
: json['kantor'].toString();
}
List<RoleData> roles = [];
if (json['roles'] is List) {
roles = (json['roles'] as List)
.map((role) {
if (role is Map) {
return RoleData.fromJson(Map<String, dynamic>.from(role));
}
return RoleData.fromJson({});
})
.toList();
}
String? fotoUrl = json['foto_profil']?.toString() ?? json['foto']?.toString();
if (fotoUrl != null && fotoUrl.isNotEmpty && !fotoUrl.startsWith('http')) {
fotoUrl = '${ApiUrl.imageBaseUrl}/$fotoUrl';
}
return UserModel(
id: json['id'] is int ? json['id'] : int.parse(json['id'].toString()),
namaLengkap: json['nama_lengkap']?.toString() ?? '',
email: json['email']?.toString() ?? '',
foto: fotoUrl,
jabatan: jabatanName ?? 'N/A',
divisi: divisiName ?? 'N/A',
kantor: kantorName,
nik: json['nik']?.toString(),
noTelp: json['no_telp']?.toString(),
alamat: json['alamat']?.toString(),
tglBergabung: json['tgl_bergabung']?.toString(),
sisaCuti: json['sisa_cuti'] is int ? json['sisa_cuti'] : int.tryParse(json['sisa_cuti']?.toString() ?? '0') ?? 0,
statusAktif: _mapStatusAktif(json['status_aktif']),
roles: roles,
);
}
static String _mapStatusAktif(dynamic value) {
if (value == null) return 'Tidak Diketahui';
String str = value.toString();
switch (str) {
case '1':
return 'Aktif';
case '0':
return 'Menunggu Verifikasi';
default:
return str;
}
}
}
class RoleData {
final int idRole;
final String namaRole;
RoleData({
required this.idRole,
required this.namaRole,
});
factory RoleData.fromJson(Map<String, dynamic> json) {
return RoleData(
idRole: json['id_role'] is int
? json['id_role']
: int.tryParse(json['id_role']?.toString() ?? '0') ?? 0,
namaRole: json['nama_role']?.toString() ?? '',
);
}
}

View File

@ -0,0 +1,168 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:geolocator/geolocator.dart';
import '../repositories/attendance_repository.dart';
import '../models/presensi_model.dart';
class AttendanceProvider extends ChangeNotifier {
final AttendanceRepository _repository = AttendanceRepository();
List<PresensiHistoryModel> historyList = [];
bool isLoading = false;
String currentLocationName = "Tegal Besar, Jember";
LatLng? currentLocation;
bool isWithinRadius = false;
double distanceToOffice = 0.0;
int selectedMonth = DateTime.now().month;
int selectedYear = DateTime.now().year;
String? reasonOutsideRadius;
String? reasonEarlyCheckout;
Future<bool> getCurrentPosition() async {
bool serviceEnabled;
LocationPermission permission;
isLoading = true;
notifyListeners();
try {
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
throw Exception('Layanan lokasi (GPS) tidak aktif. Mohon nyalakan GPS Anda.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
throw Exception('Izin lokasi ditolak aplikasinya.');
}
}
if (permission == LocationPermission.deniedForever) {
throw Exception('Izin lokasi ditolak permanen. Buka pengaturan untuk mengizinkan.');
}
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.bestForNavigation,
);
await updateLocation(position.latitude, position.longitude);
isLoading = false;
notifyListeners();
return true;
} catch (e) {
print('Error getting location: $e');
isLoading = false;
notifyListeners();
rethrow;
}
}
Future<void> updateLocation(double lat, double lng) async {
currentLocation = LatLng(lat, lng);
try {
final radiusCheck = await _repository.checkRadius(lat, lng);
isWithinRadius = radiusCheck['is_within_radius'] ?? false;
distanceToOffice = (radiusCheck['distance'] ?? 0.0).toDouble();
notifyListeners();
} catch (e) {
print('Error checking radius: $e');
isWithinRadius = false;
notifyListeners();
}
}
void setReasonOutsideRadius(String? reason) {
reasonOutsideRadius = reason;
notifyListeners();
}
void setReasonEarlyCheckout(String? reason) {
reasonEarlyCheckout = reason;
notifyListeners();
}
Future<void> fetchHistory() async {
isLoading = true;
notifyListeners();
try {
historyList = await _repository.getHistory();
} catch (e) {
print('Error fetching history: $e');
} finally {
isLoading = false;
notifyListeners();
}
}
Future<bool> submitPresensi(String type, File photoFile) async {
if (currentLocation == null) {
print('Location not available');
return false;
}
isLoading = true;
notifyListeners();
try {
final success = await _repository.submitPresensi(
type,
currentLocation!.latitude,
currentLocation!.longitude,
photoFile,
keteranganLuarRadius: !isWithinRadius ? reasonOutsideRadius : null,
keteranganPulang: type == 'pulang' ? reasonEarlyCheckout : null,
);
if (success) {
reasonOutsideRadius = null;
reasonEarlyCheckout = null;
}
return success;
} catch (e) {
print('Error submitting attendance: $e');
rethrow;
} finally {
isLoading = false;
notifyListeners();
}
}
void setMonth(int month) {
selectedMonth = month;
fetchHistory();
notifyListeners();
}
Future<bool> resubmitPresensi(int idPresensi, String keterangan) async {
isLoading = true;
notifyListeners();
try {
final success = await _repository.resubmitPresensi(idPresensi, keterangan);
if (success) {
await fetchHistory();
}
return success;
} catch (e) {
rethrow;
} finally {
isLoading = false;
notifyListeners();
}
}
void reset() {
reasonOutsideRadius = null;
reasonEarlyCheckout = null;
notifyListeners();
}
}

View File

@ -0,0 +1,134 @@
import 'dart:io';
import 'package:flutter/material.dart';
import '../../repositories/auth_repository.dart';
import '../../models/user_model.dart';
import '../../core/cache_manager.dart';
class AuthProvider extends ChangeNotifier {
final AuthRepository _repository = AuthRepository();
UserModel? _user;
UserModel? get user => _user;
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _errorMessage;
String? get errorMessage => _errorMessage;
bool get isLoggedIn => _user != null;
Future<bool> login(String email, String password) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final result = await _repository.login(email, password);
if (result['success'] == true) {
final data = result['data'];
final token = data['token'];
final userData = data['user'];
await CacheManager.authBox.put('auth_token', token);
await CacheManager.authBox.put('user_data', userData);
_user = UserModel.fromJson(userData);
_isLoading = false;
notifyListeners();
return true;
} else {
_errorMessage = result['message'] ?? 'Login gagal';
_isLoading = false;
notifyListeners();
return false;
}
} catch (e) {
_errorMessage = e.toString();
_isLoading = false;
notifyListeners();
return false;
}
}
Future<void> refreshUser() async {
try {
final userData = await _repository.getUser();
if (userData['success'] == true && userData['data'] != null) {
_user = UserModel.fromJson(userData['data']);
await CacheManager.authBox.put('user_data', userData['data']);
notifyListeners();
}
} catch (e) {
}
}
Future<bool> changePassword(String currentPassword, String newPassword, String confirmPassword) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final result = await _repository.changePassword(
currentPassword,
newPassword,
confirmPassword
);
_isLoading = false;
if (result['success'] == true) {
notifyListeners();
return true;
} else {
_errorMessage = result['message'] ?? 'Gagal mengubah password';
notifyListeners();
return false;
}
} catch (e) {
_isLoading = false;
_errorMessage = e.toString();
notifyListeners();
return false;
}
}
Future<bool> updateProfile({String? noTelp, String? alamat, File? foto}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final result = await _repository.updateProfile(
noTelp: noTelp,
alamat: alamat,
foto: foto,
);
_isLoading = false;
if (result['success'] == true) {
await refreshUser();
notifyListeners();
return true;
} else {
_errorMessage = result['message'] ?? 'Gagal memperbarui profil';
notifyListeners();
return false;
}
} catch (e) {
_isLoading = false;
_errorMessage = e.toString();
notifyListeners();
return false;
}
}
void logout() {
_user = null;
notifyListeners();
}
}

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import '../models/schedule_model.dart';
import '../repositories/home_repository.dart';
class CalendarProvider extends ChangeNotifier {
final HomeRepository _repository = HomeRepository();
bool _isLoading = false;
bool get isLoading => _isLoading;
List<ScheduleModel> _schedules = [];
List<ScheduleModel> get schedules => _schedules;
String? _errorMessage;
String? get errorMessage => _errorMessage;
Future<void> fetchMonthlySchedule(int month, int year) async {
print("--- FETCHING JADWAL BULAN $month TAHUN $year ---");
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final result = await _repository.getMonthlySchedule(month, year);
_schedules = result;
print("--- SUKSES: DAPAT ${_schedules.length} DATA ---");
} catch (e) {
print("--- ERROR: $e ---");
_errorMessage = e.toString();
_schedules = [];
} finally {
_isLoading = false;
notifyListeners();
}
}
ScheduleModel? getScheduleByDate(DateTime date) {
if (_schedules.isEmpty) return null;
String dateKey = "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}";
try {
return _schedules.firstWhere((s) => s.tanggal == dateKey);
} catch (e) {
return null;
}
}
void clearData() {
_schedules = [];
notifyListeners();
}
}

View File

@ -0,0 +1,204 @@
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
import '../repositories/face_repository.dart';
import '../core/utils/camera_utils.dart';
enum EnrollmentStep { depan, kanan, kiri, bawah, selesai }
class FaceProvider with ChangeNotifier {
final FaceRepository _repository = FaceRepository();
final FaceDetector _faceDetector = FaceDetector(
options: FaceDetectorOptions(
enableClassification: false,
enableLandmarks: false,
enableContours: false,
enableTracking: true,
performanceMode: FaceDetectorMode.accurate,
),
);
EnrollmentStep _currentStep = EnrollmentStep.depan;
bool _isProcessing = false;
bool _isUploading = false;
String _instructionText = "Hadapkan wajah lurus ke depan";
File? _fotoDepan;
File? _fotoKanan;
File? _fotoKiri;
File? _fotoBawah;
EnrollmentStep get currentStep => _currentStep;
bool get isUploading => _isUploading;
String get instructionText => _instructionText;
void reset() {
_currentStep = EnrollmentStep.depan;
_instructionText = "Hadapkan wajah lurus ke depan";
_fotoDepan = null;
_fotoKanan = null;
_fotoKiri = null;
_fotoBawah = null;
_isProcessing = false;
_isUploading = false;
notifyListeners();
}
Future<void> processCameraImage(CameraImage image, CameraDescription camera, InputImageRotation rotation) async {
if (_isProcessing || _currentStep == EnrollmentStep.selesai) return;
_isProcessing = true;
try {
final inputImage = CameraUtils.inputImageFromCameraImage(
image: image,
camera: camera,
rotation: rotation,
);
final List<Face> faces = await _faceDetector.processImage(inputImage);
if (faces.isNotEmpty) {
final Face face = faces.first;
_checkPoseAndCapture(face, image);
} else {
}
} catch (e) {
debugPrint("Error processing face: $e");
} finally {
_isProcessing = false;
}
}
void _checkPoseAndCapture(Face face, CameraImage originalImage) async {
const double thresholdFront = 10.0;
const double thresholdSide = 15.0;
const double thresholdDown = 5.0;
double? rotY = face.headEulerAngleY;
double? rotX = face.headEulerAngleX;
debugPrint("ROT X (Atas/Bawah): $rotX | ROT Y (Kiri/Kanan): $rotY");
if (rotY == null || rotX == null) return;
bool isPoseValid = false;
switch (_currentStep) {
case EnrollmentStep.depan:
if (rotY.abs() < thresholdFront && rotX.abs() < thresholdFront) {
isPoseValid = true;
}
break;
case EnrollmentStep.kanan:
if (rotY < -thresholdSide) {
isPoseValid = true;
}
break;
case EnrollmentStep.kiri:
if (rotY > thresholdSide) {
isPoseValid = true;
}
break;
case EnrollmentStep.bawah:
bool isLookingForward = rotY.abs() < thresholdFront;
if (rotX < -thresholdDown && isLookingForward) {
isPoseValid = true;
}
break;
default:
break;
}
if (isPoseValid) {
await _captureImage(originalImage);
}
}
Future<void> _captureImage(CameraImage image) async {
_isProcessing = true;
if (_currentStep == EnrollmentStep.depan) {
_currentStep = EnrollmentStep.kanan;
_instructionText = "Putar wajah perlahan ke KANAN";
} else if (_currentStep == EnrollmentStep.kanan) {
_currentStep = EnrollmentStep.kiri;
_instructionText = "Putar wajah perlahan ke KIRI";
} else if (_currentStep == EnrollmentStep.kiri) {
_currentStep = EnrollmentStep.bawah;
_instructionText = "Tundukkan kepala ke BAWAH";
} else if (_currentStep == EnrollmentStep.bawah) {
_currentStep = EnrollmentStep.selesai;
_instructionText = "Data lengkap. Mengunggah...";
}
notifyListeners();
await Future.delayed(const Duration(seconds: 2));
_isProcessing = false;
}
void saveCapturedFile(File file) {
if (_fotoDepan == null) _fotoDepan = file;
else if (_fotoKanan == null) _fotoKanan = file;
else if (_fotoKiri == null) _fotoKiri = file;
else if (_fotoBawah == null) {
_fotoBawah = file;
if (_currentStep == EnrollmentStep.selesai) {
submitEnrollment();
}
}
}
Future<void> submitEnrollment() async {
if (_fotoDepan == null || _fotoKanan == null || _fotoKiri == null || _fotoBawah == null) {
_instructionText = "Gagal: Foto tidak lengkap.";
notifyListeners();
return;
}
_isUploading = true;
notifyListeners();
try {
await _repository.enrollFace(
fotoDepan: _fotoDepan!,
fotoKanan: _fotoKanan!,
fotoKiri: _fotoKiri!,
fotoBawah: _fotoBawah!,
);
_instructionText = "Pendaftaran Berhasil!";
} catch (e) {
_instructionText = "Gagal Upload: ${e.toString()}";
_currentStep = EnrollmentStep.depan;
_fotoDepan = null;
_fotoKanan = null;
_fotoKiri = null;
_fotoBawah = null;
} finally {
_isUploading = false;
notifyListeners();
}
}
Map<String, dynamic> _faceStatus = {};
Map<String, dynamic> get faceStatus => _faceStatus;
Future<void> loadFaceStatus() async {
try {
final status = await _repository.getFaceStatus();
_faceStatus = status;
notifyListeners();
} catch (e) {
debugPrint("Error loading face status: $e");
}
}
@override
void dispose() {
_faceDetector.close();
super.dispose();
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import '../../repositories/home_repository.dart';
import '../../models/user_model.dart';
import '../../models/presensi_model.dart';
import '../../models/announcement_model.dart';
class HomeProvider extends ChangeNotifier {
final HomeRepository _repository = HomeRepository();
UserModel? user;
int poinLembur = 0;
PresensiModel? presensiToday;
List<AnnouncementModel> pengumumanList = [];
int sisaCuti = 0;
int izinCount = 0;
int alphaCount = 0;
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _errorMessage;
String? get errorMessage => _errorMessage;
Future<void> fetchDashboardData() async {
_isLoading = true;
notifyListeners();
try {
final data = await _repository.getDashboardData();
user = data['user'];
poinLembur = data['poin'];
presensiToday = data['presensi_today'];
pengumumanList = data['pengumuman_list'];
sisaCuti = data['sisa_cuti'] ?? 0;
izinCount = data['izin_count'] ?? 0;
alphaCount = data['alpha_count'] ?? 0;
_isLoading = false;
} catch (e) {
_errorMessage = e.toString();
_isLoading = false;
}
notifyListeners();
}
}

View File

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import '../repositories/home_repository.dart';
import '../models/announcement_model.dart';
class NotificationProvider extends ChangeNotifier {
final HomeRepository _repository = HomeRepository();
List<AnnouncementModel> notifications = [];
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _errorMessage;
String? get errorMessage => _errorMessage;
Future<void> fetchNotifications() async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final data = await _repository.getPengumuman();
notifications = data;
_isLoading = false;
} catch (e) {
_errorMessage = e.toString();
_isLoading = false;
}
notifyListeners();
}
}

View File

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import '../../repositories/pengajuan_repository.dart';
import '../../repositories/lembur_repository.dart';
import '../../repositories/signature_repository.dart';
import '../../models/pengajuan_model.dart';
class PengajuanProvider extends ChangeNotifier {
final PengajuanRepository _repository = PengajuanRepository();
final LemburRepository _lemburRepository = LemburRepository();
final SignatureRepository _signatureRepository = SignatureRepository();
List<PengajuanModel> listPengajuan = [];
bool isLoading = false;
int selectedTabIndex = 0;
bool hasSignature = false;
Future<void> checkSignatureStatus() async {
final result = await _signatureRepository.getActiveSignature();
hasSignature = result['success'] == true;
notifyListeners();
}
void setTabIndex(int index) {
selectedTabIndex = index;
fetchPengajuan();
notifyListeners();
}
Future<void> fetchPengajuan() async {
isLoading = true;
notifyListeners();
try {
String status = 'approved';
if (selectedTabIndex == 1) status = 'pending';
if (selectedTabIndex == 2) status = 'rejected';
final submissions = await _repository.getPengajuan(status: status);
print('📋 Submissions count: ${submissions.length}');
final lemburResult = await _lemburRepository.getLemburHistory();
print('🔥 Lembur API result: $lemburResult');
List<PengajuanModel> lemburList = [];
if (lemburResult['success'] == true && lemburResult['data'] != null) {
print('✅ Lembur success = true');
final dynamic lemburData = lemburResult['data'];
print('📦 Lembur data type: ${lemburData.runtimeType}');
print('📦 Lembur data: $lemburData');
List rawData = [];
if (lemburData is List) {
rawData = lemburData;
print('✅ Detected as List, count: ${rawData.length}');
} else if (lemburData is Map && lemburData['data'] != null) {
rawData = lemburData['data'];
print('✅ Detected as Map with data, count: ${rawData.length}');
}
print('🔍 Raw lembur count before filter: ${rawData.length}');
lemburList = rawData
.map((json) => PengajuanModel.fromLemburJson(json))
.where((item) {
print('🔍 Lembur item status: ${item.status}, target: $status');
return item.status == status;
})
.toList();
print('📊 Lembur count after filter: ${lemburList.length}');
} else {
print('❌ Lembur failed or no data');
print(' success: ${lemburResult['success']}');
print(' data: ${lemburResult['data']}');
}
listPengajuan = [...submissions, ...lemburList];
print('📊 Total pengajuan count: ${listPengajuan.length}');
listPengajuan.sort((a, b) => b.tanggalPengajuan.compareTo(a.tanggalPengajuan));
} catch (e, stack) {
print('❌ Error in fetchPengajuan: $e');
print(stack);
} finally {
isLoading = false;
notifyListeners();
}
}
Future<bool> submitCuti(Map<String, dynamic> data) async {
return await _submitGeneric('Cuti', data);
}
Future<bool> submitSakit(Map<String, dynamic> data) async {
return await _submitGeneric('Sakit', data);
}
Future<bool> submitIzin(Map<String, dynamic> data) async {
return await _submitGeneric('Izin', data);
}
Future<bool> submitLembur(Map<String, dynamic> data) async {
isLoading = true;
notifyListeners();
try {
final dateParts = (data['date'] as String).split('/');
final tanggalLembur = '${dateParts[2]}-${dateParts[1].padLeft(2, '0')}-${dateParts[0].padLeft(2, '0')}';
final result = await _lemburRepository.submitLembur(
tanggalLembur: tanggalLembur,
jamMulai: _convertTo24Hour(data['start_time']),
jamSelesai: _convertTo24Hour(data['end_time']),
keterangan: data['reason'],
idKompensasi: data['id_kompensasi'],
);
return result['success'] == true;
} catch(e) {
print('Error submitting lembur: $e');
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
String _convertTo24Hour(String time12h) {
if (!time12h.contains('AM') && !time12h.contains('PM')) {
return time12h;
}
final parts = time12h.split(' ');
final timeParts = parts[0].split(':');
int hour = int.parse(timeParts[0]);
final minute = timeParts[1];
final period = parts[1];
if (period == 'PM' && hour != 12) hour += 12;
if (period == 'AM' && hour == 12) hour = 0;
return '${hour.toString().padLeft(2, '0')}:$minute';
}
Future<bool> _submitGeneric(String type, Map<String, dynamic> data) async {
isLoading = true;
notifyListeners();
try {
return await _repository.submitPengajuan(type, data);
} catch(e) {
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
}

View File

@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import '../repositories/poin_repository.dart';
class PoinProvider extends ChangeNotifier {
final PoinRepository _repository;
PoinProvider({PoinRepository? repository})
: _repository = repository ?? PoinRepository();
bool _isLoading = false;
String? _errorMessage;
int? _totalPoin;
int? _expiringPoints;
DateTime? _expiryDate;
List<Map<String, dynamic>> _pointHistory = [];
TimeOfDay? _shiftStart;
TimeOfDay? _shiftEnd;
String? _namaShift;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
int? get totalPoin => _totalPoin;
int? get expiringPoints => _expiringPoints;
DateTime? get expiryDate => _expiryDate;
List<Map<String, dynamic>> get pointHistory => _pointHistory;
TimeOfDay? get shiftStart => _shiftStart;
TimeOfDay? get shiftEnd => _shiftEnd;
String? get namaShift => _namaShift;
Future<void> loadExpiringPoints() async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final result = await _repository.getExpiringPoints();
if (result['success']) {
_totalPoin = result['data']['total_poin'] as int?;
final expiring = result['data']['expiring_points'];
if (expiring != null) {
_expiringPoints = expiring['poin'] as int?;
if (expiring['tanggal_kadaluarsa'] != null) {
_expiryDate = DateTime.tryParse(expiring['tanggal_kadaluarsa']);
}
}
_errorMessage = null;
} else {
_errorMessage = result['message'];
}
_isLoading = false;
notifyListeners();
} catch (e) {
_errorMessage = 'Terjadi kesalahan: ${e.toString()}';
_isLoading = false;
notifyListeners();
}
}
Future<void> loadPointHistory() async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final result = await _repository.getPointHistory();
if (result['success']) {
_pointHistory = List<Map<String, dynamic>>.from(
result['data']['history'] ?? []
);
_errorMessage = null;
} else {
_errorMessage = result['message'];
}
_isLoading = false;
notifyListeners();
} catch (e) {
_errorMessage = 'Terjadi kesalahan: ${e.toString()}';
_isLoading = false;
notifyListeners();
}
}
Future<bool> checkShiftSchedule(DateTime date) async {
_isLoading = true;
_errorMessage = null;
_shiftStart = null;
_shiftEnd = null;
_namaShift = null;
notifyListeners();
try {
String dateStr = "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}";
final result = await _repository.checkSchedule(dateStr);
if (result['success']) {
final data = result['data'];
if (data['jam_mulai'] != null) {
final parts = (data['jam_mulai'] as String).split(':');
_shiftStart = TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
}
if (data['jam_selesai'] != null) {
final parts = (data['jam_selesai'] as String).split(':');
_shiftEnd = TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1]));
}
_namaShift = data['nama_shift'];
_errorMessage = null;
_isLoading = false;
notifyListeners();
return true;
} else {
_errorMessage = result['message'];
_isLoading = false;
notifyListeners();
return false;
}
} catch (e) {
_errorMessage = 'Gagal memuat jadwal: ${e.toString()}';
_isLoading = false;
notifyListeners();
return false;
}
}
Future<Map<String, dynamic>> tukarPoin({
required int jumlah,
required String keterangan,
required int idPengurangan,
String? jamMasukCustom,
String? jamPulangCustom,
}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final result = await _repository.tukarPoin(
jumlah: jumlah,
keterangan: keterangan,
idPengurangan: idPengurangan,
jamMasukCustom: jamMasukCustom,
jamPulangCustom: jamPulangCustom,
);
if (result['success']) {
await Future.wait([
loadExpiringPoints(),
loadPointHistory(),
]);
_errorMessage = null;
} else {
_errorMessage = result['message'];
}
_isLoading = false;
notifyListeners();
return result;
} catch (e) {
_errorMessage = 'Terjadi kesalahan: ${e.toString()}';
_isLoading = false;
notifyListeners();
return {
'success': false,
'message': _errorMessage,
};
}
}
}

View File

@ -0,0 +1,76 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../repositories/signature_repository.dart';
import '../models/tanda_tangan_model.dart';
class SignatureProvider extends ChangeNotifier {
final SignatureRepository _repository = SignatureRepository();
TandaTanganModel? currentSignature;
bool isLoading = false;
String? errorMessage;
Future<void> fetchSignature() async {
isLoading = true;
errorMessage = null;
notifyListeners();
try {
final result = await _repository.getActiveSignature();
if (result['success'] == true && result['data'] != null) {
currentSignature = TandaTanganModel.fromJson(result['data']);
} else {
currentSignature = null;
}
} catch (e) {
errorMessage = e.toString();
currentSignature = null;
} finally {
isLoading = false;
notifyListeners();
}
}
Future<bool> uploadSignature(Uint8List pngBytes) async {
isLoading = true;
notifyListeners();
try {
final result = await _repository.uploadSignature(pngBytes);
if (result['success'] == true) {
await fetchSignature();
return true;
}
errorMessage = result['message'];
return false;
} catch (e) {
errorMessage = e.toString();
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
Future<bool> deleteSignature() async {
if (currentSignature == null) return false;
isLoading = true;
notifyListeners();
try {
final result = await _repository.deleteSignature(currentSignature!.id);
if (result['success'] == true) {
currentSignature = null;
notifyListeners();
return true;
}
return false;
} catch (e) {
return false;
} finally {
isLoading = false;
notifyListeners();
}
}
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import '../repositories/surat_izin_repository.dart';
import '../models/surat_izin_model.dart';
class SuratIzinProvider extends ChangeNotifier {
final SuratIzinRepository _repository = SuratIzinRepository();
List<SuratIzinModel> listSurat = [];
SuratIzinModel? selectedSurat;
bool isLoading = false;
bool isLoadingDetail = false;
String? errorMessage;
Future<void> fetchSurat() async {
isLoading = true;
errorMessage = null;
notifyListeners();
try {
listSurat = await _repository.getSuratIzin();
} catch (e) {
errorMessage = e.toString();
listSurat = [];
} finally {
isLoading = false;
notifyListeners();
}
}
Future<void> fetchDetail(String id) async {
isLoadingDetail = true;
notifyListeners();
try {
selectedSurat = await _repository.getSuratIzinDetail(id);
} catch (e) {
selectedSurat = null;
} finally {
isLoadingDetail = false;
notifyListeners();
}
}
Future<Map<String, dynamic>> createSurat({
required String idIzin,
required String isiSurat,
}) async {
isLoading = true;
notifyListeners();
try {
final result = await _repository.createSuratIzin(
idIzin: idIzin,
isiSurat: isiSurat,
);
if (result['success'] == true) {
await fetchSurat();
}
return result;
} catch (e) {
return {'success': false, 'message': e.toString()};
} finally {
isLoading = false;
notifyListeners();
}
}
}

View File

@ -0,0 +1,123 @@
import 'dart:io';
import 'package:dio/dio.dart';
import '../core/constants/api_url.dart';
import '../services/api_client.dart';
import '../models/presensi_model.dart';
class AttendanceRepository {
final ApiClient _client = ApiClient();
Future<Map<String, dynamic>> checkRadius(double userLat, double userLng) async {
try {
final response = await _client.dio.post(
'${ApiUrl.baseUrl}/presensi/check-radius',
data: {
'latitude': userLat,
'longitude': userLng,
},
);
if (response.statusCode == 200 && response.data['status'] == 'success') {
return response.data['data'];
} else {
throw Exception(response.data['message'] ?? 'Failed to check radius');
}
} catch (e) {
print('Error checking radius: $e');
rethrow;
}
}
Future<bool> submitPresensi(
String type,
double lat,
double lng,
File photoFile, {
String? keteranganLuarRadius,
String? keteranganPulang,
}) async {
try {
String fileName = photoFile.path.split('/').last;
FormData formData = FormData.fromMap({
'status': type,
'latitude': lat,
'longitude': lng,
'foto': await MultipartFile.fromFile(
photoFile.path,
filename: fileName,
),
if (keteranganLuarRadius != null)
'keterangan_luar_radius': keteranganLuarRadius,
if (keteranganPulang != null)
'keterangan_pulang': keteranganPulang,
});
final response = await _client.dio.post(
'${ApiUrl.baseUrl}/presensi',
data: formData,
options: Options(
headers: {
'Accept': 'application/json',
},
),
);
if (response.statusCode == 200 &&
(response.data['success'] == true || response.data['status'] == 'success')) {
return true;
} else {
throw Exception(response.data['message'] ?? 'Gagal mengirim absensi');
}
} catch (e) {
if (e is DioException) {
if (e.response?.statusCode == 401) {
throw Exception("Wajah tidak dikenali. Silakan coba lagi.");
}
if (e.response?.statusCode == 422) {
throw Exception("Kualitas foto buruk atau data tidak lengkap.");
}
throw Exception(e.response?.data['message'] ?? e.message);
}
print('Error submitting attendance: $e');
rethrow;
}
}
Future<List<PresensiHistoryModel>> getHistory() async {
try {
final response = await _client.dio.get('${ApiUrl.baseUrl}/presensi/history');
if (response.statusCode == 200 && response.data['success'] == true) {
final List data = response.data['data']['data'] ?? [];
return data.map((json) => PresensiHistoryModel.fromJson(json)).toList();
} else {
throw Exception(response.data['message'] ?? 'Failed to load history');
}
} catch (e) {
print('Error fetching history: $e');
return [];
}
}
Future<bool> resubmitPresensi(int idPresensi, String keterangan) async {
try {
final response = await _client.dio.post(
'${ApiUrl.baseUrl}/presensi/$idPresensi/resubmit',
data: {'keterangan': keterangan},
);
if (response.statusCode == 200 &&
(response.data['success'] == true || response.data['status'] == 'success')) {
return true;
} else {
throw Exception(response.data['message'] ?? 'Gagal mengajukan ulang');
}
} catch (e) {
if (e is DioException) {
throw Exception(e.response?.data['message'] ?? e.message);
}
rethrow;
}
}
}

View File

@ -0,0 +1,82 @@
import '../services/api_client.dart';
import 'package:dio/dio.dart';
import 'dart:io';
class AuthRepository {
final ApiClient _apiClient = ApiClient();
Future<Map<String, dynamic>> login(String email, String password) async {
try {
final response = await _apiClient.dio.post('/login', data: {
'email': email,
'password': password,
});
return response.data;
} on DioException catch (e) {
if (e.response != null) {
throw e.response?.data['message'] ?? 'Login gagal';
}
throw 'Terjadi kesalahan koneksi';
}
}
Future<Map<String, dynamic>> getUser() async {
try {
final response = await _apiClient.dio.get('/user');
return response.data;
} on DioException catch (e) {
if (e.response != null) {
throw e.response?.data['message'] ?? 'Gagal mengambil data user';
}
throw 'Terjadi kesalahan koneksi';
}
}
Future<Map<String, dynamic>> changePassword(String currentPassword, String newPassword, String confirmPassword) async {
try {
final response = await _apiClient.dio.post('/profile/password', data: {
'current_password': currentPassword,
'new_password': newPassword,
'new_password_confirmation': confirmPassword,
});
return response.data;
} on DioException catch (e) {
if (e.response != null) {
throw e.response?.data['message'] ?? 'Gagal mengubah password';
}
throw 'Terjadi kesalahan koneksi';
}
}
Future<Map<String, dynamic>> updateProfile({
String? noTelp,
String? alamat,
File? foto,
}) async {
try {
final Map<String, dynamic> formMap = {};
if (noTelp != null) formMap['no_telp'] = noTelp;
if (alamat != null) formMap['alamat'] = alamat;
if (foto != null) {
formMap['foto'] = await MultipartFile.fromFile(
foto.path,
filename: foto.path.split('/').last,
);
}
final formData = FormData.fromMap(formMap);
final response = await _apiClient.dio.post('/profile/update',
data: formData,
options: Options(headers: {'Accept': 'application/json'}),
);
return response.data;
} on DioException catch (e) {
if (e.response != null) {
throw e.response?.data['message'] ?? 'Gagal memperbarui profil';
}
throw 'Terjadi kesalahan koneksi';
}
}
}

View File

@ -0,0 +1,92 @@
import 'dart:io';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../core/constants/api_url.dart';
import '../core/cache_manager.dart';
import '../core/error_handler.dart';
class FaceRepository {
Future<Map<String, dynamic>> enrollFace({
required File fotoDepan,
required File fotoKanan,
required File fotoKiri,
required File fotoBawah,
}) async {
try {
final token = CacheManager.authBox.get('auth_token');
var request = http.MultipartRequest(
'POST',
Uri.parse('${ApiUrl.baseUrl}/face/enroll'),
);
request.headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
});
request.files.add(await http.MultipartFile.fromPath('foto_depan', fotoDepan.path));
request.files.add(await http.MultipartFile.fromPath('foto_kanan', fotoKanan.path));
request.files.add(await http.MultipartFile.fromPath('foto_kiri', fotoKiri.path));
request.files.add(await http.MultipartFile.fromPath('foto_bawah', fotoBawah.path));
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200 || response.statusCode == 201) {
return {'status': true, 'message': 'Pendaftaran wajah berhasil'};
} else {
throw Exception('Gagal mendaftarkan wajah: ${response.statusCode}');
}
} catch (e) {
rethrow;
}
}
Future<Map<String, dynamic>> verifyFace(File imageFile) async {
final token = CacheManager.authBox.get('auth_token');
var request = http.MultipartRequest(
'POST',
Uri.parse('${ApiUrl.baseUrl}/face/verify'),
);
request.headers.addAll({
'Authorization': 'Bearer $token',
'Accept': 'application/json',
});
request.files.add(await http.MultipartFile.fromPath('foto', imageFile.path));
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);
final body = jsonDecode(response.body);
if (response.statusCode == 200 && body['success'] == true) {
return body['data'];
}
throw Exception(body['message'] ?? 'Verifikasi gagal');
}
Future<Map<String, dynamic>> getFaceStatus() async {
try {
final token = CacheManager.authBox.get('auth_token');
final response = await http.get(
Uri.parse('${ApiUrl.baseUrl}/face/status'),
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['data'];
}
throw Exception('Gagal mengambil status: ${response.statusCode}');
} catch (e) {
rethrow;
}
}
}

View File

@ -0,0 +1,115 @@
import '../services/api_client.dart';
import '../../models/user_model.dart';
import '../../models/presensi_model.dart';
import '../../models/announcement_model.dart';
import '../../models/schedule_model.dart';
class HomeRepository {
final ApiClient _apiClient = ApiClient();
Future<Map<String, dynamic>> getDashboardData() async {
try {
final response = await _apiClient.dio.get('/dashboard');
if (response.data['success'] == true) {
final data = response.data['data'];
final user = UserModel.fromJson(data['user']);
final statistik = data['statistik_absensi'];
final izinCount = statistik?['izin'] ?? 0;
final alphaCount = statistik?['alpha'] ?? 0;
PresensiModel? presensiToday;
if (data['jadwal_hari_ini'] != null) {
final jadwal = data['jadwal_hari_ini'];
final presensi = data['presensi_hari_ini'];
final bool sudahMasuk = presensi != null && presensi['jam_masuk'] != null;
final bool sudahPulang = presensi != null && presensi['jam_pulang'] != null;
presensiToday = PresensiModel(
tanggal: jadwal['hari'] ?? '-',
shift: jadwal['shift'] ?? '-',
jamMasuk: sudahMasuk ? presensi['jam_masuk'] : jadwal['jam_masuk'] ?? '-',
jamPulang: sudahPulang ? presensi['jam_pulang'] : jadwal['jam_pulang'] ?? '-',
lokasi: '-',
statusJadwal: jadwal['status_jadwal'],
isAdjusted: jadwal['is_adjusted'] ?? false,
adjustmentNote: jadwal['note'],
sudahAbsenMasuk: sudahMasuk,
sudahAbsenPulang: sudahPulang,
statusMasuk: presensi?['keterangan']?.toString(),
statusPulang: presensi?['keterangan_pulang']?.toString(),
jadwalJamMasuk: jadwal['jam_masuk']?.toString(),
jadwalJamPulang: jadwal['jam_pulang']?.toString(),
kantorNama: jadwal['kantor_nama']?.toString(),
kantorLat: (jadwal['kantor_lat'] is num) ? (jadwal['kantor_lat'] as num).toDouble() : null,
kantorLon: (jadwal['kantor_lon'] is num) ? (jadwal['kantor_lon'] as num).toDouble() : null,
kantorRadius: (jadwal['kantor_radius'] is num) ? (jadwal['kantor_radius'] as num).toDouble() : null,
);
}
final pengumumanList = await getPengumuman();
return {
'user': user,
'poin': data['poin'] ?? 0,
'presensi_today': presensiToday,
'pengumuman_list': pengumumanList,
'sisa_cuti': user.sisaCuti,
'izin_count': izinCount,
'alpha_count': alphaCount,
};
} else {
throw Exception(response.data['message'] ?? 'Failed to load dashboard');
}
} catch (e) {
print('Error fetching dashboard: $e');
return {
'user': null,
'poin': 0,
'presensi_today': null,
'pengumuman_list': <AnnouncementModel>[],
'sisa_cuti': 0,
'izin_count': 0,
'alpha_count': 0,
};
}
}
Future<List<AnnouncementModel>> getPengumuman() async {
try {
final response = await _apiClient.dio.get('/pengumuman');
if (response.data['success'] == true) {
final List data = response.data['data'];
return data.map((json) => AnnouncementModel.fromJson(json)).toList();
}
return [];
} catch (e) {
print('Error fetching pengumuman: $e');
return [];
}
}
Future<List<ScheduleModel>> getMonthlySchedule(int month, int year) async {
try {
final response = await _apiClient.dio.get(
'/schedule/monthly',
queryParameters: {'month': month.toString(), 'year': year.toString()},
);
if (response.statusCode == 200) {
final List data = response.data['data'];
print("Data Jadwal Diterima: ${data.length} item");
return data.map((json) => ScheduleModel.fromJson(json)).toList();
}
return [];
} catch (e) {
print("Error Repository: $e");
throw Exception('Gagal memuat jadwal bulanan');
}
}
}

View File

@ -0,0 +1,50 @@
import '../services/api_client.dart';
class LemburRepository {
final ApiClient _apiClient = ApiClient();
Future<Map<String, dynamic>> submitLembur({
required String tanggalLembur,
required String jamMulai,
required String jamSelesai,
String? keterangan,
int? idKompensasi,
}) async {
try {
final response = await _apiClient.dio.post('/lembur', data: {
'tanggal_lembur': tanggalLembur,
'jam_mulai': jamMulai,
'jam_selesai': jamSelesai,
'keterangan': keterangan,
'id_kompensasi': idKompensasi,
});
return {
'success': true,
'message': response.data['message'] ?? 'Lembur berhasil diajukan',
'data': response.data['data'],
};
} catch (e) {
return {
'success': false,
'message': 'Gagal mengajukan lembur: ${e.toString()}',
};
}
}
Future<Map<String, dynamic>> getLemburHistory() async {
try {
final response = await _apiClient.dio.get('/lembur/history');
return {
'success': true,
'data': response.data['data'],
};
} catch (e) {
return {
'success': false,
'message': 'Gagal memuat riwayat lembur',
};
}
}
}

View File

@ -0,0 +1,70 @@
import 'package:dio/dio.dart';
import 'dart:io';
import '../services/api_client.dart';
import '../../models/pengajuan_model.dart';
class PengajuanRepository {
final ApiClient _apiClient = ApiClient();
Future<List<PengajuanModel>> getPengajuan({String status = 'pending'}) async {
try {
final response = await _apiClient.dio.get(
'/submission/history',
queryParameters: {'status': status},
);
if (response.statusCode == 200 && response.data['success'] == true) {
final List data = response.data['data']['data'] ?? [];
return data.map((json) => PengajuanModel.fromJson(json)).toList();
}
return [];
} catch (e) {
print('Error fetching pengajuan: $e');
return [];
}
}
Future<bool> submitPengajuan(String jenis, Map<String, dynamic> data) async {
try {
final typesResponse = await _apiClient.dio.get('/submission/types');
if (typesResponse.data['success'] != true) return false;
final List types = typesResponse.data['data'];
final jenisObj = types.firstWhere(
(t) => t['nama_izin'].toString().toLowerCase() == jenis.toLowerCase(),
orElse: () => null,
);
if (jenisObj == null) throw Exception('Jenis izin tidak ditemukan');
final int jenisId = jenisObj['id_jenis_izin'];
FormData formData = FormData.fromMap({
'id_jenis_izin': jenisId,
'tanggal_mulai': data['tanggal_mulai'],
'tanggal_selesai': data['tanggal_selesai'],
'alasan': data['alasan'],
});
if (data['bukti_file'] != null) {
try {
final file = File(data['bukti_file']);
if (await file.exists()) {
formData.files.add(MapEntry(
'bukti_file',
await MultipartFile.fromFile(data['bukti_file']),
));
}
} catch (e) {
print('File upload skipped: $e');
}
}
final response = await _apiClient.dio.post('/submission', data: formData);
return response.data['success'] == true;
} catch (e) {
print('Error submitting pengajuan: $e');
return false;
}
}
}

View File

@ -0,0 +1,143 @@
import 'package:dio/dio.dart';
import '../services/api_client.dart';
class PoinRepository {
final ApiClient _apiClient;
PoinRepository({ApiClient? apiClient})
: _apiClient = apiClient ?? ApiClient();
Future<Map<String, dynamic>> getExpiringPoints() async {
try {
final response = await _apiClient.dio.get('/poin/expiring');
if (response.statusCode == 200 && response.data['success']) {
return {
'success': true,
'data': response.data['data'],
};
} else {
return {
'success': false,
'message': response.data['message'] ?? 'Gagal mengambil data poin.',
};
}
} on DioException catch (e) {
return {
'success': false,
'message': e.response?.data['message'] ?? 'Terjadi kesalahan server.',
};
} catch (e) {
return {
'success': false,
'message': 'Terjadi kesalahan: ${e.toString()}',
};
}
}
Future<Map<String, dynamic>> getPointHistory() async {
try {
final response = await _apiClient.dio.get('/poin/history');
if (response.statusCode == 200 && response.data['success']) {
return {
'success': true,
'data': response.data['data'],
};
} else {
return {
'success': false,
'message': response.data['message'] ?? 'Gagal mengambil riwayat poin.',
};
}
} on DioException catch (e) {
return {
'success': false,
'message': e.response?.data['message'] ?? 'Terjadi kesalahan server.',
};
} catch (e) {
return {
'success': false,
'message': 'Terjadi kesalahan: ${e.toString()}',
};
}
}
Future<Map<String, dynamic>> tukarPoin({
required int jumlah,
required String keterangan,
required int idPengurangan,
String? jamMasukCustom,
String? jamPulangCustom,
}) async {
try {
final Map<String, dynamic> data = {
'jumlah': jumlah,
'keterangan': keterangan,
'id_pengurangan': idPengurangan,
};
if (jamMasukCustom != null) {
data['jam_masuk_custom'] = jamMasukCustom;
}
if (jamPulangCustom != null) {
data['jam_pulang_custom'] = jamPulangCustom;
}
final response = await _apiClient.dio.post('/poin/redeem', data: data);
if (response.statusCode == 200 && response.data['success']) {
return {
'success': true,
'message': response.data['message'] ?? 'Poin berhasil ditukar',
};
} else {
return {
'success': false,
'message': response.data['message'] ?? 'Gagal menukar poin',
};
}
} on DioException catch (e) {
return {
'success': false,
'message': e.response?.data['message'] ?? 'Terjadi kesalahan server',
};
} catch (e) {
return {
'success': false,
'message': 'Terjadi kesalahan: ${e.toString()}',
};
}
}
Future<Map<String, dynamic>> checkSchedule(String tanggal) async {
try {
final response = await _apiClient.dio.get('/schedule/check', queryParameters: {
'tanggal': tanggal,
});
if (response.statusCode == 200 && response.data['success']) {
return {
'success': true,
'data': response.data['data'],
};
} else {
return {
'success': false,
'message': response.data['message'] ?? 'Tidak ada jadwal pada tanggal ini',
};
}
} on DioException catch (e) {
return {
'success': false,
'message': e.response?.data['message'] ?? 'Gagal mengecek jadwal',
};
} catch (e) {
return {
'success': false,
'message': 'Terjadi kesalahan: ${e.toString()}',
};
}
}
}

Some files were not shown because too many files have changed in this diff Show More