This commit is contained in:
BakoL2323 2026-06-19 11:09:54 +07:00
commit 5a0182d69e
172 changed files with 13073 additions and 0 deletions

42
.github/workflows/dart.yml vendored Normal file
View File

@ -0,0 +1,42 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Dart
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Note: This workflow uses the latest stable version of the Dart SDK.
# You can specify other versions if desired, see documentation here:
# https://github.com/dart-lang/setup-dart/blob/main/README.md
# - uses: dart-lang/setup-dart@v1
- uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603
- name: Install dependencies
run: dart pub get
# Uncomment this step to verify the use of 'dart format' on each commit.
# - name: Verify formatting
# run: dart format --output=none --set-exit-if-changed .
# Consider passing '--fatal-infos' for slightly stricter analysis.
- name: Analyze project source
run: dart analyze
# Your project will need to have tests in test/ and a dependency on
# package:test for this step to succeed. Note that Flutter projects will
# want to change this to 'flutter test'.
- name: Run tests
run: dart test

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# 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

30
.metadata Normal file
View File

@ -0,0 +1,30 @@
# 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: "582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
- platform: android
create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
# 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'

25
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "sidakpelem-mobile-main",
"request": "launch",
"type": "dart"
},
{
"name": "sidakpelem-mobile-main (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "sidakpelem-mobile-main (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# sidak_desa_mobile
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

33
analysis_options.yaml Normal file
View File

@ -0,0 +1,33 @@
# 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.
analyzer:
errors:
avoid_print: ignore
deprecated_member_use: ignore
unused_import: ignore
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,44 @@
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.sidak_desa_mobile"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.sidak_desa_mobile"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

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,51 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:label="sidak_desa_mobile"
android:name="${applicationName}"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher">
<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.sidak_desa_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)
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
dev.steenbakker.mobile_scanner.useUnbundled=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.14-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.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

BIN
assets/images/Union.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/images/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
assets/images/gito.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
assets/images/icon_izin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
assets/images/ilustrasi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
assets/images/indro.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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:

BIN
docs/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

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.sidakDesaMobile;
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.sidakDesaMobile.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.sidakDesaMobile.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.sidakDesaMobile.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.sidakDesaMobile;
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.sidakDesaMobile;
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,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
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>

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

@ -0,0 +1,53 @@
<?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>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Sidak Desa 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>sidak_desa_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/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Aplikasi membutuhkan lokasi untuk validasi kehadiran.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Aplikasi membutuhkan lokasi untuk validasi kehadiran.</string>
</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.
}
}

3
lib/core/api/api.dart Normal file
View File

@ -0,0 +1,3 @@
class Apiconfig {
static const String baseUrl = 'https://sidakdesapelem.my.id';
}

View File

@ -0,0 +1,5 @@
import 'package:flutter/material.dart';
class AppColors {
static const Color primaryColor = Color(0xFF077b7a);
}

View File

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:sidak_desa_mobile/core/const/app_colors.dart';
class AppBottomNavbar extends StatelessWidget {
const AppBottomNavbar({
required this.currentIndex,
required this.onTabSelected,
super.key,
this.onCenterTap,
});
final int currentIndex;
final ValueChanged<int> onTabSelected;
final VoidCallback? onCenterTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 96,
child: Stack(
alignment: Alignment.topCenter,
clipBehavior: Clip.none,
children: [
Positioned.fill(
top: 20,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFECECEC),
borderRadius: BorderRadius.circular(24),
boxShadow: const [
BoxShadow(
color: Color(0x26000000),
blurRadius: 8,
offset: Offset(0, -1),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_NavItem(
icon: Icons.menu_book_outlined,
isSelected: currentIndex == 0,
onTap: () => onTabSelected(0),
),
const SizedBox(width: 72),
_NavItem(
icon: Icons.person_outline,
isSelected: currentIndex == 1,
onTap: () => onTabSelected(1),
),
],
),
),
),
GestureDetector(
onTap: onCenterTap,
child: Container(
width: 74,
height: 74,
decoration: const BoxDecoration(
color: AppColors.primaryColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Color(0x33000000),
blurRadius: 10,
offset: Offset(0, 3),
),
],
),
child: const Icon(
Icons.qr_code_scanner_rounded,
color: Colors.white,
size: 34,
),
),
),
],
),
);
}
}
class _NavItem extends StatelessWidget {
const _NavItem({
required this.icon,
required this.isSelected,
required this.onTap,
});
final IconData icon;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: onTap,
splashRadius: 24,
icon: Icon(
icon,
size: 34,
color: isSelected
? AppColors.primaryColor
: AppColors.primaryColor.withOpacity(0.8),
),
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:sidak_desa_mobile/core/const/app_colors.dart';
class AppButton extends StatelessWidget {
const AppButton({
required this.text,
required this.onPressed,
super.key,
this.height = 55,
});
final String text;
final VoidCallback? onPressed;
final double height;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryColor,
minimumSize: Size.fromHeight(height),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
text,
style: const TextStyle(color: Colors.white, fontSize: 18),
),
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:sidak_desa_mobile/core/const/app_colors.dart';
class AppTextField extends StatelessWidget {
const AppTextField({
required this.hintText,
super.key,
this.controller,
this.keyboardType,
this.textInputAction,
this.onChanged,
this.readOnly = false,
});
final String hintText;
final TextEditingController? controller;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final ValueChanged<String>? onChanged;
final bool readOnly;
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
keyboardType: keyboardType,
textInputAction: textInputAction,
onChanged: onChanged,
readOnly: readOnly,
decoration: InputDecoration(
hintText: hintText,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: AppColors.primaryColor, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: AppColors.primaryColor, width: 2),
),
),
);
}
}

View File

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:sidak_desa_mobile/core/const/app_colors.dart';
class AppTextFieldPassword extends StatefulWidget {
const AppTextFieldPassword({
super.key,
this.hintText = 'Password',
this.controller,
this.textInputAction,
this.onChanged,
});
final String hintText;
final TextEditingController? controller;
final TextInputAction? textInputAction;
final ValueChanged<String>? onChanged;
@override
State<AppTextFieldPassword> createState() => _AppTextFieldPasswordState();
}
class _AppTextFieldPasswordState extends State<AppTextFieldPassword> {
bool _obscureText = true;
@override
Widget build(BuildContext context) {
return TextField(
controller: widget.controller,
obscureText: _obscureText,
textInputAction: widget.textInputAction,
onChanged: widget.onChanged,
decoration: InputDecoration(
hintText: widget.hintText,
suffixIcon: IconButton(
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColors.primaryColor,
width: 1,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColors.primaryColor,
width: 2,
),
),
),
);
}
}

View File

@ -0,0 +1,69 @@
import 'package:sidak_desa_mobile/core/api/api.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class AttedanceApi {
Future<Map<String, dynamic>> verifyAttendance({
required String token,
required int userId,
String? deviceInfo,
double? latitude,
double? longitude,
}) async {
final uri = Uri.parse('${Apiconfig.baseUrl}/api/attendance/verify');
final res = await http.post(
uri,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: jsonEncode({
'token': token,
'user_id': userId,
'device_info': deviceInfo, // boleh null
'latitude': latitude,
'longitude': longitude,
}),
);
final data = jsonDecode(res.body);
if (res.statusCode >= 200 && res.statusCode < 300) {
if (data is Map<String, dynamic>) return data;
return {'data': data};
}
// Laravel biasanya kasih message + errors
if (data is Map<String, dynamic>) {
final msg = (data['message'] ?? 'Verifikasi absen gagal').toString();
throw Exception(msg);
}
throw Exception('Verifikasi absen gagal');
}
Future<Map<String, dynamic>> getDailyAttendance({
required int userId,
required String date,
}) async {
final url = Uri.parse(
"${Apiconfig.baseUrl}/api/riwayat/$userId?date=$date",
);
final response = await http.get(
url,
headers: {'Accept': 'application/json'},
);
print("DAILY STATUS: ${response.statusCode}");
print("DAILY BODY: ${response.body}");
final data = jsonDecode(response.body);
if (response.statusCode == 200 && data['ok'] == true) {
return data;
}
throw Exception("Gagal ambil data");
}
}

View File

@ -0,0 +1,84 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../core/api/api.dart';
import '../model/user_model.dart';
class AuthApi {
Future<UserModel> login({
required String email,
required String password,
required String deviceId,
}) async {
final url = Uri.parse('${Apiconfig.baseUrl}/api/mobile/login');
print("FULL URL: $url");
final response = await http.post(
url,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
'email': email,
'password': password,
'device_id': deviceId,
},
);
print("STATUS CODE: ${response.statusCode}");
print("RAW BODY: ${response.body}");
// 🔥 Antisipasi server mati / tidak merespon JSON
if (response.body.isEmpty) {
throw Exception("Server tidak merespon");
}
// 🔥 Cegah error HTML (route salah / 500 Laravel)
if (response.body.startsWith('<!DOCTYPE html>')) {
throw Exception(
"Server mengembalikan HTML. Cek URL atau route Laravel.");
}
final data = jsonDecode(response.body);
// SUKSES
if (response.statusCode == 200 && data['ok'] == true) {
final userJson = data['data'];
// 🔥 inject token ke user model (penting!)
userJson['token'] = data['token'];
// 🔥 Fix URL foto
if (userJson['url_photo'] != null &&
userJson['url_photo'].toString().isNotEmpty &&
!userJson['url_photo'].toString().startsWith('http')) {
userJson['url_photo'] =
"${Apiconfig.baseUrl}/storage/${userJson['url_photo']}";
}
print("FINAL PHOTO URL: ${userJson['url_photo']}");
print("TOKEN: ${data['token']}");
return UserModel.fromJson(userJson);
}
// HANDLE ERROR LEBIH SPESIFIK
if (response.statusCode == 401) {
throw Exception("Email atau password salah");
}
if (response.statusCode == 403) {
throw Exception(data['message'] ??
"Akun sudah digunakan di device lain");
}
if (response.statusCode == 422) {
throw Exception("Validasi gagal");
}
// fallback
throw Exception(data['message'] ?? 'Login gagal');
}
}

View File

@ -0,0 +1,185 @@
import 'dart:convert';
/// =======================
/// USER MODEL
/// =======================
class UserModel {
const UserModel({
this.id,
this.nik,
this.name,
this.tempatLahir,
this.tanggalLahir,
this.email,
this.role,
this.jabatan,
this.phone,
this.noTelepon,
this.employeeId,
this.status,
this.address,
this.alamat,
this.hireDate,
this.jenisKelamin,
this.urlPhoto,
this.password,
});
final int? id;
final String? nik;
final String? name;
final String? tempatLahir;
final String? tanggalLahir;
final String? email;
final String? role;
final String? jabatan;
final String? phone;
final String? noTelepon;
final String? employeeId;
final String? status;
final String? address;
final String? alamat;
final String? hireDate;
final String? jenisKelamin;
final String? urlPhoto;
final String? password;
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: _parseInt(json['id']),
nik: json['nik']?.toString(),
name: json['name']?.toString(),
tempatLahir: json['tempat_lahir']?.toString(),
tanggalLahir: json['tanggal_lahir']?.toString(),
email: json['email']?.toString(),
role: json['role']?.toString(),
jabatan: json['jabatan']?.toString(),
phone: json['phone']?.toString(),
noTelepon: json['no_telepon']?.toString(),
employeeId: json['employee_id']?.toString(),
status: json['status']?.toString(),
address: json['address']?.toString(),
alamat: json['alamat']?.toString(),
hireDate: json['hire_date']?.toString(),
jenisKelamin: json['jenis_kelamin']?.toString(),
urlPhoto: json['url_photo']?.toString(),
password: json['password']?.toString(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'nik': nik,
'name': name,
'tempat_lahir': tempatLahir,
'tanggal_lahir': tanggalLahir,
'email': email,
'role': role,
'jabatan': jabatan,
'phone': phone,
'no_telepon': noTelepon,
'employee_id': employeeId,
'status': status,
'address': address,
'alamat': alamat,
'hire_date': hireDate,
'jenis_kelamin': jenisKelamin,
'url_photo': urlPhoto,
'password': password,
};
}
String toJsonString() => jsonEncode(toJson());
factory UserModel.fromJsonString(String source) =>
UserModel.fromJson(jsonDecode(source));
Null get foto => null;
UserModel copyWith({
int? id,
String? nik,
String? name,
String? tempatLahir,
String? tanggalLahir,
String? email,
String? role,
String? jabatan,
String? phone,
String? noTelepon,
String? employeeId,
String? status,
String? address,
String? alamat,
String? hireDate,
String? jenisKelamin,
String? urlPhoto,
String? password,
}) {
return UserModel(
id: id ?? this.id,
nik: nik ?? this.nik,
name: name ?? this.name,
tempatLahir: tempatLahir ?? this.tempatLahir,
tanggalLahir: tanggalLahir ?? this.tanggalLahir,
email: email ?? this.email,
role: role ?? this.role,
jabatan: jabatan ?? this.jabatan,
phone: phone ?? this.phone,
noTelepon: noTelepon ?? this.noTelepon,
employeeId: employeeId ?? this.employeeId,
status: status ?? this.status,
address: address ?? this.address,
alamat: alamat ?? this.alamat,
hireDate: hireDate ?? this.hireDate,
jenisKelamin: jenisKelamin ?? this.jenisKelamin,
urlPhoto: urlPhoto ?? this.urlPhoto,
password: password ?? this.password,
);
}
static int? _parseInt(dynamic value) {
if (value == null) return null;
if (value is int) return value;
return int.tryParse(value.toString());
}
}
/// =======================
/// DAILY MODEL
/// =======================
class DailyModel {
final int id;
final String type;
final String jenis;
final String tanggal;
final String jam;
final String icon;
final String statusValidasi;
final String sortDate;
DailyModel({
required this.id,
required this.type,
required this.jenis,
required this.tanggal,
required this.jam,
required this.icon,
required this.statusValidasi,
required this.sortDate,
});
factory DailyModel.fromJson(Map<String, dynamic> json) {
return DailyModel(
id: json['id'] ?? 0,
type: json['type'] ?? '',
jenis: json['jenis'] ?? '',
tanggal: json['tanggal'] ?? '',
jam: json['jam'] ?? '',
icon: json['icon'] ?? '',
statusValidasi: json['status_validasi'] ?? '',
sortDate: json['sort_date'] ?? '',
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:shared_preferences/shared_preferences.dart';
import '../model/user_model.dart';
class AuthStorage {
static const _keyUser = 'auth_user';
static const _keyLoggedIn = 'auth_logged_in';
Future<void> saveUser(UserModel user) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyUser, user.toJsonString());
await prefs.setBool(_keyLoggedIn, true);
}
Future<UserModel?> getUser() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_keyUser);
if (raw == null || raw.isEmpty) return null;
return UserModel.fromJsonString(raw);
}
Future<bool> isLoggedIn() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keyLoggedIn) ?? false;
}
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyUser);
await prefs.setBool(_keyLoggedIn, false);
}
}

23
lib/main.dart Normal file
View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sidak_desa_mobile/presentation/views/splash_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Sidak Desa Mobile',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: SplashScreen(),
);
}
}

View File

@ -0,0 +1,325 @@
// ignore_for_file: unused_local_variable
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:sidak_desa_mobile/presentation/views/main_screen.dart';
import '../../data/fetch/attedance_api.dart';
import '../../data/model/user_model.dart';
import '../../data/shared/auth_storage.dart';
import '../../utils/device_id.dart';
class AbsenController extends GetxController {
AbsenController({required Map<String, dynamic> qrData}) {
this.qrData.value = qrData;
}
final _api = AttedanceApi();
final _storage = AuthStorage();
final user = Rxn<UserModel>();
final qrData = <String, dynamic>{}.obs;
final nameController = TextEditingController();
final isSubmitting = false.obs;
final isLoadingDaily = false.obs;
final isLoadingLocation = false.obs;
final latitude = RxnDouble();
final longitude = RxnDouble();
final locationError = RxnString();
final selectedDate = ''.obs;
final daily = Rxn<Map<String, dynamic>>();
@override
void onInit() {
_init();
super.onInit();
}
Future<void> _init() async {
selectedDate.value = _todayDate();
await loadUser();
await fetchDaily();
await getCurrentLocation();
}
Future<void> loadUser() async {
user.value = await _storage.getUser();
nameController.text = user.value?.name ?? '';
}
@override
void onClose() {
nameController.dispose();
super.onClose();
}
String get checkInText => _formatTime(daily.value?['check_in']);
String get checkOutText => _formatTime(daily.value?['check_out']);
Future<void> fetchDaily() async {
final uid = user.value?.id;
if (uid == null) return;
isLoadingDaily.value = true;
try {
final res = await _api.getDailyAttendance(
userId: uid,
date: selectedDate.value,
);
if (res['ok'] == true) {
if (res['data'] != null && res['data'] is Map<String, dynamic>) {
daily.value = Map<String, dynamic>.from(res['data']);
} else {
daily.value = null;
}
} else {
daily.value = null;
}
} catch (_) {
daily.value = null;
} finally {
isLoadingDaily.value = false;
}
}
Future<void> submitAbsen() async {
if (isSubmitting.value) return;
final token = (qrData['raw'] ?? '').toString().trim();
final uid = user.value?.id;
if (token.isEmpty || uid == null) return;
isSubmitting.value = true;
try {
final deviceId = await DeviceIdUtil.getDeviceId();
final response = await _api.verifyAttendance(
token: token,
userId: uid,
deviceInfo: deviceId,
latitude: latitude.value,
longitude: longitude.value,
);
await fetchDaily();
// DIALOG BERHASIL
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
color: Color(0xFF0F766E), // hijau teal
shape: BoxShape.circle,
),
child: const Icon(Icons.check, color: Colors.white, size: 45),
),
const SizedBox(height: 16),
const Text(
"Absensi Berhasil",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"Absensi berhasil dilakukan",
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Get.offAll(() => const MainScreen());
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text(
"Oke",
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
),
barrierDismissible: false,
);
} catch (e) {
// DIALOG DITOLAK
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white, size: 45),
),
const SizedBox(height: 16),
const Text(
"Absen Ditolak",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
e.toString().replaceAll('Exception:', ''),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text(
"Tutup",
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
),
barrierDismissible: false,
);
} finally {
isSubmitting.value = false;
}
}
Future<void> getCurrentLocation() async {
isLoadingLocation.value = true;
locationError.value = null;
try {
final isServiceEnabled = await Geolocator.isLocationServiceEnabled();
if (!isServiceEnabled) {
locationError.value = 'Layanan lokasi tidak aktif';
return;
}
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
locationError.value = 'Izin lokasi ditolak';
return;
}
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
),
);
latitude.value = position.latitude;
longitude.value = position.longitude;
} catch (_) {
locationError.value = 'Gagal mengambil lokasi terkini';
} finally {
isLoadingLocation.value = false;
}
}
String _todayDate() {
final now = DateTime.now();
final yyyy = now.year.toString().padLeft(4, '0');
final mm = now.month.toString().padLeft(2, '0');
final dd = now.day.toString().padLeft(2, '0');
return '$yyyy-$mm-$dd';
}
String _formatTime(dynamic value) {
if (value == null) return '-';
final s = value.toString().trim();
if (s.isEmpty || s == 'null') return '-';
if (s.contains('T')) {
try {
final dt = DateTime.parse(s).toLocal();
final hh = dt.hour.toString().padLeft(2, '0');
final mm = dt.minute.toString().padLeft(2, '0');
return '$hh:$mm';
} catch (_) {}
}
final parts = s.split(':');
if (parts.length >= 2) {
return '${parts[0].padLeft(2, '0')}:${parts[1].padLeft(2, '0')}';
}
return s;
}
String get tanggalFormatted {
final now = DateTime.now();
return "${now.day} ${_bulanIndo(now.month)} ${now.year}";
}
String _bulanIndo(int bulan) {
const bulanList = [
"",
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember",
];
return bulanList[bulan];
}
String get statusText {
if (daily.value == null) return "-";
if (daily.value?['check_in'] != null) {
return "Hadir";
}
return "-";
}
String get keteranganText {
if (daily.value == null) return "-";
final checkIn = daily.value?['check_in'];
if (checkIn == null) return "-";
return "Tepat Waktu";
}
}

View File

@ -0,0 +1,140 @@
import 'package:get/get.dart';
import '../../data/fetch/attedance_api.dart';
import '../../data/model/user_model.dart';
import '../../data/shared/auth_storage.dart';
class DashboardController extends GetxController {
final _api = AttedanceApi();
final _storage = AuthStorage();
final user = Rxn<UserModel>();
final dailyList = <DailyModel>[].obs;
final datangInfo = '- WIB'.obs;
final pulangInfo = '- WIB'.obs;
final isLoadingDaily = false.obs;
final selectedDate = ''.obs;
final isLoading = false.obs;
@override
void onInit() {
super.onInit();
_initialize();
}
Future<void> _initialize() async {
selectedDate.value = _todayDate();
await fetchUser();
if (user.value != null) {
await fetchDaily();
} else {
print("USER NULL - fetchDaily dibatalkan");
}
}
/// ================= REFRESH =================
Future<void> refreshAll() async {
await fetchUser();
await fetchDaily();
}
/// ================= FETCH USER =================
Future<void> fetchUser() async {
try {
isLoading.value = true;
final userData = await _storage.getUser();
if (userData == null) {
print("User kosong");
isLoading.value = false;
return;
}
// FIX: langsung assign
user.value = userData;
print("USER FETCHED: ${user.value?.id}");
isLoading.value = false;
} catch (e) {
print("ERROR FETCH USER: $e");
isLoading.value = false;
}
}
/// ================= LOGOUT =================
Future<void> logout() async {
await _storage.logout();
}
/// ================= FETCH DAILY =================
Future<void> fetchDaily() async {
final uid = user.value?.id;
if (uid == null) {
print("UID NULL - Tidak bisa fetchDaily");
return;
}
isLoadingDaily.value = true;
try {
print("FETCH DAILY UID: $uid");
print("DATE: ${selectedDate.value}");
final res = await _api.getDailyAttendance(
userId: uid,
date: selectedDate.value,
);
print("API RESULT: $res");
if (res['ok'] == true && res['data'] is List) {
List listData = res['data'];
dailyList.value = listData.map((e) => DailyModel.fromJson(e)).toList();
if (dailyList.isNotEmpty) {
final today = dailyList.first;
if (today.jam.contains('-')) {
final split = today.jam.split('-');
datangInfo.value = "${split[0].trim()} WIB";
pulangInfo.value = "${split.last.trim()} WIB";
} else {
_resetAttendance();
}
}
print("TOTAL DATA DASHBOARD: ${dailyList.length}");
} else {
_resetAttendance();
}
} catch (e) {
print("ERROR FETCH DAILY: $e");
_resetAttendance();
} finally {
isLoadingDaily.value = false;
}
}
/// ================= RESET =================
void _resetAttendance() {
dailyList.clear();
datangInfo.value = '- WIB';
pulangInfo.value = '- WIB';
}
/// ================= DATE =================
String _todayDate() {
final now = DateTime.now();
return '${now.year.toString().padLeft(4, '0')}-'
'${now.month.toString().padLeft(2, '0')}-'
'${now.day.toString().padLeft(2, '0')}';
}
}

View File

@ -0,0 +1,62 @@
import 'package:get/get.dart';
import 'package:sidak_desa_mobile/presentation/views/main_screen.dart';
import '../../data/fetch/auth_api.dart';
import '../../data/shared/auth_storage.dart';
import '../../utils/device_id.dart';
class LoginController extends GetxController {
final AuthApi _api = AuthApi();
final AuthStorage _storage = AuthStorage();
final isLoading = false.obs;
final errorMessage = RxnString();
Future<void> login({required String email, required String password}) async {
errorMessage.value = null;
if (email.isEmpty || password.isEmpty) {
errorMessage.value = 'Email dan password wajib diisi';
return;
}
isLoading.value = true;
try {
// Ambil device ID
final deviceId = await DeviceIdUtil.getDeviceId();
if (deviceId == null || deviceId.isEmpty) {
throw Exception('Device ID tidak ditemukan');
}
// Hit API login (pastikan kirim device_id)
final response = await _api.login(
email: email,
password: password,
deviceId: deviceId,
);
// Simpan user + token
await _storage.saveUser(response);
// Pindah ke halaman utama
Get.offAll(() => MainScreen());
} catch (e) {
final message = e.toString().replaceFirst('Exception: ', '');
errorMessage.value = message;
// 🔥 Custom handling dari backend
if (message.contains('device lain')) {
Get.snackbar('Akses Ditolak', message);
} else if (message.contains('Email atau password salah')) {
Get.snackbar('Login Gagal', message);
} else {
Get.snackbar('Error', message);
}
} finally {
isLoading.value = false;
}
}
}

View File

@ -0,0 +1,32 @@
import 'package:get/get.dart';
import 'package:sidak_desa_mobile/data/shared/auth_storage.dart';
import 'package:sidak_desa_mobile/presentation/views/login_screen.dart';
import 'package:sidak_desa_mobile/presentation/views/main_screen.dart';
class SplashScreenController extends GetxController {
final AuthStorage _authStorage = AuthStorage();
@override
void onInit() {
super.onInit();
_checkAuthSession();
}
Future<void> _checkAuthSession() async {
await Future.delayed(const Duration(seconds: 2));
try {
final isLoggedIn = await _authStorage.isLoggedIn();
final user = await _authStorage.getUser();
if (isLoggedIn && user != null) {
Get.offAll(() => const MainScreen());
return;
}
} catch (_) {
// Fallback to login when local session data cannot be read.
}
Get.offAll(() => LoginScreen());
}
}

View File

@ -0,0 +1,284 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sidak_desa_mobile/core/const/app_colors.dart';
import 'package:sidak_desa_mobile/core/widgets/app_button.dart';
import 'package:sidak_desa_mobile/core/widgets/app_textfield.dart';
import 'package:sidak_desa_mobile/core/widgets/app_textfield_password.dart';
import 'package:sidak_desa_mobile/presentation/controllers/login_controller.dart';
import 'package:sidak_desa_mobile/presentation/views/pages/forgotpassword_page.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final LoginController controller = Get.put(LoginController());
static const Color primaryColor = Color(0xFF0B7D77);
static const Color accentColor = Color(0xFFFF8C32);
static const Color backgroundColor = Color(0xFFF4F7F8);
static const Color textDarkColor = Color(0xFF1E293B);
static const Color textSoftColor = Color(0xFF64748B);
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
super.dispose();
}
void login() {
final email = emailController.text.trim();
final password = passwordController.text;
if (email.isEmpty || password.isEmpty) {
Get.snackbar(
'Data Belum Lengkap',
'Email dan password wajib diisi.',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16),
borderRadius: 12,
);
return;
}
controller.login(email: email, password: password);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: SafeArea(
child: Stack(
children: [
Container(
height: 310,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF0B7D77), Color(0xFF0FA39A)],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(36),
bottomRight: Radius.circular(36),
),
),
),
SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(22, 28, 22, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
Column(
children: [
Container(
width: 250,
height: 102,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: const [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.12),
blurRadius: 22,
offset: Offset(0, 10),
),
],
),
child: Image.asset(
'assets/images/logo.png',
fit: BoxFit.contain,
),
),
const SizedBox(height: 20),
const Text(
'Selamat Datang',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 27,
height: 1.2,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
const SizedBox(height: 8),
const Text(
'Silakan masuk untuk melanjutkan ke aplikasi SIDAK Desa.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
height: 1.5,
color: Color.fromRGBO(255, 255, 255, 0.88),
),
),
],
),
const SizedBox(height: 34),
Container(
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.08),
blurRadius: 24,
offset: Offset(0, 12),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Image.asset(
'assets/images/ilustrasi.png',
width: 190,
fit: BoxFit.contain,
),
),
const SizedBox(height: 22),
const Text(
'Masuk Akun',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 6),
const Text(
'Gunakan email dan password yang sudah terdaftar.',
style: TextStyle(
fontSize: 13,
height: 1.5,
color: textSoftColor,
),
),
const SizedBox(height: 24),
const Text(
'Email',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textDarkColor,
),
),
const SizedBox(height: 8),
AppTextField(
hintText: 'Masukkan email',
keyboardType: TextInputType.emailAddress,
controller: emailController,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 18),
const Text(
'Password',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: textDarkColor,
),
),
const SizedBox(height: 8),
AppTextFieldPassword(
hintText: 'Masukkan password',
controller: passwordController,
textInputAction: TextInputAction.done,
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: GestureDetector(
onTap: () {
Get.to(() => const ForgotPasswordPage());
},
child: const Text(
'Lupa kata sandi?',
style: TextStyle(
color: primaryColor,
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(height: 26),
Obx(
() => controller.isLoading.value
? const SizedBox(
height: 54,
child: Center(
child: CircularProgressIndicator(
color: primaryColor,
),
),
)
: SizedBox(
height: 54,
child: AppButton(
text: 'Masuk',
onPressed: login,
),
),
),
],
),
),
const SizedBox(height: 22),
const Text(
'Pastikan data login Anda benar sebelum masuk ke aplikasi.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
height: 1.5,
color: textSoftColor,
),
),
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sidak_desa_mobile/core/widgets/app_bottom_navbar.dart';
import 'package:sidak_desa_mobile/presentation/views/pages/dashboard_screen.dart';
import 'package:sidak_desa_mobile/presentation/views/pages/profile_screen.dart';
import 'package:sidak_desa_mobile/presentation/views/pages/scanner_screen.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final _pages = const [DashboardScreen(), ProfileScreen()];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: IndexedStack(index: _currentIndex, children: _pages),
bottomNavigationBar: SafeArea(
minimum: const EdgeInsets.fromLTRB(14, 0, 14, 10),
child: AppBottomNavbar(
currentIndex: _currentIndex,
onTabSelected: (index) {
setState(() {
_currentIndex = index;
});
},
onCenterTap: () {
Get.to(() => const ScannerScreen());
},
),
),
);
}
}

View File

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
import '../../controllers/absen_controller.dart';
class AbsenScreen extends StatelessWidget {
const AbsenScreen({super.key, required this.qrData});
final Map<String, dynamic> qrData;
@override
Widget build(BuildContext context) {
final c = Get.put(AbsenController(qrData: qrData));
return Scaffold(
appBar: AppBar(title: const Text("ABSEN"), centerTitle: true),
body: Padding(
padding: const EdgeInsets.all(16),
child: Obx(() {
final lat = c.latitude.value;
final lng = c.longitude.value;
Widget buildField(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
SizedBox(
width: 95,
child: Text(
"$label :",
style: const TextStyle(fontSize: 14),
),
),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 14,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.teal),
),
child: Text(
value,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
);
}
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildField("Nama", c.user.value?.name ?? "-"),
buildField("Tanggal", c.tanggalFormatted),
buildField("Jabatan", c.user.value?.jabatan ?? "-"),
buildField("Jenis Kelamin", c.user.value?.jenisKelamin ?? "-"),
buildField("Alamat", c.user.value?.address ?? "-"),
const SizedBox(height: 10),
const Text("Lokasi :", style: TextStyle(fontSize: 14)),
const SizedBox(height: 8),
if (lat != null && lng != null)
SizedBox(
height: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FlutterMap(
options: MapOptions(
initialCenter: LatLng(lat, lng),
initialZoom: 16,
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.sidakdesa.mobile',
),
MarkerLayer(
markers: [
Marker(
point: LatLng(lat, lng),
width: 40,
height: 40,
child: const Icon(
Icons.location_on,
color: Colors.red,
size: 36,
),
),
],
),
],
),
),
)
else
Container(
height: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade200,
),
child: const Text("Lokasi belum tersedia"),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: c.isSubmitting.value ? null : c.submitAbsen,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
minimumSize: const Size.fromHeight(55),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: c.isSubmitting.value
? const Text(
"Memproses...",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
)
: const Text(
"Kirim",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
],
),
);
}),
),
);
}
}

View File

@ -0,0 +1,582 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sidak_desa_mobile/presentation/views/login_screen.dart';
import 'package:sidak_desa_mobile/presentation/views/pages/detail_riwayat.dart';
import 'package:sidak_desa_mobile/presentation/views/pages/fiturizin_page.dart';
import 'package:sidak_desa_mobile/presentation/views/pages/riwayat_page.dart';
import '../../controllers/dashboard_controller.dart';
class DashboardScreen extends StatelessWidget {
const DashboardScreen({super.key});
static const Color primaryColor = Color(0xFF0B7D77);
static const Color secondaryColor = Color(0xFF0FA39A);
static const Color accentColor = Color(0xFFFF8C32);
static const Color yellowColor = Color(0xFFF3C644);
static const Color backgroundColor = Color(0xFFF4F7F8);
static const Color textDarkColor = Color(0xFF1E293B);
static const Color textSoftColor = Color(0xFF64748B);
@override
Widget build(BuildContext context) {
final DashboardController controller =
Get.isRegistered<DashboardController>()
? Get.find<DashboardController>()
: Get.put(DashboardController());
return Scaffold(
backgroundColor: backgroundColor,
body: RefreshIndicator(
color: primaryColor,
onRefresh: () async {
await controller.refreshAll();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SafeArea(
child: Stack(
children: [
Container(
height: 300,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor,
secondaryColor,
],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(0),
bottomRight: Radius.circular(0),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 18, 16, 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(controller),
const SizedBox(height: 24),
_buildBanner(),
const SizedBox(height: 22),
_buildMenuSection(controller),
const SizedBox(height: 24),
_buildTodayInfo(controller),
const SizedBox(height: 30),
],
),
),
],
),
),
),
),
);
}
Widget _buildHeader(DashboardController controller) {
return Obx(() {
final user = controller.user.value;
final photoUrl = user?.urlPhoto;
return Row(
children: [
_buildProfileAvatar(photoUrl),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Selamat Datang 👋',
style: TextStyle(
fontSize: 13,
color: Color.fromRGBO(255, 255, 255, 0.85),
),
),
const SizedBox(height: 4),
Text(
user?.name ?? 'User',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 19,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
],
),
),
const SizedBox(width: 10),
InkWell(
onTap: () {
controller.logout().then((_) {
Get.offAll(() => const LoginScreen());
});
},
borderRadius: BorderRadius.circular(14),
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 255, 255, 0.18),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.logout_rounded,
color: Colors.white,
size: 22,
),
),
),
],
);
});
}
Widget _buildProfileAvatar(String? photoUrl) {
return Container(
width: 58,
height: 58,
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 255, 255, 0.25),
shape: BoxShape.circle,
border: Border.all(
color: const Color.fromRGBO(255, 255, 255, 0.45),
width: 1.4,
),
),
child: ClipOval(
child: Container(
color: Colors.white,
child: (photoUrl != null && photoUrl.isNotEmpty)
? Image.network(
photoUrl,
width: 52,
height: 52,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(
Icons.person_rounded,
color: primaryColor,
size: 30,
);
},
)
: const Icon(
Icons.person_rounded,
color: primaryColor,
size: 30,
),
),
),
);
}
Widget _buildBanner() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.08),
blurRadius: 24,
offset: Offset(0, 12),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(11, 125, 119, 0.10),
borderRadius: BorderRadius.circular(100),
),
child: const Text(
'Absensi Hari Ini',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
),
const SizedBox(height: 12),
const Text(
'Sudah absen\nhari ini?',
style: TextStyle(
fontSize: 23,
height: 1.15,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 8),
const Text(
'Ayo laporkan kegiatanmu hari ini dengan cepat dan mudah.',
style: TextStyle(
fontSize: 13,
height: 1.45,
color: textSoftColor,
),
),
],
),
),
const SizedBox(width: 12),
Image.asset(
'assets/images/banner.png',
height: 105,
fit: BoxFit.contain,
),
],
),
);
}
Widget _buildMenuSection(DashboardController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Menu Utama',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 6),
const Text(
'Pilih fitur yang ingin digunakan.',
style: TextStyle(
fontSize: 13,
color: textSoftColor,
),
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: _buildMenuCard(
title: 'Izin',
subtitle: 'Ajukan izin',
imagePath: 'assets/images/Union.png',
color: yellowColor,
icon: Icons.edit_calendar_rounded,
onTap: () async {
await Get.to(() => const IzinPage());
controller.refreshAll();
},
),
),
const SizedBox(width: 14),
Expanded(
child: _buildMenuCard(
title: 'Riwayat',
subtitle: 'Lihat data',
imagePath: 'assets/images/icon_riwayat.png',
color: primaryColor,
icon: Icons.history_rounded,
onTap: () async {
await Get.to(() => const RiwayatPage());
controller.refreshAll();
},
),
),
],
),
],
);
}
Widget _buildMenuCard({
required String title,
required String subtitle,
required String imagePath,
required Color color,
required IconData icon,
required VoidCallback onTap,
}) {
final Color shadowColor = color == primaryColor
? const Color.fromRGBO(11, 125, 119, 0.28)
: const Color.fromRGBO(243, 198, 68, 0.32);
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(24),
child: Ink(
height: 178,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: shadowColor,
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: Stack(
children: [
Positioned(
right: -8,
bottom: -4,
child: Opacity(
opacity: 0.92,
child: Image.asset(
imagePath,
height: 92,
fit: BoxFit.contain,
),
),
),
Positioned(
right: 0,
top: 0,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 255, 255, 0.20),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: Colors.white,
size: 21,
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 21,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(
color: Color.fromRGBO(255, 255, 255, 0.88),
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 7,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 255, 255, 0.18),
borderRadius: BorderRadius.circular(100),
),
child: const Text(
'Buka',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
],
),
],
),
),
);
}
Widget _buildTodayInfo(DashboardController controller) {
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: const [
BoxShadow(
color: Color.fromRGBO(0, 0, 0, 0.06),
blurRadius: 20,
offset: Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(
Icons.today_rounded,
color: primaryColor,
size: 22,
),
SizedBox(width: 8),
Text(
'Info Hari Ini',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
],
),
const SizedBox(height: 16),
Obx(
() => _buildInfoTile(
icon: Icons.login_rounded,
title: 'Datang',
value: controller.isLoadingDaily.value
? 'Memuat...'
: controller.datangInfo.value,
iconColor: primaryColor,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 12),
child: Divider(
height: 1,
color: Color(0xFFE2E8F0),
),
),
Obx(
() => _buildInfoTile(
icon: Icons.logout_rounded,
title: 'Pulang',
value: controller.isLoadingDaily.value
? 'Memuat...'
: controller.pulangInfo.value,
iconColor: accentColor,
),
),
],
),
);
}
Widget _buildInfoTile({
required IconData icon,
required String title,
required String value,
required Color iconColor,
}) {
final Color backgroundIconColor = iconColor == primaryColor
? const Color.fromRGBO(11, 125, 119, 0.10)
: const Color.fromRGBO(255, 140, 50, 0.10);
return Row(
children: [
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: backgroundIconColor,
borderRadius: BorderRadius.circular(16),
),
child: Icon(
icon,
color: iconColor,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 4),
Text(
value,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 13,
height: 1.4,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
],
),
),
],
);
}
}

View File

@ -0,0 +1,749 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sidak_desa_mobile/core/api/api.dart';
class DetailRiwayatPage extends StatefulWidget {
final Map<String, dynamic> data;
final int attendancesId;
const DetailRiwayatPage({
super.key,
required this.data,
required this.attendancesId,
});
@override
State<DetailRiwayatPage> createState() => _DetailRiwayatPageState();
}
class _DetailRiwayatPageState extends State<DetailRiwayatPage> {
late Future<Map<String, dynamic>> futureDetail;
static const Color primaryColor = Color(0xFF0B7D77);
static const Color accentColor = Color(0xFFFF8C32);
static const Color backgroundColor = Color(0xFFF4F7F8);
static const Color textDarkColor = Color(0xFF1E293B);
static const Color textSoftColor = Color(0xFF64748B);
@override
void initState() {
super.initState();
futureDetail = loadDetailRiwayat();
}
Future<Map<String, dynamic>> loadDetailRiwayat() async {
final listData = _unwrapData(_safeMap(widget.data));
debugPrint('============== DEBUG DATA DARI LIST ==============');
debugPrint('DATA DARI LIST: $listData');
final statusDariList = _normalizeStatusFromData(listData);
debugPrint('STATUS DARI LIST: $statusDariList');
// Untuk izin, sakit, dan cuti, status dari list lebih akurat.
// Jangan biarkan data detail attendance menimpa status menjadi "Hadir".
if (statusDariList == 'izin' ||
statusDariList == 'sakit' ||
statusDariList == 'cuti') {
return _normalizeDisplayData({
...listData,
'status': statusDariList,
'status_jenis': statusDariList,
'check_in': null,
'check_out': null,
'jam_masuk': null,
'jam_pulang': null,
'jam': null,
});
}
final int id = widget.attendancesId;
debugPrint('ATTENDANCES ID DARI RIWAYAT PAGE: $id');
try {
final detailData = await getDetailRiwayat(id);
debugPrint('============== DEBUG MERGE DATA DETAIL ==============');
debugPrint('DATA DARI API DETAIL: $detailData');
debugPrint('DATA DARI LIST RIWAYAT: $listData');
final mergedData = _mergeDetailWithListData(
detailData: detailData,
listData: listData,
);
debugPrint('DATA SETELAH DIGABUNG: $mergedData');
return _normalizeDisplayData(mergedData);
} catch (e) {
debugPrint('ERROR DETAIL RIWAYAT: $e');
debugPrint('Gagal ambil API detail. Menampilkan data dari list.');
return _normalizeDisplayData(listData);
}
}
Future<Map<String, dynamic>> getDetailRiwayat(int attendancesId) async {
final urlString = '${Apiconfig.baseUrl}/api/riwayat/detail/$attendancesId';
final url = Uri.parse(urlString);
debugPrint('============== DEBUG DETAIL RIWAYAT ==============');
debugPrint('ATTENDANCES ID DIKIRIM KE API: $attendancesId');
debugPrint('URL DETAIL RIWAYAT: $urlString');
final response = await http.get(url);
debugPrint('STATUS CODE DETAIL RIWAYAT: ${response.statusCode}');
debugPrint('BODY DETAIL RIWAYAT: ${response.body}');
if (response.statusCode != 200) {
throw Exception('Gagal mengambil detail riwayat');
}
final decoded = jsonDecode(response.body);
debugPrint('DECODED DETAIL RIWAYAT: $decoded');
if (decoded is Map<String, dynamic>) {
final bool isSuccess = decoded['success'] == true || decoded['ok'] == true;
debugPrint('SUCCESS DETAIL RIWAYAT: $isSuccess');
final detailData = decoded['data'];
if (isSuccess && detailData is Map<String, dynamic>) {
debugPrint('DATA DETAIL RIWAYAT: $detailData');
return detailData;
}
if (isSuccess && detailData is Map) {
final safeDetailData = Map<String, dynamic>.from(detailData);
debugPrint('DATA DETAIL RIWAYAT: $safeDetailData');
return safeDetailData;
}
if (detailData is Map<String, dynamic>) {
return detailData;
}
if (detailData is Map) {
return Map<String, dynamic>.from(detailData);
}
}
throw Exception('Format data detail tidak sesuai');
}
Map<String, dynamic> _mergeDetailWithListData({
required Map<String, dynamic> detailData,
required Map<String, dynamic> listData,
}) {
final statusTextSource = _buildStatusTextSource(
listData: listData,
detailData: detailData,
);
return {
...detailData,
// ID
'attendances_id': detailData['attendances_id'] ??
detailData['id'] ??
listData['attendances_id'] ??
listData['id'],
// Data tanggal dari detail tetap diprioritaskan.
'date': detailData['date'] ?? listData['date'],
'tanggal': detailData['tanggal'] ?? listData['tanggal'],
'sort_date': detailData['sort_date'] ?? listData['sort_date'],
// Data waktu dari detail tetap diprioritaskan.
'check_in': detailData['check_in'] ?? listData['check_in'],
'check_out': detailData['check_out'] ?? listData['check_out'],
'jam_masuk': detailData['jam_masuk'] ?? listData['jam_masuk'],
'jam_pulang': detailData['jam_pulang'] ?? listData['jam_pulang'],
'jam': detailData['jam'] ?? listData['jam'],
// Data status dari list diprioritaskan karena list riwayat yang tahu sakit/izin/cuti.
'status_jenis': listData['status_jenis'] ?? detailData['status_jenis'],
'jenis': listData['jenis'] ?? detailData['jenis'],
'tipe': listData['tipe'] ?? detailData['tipe'],
'kategori': listData['kategori'] ?? detailData['kategori'],
'alasan': listData['alasan'] ?? detailData['alasan'],
'keterangan': listData['keterangan'] ?? detailData['keterangan'],
'catatan': listData['catatan'] ?? detailData['catatan'],
'notes': listData['notes'] ?? detailData['notes'],
'status_validasi':
listData['status_validasi'] ?? detailData['status_validasi'],
'status_kehadiran':
listData['status_kehadiran'] ?? detailData['status_kehadiran'],
'icon': listData['icon'] ?? detailData['icon'],
// Simpan dua sumber status agar bisa dicek saat debug.
'status_from_list': listData['status'] ??
listData['status_jenis'] ??
listData['jenis'] ??
listData['status_validasi'],
'status_from_detail': detailData['status'],
// Status utama diprioritaskan dari list.
// Ini yang mencegah "Sakit" berubah menjadi "Hadir" di detail.
'status': listData['status_jenis'] ??
listData['status'] ??
listData['jenis'] ??
detailData['status'],
// Sumber gabungan untuk menentukan status final.
'status_text_source': statusTextSource,
};
}
String _buildStatusTextSource({
required Map<String, dynamic> listData,
required Map<String, dynamic> detailData,
}) {
final keys = [
'status_jenis',
'jenis',
'tipe',
'kategori',
'alasan',
'keterangan',
'catatan',
'notes',
'status_validasi',
'status_kehadiran',
'icon',
'status',
];
final values = <dynamic>[];
for (final key in keys) {
values.add(listData[key]);
}
for (final key in keys) {
values.add(detailData[key]);
}
return values
.where((value) => _hasValue(value))
.map((value) => value.toString().toLowerCase().trim())
.join(' ');
}
Map<String, dynamic> _safeMap(Map value) {
return Map<String, dynamic>.from(value);
}
Map<String, dynamic> _unwrapData(Map<String, dynamic> value) {
final data = value['data'];
if (data is Map<String, dynamic>) {
return data;
}
if (data is Map) {
return Map<String, dynamic>.from(data);
}
return value;
}
Map<String, dynamic> _normalizeDisplayData(Map<String, dynamic> data) {
final String? jam = _nullableText(data['jam']);
String? checkIn = _nullableText(data['check_in'] ?? data['jam_masuk']);
String? checkOut = _nullableText(data['check_out'] ?? data['jam_pulang']);
if ((checkIn == null || checkOut == null) &&
jam != null &&
jam.contains('-')) {
final parts = jam.split('-');
if (parts.isNotEmpty && checkIn == null) {
checkIn = parts[0].trim();
}
if (parts.length > 1 && checkOut == null) {
checkOut = parts[1].trim();
}
}
final status = _normalizeStatusFromData(data);
return {
'attendances_id': data['attendances_id'] ?? data['id'],
'date': data['date'] ?? data['tanggal'] ?? data['sort_date'],
'check_in': checkIn,
'check_out': checkOut,
'status': status,
'lates_minutes': data['lates_minutes'] ?? data['late_minutes'],
'notes': _pickNotesByStatus(data, status),
'device_info': data['device_info'],
'location': data['location'] ?? data['lokasi'],
};
}
dynamic _pickNotesByStatus(Map<String, dynamic> data, String status) {
if (status == 'sakit' || status == 'izin' || status == 'cuti') {
return data['alasan'] ??
data['keterangan'] ??
data['catatan'] ??
data['notes'];
}
return data['notes'] ??
data['catatan'] ??
data['keterangan'] ??
data['alasan'];
}
String _normalizeStatusFromData(Map<String, dynamic> data) {
final List<dynamic> priorityValues = [
data['status_jenis'],
data['status_text_source'],
data['jenis'],
data['tipe'],
data['kategori'],
data['alasan'],
data['keterangan'],
data['catatan'],
data['notes'],
data['status_validasi'],
data['status_kehadiran'],
data['icon'],
data['status_from_list'],
data['status_from_detail'],
data['status'],
];
final joinedText = priorityValues
.where((value) => _hasValue(value))
.map((value) => value.toString().toLowerCase().trim())
.join(' ');
debugPrint('============== DEBUG NORMALIZE STATUS ==============');
debugPrint('TEXT STATUS GABUNGAN: $joinedText');
if (joinedText.contains('sakit')) {
return 'sakit';
}
if (joinedText.contains('cuti')) {
return 'cuti';
}
if (joinedText.contains('izin')) {
return 'izin';
}
if (joinedText.contains('diajukan') || joinedText.contains('menunggu')) {
return 'izin';
}
if (joinedText.contains('alpha') || joinedText.contains('ditolak')) {
return 'alpha';
}
if (joinedText.contains('tidak hadir')) {
return 'tidak hadir';
}
if (joinedText.contains('hadir')) {
return 'hadir';
}
return 'tidak diketahui';
}
bool _hasValue(dynamic value) {
if (value == null) {
return false;
}
final text = value.toString().trim();
if (text.isEmpty || text.toLowerCase() == 'null') {
return false;
}
return true;
}
String? _nullableText(dynamic value) {
if (!_hasValue(value)) {
return null;
}
return value.toString().trim();
}
String _safeText(dynamic value, {String fallback = '-'}) {
if (!_hasValue(value)) {
return fallback;
}
return value.toString().trim();
}
Color statusColor(dynamic value) {
final status = _safeText(value, fallback: '').toLowerCase();
switch (status) {
case 'hadir':
return primaryColor;
case 'izin':
case 'sakit':
case 'cuti':
return accentColor;
case 'alpha':
case 'tidak hadir':
return Colors.red;
default:
return Colors.grey;
}
}
String statusText(dynamic value) {
final status = _safeText(value, fallback: '').toLowerCase();
switch (status) {
case 'hadir':
return 'Hadir';
case 'izin':
return 'Izin';
case 'sakit':
return 'Sakit';
case 'cuti':
return 'Cuti';
case 'alpha':
return 'Alpha';
case 'tidak hadir':
return 'Tidak Hadir';
default:
return 'Tidak diketahui';
}
}
IconData statusIcon(dynamic value) {
final status = _safeText(value, fallback: '').toLowerCase();
switch (status) {
case 'hadir':
return Icons.check_rounded;
case 'izin':
case 'sakit':
case 'cuti':
return Icons.hourglass_top_rounded;
case 'alpha':
case 'tidak hadir':
return Icons.close_rounded;
default:
return Icons.help_outline_rounded;
}
}
Color _softBackgroundColor(Color color) {
if (color == primaryColor) {
return const Color.fromRGBO(11, 125, 119, 0.10);
}
if (color == accentColor) {
return const Color.fromRGBO(255, 140, 50, 0.10);
}
if (color == Colors.red) {
return const Color.fromRGBO(244, 67, 54, 0.10);
}
return const Color.fromRGBO(158, 158, 158, 0.10);
}
Color _softBorderColor(Color color) {
if (color == primaryColor) {
return const Color.fromRGBO(11, 125, 119, 0.18);
}
if (color == accentColor) {
return const Color.fromRGBO(255, 140, 50, 0.18);
}
if (color == Colors.red) {
return const Color.fromRGBO(244, 67, 54, 0.18);
}
return const Color.fromRGBO(158, 158, 158, 0.18);
}
Widget _buildStatusCard(Map<String, dynamic> data) {
final status = data['status'];
final color = statusColor(status);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: _softBackgroundColor(color),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: _softBorderColor(color),
),
),
child: Column(
children: [
Container(
width: 62,
height: 62,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Icon(
statusIcon(status),
color: Colors.white,
size: 34,
),
),
const SizedBox(height: 14),
const Text(
'Status Kehadiran',
style: TextStyle(
fontSize: 13,
color: textSoftColor,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(
statusText(status),
style: TextStyle(
color: color,
fontWeight: FontWeight.w900,
fontSize: 21,
),
),
],
),
);
}
Widget _buildItem({
required IconData icon,
required String title,
required dynamic value,
Color iconColor = primaryColor,
}) {
final textValue = _safeText(value);
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: const [
BoxShadow(
blurRadius: 18,
color: Color.fromRGBO(0, 0, 0, 0.06),
offset: Offset(0, 8),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: iconColor == primaryColor
? const Color.fromRGBO(11, 125, 119, 0.10)
: const Color.fromRGBO(255, 140, 50, 0.10),
borderRadius: BorderRadius.circular(15),
),
child: Icon(
icon,
color: iconColor,
size: 22,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: textSoftColor,
),
),
const SizedBox(height: 4),
Text(
textValue,
style: const TextStyle(
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
],
),
),
],
),
);
}
Widget _buildLoadingView() {
return const Center(
child: CircularProgressIndicator(
color: primaryColor,
),
);
}
Widget _buildErrorView(Object? error) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.warning_amber_rounded,
color: Colors.red,
size: 52,
),
const SizedBox(height: 14),
const Text(
'Gagal Memuat Detail',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 8),
Text(
error.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13,
height: 1.5,
color: textSoftColor,
),
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: () {
setState(() {
futureDetail = loadDetailRiwayat();
});
},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text('Coba Lagi'),
),
],
),
),
);
}
Widget _buildDetailContent(Map<String, dynamic> data) {
debugPrint('============== DATA YANG DITAMPILKAN ==============');
debugPrint(data.toString());
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildStatusCard(data),
const SizedBox(height: 18),
_buildItem(
icon: Icons.calendar_today_rounded,
title: 'Tanggal',
value: data['date'],
),
_buildItem(
icon: Icons.login_rounded,
title: 'Check In',
value: data['check_in'],
iconColor: accentColor,
),
_buildItem(
icon: Icons.logout_rounded,
title: 'Check Out',
value: data['check_out'],
iconColor: accentColor,
),
_buildItem(
icon: Icons.devices_rounded,
title: 'Device Info',
value: data['device_info'],
iconColor: accentColor,
),
_buildItem(
icon: Icons.location_on_outlined,
title: 'Lokasi',
value: data['location'],
),
const SizedBox(height: 20),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
appBar: AppBar(
title: const Text(
'Detail Riwayat',
style: TextStyle(
fontWeight: FontWeight.w800,
),
),
backgroundColor: Colors.white,
foregroundColor: primaryColor,
elevation: 0.8,
),
body: FutureBuilder<Map<String, dynamic>>(
future: futureDetail,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildLoadingView();
}
if (snapshot.hasError) {
return _buildErrorView(snapshot.error);
}
final data = snapshot.data;
if (data == null || data.isEmpty) {
return _buildErrorView('Data detail kosong');
}
return _buildDetailContent(data);
},
),
);
}
}

View File

@ -0,0 +1,452 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:http/http.dart' as http;
import 'package:sidak_desa_mobile/core/api/api.dart';
import 'package:sidak_desa_mobile/data/shared/auth_storage.dart';
import 'package:sidak_desa_mobile/data/model/user_model.dart';
class IzinPage extends StatefulWidget {
const IzinPage({super.key});
@override
State<IzinPage> createState() => _IzinPageState();
}
class _IzinPageState extends State<IzinPage> {
final TextEditingController namaController = TextEditingController();
final TextEditingController jabatanController = TextEditingController();
final TextEditingController keteranganController = TextEditingController();
DateTime selectedDate = DateTime.now();
// Jangan pakai "cuti" kalau kolom status di database tidak menerima cuti.
String jenis = "izin";
File? imageFile;
bool isLoading = false;
late Uri baseUrl;
UserModel? user;
@override
void initState() {
super.initState();
baseUrl = Uri.parse("${Apiconfig.baseUrl}/api/mobile/izin");
loadUser();
}
@override
void dispose() {
namaController.dispose();
jabatanController.dispose();
keteranganController.dispose();
super.dispose();
}
Future<void> loadUser() async {
final authStorage = AuthStorage();
user = await authStorage.getUser();
if (user != null) {
namaController.text = user!.name ?? '';
jabatanController.text = user!.jabatan ?? '';
}
if (!mounted) return;
setState(() {});
}
InputDecoration customInput(String hint) {
return InputDecoration(
hintText: hint,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xFF0097A7)),
borderRadius: BorderRadius.circular(6),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Color(0xFF0097A7), width: 2),
borderRadius: BorderRadius.circular(6),
),
);
}
String normalizeJenisValue(dynamic value) {
final text = value.toString().trim().toLowerCase();
if (text == "sakit") {
return "sakit";
}
// Default aman untuk database attendance.
return "izin";
}
String formatTanggalApi(DateTime date) {
final year = date.year.toString();
final month = date.month.toString().padLeft(2, '0');
final day = date.day.toString().padLeft(2, '0');
return "$year-$month-$day";
}
String formatTanggalView(DateTime date) {
final day = date.day.toString().padLeft(2, '0');
final month = date.month.toString().padLeft(2, '0');
final year = date.year.toString();
return "$day-$month-$year";
}
Future<void> pickImage() async {
final picked = await ImagePicker().pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
if (picked == null) return;
setState(() {
imageFile = File(picked.path);
});
}
void showSuccessDialog(String message) {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircleAvatar(
radius: 35,
backgroundColor: Color(0xFF009688),
child: Icon(Icons.check, color: Colors.white, size: 40),
),
const SizedBox(height: 15),
const Text(
"Berhasil",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(message, textAlign: TextAlign.center),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
onPressed: () {
Navigator.pop(context);
Navigator.pop(context, true);
},
child: const Text("Oke", style: TextStyle(color: Colors.white)),
),
),
],
),
),
);
}
void showErrorDialog(String message) {
showDialog(
context: context,
builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircleAvatar(
radius: 35,
backgroundColor: Colors.red,
child: Icon(Icons.close, color: Colors.white, size: 40),
),
const SizedBox(height: 15),
const Text(
"Gagal",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Navigator.pop(context),
child: const Text(
"Tutup",
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
);
}
Future<void> submitIzin() async {
final authStorage = AuthStorage();
final currentUser = await authStorage.getUser();
if (currentUser == null) {
showErrorDialog("User tidak ditemukan. Silakan login ulang.");
return;
}
final nama = namaController.text.trim();
final jabatan = jabatanController.text.trim();
final keterangan = keteranganController.text.trim();
final jenisIzin = normalizeJenisValue(jenis);
if (nama.isEmpty || jabatan.isEmpty || keterangan.isEmpty) {
showErrorDialog("Semua field wajib diisi.");
return;
}
if (imageFile == null) {
showErrorDialog("Upload bukti terlebih dahulu.");
return;
}
setState(() => isLoading = true);
try {
final request = http.MultipartRequest("POST", baseUrl);
request.headers.addAll({"Accept": "application/json"});
request.fields['user_id'] = currentUser.id.toString();
request.fields['nama'] = nama;
request.fields['jabatan'] = jabatan;
request.fields['tanggal'] = formatTanggalApi(selectedDate);
// Field ini dikirim sebagai jenis izin.
// Nilainya hanya "izin" atau "sakit" supaya tidak merusak kolom status database.
request.fields['jenis'] = jenisIzin;
request.fields['keterangan'] = keterangan;
request.files.add(
await http.MultipartFile.fromPath(
'bukti',
imageFile!.path,
filename: imageFile!.path.split('/').last,
),
);
final response = await request.send();
final respStr = await response.stream.bytesToString();
debugPrint('============== DEBUG SUBMIT IZIN ==============');
debugPrint('URL: $baseUrl');
debugPrint('STATUS CODE: ${response.statusCode}');
debugPrint('RESPONSE BODY: $respStr');
debugPrint('USER ID: ${currentUser.id}');
debugPrint('TANGGAL DIKIRIM: ${formatTanggalApi(selectedDate)}');
debugPrint('JENIS DIKIRIM: $jenisIzin');
Map<String, dynamic> jsonData = {};
try {
final decoded = json.decode(respStr);
if (decoded is Map<String, dynamic>) {
jsonData = decoded;
} else if (decoded is Map) {
jsonData = Map<String, dynamic>.from(decoded);
}
} catch (_) {
jsonData = {};
}
if (!mounted) return;
setState(() => isLoading = false);
final bool success =
response.statusCode == 200 || response.statusCode == 201;
final bool apiOk = jsonData['ok'] == true || jsonData['success'] == true;
if (success && apiOk) {
showSuccessDialog("Izin berhasil dikirim.");
} else {
final message = jsonData['message']?.toString();
showErrorDialog(
message == null || message.isEmpty ? "Gagal mengirim izin." : message,
);
}
} catch (e) {
debugPrint('ERROR SUBMIT IZIN: $e');
if (!mounted) return;
setState(() => isLoading = false);
showErrorDialog("Terjadi kesalahan sistem.");
}
}
Widget label(String text) {
return Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Text(text, style: const TextStyle(fontWeight: FontWeight.w500)),
),
);
}
@override
Widget build(BuildContext context) {
final selectedJenis = normalizeJenisValue(jenis);
return Scaffold(
backgroundColor: const Color(0xFFF2F2F2),
appBar: AppBar(
title: const Text("Izin", style: TextStyle(color: Color(0xFF0097A7))),
backgroundColor: Colors.white,
elevation: 1,
iconTheme: const IconThemeData(color: Color(0xFF0097A7)),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
label("Nama"),
TextField(
controller: namaController,
readOnly: true,
decoration: customInput("Masukkan nama"),
),
const SizedBox(height: 15),
label("Jabatan"),
TextField(
controller: jabatanController,
readOnly: true,
decoration: customInput("Masukkan jabatan"),
),
const SizedBox(height: 15),
label("Tanggal"),
InkWell(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked == null) return;
setState(() {
selectedDate = picked;
});
},
child: InputDecorator(
decoration: customInput(""),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(formatTanggalView(selectedDate)),
const Icon(Icons.calendar_today, color: Color(0xFF0097A7)),
],
),
),
),
const SizedBox(height: 15),
label("Jenis Izin"),
DropdownButtonFormField<String>(
value: selectedJenis,
items: const [
DropdownMenuItem(value: "izin", child: Text("Izin")),
DropdownMenuItem(value: "sakit", child: Text("Sakit")),
],
onChanged: (val) {
if (val == null) return;
setState(() {
jenis = normalizeJenisValue(val);
});
},
decoration: customInput(""),
),
const SizedBox(height: 15),
label("Keterangan"),
TextField(
controller: keteranganController,
minLines: 1,
maxLines: 3,
decoration: customInput("Masukkan keterangan"),
),
const SizedBox(height: 20),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
minimumSize: const Size.fromHeight(45),
),
onPressed: isLoading ? null : pickImage,
icon: const Icon(Icons.camera_alt, color: Colors.white),
label: const Text(
"Upload gambar",
style: TextStyle(color: Colors.white),
),
),
if (imageFile != null)
Padding(
padding: const EdgeInsets.all(8),
child: Image.file(imageFile!, height: 150, fit: BoxFit.cover),
),
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
minimumSize: const Size.fromHeight(50),
),
onPressed: isLoading ? null : submitIzin,
child: isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
"Kirim",
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,468 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sidak_desa_mobile/core/api/api.dart';
import 'package:sidak_desa_mobile/presentation/views/pages/veryfikasiOTP.dart';
class ForgotPasswordPage extends StatefulWidget {
const ForgotPasswordPage({super.key});
@override
State<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
}
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
final TextEditingController emailController = TextEditingController();
bool isLoading = false;
static const Color primaryColor = Color(0xFF0B7D77);
static const Color accentColor = Color(0xFFFF8C32);
static const Color backgroundColor = Color(0xFFF4F7F8);
static const Color textDarkColor = Color(0xFF1E293B);
static const Color textSoftColor = Color(0xFF64748B);
@override
void dispose() {
emailController.dispose();
super.dispose();
}
Future<void> checkEmail() async {
final email = emailController.text.trim();
if (email.isEmpty) {
showMessage(
title: 'Email Kosong',
message: 'Silakan masukkan email terlebih dahulu.',
isError: true,
);
return;
}
if (!_isValidEmail(email)) {
showMessage(
title: 'Email Tidak Valid',
message: 'Format email yang Anda masukkan belum benar.',
isError: true,
);
return;
}
setState(() => isLoading = true);
try {
final response = await http.post(
Uri.parse('${Apiconfig.baseUrl}/api/lupa-kata-sandi'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': email,
'type': 'email',
}),
);
final data = jsonDecode(response.body);
if (!mounted) return;
if (data['status'] == true) {
setState(() => isLoading = false);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OtpVerificationPage(email: email),
),
);
} else {
setState(() => isLoading = false);
showMessage(
title: 'Email Tidak Terdaftar',
message: data['message'] ?? 'Email yang Anda masukkan tidak ditemukan.',
isError: true,
);
}
} catch (e) {
if (!mounted) return;
setState(() => isLoading = false);
showMessage(
title: 'Koneksi Bermasalah',
message: 'Terjadi kesalahan koneksi. Silakan coba lagi.',
isError: true,
);
}
}
bool _isValidEmail(String email) {
final emailRegex = RegExp(
r'^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$',
);
return emailRegex.hasMatch(email);
}
void showMessage({
required String title,
required String message,
required bool isError,
}) {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 76,
height: 76,
decoration: BoxDecoration(
color: isError
? Colors.red.withOpacity(0.10)
: primaryColor.withOpacity(0.12),
shape: BoxShape.circle,
),
child: Icon(
isError
? Icons.warning_amber_rounded
: Icons.check_circle_outline_rounded,
color: isError ? Colors.red : primaryColor,
size: 42,
),
),
const SizedBox(height: 20),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 19,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 10),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
height: 1.5,
color: textSoftColor,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
'Kembali',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
),
);
}
InputDecoration _emailInputDecoration() {
return InputDecoration(
labelText: 'Email',
hintText: 'Masukkan email Anda',
prefixIcon: const Icon(
Icons.email_outlined,
color: primaryColor,
),
filled: true,
fillColor: const Color(0xFFF8FAFC),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 18,
),
labelStyle: const TextStyle(color: textSoftColor),
hintStyle: const TextStyle(color: Color(0xFF94A3B8)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(
color: primaryColor,
width: 1.4,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Colors.red),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Colors.red),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: SafeArea(
child: Stack(
children: [
Container(
height: 310,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0B7D77),
Color(0xFF0FA39A),
],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(36),
bottomRight: Radius.circular(36),
),
),
),
SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(22, 16, 22, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
if (Navigator.canPop(context))
InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(14),
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.18),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.arrow_back_ios_new_rounded,
color: Colors.white,
size: 18,
),
),
),
],
),
const SizedBox(height: 18),
Column(
children: [
Container(
width: 250,
height: 96,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 22,
offset: const Offset(0, 10),
),
],
),
child: Image.asset(
'assets/images/logo.png',
fit: BoxFit.contain,
),
),
const SizedBox(height: 22),
const Text(
'Lupa Kata Sandi?',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 27,
height: 1.2,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
const SizedBox(height: 10),
Text(
'Masukkan email akun Anda untuk menerima kode verifikasi.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
height: 1.5,
color: Colors.white.withOpacity(0.9),
),
),
],
),
const SizedBox(height: 36),
Container(
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Verifikasi Email',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 6),
const Text(
'Kode OTP akan dikirimkan ke email yang terdaftar pada akun Anda.',
style: TextStyle(
fontSize: 13,
height: 1.5,
color: textSoftColor,
),
),
const SizedBox(height: 24),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
decoration: _emailInputDecoration(),
),
const SizedBox(height: 18),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: primaryColor.withOpacity(0.12),
),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline_rounded,
color: primaryColor,
size: 20,
),
SizedBox(width: 10),
Expanded(
child: Text(
'Pastikan email yang dimasukkan sama dengan email saat pendaftaran.',
style: TextStyle(
fontSize: 12.5,
height: 1.45,
color: textSoftColor,
),
),
),
],
),
),
const SizedBox(height: 26),
SizedBox(
height: 54,
child: ElevatedButton(
onPressed: isLoading ? null : checkEmail,
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
disabledBackgroundColor:
accentColor.withOpacity(0.55),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.4,
),
)
: const Text(
'Kirim Kode OTP',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
),
),
],
),
),
const SizedBox(height: 22),
const Text(
'Jika email terdaftar, Anda akan diarahkan ke halaman verifikasi OTP.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
height: 1.5,
color: textSoftColor,
),
),
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,499 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sidak_desa_mobile/core/api/api.dart';
import 'package:sidak_desa_mobile/presentation/views/login_screen.dart';
class ResetPasswordPage extends StatefulWidget {
final String email;
const ResetPasswordPage({super.key, required this.email});
@override
State<ResetPasswordPage> createState() => _ResetPasswordPageState();
}
class _ResetPasswordPageState extends State<ResetPasswordPage> {
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmController = TextEditingController();
bool isLoading = false;
bool isPasswordHidden = true;
bool isConfirmHidden = true;
static const Color primaryColor = Color(0xFF0B7D77);
static const Color accentColor = Color(0xFFFF8C32);
static const Color backgroundColor = Color(0xFFF4F7F8);
static const Color textDarkColor = Color(0xFF1E293B);
static const Color textSoftColor = Color(0xFF64748B);
@override
void dispose() {
passwordController.dispose();
confirmController.dispose();
super.dispose();
}
Future<void> resetPassword() async {
final password = passwordController.text;
final confirmPassword = confirmController.text;
if (password.isEmpty || confirmPassword.isEmpty) {
_showSnackBar('Kata sandi tidak boleh kosong', Colors.red);
return;
}
if (password.length < 6) {
_showSnackBar('Kata sandi minimal 6 karakter', Colors.red);
return;
}
if (password != confirmPassword) {
_showSnackBar('Konfirmasi sandi tidak cocok', Colors.red);
return;
}
setState(() => isLoading = true);
try {
final response = await http.post(
Uri.parse('${Apiconfig.baseUrl}/api/reset-password'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': widget.email,
'password': password,
'password_confirmation': confirmPassword,
}),
);
final data = jsonDecode(response.body);
if (!mounted) return;
setState(() => isLoading = false);
if (response.statusCode == 200 && data['status'] == true) {
_showSuccessDialog();
} else {
_showSnackBar(data['message'] ?? 'Gagal membuat password', Colors.red);
}
} catch (e) {
if (!mounted) return;
setState(() => isLoading = false);
_showSnackBar('Terjadi kesalahan server', Colors.red);
}
}
void _showSnackBar(String message, Color color) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: color,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.all(16),
),
);
}
void _showSuccessDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 86,
height: 86,
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.12),
shape: BoxShape.circle,
),
child: Center(
child: Container(
width: 62,
height: 62,
decoration: const BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_rounded,
color: Colors.white,
size: 36,
),
),
),
),
const SizedBox(height: 22),
const Text(
'Kata Sandi Berhasil Diubah',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
height: 1.3,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 10),
const Text(
'Silakan masuk kembali menggunakan kata sandi baru Anda.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
height: 1.5,
color: textSoftColor,
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => LoginScreen()),
);
},
child: const Text(
'Masuk Sekarang',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
),
),
),
],
),
),
),
);
}
InputDecoration _inputDecoration({
required String label,
required String hint,
required IconData icon,
required Widget suffixIcon,
}) {
return InputDecoration(
labelText: label,
hintText: hint,
prefixIcon: Icon(icon, color: primaryColor),
suffixIcon: suffixIcon,
filled: true,
fillColor: const Color(0xFFF8FAFC),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
labelStyle: const TextStyle(color: textSoftColor),
hintStyle: const TextStyle(color: Color(0xFF94A3B8)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: primaryColor, width: 1.4),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Colors.red),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: const BorderSide(color: Colors.red),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: SafeArea(
child: Stack(
children: [
Container(
height: 290,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF0B7D77), Color(0xFF0FA39A)],
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(36),
bottomRight: Radius.circular(36),
),
),
),
SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(22, 16, 22, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
if (Navigator.canPop(context))
InkWell(
onTap: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(14),
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.18),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.arrow_back_ios_new_rounded,
color: Colors.white,
size: 18,
),
),
),
],
),
const SizedBox(height: 18),
Column(
children: [
Container(
width: 250,
height: 96,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 22,
offset: const Offset(0, 10),
),
],
),
child: Image.asset(
'assets/images/logo.png',
fit: BoxFit.contain,
),
),
const SizedBox(height: 22),
const Text(
'Buat Kata Sandi Baru',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26,
height: 1.2,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
const SizedBox(height: 10),
Text(
'Akun: ${widget.email}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.88),
),
),
],
),
const SizedBox(height: 34),
Container(
padding: const EdgeInsets.all(22),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Atur ulang kata sandi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: textDarkColor,
),
),
const SizedBox(height: 6),
const Text(
'Gunakan kata sandi yang mudah diingat, tetapi sulit ditebak orang lain.',
style: TextStyle(
fontSize: 13,
height: 1.5,
color: textSoftColor,
),
),
const SizedBox(height: 24),
TextField(
controller: passwordController,
obscureText: isPasswordHidden,
decoration: _inputDecoration(
label: 'Kata Sandi Baru',
hint: 'Minimal 6 karakter',
icon: Icons.lock_outline_rounded,
suffixIcon: IconButton(
onPressed: () {
setState(() {
isPasswordHidden = !isPasswordHidden;
});
},
icon: Icon(
isPasswordHidden
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: textSoftColor,
),
),
),
),
const SizedBox(height: 16),
TextField(
controller: confirmController,
obscureText: isConfirmHidden,
decoration: _inputDecoration(
label: 'Konfirmasi Sandi',
hint: 'Ulangi kata sandi baru',
icon: Icons.verified_user_outlined,
suffixIcon: IconButton(
onPressed: () {
setState(() {
isConfirmHidden = !isConfirmHidden;
});
},
icon: Icon(
isConfirmHidden
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: textSoftColor,
),
),
),
),
const SizedBox(height: 18),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: primaryColor.withOpacity(0.12),
),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline_rounded,
color: primaryColor,
size: 20,
),
SizedBox(width: 10),
Expanded(
child: Text(
'Disarankan memakai kombinasi huruf, angka, dan simbol agar akun lebih aman.',
style: TextStyle(
fontSize: 12.5,
height: 1.45,
color: textSoftColor,
),
),
),
],
),
),
const SizedBox(height: 26),
SizedBox(
height: 54,
child: ElevatedButton(
onPressed: isLoading ? null : resetPassword,
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
disabledBackgroundColor: accentColor.withOpacity(
0.55,
),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.4,
),
)
: const Text(
'Simpan Kata Sandi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
),
),
),
),
],
),
),
const SizedBox(height: 22),
const Text(
'Pastikan Anda tidak membagikan kata sandi kepada siapa pun.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
height: 1.5,
color: textSoftColor,
),
),
],
),
),
],
),
),
);
}
}

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