This commit is contained in:
BakoL2323 2026-03-31 15:45:34 +07:00
commit 4da9ee269f
164 changed files with 12117 additions and 0 deletions

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

45
.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: android
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: ios
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: linux
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: macos
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: web
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: windows
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
# 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'

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# sijentik
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.

28
analysis_options.yaml Normal file
View File

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

14
android/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,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.sijentik"
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.sijentik"
// 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,52 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="sijentik"
android:name="${applicationName}"
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.sijentik
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)
}

View File

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=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/daftar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
assets/images/google.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
assets/images/logodua.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

BIN
assets/images/sandi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
assets/images/sandidua.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 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:

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.sijentik;
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.sijentik.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.sijentik.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.sijentik.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.sijentik;
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.sijentik;
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>

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

@ -0,0 +1,49 @@
<?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>Sijentik</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>sijentik</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

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

View File

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

View File

@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
// ========== COLOR PALETTE ==========
class AppColors {
// Primary
static const Color primary = Color(0xFF1E88E5);
static const Color primaryDark = Color(0xFF1565C0);
static const Color button = Color(0xFF206E97);
// Text Colors
static const Color textDark = Color(0xFF1F2A37);
static const Color textGrey = Color(0xFF6B7280);
static const Color textLight = Color(0xFF9CA3AF);
// Background
static const Color background = Color(0xFFF9FAFB);
static const Color white = Color(0xFFFFFFFF);
static const Color inputBackground = Color(0xFFF3F4F6);
// System
static const Color success = Color(0xFF10B981);
static const Color warning = Color(0xFFF59E0B);
static const Color error = Color(0xFFEF4444);
static const Color info = Color(0xFF3B82F6);
// Neutral
static const Color black = Color(0xFF000000);
static const Color grey100 = Color(0xFFF3F4F6);
static const Color grey200 = Color(0xFFE5E7EB);
static const Color grey300 = Color(0xFFD1D5DB);
static const Color grey400 = Color(0xFF9CA3AF);
}
// ========== TEXT STYLES ==========
class AppTextStyles {
// App Name
static TextStyle appName = GoogleFonts.inter(
fontSize: 30,
fontWeight: FontWeight.w700,
height: 1.5,
color: AppColors.textDark,
);
// Page Title
static TextStyle pageTitle = GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w600,
height: 1.4,
color: AppColors.textDark,
);
// Input Label
static TextStyle inputLabel = GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
color: AppColors.textDark,
letterSpacing: 0.1,
);
// Input Text
static TextStyle inputText = GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.5,
color: AppColors.textDark,
);
// Hint Text
static TextStyle hintText = GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.5,
color: AppColors.textLight,
);
// Small Text
static TextStyle smallText = GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
color: AppColors.textGrey,
);
// Small Link Text
static TextStyle smallLinkText = GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
color: AppColors.primary,
);
// Button Text
static TextStyle buttonText = GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.5,
color: AppColors.white,
);
// Body Medium
static TextStyle bodyMedium = GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
color: AppColors.textDark,
);
// Body Small
static TextStyle bodySmall = GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.3,
color: AppColors.textDark,
);
// Label Small
static TextStyle labelSmall = GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w500,
height: 1.2,
color: AppColors.textGrey,
);
}
// ========== INPUT DECORATION ==========
class AppInputDecoration {
static InputDecoration inputWithIcon(IconData icon) {
return InputDecoration(
filled: true,
fillColor: AppColors.inputBackground,
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.grey300, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.primary, width: 2),
),
prefixIcon: Icon(icon, color: AppColors.textLight),
hintStyle: AppTextStyles.hintText,
);
}
}
// ========== BUTTON STYLES ==========
class AppButtonStyles {
static ButtonStyle primaryButton = ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
textStyle: AppTextStyles.buttonText,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
);
}

View File

@ -0,0 +1,162 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class RegisterResult {
final bool success;
final String message;
RegisterResult({required this.success, required this.message});
}
class RegisterKaderController extends ChangeNotifier {
final formKey = GlobalKey<FormState>();
final namaController = TextEditingController();
final emailController = TextEditingController();
final alamatController = TextEditingController();
final rtrwController = TextEditingController();
final passwordController = TextEditingController();
final konfirmasiPasswordController = TextEditingController();
bool obscurePassword = true;
bool obscureKonfirmasiPassword = true;
bool isLoading = false;
/// GANTI DENGAN URL API LARAVEL ANDA
final String baseUrl = "http://192.168.1.6:8000/api/register";
void togglePassword() {
obscurePassword = !obscurePassword;
notifyListeners();
}
void toggleKonfirmasiPassword() {
obscureKonfirmasiPassword = !obscureKonfirmasiPassword;
notifyListeners();
}
String? validateNama(String? value) {
if (value == null || value.isEmpty) {
return "Nama wajib diisi";
}
return null;
}
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return "Email wajib diisi";
}
if (!value.contains("@")) {
return "Format email tidak valid";
}
return null;
}
String? validateAlamat(String? value) {
if (value == null || value.isEmpty) {
return "Alamat wajib diisi";
}
return null;
}
String? validateRtRw(String? value) {
if (value == null || value.isEmpty) {
return "RT/RW wajib diisi";
}
return null;
}
String? validatePassword(String? value) {
if (value == null || value.length < 6) {
return "Password minimal 6 karakter";
}
return null;
}
String? validateKonfirmasiPassword(String? value) {
if (value != passwordController.text) {
return "Password tidak sama";
}
return null;
}
Future<RegisterResult> register() async {
if (!formKey.currentState!.validate()) {
return RegisterResult(
success: false,
message: "Form belum lengkap",
);
}
isLoading = true;
notifyListeners();
try {
final response = await http.post(
Uri.parse(baseUrl),
headers: {
"Content-Type": "application/json",
},
body: jsonEncode({
"name": namaController.text,
"email": emailController.text,
"address": alamatController.text,
"rtrw": rtrwController.text,
"password": passwordController.text,
"password_confirmation": konfirmasiPasswordController.text,
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 201) {
return RegisterResult(
success: true,
message: data["message"] ?? "Register berhasil",
);
} else {
return RegisterResult(
success: false,
message: data["message"] ??
data["error"] ??
"Register gagal",
);
}
} catch (e) {
return RegisterResult(
success: false,
message: "Tidak dapat terhubung ke server",
);
} finally {
isLoading = false;
notifyListeners();
}
}
String getErrorMessage(RegisterResult result) {
return result.message;
}
void dispose() {
namaController.dispose();
emailController.dispose();
alamatController.dispose();
rtrwController.dispose();
passwordController.dispose();
konfirmasiPasswordController.dispose();
super.dispose();
}
}

23
lib/main.dart Normal file
View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'screens/splash_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Sijentik',
theme: ThemeData(
primarySwatch: Colors.blue,
scaffoldBackgroundColor: const Color(0xFFF2ECF4),
),
home: const SplashScreen(),
);
}
}

View File

@ -0,0 +1,43 @@
class Kader {
final String id;
final String name;
final String email;
final String phone;
final String area;
final DateTime joinDate;
final int totalReports;
Kader({
required this.id,
required this.name,
required this.email,
required this.phone,
required this.area,
required this.joinDate,
this.totalReports = 0,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'email': email,
'phone': phone,
'area': area,
'joinDate': joinDate.toIso8601String(),
'totalReports': totalReports,
};
}
factory Kader.fromMap(Map<String, dynamic> map) {
return Kader(
id: map['id'],
name: map['name'],
email: map['email'],
phone: map['phone'],
area: map['area'],
joinDate: DateTime.parse(map['joinDate']),
totalReports: map['totalReports'] ?? 0,
);
}
}

View File

@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
class Report {
final String id;
final String judul;
final String address;
final String ownerName;
final DateTime date;
final String containerType;
final String finding;
final String status;
final String? note;
final String? notes;
final String? imageUrl;
final String? gambar;
final String? gambarUrl;
final List<String>? images;
final String? petugasFeedback;
final DateTime? inspectionDate;
final String? inspectorName;
final double? latitude;
final double? longitude;
final String? alamat;
final bool? adaJentik;
Report({
required this.id,
required this.judul,
required this.address,
required this.ownerName,
required this.date,
required this.containerType,
required this.finding,
this.status = 'Menunggu',
this.note,
this.notes,
this.imageUrl,
this.images,
this.petugasFeedback,
this.inspectionDate,
this.inspectorName,
this.latitude,
this.longitude,
this.alamat,
this.gambar,
this.gambarUrl,
this.adaJentik,
});
Color get statusColor {
switch (status) {
case 'Disetujui':
return Colors.green;
case 'Ditolak':
return Colors.red;
case 'Menunggu':
default:
return Colors.orange;
}
}
Map<String, dynamic> toMap() {
return {
'id': id,
'judul': judul,
'address': address,
'ownerName': ownerName,
'date': date.toIso8601String(),
'containerType': containerType,
'finding': finding,
'status': status,
'note': note,
'notes': notes,
'imageUrl': imageUrl,
'images': images,
'petugasFeedback': petugasFeedback,
'inspectionDate': inspectionDate?.toIso8601String(),
'inspectorName': inspectorName,
'latitude': latitude,
'longitude': longitude,
'alamat': alamat,
'gambar': gambar,
'gambarUrl': gambarUrl,
'adaJentik': adaJentik,
};
}
factory Report.fromMap(Map<String, dynamic> map) {
return Report(
id: map['id'].toString(),
judul: map['judul'] ?? '',
address: map['alamat'] ?? '',
ownerName: '',
date: map['tanggal'] != null
? DateTime.parse(map['tanggal'])
: DateTime.now(),
containerType: map['judul'] ?? '',
finding: (map['ada_jentik'] ?? false) ? 'Ada Jentik' : 'Tidak Ada Jentik',
status: _convertStatus(map['status']),
notes: map['catatan_petugas'],
imageUrl: map['gambar_url'],
images: map['gambar_url'] != null ? [map['gambar_url']] : null,
latitude: map['latitude'] != null
? double.tryParse(map['latitude'].toString())
: null,
longitude: map['longitude'] != null
? double.tryParse(map['longitude'].toString())
: null,
inspectionDate: map['tanggal'] != null
? DateTime.parse(map['tanggal'])
: null,
inspectorName: 'Petugas',
alamat: map['alamat'],
gambar: map['gambar'],
gambarUrl: map['gambar_url'],
adaJentik: map['ada_jentik'],
);
}
static String _convertStatus(String? status) {
switch (status) {
case "proses":
return "Menunggu";
case "diterima":
return "Disetujui";
case "ditolak":
return "Ditolak";
default:
return "Menunggu";
}
}
}

View File

@ -0,0 +1,76 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class RegisterResult {
final bool success;
final String message;
final Map<String, dynamic>? errors;
RegisterResult({
required this.success,
required this.message,
this.errors,
});
}
class AuthService {
// Android Emulator => 10.0.2.2
// HP fisik => ganti dengan IP laptop/PC, misalnya 192.168.1.10
static const String baseUrl = 'http://192.168.1.6:8000';
Future<RegisterResult> registerKader({
required String name,
required String email,
required String address,
required String rtrw,
required String password,
required String passwordConfirmation,
}) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/api/register'),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: jsonEncode({
'name': name,
'email': email,
'address': address,
'rtrw': rtrw,
'password': password,
'password_confirmation': passwordConfirmation,
}),
);
final Map<String, dynamic> data = response.body.isNotEmpty
? jsonDecode(response.body)
: {};
if (response.statusCode == 201) {
return RegisterResult(
success: true,
message: data['message'] ?? 'Register berhasil',
);
}
if (response.statusCode == 422) {
return RegisterResult(
success: false,
message: data['message'] ?? 'Validasi gagal',
errors: data['errors'],
);
}
return RegisterResult(
success: false,
message: data['message'] ?? 'Terjadi kesalahan server',
);
} catch (e) {
return RegisterResult(
success: false,
message: 'Gagal terhubung ke server: $e',
);
}
}
}

View File

@ -0,0 +1,50 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class KaderService {
final String baseUrl = "http://192.168.1.6:8000/api";
Future<bool> approveKader(int id) async {
try {
final response = await http.post(
Uri.parse("$baseUrl/kader/approve/$id"),
headers: {
"Content-Type": "application/json",
},
);
if (response.statusCode == 200) {
return true;
}
return false;
} catch (e) {
return false;
}
}
Future<bool> rejectKader(int id) async {
try {
final response = await http.post(
Uri.parse("$baseUrl/kader/reject/$id"),
headers: {
"Content-Type": "application/json",
},
);
if (response.statusCode == 200) {
return true;
}
return false;
} catch (e) {
return false;
}
}
}

View File

@ -0,0 +1,368 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class BuatKataSandiBaruPage extends StatefulWidget {
final String email;
const BuatKataSandiBaruPage({super.key, required this.email});
@override
State<BuatKataSandiBaruPage> createState() => _BuatKataSandiBaruPageState();
}
class _BuatKataSandiBaruPageState extends State<BuatKataSandiBaruPage> {
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController =
TextEditingController();
final FocusNode passwordFocusNode = FocusNode();
final FocusNode confirmPasswordFocusNode = FocusNode();
bool obscurePassword = true;
bool obscureConfirmPassword = true;
// Android Emulator: 10.0.2.2
// HP Fisik: ganti dengan IP laptop, misalnya 192.168.1.8
static const String resetPasswordUrl =
'http://192.168.1.6:8000/api/reset-password';
@override
void dispose() {
passwordController.dispose();
confirmPasswordController.dispose();
passwordFocusNode.dispose();
confirmPasswordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F9FF),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 16, left: 16),
child: Align(
alignment: Alignment.centerLeft,
child: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(
Icons.arrow_back,
color: Colors.black,
size: 25,
),
),
),
),
const SizedBox(height: 20),
const Text(
'Buat Kata Sandi Baru',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 30),
Image.asset(
'assets/images/sandidua.png',
width: 150,
),
Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Kata Sandi Baru',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
const SizedBox(height: 8),
TextField(
controller: passwordController,
focusNode: passwordFocusNode,
obscureText: obscurePassword,
decoration: InputDecoration(
hintText: 'Masukkan kata sandi baru',
hintStyle: TextStyle(color: Colors.grey[500]),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.grey[300]!,
width: 1.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.grey[300]!,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: Color(0xFF206E97),
width: 1.5,
),
),
prefixIcon: Icon(
Icons.lock_outline,
color: Colors.grey[600],
size: 20,
),
suffixIcon: IconButton(
icon: Icon(
obscurePassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey[600],
size: 20,
),
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
});
},
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
const SizedBox(height: 20),
const Text(
'Konfirmasi Kata Sandi Baru',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
const SizedBox(height: 8),
TextField(
controller: confirmPasswordController,
focusNode: confirmPasswordFocusNode,
obscureText: obscureConfirmPassword,
decoration: InputDecoration(
hintText: 'Masukkan konfirmasi kata sandi baru',
hintStyle: TextStyle(color: Colors.grey[500]),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.grey[300]!,
width: 1.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.grey[300]!,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: Color(0xFF206E97),
width: 1.5,
),
),
prefixIcon: Icon(
Icons.lock_outline,
color: Colors.grey[600],
size: 20,
),
suffixIcon: IconButton(
icon: Icon(
obscureConfirmPassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey[600],
size: 20,
),
onPressed: () {
setState(() {
obscureConfirmPassword =
!obscureConfirmPassword;
});
},
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
_simpanKataSandiBaru();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF206E97),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: const Text(
'Simpan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
],
),
),
),
);
}
Future<void> _simpanKataSandiBaru() async {
if (passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Harap masukkan kata sandi baru'),
backgroundColor: Colors.red,
),
);
return;
}
if (confirmPasswordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Harap masukkan konfirmasi kata sandi'),
backgroundColor: Colors.red,
),
);
return;
}
if (passwordController.text.length < 6) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Kata sandi minimal 6 karakter'),
backgroundColor: Colors.red,
),
);
return;
}
if (passwordController.text != confirmPasswordController.text) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Kata sandi tidak cocok'),
backgroundColor: Colors.red,
),
);
return;
}
FocusScope.of(context).unfocus();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Menyimpan kata sandi baru...'),
],
),
),
);
try {
final response = await http.post(
Uri.parse(resetPasswordUrl),
headers: {
'Accept': 'application/json',
},
body: {
'email': widget.email,
'password': passwordController.text.trim(),
'password_confirmation': confirmPasswordController.text.trim(),
},
);
final dynamic data = jsonDecode(response.body);
if (!mounted) return;
Navigator.pop(context);
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
data['message'] ??
'Kata sandi berhasil diperbarui untuk email ${widget.email}',
style: const TextStyle(color: Colors.white),
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 3),
),
);
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
Navigator.popUntil(context, (route) => route.isFirst);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
data['message'] ??
data['error'] ??
'Gagal memperbarui kata sandi',
),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
} catch (e) {
if (!mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Terjadi kesalahan koneksi: $e'),
backgroundColor: Colors.red,
),
);
}
}
}

View File

@ -0,0 +1,44 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
class DashboardService {
static const String baseUrl = "http://192.168.1.6:8000/api";
// kalau pakai HP asli ganti ke IP laptop, misalnya:
// static const String baseUrl = "http://192.168.1.8:8000/api";
static Future<Map<String, dynamic>?> getDashboard() async {
try {
final url = Uri.parse("$baseUrl/dashboard-petugas");
debugPrint("GET DASHBOARD => $url");
final response = await http.get(
url,
headers: {
"Accept": "application/json",
},
);
debugPrint("STATUS CODE => ${response.statusCode}");
debugPrint("BODY => ${response.body}");
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
if (decoded is Map<String, dynamic>) {
return decoded;
}
if (decoded is Map) {
return Map<String, dynamic>.from(decoded);
}
}
return null;
} catch (e) {
debugPrint("DASHBOARD ERROR => $e");
return null;
}
}
}

350
lib/screens/auth/login.dart Normal file
View File

@ -0,0 +1,350 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sijentik/component/app_theme.dart';
import 'package:sijentik/screens/auth/registerkader.dart';
import 'package:sijentik/screens/auth/lupasandiemail.dart';
import 'package:sijentik/screens/kader/homekader.dart';
import 'package:sijentik/screens/petugas/dashboardpetugas.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final FocusNode emailFocusNode = FocusNode();
final FocusNode passwordFocusNode = FocusNode();
bool obscurePassword = true;
bool isLoading = false;
static const String baseUrl = 'http://192.168.1.6:8000/api/login';
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
emailFocusNode.dispose();
passwordFocusNode.dispose();
super.dispose();
}
// =============================
// SIMPAN DATA USER
// =============================
Future<void> saveUserData(Map user) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('id', user['id']);
await prefs.setString('name', user['name'] ?? '');
await prefs.setString('email', user['email'] ?? '');
await prefs.setString('address', user['address'] ?? '');
await prefs.setString('rtrw', user['rtrw'] ?? '');
await prefs.setString('role', user['role'] ?? '');
await prefs.setString('status', user['status'] ?? '');
}
Future<void> _login() async {
if (emailController.text.isEmpty || passwordController.text.isEmpty) {
_showErrorDialog('Email dan kata sandi harus diisi');
return;
}
if (!emailController.text.contains('@')) {
_showErrorDialog('Format email tidak valid');
return;
}
setState(() {
isLoading = true;
});
try {
final response = await http.post(
Uri.parse(baseUrl),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: jsonEncode({
'email': emailController.text.trim(),
'password': passwordController.text.trim(),
}),
);
final data = jsonDecode(response.body);
if (!mounted) return;
setState(() {
isLoading = false;
});
if (response.statusCode == 200) {
final user = data['user'];
// simpan data user dari backend
await saveUserData(user);
final String role = (user['role'] ?? '').toString().toLowerCase();
if (role == 'petugas') {
_navigateToDashboardPetugas();
} else if (role == 'kader') {
_navigateToHomeKader(user);
} else {
_showErrorDialog('Role pengguna tidak dikenali');
}
} else {
_showErrorDialog(data['error'] ?? data['message'] ?? 'Login gagal');
}
} catch (e) {
if (!mounted) return;
setState(() {
isLoading = false;
});
_showErrorDialog('Tidak dapat terhubung ke server: $e');
}
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Login Gagal'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F9FF),
body: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
height: 280,
decoration: BoxDecoration(
color: AppColors.button,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(195),
bottomRight: Radius.circular(195),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 15,
spreadRadius: 1,
offset: const Offset(0, 13),
),
],
),
),
),
SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 40),
Image.asset('assets/images/logodua.png', width: 150),
const SizedBox(height: 55),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 12,
spreadRadius: 1,
offset: const Offset(0, 6),
),
],
),
child: Column(
children: [
const Text(
'MASUK',
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 30),
// EMAIL
Align(
alignment: Alignment.centerLeft,
child: Text(
'Email',
style: TextStyle(
fontSize: 16,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 8),
TextField(
controller: emailController,
focusNode: emailFocusNode,
decoration: InputDecoration(
hintText: 'email@gmail.com',
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
onSubmitted: (_) => _login(),
),
const SizedBox(height: 20),
// PASSWORD
Align(
alignment: Alignment.centerLeft,
child: Text(
'Kata Sandi',
style: TextStyle(
fontSize: 16,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 8),
TextField(
controller: passwordController,
focusNode: passwordFocusNode,
obscureText: obscurePassword,
decoration: InputDecoration(
hintText: '********',
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
obscurePassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
onSubmitted: (_) => _login(),
),
const SizedBox(height: 25),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.button,
),
child: isLoading
? const CircularProgressIndicator(
color: Colors.white,
)
: const Text(
'Masuk',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 25),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Belum punya akun? ',
style: TextStyle(color: Colors.grey[700]),
),
TextButton(
onPressed: _navigateToRegister,
child: const Text(
'Daftar disini',
style: TextStyle(
color: Color(0xFF1E88E5),
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
const SizedBox(height: 40),
],
),
),
),
],
),
);
}
void _navigateToHomeKader(Map<String, dynamic> user) {
FocusScope.of(context).unfocus();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => HomeKaderPage(user: user)),
(route) => false,
);
}
void _navigateToDashboardPetugas() {
FocusScope.of(context).unfocus();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const DashboardPetugas()),
(route) => false,
);
}
void _navigateToRegister() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const RegisterKaderPage()),
);
}
}

View File

@ -0,0 +1,278 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'lupasandiotp.dart';
class LupaKataSandiEmailPage extends StatefulWidget {
const LupaKataSandiEmailPage({super.key});
@override
State<LupaKataSandiEmailPage> createState() => _LupaKataSandiEmailPageState();
}
class _LupaKataSandiEmailPageState extends State<LupaKataSandiEmailPage> {
final TextEditingController emailController = TextEditingController();
final FocusNode emailFocusNode = FocusNode();
// Ganti sesuai device yang dipakai
// Android Emulator: 10.0.2.2
// HP Fisik: pakai IP laptop, misalnya 192.168.1.8
static const String requestOtpUrl = 'http://192.168.1.6:8000/api/RequestOtp';
@override
void dispose() {
emailController.dispose();
emailFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F9FF),
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back, color: Colors.black),
),
),
const SizedBox(height: 10),
Image.asset('assets/images/sandi.png', width: 170),
const SizedBox(height: 20),
const Text(
'LUPA KATA SANDI',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
'Masukkan email yang sudah terdaftar,\nlalu atur ulang kata sandi Anda agar\nlebih mudah diingat.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey[700],
height: 1.5,
),
),
),
const SizedBox(height: 30),
Container(
height: 1,
width: double.infinity,
color: Colors.grey[300],
),
const SizedBox(height: 30),
const Align(
alignment: Alignment.centerLeft,
child: Text(
'Email',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
const SizedBox(height: 12),
TextField(
controller: emailController,
focusNode: emailFocusNode,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'email@gmail.com',
hintStyle: TextStyle(color: Colors.grey[500]),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.grey[300]!,
width: 1.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.grey[300]!,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: Color(0xFF206E97),
width: 1.5,
),
),
prefixIcon: Icon(
Icons.email_outlined,
color: Colors.grey[600],
size: 20,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
const SizedBox(height: 30),
Container(
height: 1,
width: double.infinity,
color: Colors.grey[300],
),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
_prosesKirimEmail();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF206E97),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: const Text(
'Lanjut',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
),
);
}
Future<void> _prosesKirimEmail() async {
if (emailController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Harap masukkan email'),
backgroundColor: Colors.red,
),
);
return;
}
if (!emailController.text.contains('@') ||
!emailController.text.contains('.')) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Format email tidak valid'),
backgroundColor: Colors.red,
),
);
return;
}
FocusScope.of(context).unfocus();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Mengirim kode OTP ke email...'),
],
),
),
);
try {
final response = await http.post(
Uri.parse(requestOtpUrl),
headers: {'Accept': 'application/json'},
body: {'email': emailController.text.trim(), 'type': 'email'},
);
final dynamic data = jsonDecode(response.body);
if (!mounted) return;
Navigator.pop(context);
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(data['message'] ?? 'Kode OTP berhasil dikirim'),
backgroundColor: Colors.green,
),
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
LupaKataSandiOtpPage(email: emailController.text.trim()),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
data['message'] ?? data['error'] ?? 'Gagal mengirim kode OTP',
),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
if (!mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Terjadi kesalahan koneksi: $e'),
backgroundColor: Colors.red,
),
);
}
}
}

View File

@ -0,0 +1,470 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'buatsandibaru.dart';
class LupaKataSandiOtpPage extends StatefulWidget {
final String email;
const LupaKataSandiOtpPage({super.key, required this.email});
@override
State<LupaKataSandiOtpPage> createState() => _LupaKataSandiOtpPageState();
}
class _LupaKataSandiOtpPageState extends State<LupaKataSandiOtpPage> {
static const int otpLength = 5;
final List<TextEditingController> otpControllers = List.generate(
otpLength,
(index) => TextEditingController(),
);
final List<FocusNode> otpFocusNodes = List.generate(
otpLength,
(index) => FocusNode(),
);
// Android Emulator: 10.0.2.2
// HP Fisik: ganti dengan IP laptop, misalnya 192.168.1.8
static const String verifyOtpUrl = 'http://192.168.1.6:8000/api/verify-otp';
static const String requestOtpUrl = 'http://192.168.1.6:8000/api/RequestOtp';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
FocusScope.of(context).requestFocus(otpFocusNodes[0]);
}
});
}
@override
void dispose() {
for (var controller in otpControllers) {
controller.dispose();
}
for (var focusNode in otpFocusNodes) {
focusNode.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F9FF),
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: IconButton(
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back, color: Colors.black),
),
),
const SizedBox(height: 20),
Image.asset('assets/images/sandi.png', width: 150),
const SizedBox(height: 20),
const Text(
'KODE OTP',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Text(
'Kode telah dikirim ke alamat email Anda:',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey[700],
height: 1.5,
),
),
const SizedBox(height: 8),
Text(
widget.email,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF206E97),
height: 1.5,
),
),
const SizedBox(height: 8),
const Text(
'Silakan masukkan kode OTP yang diterima.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey,
height: 1.5,
),
),
],
),
),
const SizedBox(height: 40),
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
spreadRadius: 1,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Wrap(
alignment: WrapAlignment.center,
spacing: 10,
runSpacing: 10,
children: List.generate(otpLength, (index) {
return SizedBox(
width: 50,
height: 60,
child: TextField(
controller: otpControllers[index],
focusNode: otpFocusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.grey[300]!,
width: 1.0,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: Colors.grey[300]!,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: Color(0xFF206E97),
width: 2.0,
),
),
),
onChanged: (value) {
_handleOtpInput(index, value);
},
),
);
}),
),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {
_verifikasiOtp();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF206E97),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 2,
shadowColor: Colors.black.withOpacity(0.2),
),
child: const Text(
'Verifikasi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Tidak menerima kode? ',
style: TextStyle(
color: Colors.grey[700],
fontSize: 14,
),
),
TextButton(
onPressed: () {
_kirimUlangOtp();
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
),
child: const Text(
'Kirim ulang',
style: TextStyle(
color: Color(0xFF1E88E5),
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
const SizedBox(height: 40),
],
),
),
),
),
);
}
void _handleOtpInput(int index, String value) {
if (value.isNotEmpty) {
if (index < otpLength - 1) {
Future.delayed(const Duration(milliseconds: 50), () {
if (mounted) {
FocusScope.of(context).requestFocus(otpFocusNodes[index + 1]);
}
});
} else {
Future.delayed(const Duration(milliseconds: 50), () {
if (mounted) {
otpFocusNodes[index].unfocus();
}
});
}
} else {
if (index > 0) {
Future.delayed(const Duration(milliseconds: 50), () {
if (mounted) {
FocusScope.of(context).requestFocus(otpFocusNodes[index - 1]);
}
});
}
}
}
String _getOtpCode() {
return otpControllers.map((controller) => controller.text).join();
}
void _clearOtpFields() {
for (var controller in otpControllers) {
controller.clear();
}
}
Future<void> _verifikasiOtp() async {
final String otpCode = _getOtpCode();
if (otpCode.length != otpLength) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Harap masukkan semua $otpLength digit OTP'),
backgroundColor: Colors.red,
),
);
return;
}
FocusScope.of(context).unfocus();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Memverifikasi OTP...'),
],
),
),
);
try {
final response = await http.post(
Uri.parse(verifyOtpUrl),
headers: {'Accept': 'application/json'},
body: {'email': widget.email, 'otp': otpCode},
);
final dynamic data = jsonDecode(response.body);
if (!mounted) return;
Navigator.pop(context);
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
data['message'] ?? 'OTP berhasil diverifikasi',
style: const TextStyle(color: Colors.white),
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
await Future.delayed(const Duration(milliseconds: 800));
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BuatKataSandiBaruPage(email: widget.email),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
data['message'] ??
data['error'] ??
'Kode OTP salah atau tidak valid',
style: const TextStyle(color: Colors.white),
),
backgroundColor: Colors.red,
duration: const Duration(seconds: 2),
),
);
}
} catch (e) {
if (!mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Terjadi kesalahan koneksi: $e'),
backgroundColor: Colors.red,
),
);
}
}
Future<void> _kirimUlangOtp() async {
FocusScope.of(context).unfocus();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text('Mengirim ulang OTP ke ${widget.email}...'),
],
),
),
);
try {
final response = await http.post(
Uri.parse(requestOtpUrl),
headers: {'Accept': 'application/json'},
body: {'email': widget.email, 'type': 'email'},
);
final dynamic data = jsonDecode(response.body);
if (!mounted) return;
Navigator.pop(context);
if (response.statusCode == 200) {
_clearOtpFields();
FocusScope.of(context).requestFocus(otpFocusNodes[0]);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
data['message'] ?? 'OTP telah dikirim ulang',
style: const TextStyle(color: Colors.white),
),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
data['message'] ?? data['error'] ?? 'Gagal mengirim ulang OTP',
style: const TextStyle(color: Colors.white),
),
backgroundColor: Colors.red,
duration: const Duration(seconds: 2),
),
);
}
} catch (e) {
if (!mounted) return;
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Terjadi kesalahan koneksi: $e'),
backgroundColor: Colors.red,
),
);
}
}
}

View File

@ -0,0 +1,357 @@
import 'package:flutter/material.dart';
import 'package:sijentik/controller/register_controller.dart';
class RegisterKaderPage extends StatefulWidget {
const RegisterKaderPage({super.key});
@override
State<RegisterKaderPage> createState() => _RegisterKaderPageState();
}
class _RegisterKaderPageState extends State<RegisterKaderPage> {
late final RegisterKaderController controller;
@override
void initState() {
super.initState();
controller = RegisterKaderController();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
Future<void> _submit() async {
final result = await controller.register();
if (!mounted) return;
final message = result.success
? result.message
: controller.getErrorMessage(result);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: result.success ? Colors.green : Colors.red,
),
);
if (result.success) {
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
Navigator.popUntil(context, (route) => route.isFirst);
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
top: false,
child: SingleChildScrollView(
child: Column(
children: [
Container(
width: double.infinity,
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + 20,
bottom: 30,
left: 24,
right: 24,
),
decoration: BoxDecoration(
color: const Color(0xFF206E97),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 15,
spreadRadius: 2,
offset: const Offset(0, 5),
),
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
spreadRadius: 1,
offset: const Offset(0, 3),
),
],
),
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: IconButton(
onPressed: () {
Navigator.maybePop(context);
},
icon: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 25,
),
padding: EdgeInsets.zero,
),
),
const Text(
'DAFTAR',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 10),
child: Image.asset(
'assets/images/logo.png',
width: 160,
height: 160,
fit: BoxFit.contain,
),
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Form(
key: controller.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextFieldWithIcon(
label: 'Nama',
hint: 'Masukkan nama lengkap',
controller: controller.namaController,
icon: Icons.person_outline,
validator: controller.validateNama,
),
const SizedBox(height: 20),
_buildTextFieldWithIcon(
label: 'Email',
hint: 'email@gmail.com',
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
icon: Icons.email_outlined,
validator: controller.validateEmail,
),
const SizedBox(height: 20),
_buildTextFieldWithIcon(
label: 'Alamat',
hint: 'Masukkan alamat lengkap',
controller: controller.alamatController,
icon: Icons.home_outlined,
validator: controller.validateAlamat,
),
const SizedBox(height: 20),
_buildTextFieldWithIcon(
label: 'RT/RW',
hint: 'Contoh: 001/002',
controller: controller.rtrwController,
icon: Icons.format_list_numbered,
keyboardType: TextInputType.text,
validator: controller.validateRtRw,
),
const SizedBox(height: 20),
_buildPasswordFieldWithIcon(
label: 'Kata Sandi',
hint: 'Masukkan kata sandi',
controller: controller.passwordController,
obscureText: controller.obscurePassword,
icon: Icons.lock_outline,
onToggleVisibility: controller.togglePassword,
validator: controller.validatePassword,
),
const SizedBox(height: 20),
_buildPasswordFieldWithIcon(
label: 'Konfirmasi Kata Sandi',
hint: 'Masukkan konfirmasi kata sandi',
controller: controller.konfirmasiPasswordController,
obscureText: controller.obscureKonfirmasiPassword,
icon: Icons.lock_outline,
onToggleVisibility:
controller.toggleKonfirmasiPassword,
validator: controller.validateKonfirmasiPassword,
),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: controller.isLoading ? null : _submit,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF206E97),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: controller.isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Daftar',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
],
),
),
),
);
},
);
}
Widget _buildTextFieldWithIcon({
required String label,
required String hint,
required TextEditingController controller,
required IconData icon,
required String? Function(String?) validator,
TextInputType keyboardType = TextInputType.text,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
keyboardType: keyboardType,
validator: validator,
decoration: InputDecoration(
hintText: hint,
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!, width: 1.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!, width: 1.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: Color(0xFF206E97),
width: 1.5,
),
),
prefixIcon: Icon(icon, color: Colors.grey[600], size: 20),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
],
);
}
Widget _buildPasswordFieldWithIcon({
required String label,
required String hint,
required TextEditingController controller,
required bool obscureText,
required IconData icon,
required VoidCallback onToggleVisibility,
required String? Function(String?) validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
obscureText: obscureText,
validator: validator,
decoration: InputDecoration(
hintText: hint,
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!, width: 1.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!, width: 1.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: Color(0xFF206E97),
width: 1.5,
),
),
prefixIcon: Icon(icon, color: Colors.grey[600], size: 20),
suffixIcon: IconButton(
icon: Icon(
obscureText ? Icons.visibility_off : Icons.visibility,
color: Colors.grey[600],
size: 20,
),
onPressed: onToggleVisibility,
),
contentPadding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
),
),
],
);
}
}

View File

@ -0,0 +1,333 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:sijentik/models/report_model.dart';
class AddReportPage extends StatefulWidget {
const AddReportPage({super.key});
@override
State<AddReportPage> createState() => _AddReportPageState();
}
class _AddReportPageState extends State<AddReportPage> {
final TextEditingController judulController = TextEditingController();
final TextEditingController alamatController = TextEditingController();
bool? adaJentik;
File? selectedImage;
DateTime? selectedDate;
double? latitude;
double? longitude;
String? alamat;
bool isLoading = false;
final ImagePicker picker = ImagePicker();
static const String baseUrl = 'http://192.168.1.6:8000/api';
Future<void> simpanLaporan() async {
if (judulController.text.trim().isEmpty) {
_showMessage('Judul laporan wajib diisi');
return;
}
if (adaJentik == null) {
_showMessage('Pilih status jentik');
return;
}
if (selectedDate == null) {
_showMessage('Tanggal wajib dipilih');
return;
}
setState(() {
isLoading = true;
});
try {
final request = http.MultipartRequest(
'POST',
Uri.parse('$baseUrl/laporan'),
);
request.headers['Accept'] = 'application/json';
request.fields['judul'] = judulController.text.trim();
request.fields['ada_jentik'] = adaJentik! ? '1' : '0';
request.fields['tanggal'] = DateFormat(
'yyyy-MM-dd',
).format(selectedDate!);
request.fields['latitude'] = latitude?.toString() ?? '';
request.fields['longitude'] = longitude?.toString() ?? '';
request.fields['alamat'] = alamatController.text;
if (selectedImage != null) {
request.files.add(
await http.MultipartFile.fromPath('gambar', selectedImage!.path),
);
}
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
// PERBAIKAN: data sebagai variable non-final
Map<String, dynamic>? data;
if (response.body.isNotEmpty) {
data = jsonDecode(response.body);
}
if (response.statusCode == 201) {
_showMessage(data?['message'] ?? 'Laporan berhasil disimpan');
// reset form
judulController.clear();
alamatController.clear();
setState(() {
adaJentik = null;
selectedImage = null;
selectedDate = null;
latitude = null;
longitude = null;
alamat = null;
});
} else {
if (data != null && data['errors'] != null) {
_showMessage(data['errors'].toString());
} else {
_showMessage(data?['message'] ?? 'Gagal menyimpan laporan');
}
}
} catch (e) {
_showMessage('Error: $e');
} finally {
setState(() {
isLoading = false;
});
}
}
void _showMessage(String message) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
Future<void> pickDate() async {
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: selectedDate ?? now,
firstDate: DateTime(2020),
lastDate: DateTime(2100),
);
if (picked != null) setState(() => selectedDate = picked);
}
Future<void> ambilLokasi() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return _showMessage('GPS belum aktif');
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied)
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
return _showMessage('Izin lokasi ditolak');
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
String alamatLengkap = '';
try {
final placemarks = await placemarkFromCoordinates(
position.latitude,
position.longitude,
);
if (placemarks.isNotEmpty) {
final p = placemarks.first;
alamatLengkap =
'${p.street ?? ''}, ${p.subLocality ?? ''}, ${p.locality ?? ''}, ${p.administrativeArea ?? ''}, ${p.country ?? ''}';
}
} catch (_) {}
setState(() {
latitude = position.latitude;
longitude = position.longitude;
alamat = alamatLengkap;
});
_showMessage('Lokasi berhasil diambil');
}
Future<void> pickImage() async {
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) setState(() => selectedImage = File(image.path));
}
Widget buildRadioJentik({
required bool value,
required String title,
required String subtitle,
required Color iconColor,
required IconData icon,
}) {
return RadioListTile<bool>(
contentPadding: EdgeInsets.zero,
value: value,
groupValue: adaJentik,
onChanged: (val) => setState(() => adaJentik = val),
title: Row(
children: [
Icon(icon, color: iconColor),
const SizedBox(width: 8),
Expanded(child: Text(title)),
],
),
subtitle: Text(subtitle),
);
}
@override
Widget build(BuildContext context) {
final tanggalText = selectedDate == null
? 'Pilih tanggal'
: DateFormat('dd-MM-yyyy').format(selectedDate!);
return Scaffold(
appBar: AppBar(
title: const Text('Buat Laporan'),
backgroundColor: const Color(0xFF2D7AA1),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: judulController,
decoration: InputDecoration(
hintText: 'Judul Laporan',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 16),
const Text(
'Apakah terdapat jentik?',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
buildRadioJentik(
value: true,
title: 'Ya, terdapat jentik',
subtitle: 'Lokasi ini terdapat jentik nyamuk',
iconColor: Colors.green,
icon: Icons.check_circle,
),
buildRadioJentik(
value: false,
title: 'Tidak ada jentik',
subtitle: 'Lokasi ini aman dari jentik nyamuk',
iconColor: Colors.red,
icon: Icons.cancel,
),
const SizedBox(height: 16),
TextField(
controller: alamatController,
maxLines: 1,
decoration: InputDecoration(
hintText: 'Alamat lengkap',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 16),
InkWell(
onTap: pickDate,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFF2D7AA1)),
borderRadius: BorderRadius.circular(12),
),
child: Text(tanggalText),
),
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: ambilLokasi,
icon: const Icon(Icons.location_on),
label: const Text('Ambil Lokasi'),
),
const SizedBox(height: 8),
if (latitude != null && longitude != null)
Text(
'Latitude: $latitude\nLongitude: $longitude\nAlamat: ${alamat ?? '-'}',
),
const SizedBox(height: 16),
GestureDetector(
onTap: pickImage,
child: Container(
width: double.infinity,
height: 180,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: const Color(0xFF2D7AA1)),
borderRadius: BorderRadius.circular(12),
),
child: selectedImage == null
? const Center(
child: Icon(
Icons.add_a_photo,
size: 48,
color: Color(0xFF2D7AA1),
),
)
: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
selectedImage!,
width: double.infinity,
height: 180,
fit: BoxFit.cover,
),
),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: isLoading ? null : simpanLaporan,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2D7AA1),
),
child: isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text(
'Simpan Laporan',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,478 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:sijentik/component/app_theme.dart';
import 'history_page.dart';
import 'add_report.dart';
import 'reports_page.dart';
import 'profile_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
class DashboardPage extends StatefulWidget {
final Map<String, dynamic> user;
const DashboardPage({super.key, required this.user});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
String address = '';
@override
void initState() {
super.initState();
loadUserData();
}
Future<void> loadUserData() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
address = prefs.getString('address') ?? '';
});
}
// Data statistik dummy
final Map<String, dynamic> stats = const {
'totalReports': 24,
'approved': 18,
'pending': 4,
'rejected': 2,
'percentage': 75,
};
// Data laporan terbaru dummy
final List<Map<String, dynamic>> recentReports = const [
{
'id': 'LP001',
'address': 'Jl. Merdeka No. 12',
'date': '12 Mar 2024',
'status': 'Disetujui',
'statusColor': Colors.green,
},
{
'id': 'LP002',
'address': 'Jl. Sudirman No. 45',
'date': '11 Mar 2024',
'status': 'Menunggu',
'statusColor': Colors.orange,
},
{
'id': 'LP003',
'address': 'Jl. Gatot Subroto No. 8',
'date': '10 Mar 2024',
'status': 'Disetujui',
'statusColor': Colors.green,
},
];
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ===== HEADER SELAMAT DATANG =====
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF206E97),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Selamat Datang,',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 5),
Text(
widget.user['name'] ?? 'Kader',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 10),
Text(
'Wilayah: ${widget.user['address']}',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(height: 5),
Text(
'Total Laporan: ${stats['totalReports']}',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
const SizedBox(width: 20),
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.medical_services_outlined,
size: 40,
color: Colors.white,
),
),
],
),
),
const SizedBox(height: 25),
// ===== STATISTIK CARD =====
const Text(
'Statistik Laporan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 15),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 15,
mainAxisSpacing: 15,
childAspectRatio: 1.2,
children: [
_buildStatCard(
title: 'Total Laporan',
value: '${stats['totalReports']}',
icon: Icons.assignment_outlined,
color: const Color(0xFF206E97),
),
_buildStatCard(
title: 'Disetujui',
value: '${stats['approved']}',
icon: Icons.check_circle_outline,
color: Colors.green,
),
_buildStatCard(
title: 'Menunggu',
value: '${stats['pending']}',
icon: Icons.access_time_outlined,
color: Colors.orange,
),
_buildStatCard(
title: 'Ditolak',
value: '${stats['rejected']}',
icon: Icons.cancel_outlined,
color: Colors.red,
),
],
),
const SizedBox(height: 25),
// ===== PERSENTASE DISETUJUI =====
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(
value: stats['percentage'] / 100,
strokeWidth: 8,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(
Color(0xFF206E97),
),
),
),
Text(
'${stats['percentage']}%',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(width: 20),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Persentase Disetujui',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 5),
Text(
'75% laporan Anda telah disetujui petugas. Pertahankan!',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
],
),
),
const SizedBox(height: 25),
// ===== LAPORAN TERBARU =====
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Laporan Terbaru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
TextButton(
onPressed: () {
// Navigasi ke halaman riwayat
// Navigator.push(context, MaterialPageRoute(builder: (context) => HistoryPage()));
},
child: const Text(
'Lihat Semua',
style: TextStyle(color: Color(0xFF206E97)),
),
),
],
),
const SizedBox(height: 10),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: recentReports.map((report) {
return Column(
children: [
ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF206E97).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.home_outlined,
color: Color(0xFF206E97),
size: 20,
),
),
title: Text(
report['address'],
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
'ID: ${report['id']}${report['date']}',
style: const TextStyle(fontSize: 12),
),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: report['statusColor'].withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
report['status'],
style: TextStyle(
color: report['statusColor'],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
onTap: () {
// Navigasi ke detail laporan
},
),
if (report != recentReports.last)
const Divider(height: 1, indent: 16, endIndent: 16),
],
);
}).toList(),
),
),
const SizedBox(height: 25),
const SizedBox(height: 40),
],
),
);
}
Widget _buildStatCard({
required String title,
required String value,
required IconData icon,
required Color color,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const Spacer(),
Text(
value,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildQuickActionCard({
required IconData icon,
required String title,
required String subtitle,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
);
}
}

View File

@ -0,0 +1,396 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:sijentik/models/report_model.dart';
import 'package:sijentik/component/app_theme.dart';
class DetailHistoryPage extends StatelessWidget {
final Report report;
const DetailHistoryPage({super.key, required this.report});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.button,
elevation: 4,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
),
title: const Text(
'Detail Riwayat Laporan',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18),
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
),
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.button, AppColors.primaryDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius:
BorderRadius.vertical(bottom: Radius.circular(16)),
),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// HEADER
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'ID: ${report.id}',
style: AppTextStyles.pageTitle.copyWith(
fontSize: 18,
color: AppColors.button,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color:
report.statusColor.withOpacity(0.1),
borderRadius:
BorderRadius.circular(20),
),
child: Text(
report.status,
style:
AppTextStyles.smallText.copyWith(
color: report.statusColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
const SizedBox(height: 20),
// INFORMASI UTAMA
_buildInfoSection(
title: 'Informasi Utama',
children: [
_buildInfoRow(
icon: Icons.calendar_today,
label: 'Tanggal Laporan',
value: report.date,
labelFontSize: 14,
),
_buildInfoRow(
icon: Icons.location_on,
label: 'Alamat',
value: report.address,
labelFontSize: 14,
),
],
),
const SizedBox(height: 20),
// DETAIL TEMUAN
_buildInfoSection(
title: 'Detail Temuan',
children: [
_buildInfoRow(
icon: Icons.description,
label: 'Judul Laporan',
value: report.judul,
),
_buildInfoRow(
icon: Icons.search,
label: 'Temuan',
value: report.finding,
),
],
),
const SizedBox(height: 20),
// FOTO
_buildInfoSection(
title: 'Foto Laporan',
children: [
_buildImageSection(
imageUrl: report.imageUrl,
images: report.images,
),
],
),
const SizedBox(height: 20),
// LOKASI
_buildInfoSection(
title: 'Lokasi Pemeriksaan',
children: [
_buildLocationSection(
latitude: report.latitude,
longitude: report.longitude,
),
],
),
const SizedBox(height: 20),
// CATATAN PETUGAS
_buildInfoSection(
title: 'Catatan Petugas',
children: [
Text(
report.notes ?? "Tidak ada catatan",
style: AppTextStyles.bodyMedium,
),
],
),
],
),
),
);
}
// INFO SECTION
Widget _buildInfoSection({
required String title,
required List<Widget> children,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTextStyles.pageTitle.copyWith(
fontSize: 16,
color: AppColors.button,
),
),
const SizedBox(height: 8),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(children: children),
),
),
],
);
}
// INFO ROW
Widget _buildInfoRow({
required IconData icon,
required String label,
required dynamic value,
double labelFontSize = 12,
double valueFontSize = 14,
FontWeight labelFontWeight = FontWeight.w600,
FontWeight valueFontWeight = FontWeight.w600,
}) {
final String displayValue = value is DateTime
? '${value.day}/${value.month}/${value.year}'
: value.toString();
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: AppColors.textGrey),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: AppTextStyles.labelSmall.copyWith(
fontSize: labelFontSize,
fontWeight: labelFontWeight,
),
),
const SizedBox(height: 4),
Text(
displayValue,
style: AppTextStyles.bodyMedium.copyWith(
fontSize: valueFontSize,
fontWeight: valueFontWeight,
),
),
],
),
),
],
),
);
}
// IMAGE SECTION
Widget _buildImageSection({
List<String>? images,
String? imageUrl,
}) {
if (images != null && images.isNotEmpty) {
return SizedBox(
height: 150,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: images.length,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.only(right: 8),
width: 150,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grey300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: _buildImage(images[index]),
),
);
},
),
);
}
if (imageUrl != null && imageUrl.isNotEmpty) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: _buildImage(imageUrl, height: 200),
);
}
return Text(
'Tidak ada foto laporan',
style: AppTextStyles.smallText,
);
}
// IMAGE BUILDER
Widget _buildImage(String path, {double height = 150}) {
return path.startsWith('http')
? Image.network(
path,
height: height,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _imageError(),
)
: Image.file(
File(path),
height: height,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _imageError(),
);
}
// IMAGE ERROR
Widget _imageError() {
return Container(
color: AppColors.grey100,
child: const Center(
child: Icon(
Icons.broken_image,
size: 40,
color: Colors.grey,
),
),
);
}
// LOCATION
Widget _buildLocationSection({
double? latitude,
double? longitude,
}) {
if (latitude == null || longitude == null) {
return Text(
'Lokasi tidak tersedia',
style: AppTextStyles.smallText,
);
}
return Column(
children: [
_buildInfoRow(
icon: Icons.location_on,
label: 'Latitude',
value: latitude.toStringAsFixed(6),
),
_buildInfoRow(
icon: Icons.location_on,
label: 'Longitude',
value: longitude.toStringAsFixed(6),
),
],
);
}
}

View File

@ -0,0 +1,257 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sijentik/models/report_model.dart';
import 'detail_history_page.dart';
class HistoryPage extends StatefulWidget {
const HistoryPage({super.key});
@override
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
String _selectedFilter = 'Semua';
final List<String> _filters = ['Semua', 'Menunggu', 'Disetujui', 'Ditolak'];
List<Report> reports = [];
bool isLoading = true;
final String apiUrl = "http://192.168.1.6:8000/api/laporan";
@override
void initState() {
super.initState();
fetchReports();
}
Future<void> fetchReports() async {
try {
print("===== FETCH REPORTS =====");
print("API URL: $apiUrl");
final response = await http.get(Uri.parse(apiUrl));
print("STATUS CODE: ${response.statusCode}");
print("RESPONSE BODY:");
print(response.body);
if (response.statusCode == 200) {
final data = json.decode(response.body);
print("DECODE SUCCESS");
print("DATA FIELD:");
print(data['data']);
List list = data['data'];
print("TOTAL DATA: ${list.length}");
setState(() {
reports = list.map((e) {
print("MAP DATA:");
print(e);
Report report = Report.fromMap(e);
print("REPORT MODEL:");
print(report.id);
print(report.address);
print(report.status);
return report;
}).toList();
print("TOTAL REPORT MODEL: ${reports.length}");
isLoading = false;
});
} else {
print("ERROR STATUS CODE: ${response.statusCode}");
setState(() {
isLoading = false;
});
}
} catch (e) {
print("ERROR FETCH REPORTS:");
print(e);
setState(() {
isLoading = false;
});
}
}
List<Report> get filteredReports {
print("FILTER: $_selectedFilter");
if (_selectedFilter == 'Semua') return reports;
return reports.where((report) {
print("CHECK REPORT STATUS: ${report.status}");
return report.status == _selectedFilter;
}).toList();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: Colors.white,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _filters.map((filter) {
final isSelected = _selectedFilter == filter;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
selected: isSelected,
label: Text(filter),
selectedColor: const Color(0xFF206E97),
labelStyle: TextStyle(
color: isSelected ? Colors.white : Colors.black,
),
onSelected: (value) {
setState(() {
_selectedFilter = filter;
});
},
),
);
}).toList(),
),
),
),
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: filteredReports.isEmpty
? const Center(
child: Text(
"Tidak ada riwayat laporan",
style: TextStyle(fontSize: 16),
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: filteredReports.length,
itemBuilder: (context, index) {
final report = filteredReports[index];
return _buildReportCard(report);
},
),
),
],
);
}
Widget _buildReportCard(Report report) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID: ${report.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFF206E97),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: report.statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
report.status,
style: TextStyle(
color: report.statusColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
Text(
report.address,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.calendar_today, size: 16, color: Colors.grey),
const SizedBox(width: 4),
Text(
"${report.date.day}-${report.date.month}-${report.date.year}",
style: const TextStyle(color: Colors.grey),
),
],
),
const SizedBox(height: 8),
Text("Temuan: ${report.finding}"),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF206E97),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailHistoryPage(report: report),
),
);
},
child: const Text(
"LIHAT DETAIL",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:sijentik/component/app_theme.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'dashboardkader.dart';
import 'reports_page.dart';
import 'history_page.dart';
import 'profile_page.dart';
import 'add_report.dart';
import '../auth/login.dart';
class HomeKaderPage extends StatefulWidget {
final Map<String, dynamic> user;
const HomeKaderPage({super.key, required this.user});
@override
State<HomeKaderPage> createState() => _HomeKaderPageState();
}
class _HomeKaderPageState extends State<HomeKaderPage> {
int _selectedIndex = 0;
// Data dummy kader
// Pages untuk bottom navigation
late final List<Widget> _pages = [
const DashboardPage(user: {}),
const ReportsPage(),
const HistoryPage(),
const ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F9FF),
// APPBAR
appBar: AppBar(
backgroundColor: AppColors.button,
elevation: 4,
title: Row(
children: [
Image.asset(
'assets/images/logodua.png',
width: 35,
height: 35,
fit: BoxFit.contain,
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'SI-JENTIK',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'Kader: ${widget.user['name']}',
style: const TextStyle(
fontSize: 12,
color: Colors.white70,
),
),
],
),
],
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
),
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.button, AppColors.button],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
),
),
actions: [
// Notifikasi
IconButton(
onPressed: () {
_showNotifications();
},
icon: const Icon(Icons.notifications_none),
color: AppColors.white,
),
const SizedBox(width: 8),
],
),
// BODY
body: _pages[_selectedIndex],
// FLOATING ACTION BUTTON (hanya di halaman Laporan)
floatingActionButton: _selectedIndex == 1
? FloatingActionButton(
backgroundColor: const Color(0xFF206E97),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddReportPage(),
),
);
},
child: const Icon(Icons.add, color: Colors.white),
)
: null,
// BOTTOM NAVIGATION BAR
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
selectedItemColor: const Color(0xFF206E97),
unselectedItemColor: Colors.grey,
selectedLabelStyle: const TextStyle(fontSize: 12),
unselectedLabelStyle: const TextStyle(fontSize: 12),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.dashboard_outlined, size: 24),
activeIcon: Icon(Icons.dashboard, size: 24),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.add_circle_outline, size: 24),
activeIcon: Icon(Icons.add_circle, size: 24),
label: 'Laporan',
),
BottomNavigationBarItem(
icon: Icon(Icons.history_outlined, size: 24),
activeIcon: Icon(Icons.history, size: 24),
label: 'Riwayat',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline, size: 24),
activeIcon: Icon(Icons.person, size: 24),
label: 'Profil',
),
],
),
);
}
void _showNotifications() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Notifikasi'),
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: [
_buildNotificationItem(
title: 'Laporan Disetujui',
message: 'Laporan LP001 telah disetujui petugas',
time: '2 jam lalu',
isRead: false,
),
_buildNotificationItem(
title: 'Reminder Pemeriksaan',
message: 'Jadwal pemeriksaan rutin minggu ini',
time: '1 hari lalu',
isRead: true,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Tutup'),
),
],
);
},
);
}
Widget _buildNotificationItem({
required String title,
required String message,
required String time,
required bool isRead,
}) {
return ListTile(
leading: CircleAvatar(
backgroundColor: const Color(0xFF206E97).withOpacity(0.1),
child: Icon(
Icons.notifications,
color: const Color(0xFF206E97),
size: 20,
),
),
title: Text(
title,
style: TextStyle(
fontWeight: isRead ? FontWeight.normal : FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(message),
Text(
time,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
trailing: !isRead
? Container(
width: 10,
height: 10,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
)
: null,
);
}
void _confirmLogout() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Konfirmasi Logout'),
content: const Text('Apakah Anda yakin ingin keluar?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
TextButton(
onPressed: () {
Navigator.pop(context); // Tutup dialog
Navigator.pushNamedAndRemoveUntil(
context,
'/login',
(route) => false
);
},
child: const Text(
'Logout',
style: TextStyle(color: Colors.red),
),
),
],
);
},
);
}
}

View File

@ -0,0 +1,209 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:sijentik/component/app_theme.dart';
import 'package:sijentik/screens/auth/login.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
Map<String, dynamic>? user;
XFile? _imageFile;
final ImagePicker _picker = ImagePicker();
bool isLoading = true;
// ==========================
// GET PROFILE
// ==========================
Future getProfile() async {
final response = await http.get(
Uri.parse("http://192.168.1.6:8000/api/profile"),
headers: {"Accept": "application/json"},
);
print(response.statusCode);
print(response.body);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
user = data['user'];
isLoading = false;
});
} else {
print("Error API");
}
}
// ==========================
// PICK IMAGE
// ==========================
Future<void> _pickImage() async {
final XFile? picked = await _picker.pickImage(source: ImageSource.gallery);
if (picked != null) {
setState(() {
_imageFile = picked;
});
uploadPhoto();
}
}
// ==========================
// UPLOAD PHOTO
// ==========================
Future uploadPhoto() async {
var request = http.MultipartRequest(
"POST",
Uri.parse("http://192.168.1.6:8000/api/upload-photo"),
);
request.headers['Accept'] = 'application/json';
request.fields['user_id'] = user!['id'].toString();
request.files.add(
await http.MultipartFile.fromPath('profile_photo', _imageFile!.path),
);
var response = await request.send();
if (response.statusCode == 200) {
getProfile();
}
}
@override
void initState() {
super.initState();
getProfile();
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// ======================
// HEADER PROFILE
// ======================
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.button, AppColors.button],
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
// PHOTO
CircleAvatar(
radius: 60,
backgroundColor: Colors.white,
backgroundImage: _imageFile != null
? FileImage(File(_imageFile!.path))
: user!['photo_url'] != null
? NetworkImage(user!['photo_url'])
: null,
child: user!['photo_url'] == null && _imageFile == null
? const Icon(Icons.person, size: 60)
: null,
),
const SizedBox(height: 12),
Text(
user!['name'],
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
user!['role'],
style: const TextStyle(color: Colors.white70),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _pickImage,
icon: const Icon(Icons.camera_alt),
label: const Text("Ubah Foto"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF206E97),
),
),
],
),
),
const SizedBox(height: 20),
// ======================
// DETAIL PROFILE
// ======================
Card(
child: Column(
children: [
ListTile(
leading: const Icon(Icons.email),
title: const Text("Email"),
subtitle: Text(user!['email']),
),
const Divider(),
ListTile(
leading: const Icon(Icons.home),
title: const Text("Alamat"),
subtitle: Text(user!['address'] ?? "-"),
),
],
),
),
const SizedBox(height: 20),
// ======================
// LOGOUT
// ======================
Card(
child: ListTile(
leading: const Icon(Icons.logout, color: Colors.red),
title: const Text("Logout", style: TextStyle(color: Colors.red)),
onTap: () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false,
);
},
),
),
],
),
);
}
}

View File

@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'add_report.dart';
class ReportsPage extends StatelessWidget {
const ReportsPage({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Buat Laporan Baru',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 10),
Text(
'Laporkan temuan jentik nyamuk di rumah warga',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 30),
// Card Panduan
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF206E97).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF206E97).withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.info_outline,
color: Color(0xFF206E97),
),
const SizedBox(width: 10),
const Text(
'Panduan Singkat',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF206E97),
),
),
],
),
const SizedBox(height: 10),
const Text(
'1. Ambil foto wadah/tempat genangan air\n'
'2. Isi data alamat rumah\n'
'3. Catat temuan jentik/nyamuk\n'
'4. Kirim ke petugas untuk verifikasi',
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
],
),
),
const SizedBox(height: 30),
// Tombol Mulai
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddReportPage(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF206E97),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
'Buat Laporan Baru',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,205 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sijentik/component/app_theme.dart';
class DaftarKaderPage extends StatefulWidget {
const DaftarKaderPage({super.key});
@override
State<DaftarKaderPage> createState() => _DaftarKaderPageState();
}
class _DaftarKaderPageState extends State<DaftarKaderPage> {
List<dynamic> kaderList = [];
bool isLoading = true;
final String baseUrl = "http://192.168.1.6:8000/api";
@override
void initState() {
super.initState();
fetchKader();
}
Future<void> fetchKader() async {
final response = await http.get(Uri.parse("$baseUrl/users/kader"));
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
kaderList = data['data'];
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppColors.button,
title: const Text("Daftar Kader"),
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: kaderList.length,
itemBuilder: (context, index) {
final kader = kaderList[index];
return buildKaderCard(kader);
},
),
);
}
Widget buildKaderCard(dynamic kader) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(child: Text(kader['name'][0])),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kader['name'],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
kader['email'],
style: const TextStyle(color: Colors.grey),
),
],
),
),
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
showEditDialog(kader);
},
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.location_on, size: 16),
const SizedBox(width: 5),
Text("RT/RW : ${kader['rtrw']}"),
],
),
const SizedBox(height: 5),
Text("Alamat : ${kader['address']}"),
],
),
),
);
}
void showEditDialog(dynamic kader) {
TextEditingController name = TextEditingController(text: kader['name']);
TextEditingController email = TextEditingController(text: kader['email']);
TextEditingController address = TextEditingController(
text: kader['address'],
);
TextEditingController rtrw = TextEditingController(text: kader['rtrw']);
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Edit Data Kader"),
content: SingleChildScrollView(
child: Column(
children: [
TextField(
controller: name,
decoration: const InputDecoration(labelText: "Nama"),
),
const SizedBox(height: 10),
TextField(
controller: email,
decoration: const InputDecoration(labelText: "Email"),
),
const SizedBox(height: 10),
TextField(
controller: address,
decoration: const InputDecoration(labelText: "Alamat"),
),
const SizedBox(height: 10),
TextField(
controller: rtrw,
decoration: const InputDecoration(labelText: "RT/RW"),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
ElevatedButton(
onPressed: () async {
await http.put(
Uri.parse("$baseUrl/users/${kader['id']}"),
body: {
"name": name.text,
"email": email.text,
"address": address.text,
"rtrw": rtrw.text,
},
);
Navigator.pop(context);
fetchKader();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil diperbarui")),
);
},
child: const Text("Simpan"),
),
],
),
);
}
}

View File

@ -0,0 +1,439 @@
import 'package:flutter/material.dart';
import 'package:sijentik/component/app_theme.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:sijentik/screens/auth/dashboardService.dart';
import 'package:sijentik/screens/petugas/laporanpetugas.dart';
import 'package:sijentik/screens/petugas/daftarkader.dart';
import 'package:sijentik/screens/petugas/verifikasikader.dart';
import 'package:sijentik/screens/auth/login.dart';
class DashboardPetugas extends StatefulWidget {
const DashboardPetugas({super.key});
@override
State<DashboardPetugas> createState() => _DashboardPetugasState();
}
class _DashboardPetugasState extends State<DashboardPetugas> {
int _currentIndex = 0;
Map<String, dynamic> stats = {};
List<Map<String, dynamic>> recentReports = [];
bool isLoading = true;
String? errorMessage;
@override
void initState() {
super.initState();
fetchDashboard();
}
Future<void> fetchDashboard() async {
setState(() {
isLoading = true;
errorMessage = null;
});
try {
final result = await DashboardService.getDashboard();
if (result == null) {
setState(() {
isLoading = false;
errorMessage = "Gagal mengambil data dashboard.";
});
return;
}
final rawStats = result['stats'];
final rawReports = result['recentReports'];
setState(() {
stats = rawStats is Map<String, dynamic>
? rawStats
: rawStats is Map
? Map<String, dynamic>.from(rawStats)
: {};
recentReports = rawReports is List
? rawReports
.whereType<Map>()
.map((e) => Map<String, dynamic>.from(e))
.toList()
: [];
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
errorMessage = "Terjadi kesalahan: $e";
});
}
}
Color getStatusColor(String status) {
switch (status.toLowerCase()) {
case "diterima":
return Colors.green;
case "proses":
return Colors.orange;
case "ditolak":
return Colors.red;
default:
return Colors.grey;
}
}
String getStatusLabel(String status) {
switch (status.toLowerCase()) {
case "diterima":
return "Selesai";
case "proses":
return "Diproses";
case "ditolak":
return "Ditolak";
default:
return status;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: _currentIndex == 0
? AppBar(
backgroundColor: AppColors.button,
centerTitle: true,
title: const Text(
"Dashboard Petugas",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
)
: null,
body: IndexedStack(
index: _currentIndex,
children: [
_buildDashboardContent(),
const LaporanPetugasPage(),
const VerifikasiKaderPage(),
const DaftarKaderPage(),
],
),
bottomNavigationBar: _buildBottomNavigationBar(),
);
}
Widget _buildDashboardContent() {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (errorMessage != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(errorMessage!, textAlign: TextAlign.center),
const SizedBox(height: 12),
ElevatedButton(
onPressed: fetchDashboard,
child: const Text("Coba Lagi"),
),
],
),
),
);
}
return RefreshIndicator(
onRefresh: fetchDashboard,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeHeader(),
const SizedBox(height: 20),
_buildStatsSection(),
const SizedBox(height: 24),
_buildRecentReports(),
],
),
),
);
}
Widget _buildWelcomeHeader() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.button,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(Icons.person, size: 30, color: Colors.deepPurple),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Selamat Datang", style: TextStyle(color: Colors.white)),
Text(
"Petugas Kesehatan",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.logout, color: Colors.white),
onPressed: () => _showLogoutDialog(context),
),
],
),
);
}
Widget _buildStatsSection() {
return GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.1,
children: [
_buildStatCard(
icon: FontAwesomeIcons.file,
title: "Total Laporan",
value: (stats['totalLaporan'] ?? 0).toString(),
color: Colors.blue,
subtitle: "Semua laporan",
),
_buildStatCard(
icon: FontAwesomeIcons.spinner,
title: "Diproses",
value: (stats['laporanDiproses'] ?? 0).toString(),
color: Colors.orange,
subtitle: "${stats['prosesPercent'] ?? 0}% dari total",
),
_buildStatCard(
icon: FontAwesomeIcons.check,
title: "Selesai",
value: (stats['laporanSelesai'] ?? 0).toString(),
color: Colors.green,
subtitle: "${stats['selesaiPercent'] ?? 0}% dari total",
),
],
);
}
Widget _buildStatCard({
required IconData icon,
required String title,
required String value,
required Color color,
required String subtitle,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
FaIcon(icon, color: color),
const Spacer(),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
color: color,
),
),
],
),
const SizedBox(height: 8),
Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
);
}
Widget _buildRecentReports() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Laporan Terbaru",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
if (recentReports.isEmpty)
const Padding(
padding: EdgeInsets.all(20),
child: Center(child: Text("Belum ada laporan")),
)
else
...recentReports.map((report) => _buildReportCard(report)),
],
);
}
Widget _buildReportCard(Map<String, dynamic> report) {
final status = (report['status'] ?? '').toString();
final color = getStatusColor(status);
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
FaIcon(FontAwesomeIcons.clipboardCheck, color: color),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Laporan #${(report['id'] ?? '-').toString()}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text((report['judul'] ?? '-').toString()),
Text(
(report['alamat'] ?? '-').toString(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text((report['tanggal'] ?? '').toString()),
const SizedBox(height: 5),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(4),
),
child: Text(
getStatusLabel(status),
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
);
}
Widget _buildBottomNavigationBar() {
return BottomAppBar(
child: SizedBox(
height: 60,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavItem(Icons.dashboard, "Dashboard", 0),
_buildNavItem(Icons.list_alt, "Laporan", 1),
_buildNavItem(Icons.verified_user, "Verifikasi", 2),
_buildNavItem(Icons.people, "Kader", 3),
],
),
),
);
}
Widget _buildNavItem(IconData icon, String label, int index) {
return GestureDetector(
onTap: () {
setState(() {
_currentIndex = index;
});
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: _currentIndex == index ? AppColors.button : Colors.grey,
),
Text(
label,
style: TextStyle(
fontSize: 10,
color: _currentIndex == index ? AppColors.button : Colors.grey,
),
),
],
),
);
}
void _showLogoutDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Keluar"),
content: const Text("Apakah anda yakin ingin keluar?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
TextButton(
onPressed: () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false,
);
},
child: const Text("Keluar"),
),
],
),
);
}
}

View File

@ -0,0 +1,436 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
class LaporanPetugasPage extends StatefulWidget {
const LaporanPetugasPage({super.key});
@override
State<LaporanPetugasPage> createState() => _LaporanPetugasPageState();
}
class _LaporanPetugasPageState extends State<LaporanPetugasPage> {
static const String baseUrl = 'http://192.168.1.6:8000/api';
List<dynamic> laporanList = [];
bool isLoading = false;
String _filterStatus = 'Semua';
final List<String> _statusList = ['Semua', 'proses', 'diterima', 'ditolak'];
@override
void initState() {
super.initState();
fetchLaporan();
}
// =========================
// GET LAPORAN
// =========================
Future<void> fetchLaporan() async {
setState(() => isLoading = true);
try {
final response = await http.get(Uri.parse('$baseUrl/laporan'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
laporanList = data['data'];
});
} else {
_showMessage('Gagal mengambil data', success: false);
}
} catch (e) {
_showMessage('Error: $e', success: false);
} finally {
setState(() => isLoading = false);
}
}
// =========================
// UPDATE STATUS
// =========================
Future<void> updateStatus(int id, String status, {String? catatan}) async {
try {
final response = await http.patch(
Uri.parse('$baseUrl/laporan/$id/status'),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'status': status,
if (catatan != null) 'catatan_petugas': catatan,
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
_showMessage(data['message']);
fetchLaporan();
} else {
_showMessage(data['message'] ?? 'Gagal update status', success: false);
}
} catch (e) {
_showMessage('Error: $e', success: false);
}
}
// =========================
// MESSAGE
// =========================
void _showMessage(String message, {bool success = true}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
// =========================
// FILTER
// =========================
List<dynamic> get filteredLaporan {
if (_filterStatus == 'Semua') return laporanList;
return laporanList.where((lap) {
return lap['status'] == _filterStatus;
}).toList();
}
// =========================
// STATUS COLOR
// =========================
Color getStatusColor(String status) {
switch (status) {
case 'proses':
return Colors.orange;
case 'diterima':
return Colors.green;
case 'ditolak':
return Colors.red;
default:
return Colors.grey;
}
}
// =========================
// UPDATE STATUS DIALOG
// =========================
void showUpdateDialog(dynamic lap, String newStatus) {
final TextEditingController noteController = TextEditingController();
bool needNote = newStatus == 'ditolak';
if (!needNote) {
updateStatus(lap['id'], newStatus);
return;
}
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Catatan Penolakan'),
content: TextField(
controller: noteController,
maxLines: 3,
decoration: const InputDecoration(
hintText: 'Masukkan catatan...',
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
updateStatus(lap['id'], newStatus, catatan: noteController.text);
},
child: const Text('Simpan'),
),
],
),
);
}
// =========================
// DETAIL LAPORAN
// =========================
void showDetailDialog(dynamic lap) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Detail Laporan ${lap['id']}'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Judul: ${lap['judul'] ?? '-'}'),
Text(
'Tanggal: ${lap['tanggal'] != null ? DateFormat('dd-MM-yyyy').format(DateTime.parse(lap['tanggal'])) : '-'}',
),
Text('Alamat: ${lap['alamat'] ?? '-'}'),
Text('Status: ${lap['status'] ?? '-'}'),
if (lap['catatan_petugas'] != null &&
lap['catatan_petugas'].isNotEmpty)
Text('Catatan: ${lap['catatan_petugas']}'),
if (lap['gambar_url'] != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Image.network(
lap['gambar_url'],
height: 150,
fit: BoxFit.cover,
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Tutup'),
),
],
),
);
}
// =========================
// UI
// =========================
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Laporan Petugas'),
backgroundColor: Colors.blue,
),
body: Column(
children: [
// FILTER STATUS
Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _statusList.length,
itemBuilder: (context, index) {
final status = _statusList[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ChoiceChip(
label: Text(status),
selected: _filterStatus == status,
onSelected: (_) {
setState(() {
_filterStatus = status;
});
},
),
);
},
),
),
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: filteredLaporan.length,
itemBuilder: (context, index) {
final lap = filteredLaporan[index];
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// HEADER
Row(
children: [
const Icon(
Icons.report_problem,
color: Colors.blue,
),
const SizedBox(width: 8),
Expanded(
child: Text(
lap['judul'] ?? '-',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: getStatusColor(lap['status']).withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
),
child: Text(
lap['status'],
style: TextStyle(
color: getStatusColor(lap['status']),
fontWeight: FontWeight.bold,
),
),
)
],
),
const SizedBox(height: 10),
/// ALAMAT
Row(
children: [
const Icon(Icons.location_on, size: 18, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
lap['alamat'] ?? '-',
style: const TextStyle(color: Colors.black87),
),
),
],
),
const SizedBox(height: 6),
/// TANGGAL
Row(
children: [
const Icon(Icons.calendar_today, size: 18, color: Colors.grey),
const SizedBox(width: 6),
Text(
lap['tanggal'] != null
? DateFormat('dd MMM yyyy')
.format(DateTime.parse(lap['tanggal']))
: '-',
),
],
),
/// GAMBAR
if (lap['gambar_url'] != null)
Padding(
padding: const EdgeInsets.only(top: 10),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
lap['gambar_url'],
height: 160,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 12),
/// BUTTON ACTION
Row(
children: [
/// PROSES
Expanded(
child: ElevatedButton.icon(
onPressed: () => showUpdateDialog(lap, 'proses'),
icon: const Icon(Icons.sync),
label: const Text("Proses"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(width: 6),
/// TERIMA
Expanded(
child: ElevatedButton.icon(
onPressed: () => showUpdateDialog(lap, 'diterima'),
icon: const Icon(Icons.check),
label: const Text("Terima"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(width: 6),
/// TOLAK
Expanded(
child: ElevatedButton.icon(
onPressed: () => showUpdateDialog(lap, 'ditolak'),
icon: const Icon(Icons.close),
label: const Text("Tolak"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
],
),
const SizedBox(height: 6),
/// DETAIL BUTTON
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () => showDetailDialog(lap),
icon: const Icon(Icons.visibility),
label: const Text("Lihat Detail"),
),
)
],
),
),
);
},
),
),
],
),
);
}
}

View File

@ -0,0 +1,416 @@
import 'package:flutter/material.dart';
import 'package:sijentik/component/app_theme.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class ProfilePetugasPage extends StatefulWidget {
const ProfilePetugasPage({super.key});
@override
State<ProfilePetugasPage> createState() => _ProfilePetugasPageState();
}
class _ProfilePetugasPageState extends State<ProfilePetugasPage> {
// Data dummy petugas
final Map<String, dynamic> _petugasData = {
'nama': 'Dr. Ahmad Fauzi',
'nip': '198304122006041001',
'jabatan': 'Petugas Kesehatan',
'instansi': 'Puskesmas Mekar Jaya',
'email': 'ahmad.fauzi@puskesmas.id',
'telepon': '081234567899',
'alamat': 'Jl. Kesehatan No. 45, Bandung',
'mulaiTugas': '01 Januari 2020',
'foto': 'assets/images/profile_petugas.jpg',
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppColors.button,
elevation: 4,
centerTitle: true,
title: const Text(
'Profil Saya',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.white),
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
),
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.button, AppColors.button],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
),
),
// removed AppBar edit action per request
),
body: SingleChildScrollView(
child: Column(
children: [
// Header: remove background color keep layout, adjust colors for light background
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8), // slight downward shift
// Avatar centered
Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: AppColors.button, width: 2),
),
child: CircleAvatar(
radius: 56,
backgroundColor: Colors.white,
child: const Icon(Icons.person, size: 56, color: Colors.grey),
),
),
// Name and info centered
Text(
_petugasData['nama'],
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.textDark,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
_petugasData['jabatan'],
textAlign: TextAlign.center,
style: TextStyle(color: AppColors.textGrey, fontSize: 13),
),
const SizedBox(height: 2),
Text(
_petugasData['instansi'],
textAlign: TextAlign.center,
style: TextStyle(color: AppColors.textGrey, fontSize: 12),
),
const SizedBox(height: 12),
// Buttons centered beneath
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _editProfile,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.button,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Ubah Profil', style: TextStyle(fontWeight: FontWeight.w600)),
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: _logout,
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppColors.textGrey.withOpacity(0.4)),
foregroundColor: AppColors.textDark,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Keluar'),
),
],
),
],
),
),
// Menu profil
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Informasi pribadi
_buildInfoSection(),
const SizedBox(height: 24),
// Menu lainnya
_buildMenuSection(),
const SizedBox(height: 24),
// Statistik
],
),
),
],
),
),
);
}
Widget _buildInfoSection() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informasi Pribadi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildInfoRow(
Icons.assignment_ind,
'NIP',
_petugasData['nip'],
),
_buildInfoRow(
Icons.email,
'Email',
_petugasData['email'],
),
_buildInfoRow(
Icons.phone,
'Telepon',
_petugasData['telepon'],
),
_buildInfoRow(
Icons.location_on,
'Alamat',
_petugasData['alamat'],
),
_buildInfoRow(
Icons.date_range,
'Mulai Tugas',
_petugasData['mulaiTugas'],
),
],
),
),
);
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: AppColors.button, size: 22),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
}
Widget _buildMenuSection() {
final List<Map<String, dynamic>> menuItems = [
{
'icon': Icons.notifications,
'title': 'Notifikasi',
'subtitle': 'Pengaturan notifikasi',
'color': Colors.orange,
},
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pengaturan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
...menuItems.map((item) => _buildMenuItem(item)).toList(),
],
);
}
Widget _buildMenuItem(Map<String, dynamic> item) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 1,
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: item['color'].withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(item['icon'], color: item['color']),
),
title: Text(item['title']),
subtitle: Text(item['subtitle']),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// TODO: Navigasi ke halaman masing-masing
},
),
);
}
Widget _buildStatCard(String value, String label, IconData icon, Color color) {
return Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
shape: BoxShape.circle,
),
child: FaIcon(icon, color: color, size: 24),
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
);
}
void _editProfile() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Edit Profil'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: const InputDecoration(
labelText: 'Nama Lengkap',
border: OutlineInputBorder(),
),
controller: TextEditingController(text: _petugasData['nama']),
),
const SizedBox(height: 12),
TextField(
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
controller: TextEditingController(text: _petugasData['email']),
),
const SizedBox(height: 12),
TextField(
decoration: const InputDecoration(
labelText: 'Telepon',
border: OutlineInputBorder(),
),
controller: TextEditingController(text: _petugasData['telepon']),
),
const SizedBox(height: 12),
TextField(
decoration: const InputDecoration(
labelText: 'Alamat',
border: OutlineInputBorder(),
),
maxLines: 3,
controller: TextEditingController(text: _petugasData['alamat']),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Profil berhasil diperbarui'),
backgroundColor: Colors.green,
),
);
},
child: const Text('Simpan'),
),
],
),
);
}
void _logout() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Konfirmasi Keluar'),
content: const Text('Apakah Anda yakin ingin keluar dari aplikasi?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// TODO: Implement logout logic
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Berhasil keluar'),
backgroundColor: Colors.green,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('Keluar'),
),
],
),
);
}
}

View File

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:sijentik/component/app_theme.dart';
class ReportDetailPage extends StatelessWidget {
final Map<String, dynamic> report;
final VoidCallback? onProcess;
final VoidCallback? onComplete;
final VoidCallback? onClose;
const ReportDetailPage({
super.key,
required this.report,
this.onProcess,
this.onComplete,
this.onClose,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppColors.button,
title: Text('Detail Laporan ${report['id']}', style: const TextStyle(color: Colors.white)),
iconTheme: const IconThemeData(color: Colors.white),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRow('ID', report['id']),
_buildRow('Kader', report['kader']),
_buildRow('Alamat', report['alamat']),
_buildRow('Tanggal', report['tanggal']),
_buildRow('Jenis', report['jenis']),
_buildRow('Status', report['status']),
_buildRow('Prioritas', report['prioritas']),
const SizedBox(height: 8),
const Text('Keterangan', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
Text(report['keterangan'] ?? '-', style: const TextStyle(color: Colors.grey)),
const SizedBox(height: 12),
if (report['catatanPetugas'] != null && report['catatanPetugas'].toString().isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(
report['status'] == 'Selesai' ? 'Catatan Penyelesaian' : 'Alasan Penolakan',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
Text(report['catatanPetugas'], style: const TextStyle(color: Colors.grey)),
],
),
const SizedBox(height: 12),
Container(
height: 180,
width: double.infinity,
color: Colors.grey[200],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.photo, size: 50, color: Colors.grey),
const SizedBox(height: 8),
Text('Foto Bukti: ${report['id']}.jpg', style: const TextStyle(color: Colors.grey)),
],
),
),
),
const Spacer(),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
Navigator.of(context).pop();
if (onClose != null) onClose!();
},
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
child: const Text('Tutup'),
),
),
const SizedBox(width: 12),
if (report['status'] == 'Menunggu')
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
if (onProcess != null) onProcess!();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(48),
),
child: const Text('Proses'),
),
)
else if (report['status'] == 'Diproses')
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
if (onComplete != null) onComplete!();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(48),
),
child: const Text('Selesaikan'),
),
),
],
)
],
),
),
);
}
Widget _buildRow(String label, dynamic value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 120, child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.grey))),
Expanded(child: Text(value?.toString() ?? '-', style: const TextStyle(fontWeight: FontWeight.w500))),
],
),
);
}
}

View File

@ -0,0 +1,260 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sijentik/component/app_theme.dart';
class VerifikasiKaderPage extends StatefulWidget {
const VerifikasiKaderPage({super.key});
@override
State<VerifikasiKaderPage> createState() => _VerifikasiKaderPageState();
}
class _VerifikasiKaderPageState extends State<VerifikasiKaderPage> {
List kaderList = [];
bool isLoadingList = false;
static const String baseUrl = 'http://192.168.1.6:8000/api';
@override
void initState() {
super.initState();
fetchKader();
}
// =========================
// FETCH KADER PENDING
// =========================
Future<void> fetchKader() async {
setState(() => isLoadingList = true);
try {
final response = await http.get(
Uri.parse('$baseUrl/kader/pending'),
headers: {'Accept': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
kaderList = data['data'];
});
} else {
final data = jsonDecode(response.body);
_showMessage(data['message'] ?? 'Gagal mengambil data');
}
} catch (e) {
_showMessage('Error: $e');
} finally {
setState(() => isLoadingList = false);
}
}
// =========================
// TERIMA KADER
// =========================
Future<void> approveKader(int id) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/kader/approve/$id'),
headers: {'Accept': 'application/json'},
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
_showMessage(data['message'] ?? 'Kader diterima');
fetchKader();
} else {
_showMessage(data['message'] ?? 'Gagal menerima kader');
}
} catch (e) {
_showMessage('Error: $e');
}
}
// =========================
// TOLAK KADER
// =========================
Future<void> rejectKader(int id) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/kader/reject/$id'),
headers: {'Accept': 'application/json'},
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
_showMessage(data['message'] ?? 'Kader ditolak');
fetchKader();
} else {
_showMessage(data['message'] ?? 'Gagal menolak kader');
}
} catch (e) {
_showMessage('Error: $e');
}
}
// =========================
// MESSAGE
// =========================
void _showMessage(String message, {bool success = true}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
// =========================
// LIST KADER
// =========================
Widget buildKaderList() {
if (isLoadingList) {
return const Center(child: CircularProgressIndicator());
}
if (kaderList.isEmpty) {
return const Center(child: Text('Tidak ada kader menunggu verifikasi'));
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: kaderList.length,
itemBuilder: (context, index) {
final kader = kaderList[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kader['name'] ?? '-',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 6),
Text('Email: ${kader['email'] ?? '-'}'),
Text('RT/RW: ${kader['rtrw'] ?? '-'}'),
const SizedBox(height: 6),
Text('Alamat: ${kader['address'] ?? '-'}'),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
onPressed: () => approveKader(kader['id']),
child: const Text(
'Terima',
style: TextStyle(color: Colors.white),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () => rejectKader(kader['id']),
child: const Text(
'Tolak',
style: TextStyle(color: Colors.white),
),
),
),
],
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Verifikasi Kader'),
backgroundColor: AppColors.button,
),
body: buildKaderList(),
);
}
}

View File

@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:sijentik/component/app_theme.dart';
import 'package:sijentik/models/report_model.dart';
class VerifikasiKaderDetailPage extends StatelessWidget {
final Map<String, dynamic> kader;
final VoidCallback onAccept;
final VoidCallback onReject;
const VerifikasiKaderDetailPage({
super.key,
required this.kader,
required this.onAccept,
required this.onReject,
});
void _confirmAccept(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Terima Kader"),
content: const Text(
"Apakah anda yakin ingin menerima kader ini?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
onAccept();
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: const Text("Terima"),
),
],
);
},
);
}
void _confirmReject(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Tolak Kader"),
content: const Text(
"Apakah anda yakin ingin menolak kader ini?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
onReject();
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text("Tolak"),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppColors.button,
iconTheme: const IconThemeData(color: Colors.white),
title: const Text(
'Detail Verifikasi',
style: TextStyle(color: Colors.white),
),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 36,
backgroundColor: AppColors.button,
child: Text(
kader['nama'] != null
? kader['nama']!
.toString()
.split(' ')
.map((e) => e[0])
.join()
: '?',
style: const TextStyle(
color: Colors.white, fontSize: 20),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
kader['nama'] ?? '-',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'ID: ${kader['id'] ?? '-'}',
style:
const TextStyle(color: Colors.grey),
),
],
),
),
],
),
const SizedBox(height: 20),
TextFormField(
initialValue: kader['nama'] ?? '-',
readOnly: true,
decoration: _buildInputDecoration('Nama'),
),
const SizedBox(height: 12),
TextFormField(
initialValue:
kader['rt'] ?? kader['wilayah'] ?? '-',
readOnly: true,
decoration: _buildInputDecoration('RT'),
),
const SizedBox(height: 12),
TextFormField(
initialValue: kader['email'] ?? '-',
readOnly: true,
decoration: _buildInputDecoration('Email'),
),
const SizedBox(height: 12),
TextFormField(
initialValue: kader['alamat'] ?? '-',
readOnly: true,
maxLines: 3,
decoration: _buildInputDecoration(
'Alamat lengkap',
alignTop: true,
),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 12),
const Text(
'Keterangan',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
kader['keterangan'] ??
'Tidak ada keterangan tambahan.',
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => _confirmReject(context),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red,
side: const BorderSide(color: Colors.red),
),
child: const Text('Tolak'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () => _confirmAccept(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: const Text('Terima'),
),
),
],
),
],
),
),
);
}
InputDecoration _buildInputDecoration(String label,
{bool alignTop = false}) {
return InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
alignLabelWithHint: alignTop,
isDense: true,
);
}
}

View File

@ -0,0 +1,79 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'auth/login.dart';
import 'package:sijentik/component/app_theme.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
final double topLogoOffset = 90; // jarak dari atas ke logo
final double betweenLogoAndIllustration = 0; // jarak logo ke ilustrasi
final double textBottomOffset = 200; // jarak teks dari area bawah (plus safe area)
final double illustrationOffset = 50; // positif = naikkan ilustrasi
@override
void initState() {
super.initState();
// PASTIKAN NAVIGASI SETELAH WIDGET DIBUAT
WidgetsBinding.instance.addPostFrameCallback((_) {
Timer(const Duration(seconds: 3), () {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
}
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F9FF),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(height: topLogoOffset),
Image.asset(
'assets/images/logo.png',
width: 180,
),
SizedBox(height: betweenLogoAndIllustration),
Flexible(
fit: FlexFit.loose,
child: Center(
child: Transform.translate(
offset: Offset(0, -illustrationOffset),
child: Image.asset(
'assets/images/splashscreen.png',
width: 350,
fit: BoxFit.contain,
),
),
),
),
Padding(
padding: EdgeInsets.fromLTRB(52, 0, 52, MediaQuery.of(context).padding.bottom + textBottomOffset),
child: Text(
'Sistem Informasi Pemantauan Jentik Nyamuk',
textAlign: TextAlign.center,
style: AppTextStyles.bodyMedium.copyWith(
fontSize: 16,
color: const Color(0xFF333333),
),
),
),
],
),
),
);
}
}

1
linux/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
flutter/ephemeral

128
linux/CMakeLists.txt Normal file
View File

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "sijentik")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.sijentik")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

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