This commit is contained in:
sugengcahyono 2026-05-22 19:30:56 +07:00
parent c697b2467f
commit ca8fa2f83c
190 changed files with 15855 additions and 0 deletions

43
praresi/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
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
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
praresi/.metadata Normal file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9
base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9
- platform: android
create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9
base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9
# 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
praresi/README.md Normal file
View File

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

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

13
praresi/android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,85 @@
plugins {
id "com.android.application"
// START: FlutterFire Configuration
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace "com.example.praresi"
// compileSdkVersion flutter.compileSdkVersion --> diganti dibawah
compileSdkVersion 34
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
// defaultConfig {
// // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
// applicationId "com.example.praresi"
// // You can update the following values to match your application needs.
// // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
// minSdkVersion flutter.minSdkVersion
// targetSdkVersion flutter.targetSdkVersion
// versionCode flutterVersionCode.toInteger()
// versionName flutterVersionName
// multiDexEnabled true // tambahan
// }
// baru dibawah
defaultConfig {
applicationId "com.example.praresi"
minSdkVersion 21
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
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.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "androidx.multidex:multidex:2.0.1" // tambahan
}

View File

@ -0,0 +1,48 @@
{
"project_info": {
"project_number": "1084316054508",
"project_id": "snapresi-8e964",
"storage_bucket": "snapresi-8e964.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:1084316054508:android:86d995859cf2c85df306c9",
"android_client_info": {
"package_name": "com.example.praresi"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBR6rF7--1ZQYFCPVVjn1wMAt5qcmfMgGw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:1084316054508:android:38e16231a48eb280f306c9",
"android_client_info": {
"package_name": "com.example.snapresi"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBR6rF7--1ZQYFCPVVjn1wMAt5qcmfMgGw"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

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

View File

@ -0,0 +1,77 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.praresi">
<!-- ✅ Izin kamera & penyimpanan -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="36" />
<!-- semula 28 -->
<!-- ✅ Overlay & foreground service -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- ✅ Android 13+ (akses media) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- ✅ Clipboard -->
<uses-permission
android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND"
tools:ignore="ProtectedPermissions" />
<!-- ✅ Aplikasi utama -->
<application
android:name="${applicationName}"
android:label="Praresi"
android:icon="@mipmap/ic_launcher">
<!-- ✅ Activity utama -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Tema awal Flutter -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<!-- Intent utama -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- ✅ Tambahan untuk UCrop -->
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<!-- ✅ Service overlay -->
<service
android:name="flutter.overlay.window.flutter_overlay_window.OverlayService"
android:exported="false"
android:foregroundServiceType="specialUse" />
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="explanation_for_special_use"/>
<!-- Registrasi otomatis plugin Flutter -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -0,0 +1,6 @@
package com.example.praresi
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

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>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.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,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1976D2</color>
</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>

View File

@ -0,0 +1,30 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true

View File

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

View File

@ -0,0 +1,32 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
plugins {
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration
}
include ":app"

View File

@ -0,0 +1,738 @@
[
{
"provinsi":"Aceh",
"kota":[
"Kota Banda Aceh",
"Kota Sabang",
"Kota Lhokseumawe",
"Kota Langsa",
"Kota Subulussalam",
"Kab. Aceh Selatan",
"Kab. Aceh Tenggara",
"Kab. Aceh Timur",
"Kab. Aceh Tengah",
"Kab. Aceh Barat",
"Kab. Aceh Besar",
"Kab. Pidie",
"Kab. Aceh Utara",
"Kab. Simeulue",
"Kab. Aceh Singkil",
"Kab. Bireun",
"Kab. Aceh Barat Daya",
"Kab. Gayo Lues",
"Kab. Aceh Jaya",
"Kab. Nagan Raya",
"Kab. Aceh Tamiang",
"Kab. Bener Meriah",
"Kab. Pidie Jaya"
]
},
{
"provinsi":"Sumatera Utara",
"kota":[
"Kota Medan",
"Kota Pematang Siantar",
"Kota Sibolga",
"Kota Tanjung Balai",
"Kota Binjai",
"Kota Tebing Tinggi",
"Kota Padang Sidempuan",
"Kota Gunung Sitoli",
"Kab. Serdang Bedagai",
"Kab. Samosir ",
"Kab. Humbang Hasundutan",
"Kab. Pakpak Bharat",
"Kab. Nias Selatan",
"Kab. Mandailing Natal",
"Kab. Toba Samosir",
"Kab. Dairi",
"Kab. Labuhan Batu",
"Kab. Asahan",
"Kab. Simalungun",
"Kab. Deli Serdang",
"Kab. Karo",
"Kab. Langkat",
"Kab. Nias",
"Kab. Tapanuli Selatan",
"Kab. Tapanuli Utara",
"Kab. Tapanuli Tengah",
"Kab. Batu Bara",
"Kab. Padang Lawas Utara",
"Kab. Padang Lawas",
"Kab. Labuhanbatu Selatan",
"Kab. Labuhanbatu Utara",
"Kab. Nias Utara",
"Kab. Nias Barat"
]
},
{
"provinsi":"Sumatera Barat",
"kota":[
"Kota Padang",
"Kota Solok",
"Kota Sawhlunto",
"Kota Padang Panjang",
"Kota Bukittinggi",
"Kota Payakumbuh",
"Kota Pariaman",
"Kab. Pasaman Barat",
"Kab. Solok Selatan",
"Kab. Dharmasraya",
"Kab. Kepulauan Mentawai",
"Kab. Pasaman",
"Kab. Lima Puluh Kota",
"Kab. Agam",
"Kab. Padang Pariaman",
"Kab. Tanah Datar",
"Kab. Sijunjung",
"Kab. Solok",
"Kab. Pesisir Selatan"
]
},
{
"provinsi":"Riau",
"kota":[
"Kota Pekan Baru",
"Kota Dumai",
"Kab. Kepulauan Meranti",
"Kab. Kuantan Singingi",
"Kab. Siak",
"Kab. Rokan Hilir",
"Kab. Rokan Hulu",
"Kab. Pelalawan",
"Kab. Indragiri Hilir",
"Kab. Bengkalis",
"Kab. Indragiri Hulu",
"Kab. Kampar"
]
},
{
"provinsi":"Jambi",
"kota":[
"Kota Jambi",
"Kota Sungai Penuh",
"Kab. Tebo",
"Kab. Bungo",
"Kab. Tanjung Jabung Timur",
"Kab. Tanjung Jabung Barat",
"Kab. Muaro Jambi",
"Kab. Batanghari",
"Kab. Sarolangun",
"Kab. Merangin",
"Kab. Kerinci"
]
},
{
"provinsi":"Sumatera Selatan",
"kota":[
"Kota Palembang",
"Kota Pagar Alam",
"Kota Lubuk Linggau",
"Kota Prabumulih",
"Kab. Musi Rawas Utara",
"Kab. Penukal Abab Lematang Ilir",
"Kab. Empat Lawang",
"Kab. Ogan Ilir ",
"Kab. Ogan Komering Ulu Selatan ",
"Kab. Ogan Komering Ulu Timur ",
"Kab. Banyuasin",
"Kab. Musi Banyuasin",
"Kab. Musi Rawas",
"Kab. Lahat",
"Kab. Muara Enim",
"Kab. Ogan Komering Ilir",
"Kab. Ogan Komering Ulu"
]
},
{
"provinsi":"Bengkulu",
"kota":[
"Kota Bengkulu",
"Kab. Bengkulu Tengah",
"Kab. Kepahiang ",
"Kab. Lebong",
"Kab. Muko Muko",
"Kab. Seluma",
"Kab. Kaur",
"Kab. Bengkulu Utara",
"Kab. Rejang Lebong",
"Kab. Bengkulu Selatan"
]
},
{
"provinsi":"Lampung",
"kota":[
"Kota Bandar Lampung",
"Kota Metro",
"Kab. Pesisir Barat",
"Kab. Tulangbawang Barat",
"Kab. Mesuji",
"Kab. Pringsewu",
"Kab. Pesawaran",
"Kab. Way Kanan",
"Kab. Lampung Timur",
"Kab. Tanggamus",
"Kab. Tulang Bawang",
"Kab. Lampung Barat",
"Kab. Lampung Utara",
"Kab. Lampung Tengah",
"Kab. Lampung Selatan"
]
},
{
"provinsi":"Kepulauan Bangka Belitung",
"kota":[
"Kota Pangkal Pinang",
"Kab. Belitung Timur",
"Kab. Bangka Barat",
"Kab. Bangka Tengah",
"Kab. Bangka Selatan",
"Kab. Belitung",
"Kab. Bangka"
]
},
{
"provinsi":"Kepulauan Riau",
"kota":[
"Kota Batam",
"Kota Tanjung Pinang",
"Kab. Kepulauan Anambas",
"Kab. Lingga ",
"Kab. Natuna",
"Kab. Karimun",
"Kab. Bintan"
]
},
{
"provinsi":"DKI Jakarta",
"kota":[
"Kota Jakarta Timur",
"Kota Jakarta Selatan",
"Kota Jakarta Barat",
"Kota Jakarta Utara",
"Kota Jakarta Pusat",
"Kab. Kepulauan Seribu"
]
},
{
"provinsi":"Jawa Barat",
"kota":[
"Kota Bandung",
"Kota Banjar",
"Kota Tasikmalaya",
"Kota Cimahi",
"Kota Depok",
"Kota Bekasi",
"Kota Cirebon",
"Kota Sukabumi",
"Kota Bogor",
"Kab. Pangandaran",
"Kab. Bandung Barat",
"Kab. Bekasi",
"Kab. Karawang",
"Kab. Purwakarta",
"Kab. Subang",
"Kab. Indramayu",
"Kab. Sumedang",
"Kab. Majalengka",
"Kab. Cirebon",
"Kab. Kuningan",
"Kab. Ciamis",
"Kab. Tasikmalaya",
"Kab. Garut",
"Kab. Bandung",
"Kab. Cianjur",
"Kab. Sukabumi",
"Kab. Bogor"
]
},
{
"provinsi":"Jawa Tengah",
"kota":[
"Kota Semarang",
"Kota Tegal",
"Kota Pekalongan",
"Kota Salatiga",
"Kota Surakarta",
"Kota Magelang",
"Kab. Brebes",
"Kab. Tegal",
"Kab. Pemalang",
"Kab. Pekalongan",
"Kab. Batang",
"Kab. Kendal",
"Kab. Temanggung",
"Kab. Semarang",
"Kab. Demak",
"Kab. Jepara",
"Kab. Kudus",
"Kab. Pati",
"Kab. Rembang",
"Kab. Blora",
"Kab. Grobogan",
"Kab. Sragen",
"Kab. Karanganyar",
"Kab. Wonogiri",
"Kab. Sukoharjo",
"Kab. Klaten",
"Kab. Boyolali",
"Kab. Magelang",
"Kab. Wonosobo",
"Kab. Purworejo",
"Kab. Kebumen",
"Kab. Banjarnegara",
"Kab. Purbalingga",
"Kab. Banyumas",
"Kab. Cilacap"
]
},
{
"provinsi":"DI Yogyakarta",
"kota":[
"Kota Yogyakarta",
"Kab. Sleman",
"Kab. Gunung Kidul",
"Kab. Bantul",
"Kab. Kulon Progo"
]
},
{
"provinsi":"Jawa Timur",
"kota":[
"Kota Surabaya",
"Kota Batu",
"Kota Madiun",
"Kota Mojokerto",
"Kota Pasuruan",
"Kota Probolinggo",
"Kota Malang",
"Kota Blitar",
"Kota Kediri",
"Kab. Sumenep",
"Kab. Pamekasan",
"Kab. Sampang",
"Kab. Bangkalan",
"Kab. Gresik",
"Kab. Lamongan",
"Kab. Tuban",
"Kab. Bojonegoro",
"Kab. Ngawi",
"Kab. Magetan",
"Kab. Madiun",
"Kab. Nganjuk",
"Kab. Jombang",
"Kab. Mojokerto",
"Kab. Sidoarjo",
"Kab. Pasuruan",
"Kab. Probolinggo",
"Kab. Situbondo",
"Kab. Bondowoso",
"Kab. Banyuwangi",
"Kab. Jember",
"Kab. Lumajang",
"Kab. Malang",
"Kab. Kediri",
"Kab. Blitar",
"Kab. Tulungagung",
"Kab. Trenggalek",
"Kab. Ponorogo",
"Kab. Pacitan"
]
},
{
"provinsi":"Banten",
"kota":[
"Kota Serang",
"Kota Cilegon",
"Kota Tangerang",
"Kota Tangerang Selatan",
"Kab. Serang",
"Kab. Tangerang",
"Kab. Lebak",
"Kab. Pandeglang"
]
},
{
"provinsi":"Bali",
"kota":[
"Kota Denpasar",
"Kab. Buleleng",
"Kab. Karangasem",
"Kab. Bangli",
"Kab. Klungkung",
"Kab. Gianyar",
"Kab. Badung",
"Kab. Tabanan",
"Kab. Jembrana"
]
},
{
"provinsi":"Nusa Tenggara Barat",
"kota":[
"Kota Mataram",
"Kota Bima",
"Kab. Lombok Utara",
"Kab. Sumbawa Barat",
"Kab. Bima",
"Kab. Dompu",
"Kab. Sumbawa ",
"Kab. Lombok Timur",
"Kab. Lombok Tengah",
"Kab. Lombok Barat"
]
},
{
"provinsi":"Nusa Tenggara Timur",
"kota":[
"Kota Kupang",
"Kab. Malaka",
"Kab. Sabu Raijua",
"Kab. Manggarai Timur",
"Kab. Sumba Barat Daya",
"Kab. Sumba Tengah",
"Kab. Nagekeo",
"Kab. Manggarai Barat",
"Kab. Rote Ndao",
"Kab. Lembata",
"Kab. Sumba Barat",
"Kab. Sumba Timur",
"Kab. Manggarai",
"Kab. Ngada",
"Kab. Ende",
"Kab. Sikka",
"Kab. Flores Timur",
"Kab. Alor",
"Kab. Belu",
"Kab. Timor Tengah Utara",
"Kab. Timor Tengah Selatan",
"Kab. Kupang"
]
},
{
"provinsi":"Kalimantan Barat",
"kota":[
"Kota Pontianak",
"Kota Singkawang",
"Kab. Kubu Raya",
"Kab. Kayong Utara",
"Kab. Sekadau",
"Kab. Melawi",
"Kab. Landak",
"Kab. Bengkayang",
"Kab. Kapuas Hulu",
"Kab. Sintang ",
"Kab. Ketapang",
"Kab. Sanggau ",
"Kab. Mempawah",
"Kab. Sambas"
]
},
{
"provinsi":"Kalimantan Tengah",
"kota":[
"Kota Palangkaraya",
"Kab. Barito Timur",
"Kab. Murung Raya",
"Kab. Pulang Pisau",
"Kab. Gunung Mas",
"Kab. Lamandau",
"Kab. Sukamara",
"Kab. Seruyan",
"Kab. Katingan",
"Kab. Barito Utara",
"Kab. Barito Selatan",
"Kab. Kapuas",
"Kab. Kotawaringin Timur",
"Kab. Kotawaringin Barat"
]
},
{
"provinsi":"Kalimantan Selatan",
"kota":[
"Kota Banjarmasin",
"Kota Banjarbaru",
"Kab. Balangan",
"Kab. Tanah Bambu",
"Kab. Tabalong",
"Kab. Hulu Sungai Utara",
"Kab. Hulu Sungai Tengah",
"Kab. Hulu Sungai Selatan",
"Kab. Tapin",
"Kab. Barito Kuala",
"Kab. Banjar",
"Kab. Kotabaru",
"Kab. Tanah Laut"
]
},
{
"provinsi":"Kalimantan Timur",
"kota":[
"Kota Samarinda",
"Kota Bontang",
"Kota Balikpapan",
"Kab. Mahakam Ulu",
"Kab. Penajam Paser Utara",
"Kab. Kutai Timur",
"Kab. Kutai Barat",
"Kab. Berau",
"Kab. Kutai Kertanegara",
"Kab. Paser"
]
},
{
"provinsi":"Kalimantan Utara",
"kota":[
"Kota Tarakan",
"Kab. Tana Tidung",
"Kab. Nunukan",
"Kab. Malinau",
"Kab. Bulungan"
]
},
{
"provinsi":"Sulawesi Utara",
"kota":[
"Kota Manado",
"Kota Tomohon",
"Kota Bitung",
"Kota Kotamobagu",
"Kab. Bolaang Mangondow Selatan",
"Kab. Bolaang Mangondow Timur",
"Kab. Kepulauan Siau Tagulandang Biaro",
"Kab. Bolaang Mangondow Utara",
"Kab. Minahasa Tenggara",
"Kab. Minahasa Utara",
"Kab. Minahasa Selatan",
"Kab. Kepulauan Talaud",
"Kab. Kepulauan Sangihe",
"Kab. Minahasa",
"Kab. Bolaang Mangondow"
]
},
{
"provinsi":"Sulawesi Tengah",
"kota":[
"Kota Palu",
"Kab. Morowali Utara",
"Kab. Banggai Laut",
"Kab. Sigi",
"Kab. Tojo Una-Una",
"Kab. Parigi Moutong",
"Kab. Banggai Kepulauan",
"Kab. Morowali",
"Kab. Buol",
"Kab. Toli-Toli",
"Kab. Donggala",
"Kab. Poso",
"Kab. Banggai"
]
},
{
"provinsi":"Sulawesi Selatan",
"kota":[
"Kota Makasar",
"Kota Palopo",
"Kota Pare Pare",
"Kab. Toraja Utara",
"Kab. Luwu Timur",
"Kab. Luwu Utara",
"Kab. Tana Toraja",
"Kab. Luwu",
"Kab. Enrekang",
"Kab. Pinrang",
"Kab. Sidenreng Rappang",
"Kab. Wajo",
"Kab. Soppeng",
"Kab. Barru",
"Kab. Pangkajene Kepulauan",
"Kab. Maros",
"Kab. Bone",
"Kab. Sinjai",
"Kab. Gowa",
"Kab. Takalar",
"Kab. Jeneponto",
"Kab. Bantaeng",
"Kab. Bulukumba",
"Kab. Kepulauan Selayar"
]
},
{
"provinsi":"Sulawesi Tenggara",
"kota":[
"Kota Kendari",
"Kota Bau Bau",
"Kab. Buton Selatan",
"Kab. Buton Tengah",
"Kab. Muna Barat",
"Kab. Konawe Kepulauan",
"Kab. Kolaka Timur",
"Kab. Buton Utara",
"Kab. Konawe Utara",
"Kab. Kolaka Utara",
"Kab. Wakatobi",
"Kab. Bombana",
"Kab. Konawe Selatan",
"Kab. Buton",
"Kab. Muna",
"Kab. Konawe",
"Kab. Kolaka"
]
},
{
"provinsi":"Gorontalo",
"kota":[
"Kota Gorontalo",
"Kab. Pohuwato",
"Kab. Bone Bolango",
"Kab. Boalemo",
"Kab. Gorontalo",
"Kab. Gorontalo Utara"
]
},
{
"provinsi":"Sulawesi Barat",
"kota":[
"Kab. Majene",
"Kab. Polowali Mandar",
"Kab. Mamasa",
"Kab. Mamuju",
"Kab. Mamuju Utara",
"Kab. Mamuju Tengah"
]
},
{
"provinsi":"Maluku",
"kota":[
"Kota Ambon",
"Kota Tual",
"Kab. Buru Selatan",
"Kab. Maluku Barat Daya",
"Kab. Kepulauan Aru",
"Kab. Seram Bagian Barat ",
"Kab. Seram Bagian Timur",
"Kab. Buru",
"Kab. Maluku Tenggara Barat",
"Kab. Maluku Tenggara",
"Kab. Maluku Tengah"
]
},
{
"provinsi":"Maluku Utara",
"kota":[
"Kota Ternate",
"Kota Tidore Kepulauan",
"Kab. Pulau Taliabu",
"Kab. Pulau Morotai",
"Kab. Halmahera Timur",
"Kab. Kepulauan Sula",
"Kab. Halmahera Selatan",
"Kab. Halmahera Utara",
"Kab. Halmahera Tengah",
"Kab. Halmahera Barat"
]
},
{
"provinsi": "Papua",
"kota": [
"Kota Jayapura",
"Kab. Sarmi",
"Kab. Keerom",
"Kab. Jayapura",
"Kab. Mamberamo Raya",
"Kab. Supiori",
"Kab. Biak Numfor",
"Kab. Kepulauan Yapen",
"Kab. Waropen"
]
},
{
"provinsi": "Papua Barat",
"kota": [
"Kab. Pegunungan Arfak",
"Kab. Manokwari Selatan",
"Kab. Kaimana",
"Kab. Teluk Wondama",
"Kab. Teluk Bintuni",
"Kab. FakFak",
"Kab. Manokwari"
]
},
{
"provinsi": "Papua Barat Daya",
"kota": [
"Kota Sorong",
"Kab. Maybrat",
"Kab. Tambrauw",
"Kab. Raja Ampat",
"Kab. Sorong Selatan",
"Kab. Sorong"
]
},
{
"provinsi": "Papua Tengah",
"kota": [
"Kab. Nabire",
"Kab. Paniai",
"Kab. Mimika",
"Kab. Dogiyai",
"Kab. Intan Jaya",
"Kab. Deiyai",
"Kab. Puncak",
"Kab. Puncak Jaya"
]
},
{
"provinsi": "Papua Pegunungan",
"kota": [
"Kab. Jayawijaya",
"Kab. Lanny Jaya",
"Kab. Mamberamo Tengah",
"Kab. Nduga",
"Kab. Pegunungan Bintang",
"Kab. Tolikara",
"Kab. Yahukimo",
"Kab. Yalimo"
]
},
{
"provinsi": "Papua Selatan",
"kota": [
"Kab. Merauke",
"Kab. Asmat",
"Kab. Boven Digoel",
"Kab. Mappi"
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 KiB

1
praresi/firebase.json Normal file
View File

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"snapresi-8e964","appId":"1:1084316054508:android:86d995859cf2c85df306c9","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"snapresi-8e964","configurations":{"android":"1:1084316054508:android:86d995859cf2c85df306c9"}}}}}}

34
praresi/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>11.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,614 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
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 */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
/* 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>"; };
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>"; };
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; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
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 */,
331C807E294A63A400263BE5 /* Frameworks */,
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 = 1430;
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;
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;
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 = 11.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.praresi;
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;
baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */;
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.praresi.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;
baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */;
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.praresi.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;
baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */;
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.praresi.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;
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;
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 = 11.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;
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;
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 = 11.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.praresi;
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.praresi;
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,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
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"
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"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
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 UIKit
import Flutter
@UIApplicationMain
@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: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

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

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>Praresi</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>praresi</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,62 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for ios - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyBR6rF7--1ZQYFCPVVjn1wMAt5qcmfMgGw',
appId: '1:1084316054508:android:86d995859cf2c85df306c9',
messagingSenderId: '1084316054508',
projectId: 'snapresi-8e964',
storageBucket: 'snapresi-8e964.firebasestorage.app',
);
}

336
praresi/lib/main.dart Normal file
View File

@ -0,0 +1,336 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:praresi/presentation/controllers/internet_controller.dart';
import 'package:praresi/services/bubble_services.dart';
import 'firebase_options.dart';
import 'presentation/routes/app_pages.dart';
import 'presentation/routes/app_routes.dart';
import 'package:intl/date_symbol_data_local.dart';
// Entry point overlay
@pragma("vm:entry-point")
void overlayMain() {
runApp(
const MaterialApp(
debugShowCheckedModeBanner: false,
home: OverlayBubble(),
),
);
}
class OverlayBubble extends StatefulWidget {
const OverlayBubble({super.key});
@override
State<OverlayBubble> createState() => _OverlayBubbleState();
}
class _OverlayBubbleState extends State<OverlayBubble> with WidgetsBindingObserver {
bool _showInput = false;
final TextEditingController _controller = TextEditingController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this); // 👀 pantau lifecycle app
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
// _controller.dispose();
super.dispose();
}
// 🔔 Dipanggil saat app pause/resume/close
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
if (state == AppLifecycleState.detached || state == AppLifecycleState.paused) {
// Tutup bubble saat app ditutup atau masuk background
await BubbleService().hide();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Center(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child,
),
child: _showInput
? _buildInputBox(context)
: Stack(
alignment: Alignment.topRight, // tombol X di pojok kanan atas
children: [
GestureDetector(
key: const ValueKey('bubble'),
onTap: _expandToInput,
child: const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(Icons.book_rounded,
color: Colors.black, size: 32),
),
),
// Tombol Close
Positioned(
right: -2, // sedikit keluar dari bubble
top: -5,
child: GestureDetector(
onTap: () async {
await BubbleService().hide(); // close bubble
},
child: Container(
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.rectangle,
),
child: const Icon(
Icons.close,
size: 18,
color: Colors.white,
),
),
),
),
],
),
),
),
);
}
Widget _buildInputBox(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.deferToChild,
onTapDown: (_) => FocusScope.of(context).unfocus(),
child: Container(
key: const ValueKey('inputBox'),
width: 340,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 15,
offset: Offset(0, 6),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Masukkan Data Pengiriman",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 10),
Listener(
onPointerDown: (_) {},
child: TextField(
controller: _controller, // <-- ini yang menghubungkan TextField dengan controller
maxLines: 4,
keyboardType: TextInputType.multiline,
decoration: InputDecoration(
hintText: "Data Pengiriman...",
hintStyle: const TextStyle(color: Colors.grey),
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: Colors.blue, width: 1.8),
),
),
)
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (clipboardData?.text != null && clipboardData!.text!.isNotEmpty) {
setState(() {
_controller.text = clipboardData.text!;
_controller.selection = TextSelection.fromPosition(
TextPosition(offset: _controller.text.length),
);
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Clipboard kosong")),
);
}
},
style: TextButton.styleFrom(
foregroundColor: Colors.green,
textStyle: const TextStyle(fontSize: 12),
),
child: const Text("Tempel"),
),
// 🔘 Tombol Hapus
TextButton(
onPressed: () {
_controller.clear(); // menghapus isi TextField
},
style: TextButton.styleFrom(
foregroundColor: Colors.redAccent,
textStyle: const TextStyle(fontSize: 12),
),
child: const Text("Hapus"),
),
const SizedBox(width: 8),
TextButton(
onPressed: _collapseToBubble,
style: TextButton.styleFrom(
foregroundColor: const Color.fromARGB(255, 57, 57, 57),
textStyle: const TextStyle(fontSize: 12),
),
child: const Text("Batal"),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
debugPrint("Input terkirim: ${_controller.text}");
_collapseToBubble();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
padding:
const EdgeInsets.symmetric(horizontal: 22, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
"Simpan",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
],
),
),
);
}
// Saat bubble diklik, ubah tampilan jadi kotak input besar
Future<void> _expandToInput() async {
await BubbleService().resize(360, 260, animated: true);
await Future.delayed(const Duration(milliseconds: 200)); // biar sempat apply
setState(() => _showInput = true);
}
// Saat menekan "Batal" atau "Kirim", kembalikan ke bubble kecil
Future<void> _collapseToBubble() async {
setState(() => _showInput = false);
await BubbleService().resize(100, 100, animated: true);
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
WidgetsFlutterBinding.ensureInitialized(); // 🟢 tambahkan ini
// 🟢 inisialisasi locale "id_ID" untuk format tanggal Indonesia
await initializeDateFormatting('id_ID', null);
Get.put(InternetController());
runApp(const MyApp());
AppLifecycleListener(
onDetach: () async {
await BubbleService().hide();
},
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
if (state == AppLifecycleState.detached) {
await BubbleService().hide(); // bubble hilang hanya saat app ditutup
}
}
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Praresi',
initialRoute: AppRoutes.splash,
getPages: AppPages.routes,
);
}
}

View File

@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
// import 'package:flutter_overlay_window/flutter_overlay_window.dart';
@pragma("vm:entry-point")
void overlayMain() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: OverlayBubble(),
));
}
class OverlayBubble extends StatefulWidget {
const OverlayBubble({super.key});
@override
State<OverlayBubble> createState() => _OverlayBubbleState();
}
class _OverlayBubbleState extends State<OverlayBubble> {
bool expanded = false;
final TextEditingController controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Stack(
children: [
if (expanded)
Positioned(
right: 80,
bottom: 50,
child: Container(
width: 250,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 8)],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Salin / Tulis Alamat',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
TextField(
controller: controller,
maxLines: 3,
decoration: const InputDecoration(
hintText: 'Masukkan alamat di sini...',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => setState(() => expanded = false),
child: const Text('Tutup')),
ElevatedButton(
onPressed: () async {
// await FlutterOverlayWindow.shareData(controller.text);
controller.clear();
},
child: const Text('Kirim'),
),
],
)
],
),
),
),
Positioned(
right: 10,
bottom: 10,
child: GestureDetector(
onTap: () => setState(() => expanded = !expanded),
child: Container(
width: 70,
height: 70,
decoration: const BoxDecoration(
color: Colors.blueAccent,
shape: BoxShape.circle,
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8)],
),
child: const Icon(Icons.message, color: Colors.white, size: 32),
),
),
)
],
),
);
}
}

View File

@ -0,0 +1,51 @@
// // lib/overlays/bubble_overlay.dart
// import 'package:flutter/material.dart';
// import 'package:flutter_overlay_window_sdk34/flutter_overlay_window_sdk34.dart';
// @pragma('vm:entry-point')
// void overlayMain() {
// runApp(const BubbleOverlay());
// }
// class BubbleOverlay extends StatelessWidget {
// const BubbleOverlay({super.key});
// @override
// Widget build(BuildContext context) {
// return MaterialApp(
// debugShowCheckedModeBanner: false,
// home: Scaffold(
// backgroundColor: Colors.transparent,
// body: SafeArea(
// child: Align(
// alignment: Alignment.bottomRight,
// child: GestureDetector(
// onTap: () async {
// // Tutup bubble & buka app utama
// await FlutterOverlayWindow.closeOverlay();
// },
// child: Container(
// margin: const EdgeInsets.all(12),
// decoration: const BoxDecoration(
// color: Colors.blueAccent,
// shape: BoxShape.circle,
// boxShadow: [
// BoxShadow(
// color: Colors.black26,
// blurRadius: 6,
// offset: Offset(2, 3),
// ),
// ],
// ),
// width: 70,
// height: 70,
// child: const Icon(Icons.account_balance_wallet_rounded, color: Colors.black, size: 40),
// ),
// ),
// ),
// ),
// ),
// );
// }
// }

View File

@ -0,0 +1,133 @@
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:get/get.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:intl/intl.dart';
class AnalisisController extends GetxController {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
var monthlyData = <Map<String, dynamic>>[].obs;
var topCities = <Map<String, dynamic>>[].obs;
var selectedMonth = ''.obs;
StreamSubscription? _listenerRealtime;
// 🔥 simpan semua data di memory
List<QueryDocumentSnapshot<Map<String, dynamic>>> _allDocs = [];
@override
void onInit() {
super.onInit();
selectedMonth.value = DateFormat('MMM').format(DateTime.now());
_listenRealtimeData();
}
@override
void onClose() {
_listenerRealtime?.cancel();
super.onClose();
}
void _listenRealtimeData() {
final user = _auth.currentUser;
if (user == null) return;
_listenerRealtime = _firestore
.collection('resis')
.where('store_id', isEqualTo: user.uid)
.snapshots()
.listen((snapshot) {
_allDocs = snapshot.docs;
_updateData();
});
}
void _updateData() {
final Map<String, int> pengirimanPerBulan = {};
final Map<String, Map<String, dynamic>> kotaSummary = {};
for (var doc in _allDocs) {
final data = doc.data();
if (data['created_at'] == null || data['kota'] == null) continue;
final createdAt =
(data['created_at'] as Timestamp).toDate().toLocal();
final month = DateFormat('MMM').format(createdAt);
final kota = data['kota'].toString();
final total = double.tryParse(data['total'].toString()) ?? 0;
pengirimanPerBulan.update(month, (v) => v + 1, ifAbsent: () => 1);
final key = '$month-$kota';
if (kotaSummary.containsKey(key)) {
kotaSummary[key]!['transaksi'] += 1;
kotaSummary[key]!['pendapatan'] += total;
} else {
kotaSummary[key] = {
'month': month,
'city': kota,
'transaksi': 1,
'pendapatan': total,
};
}
}
final monthlyList = pengirimanPerBulan.entries.map((e) {
return {
'month': e.key,
'pengiriman': e.value,
};
}).toList()
..sort((a, b) {
try {
final monthA = a['month']?.toString() ?? '';
final monthB = b['month']?.toString() ?? '';
return DateFormat('MMM')
.parse(monthA)
.month
.compareTo(
DateFormat('MMM').parse(monthB).month,
);
} catch (_) {
return 0;
}
});
final kotaList = kotaSummary.values
.where((k) => k['month'] == selectedMonth.value)
.toList()
..sort((a, b) =>
(b['pendapatan'] as double).compareTo(a['pendapatan'] as double));
monthlyData.assignAll(monthlyList);
// Pastikan selectedMonth valid
if (monthlyList.isNotEmpty) {
final months = monthlyList
.map((e) => e['month'] as String)
.toList();
if (!months.contains(selectedMonth.value)) {
selectedMonth.value = months.first;
}
}
topCities.assignAll(kotaList.take(10).toList());
}
void setSelectedMonth(String month) {
selectedMonth.value = month;
_updateData(); // 🔥 INI YANG PENTING
}
String formatRupiah(num value) {
final format = NumberFormat.currency(
locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0);
return format.format(value);
}
}

View File

@ -0,0 +1,242 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class AuthController extends GetxController {
final FirebaseAuth _auth = FirebaseAuth.instance;
var isLoading = false.obs;
var user = Rxn<User>();
@override
void onInit() {
super.onInit();
user.bindStream(_auth.authStateChanges()); // listen login/logout
}
// LOGIN
Future<void> login(String email, String password) async {
try {
isLoading.value = true;
await _auth.signInWithEmailAndPassword(
email: email.trim(),
password: password.trim(),
);
Get.snackbar(
"Berhasil",
"Login berhasil",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
Get.offAllNamed('/home');
} on FirebaseAuthException catch (e) {
String message;
switch (e.code) {
case 'channel-error':
message = 'Email dan password wajib diisi.';
break;
case 'invalid-credential':
message = 'Email atau password salah.';
break;
default:
message = 'Login gagal. Silakan coba lagi.';
}
Get.snackbar(
"Login Gagal",
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
} catch (e) {
Get.snackbar(
"Error",
"Terjadi kesalahan sistem.",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
} finally {
isLoading.value = false;
}
}
// REGISTER
Future<void> register(String name, String email, String password) async {
try {
isLoading.value = true;
await _auth.createUserWithEmailAndPassword(
email: email.trim(),
password: password.trim(),
).then((userCredential) async {
await FirebaseFirestore.instance
.collection('users')
.doc(userCredential.user!.uid)
.set({
"name": name.trim(),
"email": email.trim(),
"createdAt": DateTime.now(),
});
});
Get.snackbar(
"Berhasil",
"Registrasi berhasil",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
Get.offAllNamed('/home');
} on FirebaseAuthException catch (e) {
String message;
switch (e.code) {
case 'channel-error':
message = 'Semua field wajib diisi.';
break;
case 'invalid-email':
message = 'Format email tidak valid.';
break;
case 'email-already-in-use':
message = 'Email sudah terdaftar.';
break;
case 'weak-password':
message = 'Password minimal 6 karakter.';
break;
case 'network-request-failed':
message = 'Tidak ada koneksi internet.';
break;
default:
message = 'Registrasi gagal. Silakan coba lagi.';
}
Get.snackbar(
"Registrasi Gagal",
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
} catch (e) {
Get.snackbar(
"Error",
"Terjadi kesalahan sistem.",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
} finally {
isLoading.value = false;
}
}
// RESET PASSWORD
Future<void> resetPassword(String email) async {
try {
// ==========================
// VALIDASI LOCAL
// ==========================
if (email.trim().isEmpty) {
Get.snackbar(
"Reset Password Gagal",
"Email wajib diisi.",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
return;
}
if (!GetUtils.isEmail(email.trim())) {
Get.snackbar(
"Reset Password Gagal",
"Format email tidak valid.",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
return;
}
// ==========================
// KIRIM RESET EMAIL
// ==========================
await _auth.sendPasswordResetEmail(email: email.trim());
Get.snackbar(
"Berhasil",
"Jika email terdaftar, link reset password telah dikirim.",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
} on FirebaseAuthException catch (e) {
String message;
switch (e.code) {
case 'user-not-found':
// Demi keamanan, sebaiknya jangan terlalu detail
message = "Jika email terdaftar, link reset password telah dikirim.";
break;
case 'invalid-email':
message = "Format email tidak valid.";
break;
case 'network-request-failed':
message = "Tidak ada koneksi internet.";
break;
default:
message = "Gagal mengirim email reset.";
}
Get.snackbar(
"Reset Password",
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
);
}
}
// LOGOUT
Future<void> logout() async {
await _auth.signOut();
Get.offAllNamed('/login');
}
}

View File

@ -0,0 +1,75 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ChangePasswordController extends GetxController {
var isLoading = false.obs;
Future<void> changePassword({
required String currentPassword,
required String newPassword,
}) async {
try {
isLoading.value = true;
final user = FirebaseAuth.instance.currentUser!;
final cred = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
);
// Re-authenticate
await user.reauthenticateWithCredential(cred);
// Update Password
await user.updatePassword(newPassword);
Get.snackbar(
"Berhasil",
"Kata sandi berhasil diperbarui",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
margin: const EdgeInsets.all(12),
borderRadius: 12,
icon: const Icon(Icons.check_circle, color: Colors.green),
);
} on FirebaseAuthException catch (e) {
String message = "Terjadi kesalahan";
// HANDLE ERROR FIREBASE
switch (e.code) {
case 'wrong-password':
case 'invalid-credential':
message = "Password lama tidak sesuai";
break;
case 'weak-password':
message = "Password terlalu lemah (minimal 6 karakter)";
break;
case 'requires-recent-login':
message = "Silakan login ulang terlebih dahulu";
break;
default:
message = e.message ?? "Gagal memperbarui kata sandi";
}
Get.snackbar(
"Error",
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
margin: const EdgeInsets.all(12),
borderRadius: 12,
icon: const Icon(Icons.error, color: Colors.red),
);
} finally {
isLoading.value = false;
}
}
}

View File

@ -0,0 +1,131 @@
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:get/get.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:intl/intl.dart';
import 'package:fl_chart/fl_chart.dart';
class DashboardController extends GetxController {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
var totalPendapatan = 0.0.obs;
var totalPendapatanCOD = 0.0.obs;
var totalPendapatanNonCOD = 0.0.obs;
var totalPaket = 0.obs;
var totalPaketCOD = 0.obs;
var totalPaketNonCOD = 0.obs;
var isLoading = false.obs;
var grafikPengiriman = <FlSpot>[].obs;
StreamSubscription? _listenerRealtime;
@override
void onInit() {
super.onInit();
_listenRealtimeData();
}
@override
void onClose() {
_listenerRealtime?.cancel();
super.onClose();
}
/// 🔁 Listener realtime dashboard & grafik
void _listenRealtimeData() {
final user = _auth.currentUser;
if (user == null) return;
final userId = user.uid;
// Pastikan urut berdasarkan waktu dibuat
_listenerRealtime = _firestore
.collection('resis')
.where('store_id', isEqualTo: userId)
.orderBy('created_at', descending: false)
.snapshots()
.listen((snapshot) {
_updateDashboard(snapshot.docs);
}, onError: (e) {
print("🔥 Error listen data: $e");
});
}
/// 🔹 Hitung ulang total dan grafik setiap ada perubahan data
void _updateDashboard(List<QueryDocumentSnapshot<Map<String, dynamic>>> docs) {
final now = DateTime.now();
final cutoff = now.subtract(const Duration(days: 6));
double total = 0;
double totalCod = 0;
double totalNonCod = 0;
int paket = 0;
int paketCod = 0;
int paketNonCod = 0;
Map<String, int> pengirimanPerTanggal = {};
for (var doc in docs) {
final data = doc.data();
if (data['created_at'] == null) continue;
final createdAt = (data['created_at'] as Timestamp).toDate().toLocal();
final tanggalKey = DateFormat('yyyy-MM-dd').format(createdAt);
// Total untuk hari ini
if (createdAt.year == now.year &&
createdAt.month == now.month &&
createdAt.day == now.day) {
final totalTransaksi = double.tryParse(data['total'].toString()) ?? 0;
final pembayaran = data['pembayaran']?.toString().toUpperCase() ?? '';
total += totalTransaksi;
paket++;
if (pembayaran == 'COD') {
paketCod++;
totalCod += totalTransaksi;
} else {
paketNonCod++;
totalNonCod += totalTransaksi;
}
}
// Total pengiriman 7 hari terakhir
if (!(createdAt.isBefore(cutoff) || createdAt.isAfter(now))) {
pengirimanPerTanggal.update(tanggalKey, (v) => v + 1,
ifAbsent: () => 1);
}
}
// 🔸 Update data observabel
totalPendapatan.value = total;
totalPendapatanCOD.value = totalCod;
totalPendapatanNonCOD.value = totalNonCod;
totalPaket.value = paket;
totalPaketCOD.value = paketCod;
totalPaketNonCOD.value = paketNonCod;
// 🔹 Buat data untuk 7 hari grafik
final List<FlSpot> newSpots = [];
for (int i = 0; i < 7; i++) {
final date = cutoff.add(Duration(days: i));
final key = DateFormat('yyyy-MM-dd').format(date);
final jumlah = pengirimanPerTanggal[key] ?? 0;
newSpots.add(FlSpot(i.toDouble(), jumlah.toDouble()));
}
// 🔸 Assign dan refresh supaya UI update langsung
grafikPengiriman.assignAll(newSpots);
grafikPengiriman.refresh();
print("📊 Dashboard & grafik diperbarui realtime (${DateTime.now()})");
}
String formatRupiah(double value) {
final format =
NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0);
return format.format(value);
}
}

View File

@ -0,0 +1,64 @@
import 'package:get/get.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
class InternetController extends GetxController {
var isOnline = true.obs;
bool lastStatus = true;
final Connectivity _connectivity = Connectivity();
@override
void onInit() {
super.onInit();
checkConnection();
_connectivity.onConnectivityChanged.listen((results) {
updateConnection(results);
});
}
Future<void> checkConnection() async {
final results = await _connectivity.checkConnectivity();
updateConnection(results);
}
void updateConnection(List<ConnectivityResult> results) {
bool currentStatus = !results.contains(ConnectivityResult.none);
if (currentStatus != lastStatus) {
if (!currentStatus) {
isOnline.value = false;
Get.snackbar(
"Tidak ada internet",
"Periksa koneksi internet Anda",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
} else {
isOnline.value = true;
Get.snackbar(
"Koneksi kembali",
"Internet sudah tersambung",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
lastStatus = currentStatus;
}
}
}

View File

@ -0,0 +1,76 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:get/get.dart';
import 'package:firebase_auth/firebase_auth.dart';
class PelangganController extends GetxController {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
var isLoading = true.obs;
var pelangganList = <Map<String, dynamic>>[].obs;
var totalPelanggan = 0.obs;
@override
void onInit() {
super.onInit();
fetchAllPelanggan();
}
/// Ambil data pelanggan berdasarkan user yang login
Future<void> fetchAllPelanggan() async {
try {
isLoading.value = true;
final user = _auth.currentUser;
if (user == null) {
Get.snackbar("Error", "User belum login");
return;
}
final userId = user.uid;
// Ambil semua resi yang dibuat oleh user yang login
final snapshot = await _firestore
.collection('resis')
.where('store_id', isEqualTo: userId)
.get();
// Mengelompokkan data berdasarkan nama penerima
final Map<String, Map<String, dynamic>> pelangganMap = {};
for (var doc in snapshot.docs) {
final data = doc.data();
final nama = (data['penerima'] ?? '').toString();
if (nama.isEmpty) continue;
if (!pelangganMap.containsKey(nama)) {
pelangganMap[nama] = {
'penerima': nama,
'no_wa': data['no_wa'] ?? '-',
'alamat': data['alamat'] ?? '-',
'kota': data['kota'] ?? '-',
'provinsi': data['provinsi'] ?? '-',
'barang': data['barang'] ?? '-', // 🔹 Ganti totalTransaksi barang
'totalNominal':
double.tryParse(data['total']?.toString() ?? '0') ?? 0.0,
'created_at': (data['created_at'] is Timestamp)
? (data['created_at'] as Timestamp).toDate()
: DateTime.now(), // 🔹 Tambahan untuk tanggal pembuatan
};
} else {
// Jika nama pelanggan sama, tambahkan nominal tapi barang tidak berubah
pelangganMap[nama]!['totalNominal'] +=
double.tryParse(data['total']?.toString() ?? '0') ?? 0.0;
}
}
pelangganList.assignAll(pelangganMap.values.toList());
totalPelanggan.value = pelangganList.length;
} catch (e) {
Get.snackbar("Error", "Gagal memuat data pelanggan: $e");
} finally {
isLoading.value = false;
}
}
}

View File

@ -0,0 +1,93 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ProfileController extends GetxController {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
var name = ''.obs;
var email = ''.obs;
var isLoading = true.obs;
@override
void onInit() {
super.onInit();
loadProfile();
}
Future<void> loadProfile() async {
try {
isLoading.value = true;
final user = _auth.currentUser;
if (user != null) {
email.value = user.email ?? "";
final doc =
await _firestore.collection('users').doc(user.uid).get();
if (doc.exists) {
name.value = doc.data()?['name'] ?? "";
}
}
} catch (e) {
Get.snackbar("Error", "Gagal memuat profil: $e");
} finally {
isLoading.value = false;
}
}
Future<void> updateName() async {
try {
final user = _auth.currentUser;
if (user == null) return;
/// VALIDASI NAMA
if (name.value.trim().isEmpty) {
Get.snackbar(
"Peringatan",
"Nama tidak boleh kosong",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
margin: const EdgeInsets.all(12),
borderRadius: 12,
duration: const Duration(seconds: 2),
);
return;
}
await _firestore
.collection('users')
.doc(user.uid)
.update({
"name": name.value.trim(),
});
Get.snackbar(
"Berhasil",
"Nama pengguna berhasil diperbarui",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
margin: const EdgeInsets.all(12),
borderRadius: 12,
duration: const Duration(seconds: 2),
);
} catch (e) {
Get.snackbar(
"Error",
"Gagal memperbarui nama",
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.white,
colorText: Colors.black,
margin: const EdgeInsets.all(12),
borderRadius: 12,
duration: const Duration(seconds: 2),
);
}
}
}

View File

@ -0,0 +1,505 @@
import 'dart:convert';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:get/get.dart';
import 'package:praresi/presentation/controllers/internet_controller.dart';
class ResiController extends GetxController {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
List<Map<String, dynamic>> _wilayahData = [];
var isSaving = false.obs; // loading save
@override
void onInit() {
super.onInit();
_loadWilayahData();
}
/// 🔹 Muat data provinsi & kota dari JSON lokal
Future<void> _loadWilayahData() async {
try {
final String jsonString = await rootBundle.loadString('assets/data/regions.json');
final List<dynamic> jsonData = jsonDecode(jsonString);
_wilayahData = jsonData.cast<Map<String, dynamic>>();
} catch (e) {
debugPrint("Gagal memuat data wilayah: $e");
}
}
/// 🔹 Deteksi provinsi & kota dari alamat (prioritaskan akhir alamat + fuzzy + aman)
// Map<String, String?> _extractWilayah(String alamat) {
// if (alamat.isEmpty || _wilayahData.isEmpty) {
// return {'kota': null, 'provinsi': null};
// }
// String alamatLower = alamat.toLowerCase();
// String? provinsi;
// String? kota;
// double bestScore = 0.0;
// // Ambil 4 kata terakhir, karena umumnya di situ ada kota/provinsi
// List<String> alamatWords = alamatLower.split(RegExp(r'\s+'));
// String lastPart = alamatWords.length > 4
// ? alamatWords.sublist(alamatWords.length - 4).join(' ')
// : alamatLower;
// for (var data in _wilayahData) {
// if (data['provinsi'] == null || data['kota'] == null) continue;
// final String prov = data['provinsi'].toString();
// final List kotaList = List.from(data['kota']);
// final String provLower = prov.toLowerCase();
// // 🔹 Deteksi provinsi langsung atau pakai fuzzy matching
// if (lastPart.contains(provLower)) {
// provinsi = prov;
// } else if (_similarity(lastPart, provLower) > 0.7) {
// provinsi = prov;
// } else {
// // Cek singkatan umum provinsi
// if (provLower.contains('jawa timur') && lastPart.contains('jatim')) provinsi = prov;
// if (provLower.contains('jawa tengah') && lastPart.contains('jateng')) provinsi = prov;
// if (provLower.contains('jawa barat') && lastPart.contains('jabar')) provinsi = prov;
// if (provLower.contains('daerah istimewa yogyakarta') && lastPart.contains('diy')) provinsi = prov;
// }
// // 🔹 Deteksi kota
// for (var k in kotaList) {
// if (k == null) continue;
// final String kotaNama = k.toString();
// final String kotaLower = kotaNama.toLowerCase();
// final String kotaBersih = kotaLower.replaceAll(RegExp(r'^(kab\.?|kota)\s*'), '').trim();
// // Hindari error: pastikan index valid
// int indexKota = alamatLower.contains(kotaBersih)
// ? alamatLower.lastIndexOf(kotaBersih)
// : -1;
// // Semakin ke akhir alamat, bobot lebih tinggi
// double weight = (indexKota > 0)
// ? (indexKota / alamatLower.length).clamp(0.0, 1.0)
// : 0.0;
// // Skor kemiripan
// double score;
// if (alamatLower.contains(kotaBersih)) {
// score = 0.9 + weight * 0.1;
// } else {
// score = _similarity(lastPart, kotaBersih) + weight * 0.1;
// }
// // Simpan hasil terbaik
// if (score > bestScore) {
// bestScore = score;
// kota = kotaNama;
// provinsi ??= prov;
// }
// }
// }
// return {
// 'kota': kota,
// 'provinsi': provinsi,
// };
// }
Map<String, String?> _extractWilayah(String alamat) {
if (alamat.isEmpty || _wilayahData.isEmpty) {
return {'kota': null, 'provinsi': null};
}
String alamatLower = alamat.toLowerCase();
String? provinsi;
String? kota;
// 🔹 Ambil bagian belakang alamat (lebih akurat)
List<String> words = alamatLower.split(RegExp(r'\s+'));
String lastPart = words.length > 5
? words.sublist(words.length - 5).join(' ')
: alamatLower;
// ==============================
// 🔹 STEP 1: DETEKSI PROVINSI DULU
// ==============================
for (var data in _wilayahData) {
final prov = data['provinsi']?.toString() ?? '';
final provLower = prov.toLowerCase();
if (provLower.isEmpty) continue;
if (lastPart.contains(provLower) ||
_similarity(lastPart, provLower) > 0.7 ||
(provLower.contains('jawa timur') && lastPart.contains('jatim')) ||
(provLower.contains('jawa tengah') && lastPart.contains('jateng')) ||
(provLower.contains('jawa barat') && lastPart.contains('jabar')) ||
(provLower.contains('yogyakarta') && lastPart.contains('diy'))) {
provinsi = prov;
break; // 🔥 STOP kalau sudah ketemu
}
}
// kalau provinsi tidak ketemu langsung return
if (provinsi == null) {
return {'kota': null, 'provinsi': null};
}
// ==============================
// 🔹 STEP 2: CARI KOTA DALAM PROVINSI TERSEBUT
// ==============================
final provData = _wilayahData.firstWhere(
(e) => e['provinsi'] == provinsi,
orElse: () => {},
);
final List kotaList = List.from(provData['kota'] ?? []);
double bestScore = 0;
for (var k in kotaList) {
final kotaNama = k.toString();
final kotaLower = kotaNama.toLowerCase();
final kotaBersih =
kotaLower.replaceAll(RegExp(r'^(kab\.?|kota)\s*'), '').trim();
double score;
if (alamatLower.contains(kotaBersih)) {
score = 0.95;
} else {
score = _similarity(lastPart, kotaBersih);
}
if (score > bestScore) {
bestScore = score;
kota = kotaNama;
}
}
// 🔥 Tambahan penting di sini
if (bestScore < 0.75) {
kota = null;
}
return {
'kota': kota,
'provinsi': provinsi,
};
}
/// 🔹 Fungsi bantu: fuzzy similarity sederhana antar string
double _similarity(String a, String b) {
if (a.isEmpty || b.isEmpty) return 0.0;
final aWords = a.split(' ');
final bWords = b.split(' ');
int matches = 0;
for (var aw in aWords) {
for (var bw in bWords) {
if (aw.isNotEmpty && bw.isNotEmpty && aw == bw) matches++;
}
}
return matches / ((aWords.length + bWords.length) / 2);
}
/// 🔹 Ekstrak data dari hasil OCR
Map<String, dynamic> parseResiData(String text, String storeId) {
final lines = text.split('\n').map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
final RegExp penerimaExp = RegExp(r'Penerima\s*:\s*(.*)', caseSensitive: false);
final RegExp alamatExp = RegExp(r'Alamat\s*:\s*(.*)', caseSensitive: false);
final RegExp waExp = RegExp(r'(No\.?\s*Wa|Nomor\s*Wa|WA)\s*:\s*(.*)', caseSensitive: false);
final RegExp barangExp = RegExp(r'Barang\s*:\s*(.*)', caseSensitive: false);
final RegExp totalExp = RegExp(r'Total\s*:\s*([0-9.,]+)', caseSensitive: false);
final RegExp pembayaranExp = RegExp(r'(COD|Non\s*COD)', caseSensitive: false);
String penerima = '';
String alamat = '';
String noWa = '';
String barang = '';
String total = '';
String pembayaran = '';
bool inAlamatSection = false;
for (int i = 0; i < lines.length; i++) {
final line = lines[i];
if (penerimaExp.hasMatch(line)) {
penerima = penerimaExp.firstMatch(line)?.group(1)?.trim() ?? '';
}
else if (alamatExp.hasMatch(line)) {
// mulai tangkap alamat
alamat = alamatExp.firstMatch(line)?.group(1)?.trim() ?? '';
inAlamatSection = true;
continue;
}
else if (waExp.hasMatch(line)) {
inAlamatSection = false;
noWa = waExp.firstMatch(line)?.group(2)?.trim() ?? '';
}
else if (barangExp.hasMatch(line)) {
inAlamatSection = false;
barang = barangExp.firstMatch(line)?.group(1)?.trim() ?? '';
}
else if (totalExp.hasMatch(line)) {
inAlamatSection = false;
total = totalExp.firstMatch(line)?.group(1)?.trim() ?? '';
total = total.replaceAll(RegExp(r'[^\d]'), '');
}
else if (pembayaranExp.hasMatch(line)) {
inAlamatSection = false;
pembayaran = pembayaranExp.firstMatch(line)?.group(1)?.trim().toUpperCase() ?? '';
if (pembayaran.contains('NON')) {
pembayaran = 'NON COD';
} else {
pembayaran = 'COD';
}
}
else if (inAlamatSection) {
// gabungkan baris tambahan alamat
if (line.isNotEmpty && !line.contains(':')) {
alamat += ' $line';
}
}
}
// 🔍 Ekstrak kota & provinsi dari alamat lengkap
final wilayah = _extractWilayah(alamat);
return {
'store_id': storeId,
'penerima': penerima,
'alamat': alamat,
'no_wa': noWa,
'barang': barang,
'total': total,
'pembayaran': pembayaran,
'kota': wilayah['kota'],
'provinsi': wilayah['provinsi'],
'created_at': FieldValue.serverTimestamp(),
};
}
/// 🔹 Simpan hasil OCR ke Firestore
Future<void> saveResi(String ocrText, String userId) async {
try {
final storeRef =
await _firestore.collection('stores').doc(userId).get();
// 🔹 Cek apakah user punya toko
if (!storeRef.exists) {
Get.snackbar(
'Gagal Menyimpan',
'Kamu belum memiliki data toko.\nSilakan isi data toko terlebih dahulu.',
backgroundColor: const Color(0xFFFF9800),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
return;
}
final storeId = storeRef.id;
// 🔹 Parsing hasil OCR
final data = parseResiData(ocrText, storeId);
// 🔹 Validasi field utama
List<String> missing = [];
if (data['penerima'].toString().isEmpty) missing.add('Penerima');
if (data['alamat'].toString().isEmpty) missing.add('Alamat');
if (data['no_wa'].toString().isEmpty) missing.add('No. WA');
if (data['barang'].toString().isEmpty) missing.add('Barang');
if (data['total'].toString().isEmpty) missing.add('Total');
if (data['pembayaran'].toString().isEmpty) missing.add('Pembayaran');
if (missing.isNotEmpty) {
Get.snackbar(
'Gagal Menyimpan',
'Field berikut kosong:\n${missing.join(', ')}',
backgroundColor: const Color(0xFFFF9800),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
return;
}
// 🔥 VALIDASI WILAYAH (INI YANG KAMU BUTUHKAN)
if (data['provinsi'] == null ||
data['provinsi'].toString().isEmpty) {
Get.snackbar(
'Gagal Menyimpan',
'Provinsi tidak terdeteksi dari alamat!',
backgroundColor: const Color(0xFFFF9800),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
return;
}
if (data['kota'] == null ||
data['kota'].toString().isEmpty) {
Get.snackbar(
'Gagal Menyimpan',
'Kota/Kabupaten tidak terdeteksi!',
backgroundColor: const Color(0xFFFF9800),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
return;
}
// 🔹 Simpan ke Firestore
await _firestore.collection('resis').add(data);
Get.snackbar(
'Berhasil',
'Data resi berhasil disimpan!',
backgroundColor: const Color(0xFF4CAF50),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} catch (e) {
Get.snackbar(
'Error',
'Gagal menyimpan data: $e',
backgroundColor: const Color(0xFFF44336),
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Future<void> saveResi(String ocrText, String userId) async {
// /// cegah double klik
// if (isSaving.value) return;
// /// CEK INTERNET GLOBAL
// final internet = Get.find<InternetController>();
// if (!internet.isConnected.value) {
// Get.snackbar(
// 'Offline',
// 'Tidak dapat menyimpan tanpa koneksi internet.',
// backgroundColor: const Color(0xFFFF9800),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// return;
// }
// try {
// isSaving.value = true;
// final storeRef =
// await _firestore.collection('stores').doc(userId).get();
// if (!storeRef.exists) {
// Get.snackbar(
// 'Gagal Menyimpan',
// 'Kamu belum memiliki data toko.\nSilakan isi data toko terlebih dahulu.',
// backgroundColor: const Color(0xFFFF9800),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// return;
// }
// final storeId = storeRef.id;
// final data = parseResiData(ocrText, storeId);
// /// ===============================
// /// VALIDASI FIELD OCR
// /// ===============================
// List<String> missing = [];
// if (data['penerima'].toString().trim().isEmpty)
// missing.add('Penerima');
// if (data['alamat'].toString().trim().isEmpty)
// missing.add('Alamat');
// if (data['no_wa'].toString().trim().isEmpty)
// missing.add('No. WA');
// if (data['barang'].toString().trim().isEmpty)
// missing.add('Barang');
// if (data['total'].toString().trim().isEmpty)
// missing.add('Total');
// if (data['pembayaran'].toString().trim().isEmpty)
// missing.add('Pembayaran');
// if (missing.isNotEmpty) {
// Get.snackbar(
// 'Gagal Menyimpan',
// 'Field berikut kosong:\n${missing.join(', ')}',
// backgroundColor: const Color(0xFFFF9800),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// return;
// }
// /// ===============================
// /// ANTI DUPLICATE FIRESTORE
// /// ===============================
// /// buat document id unik dari data resi
// final docId =
// "${data['no_wa']}_${data['total']}_${data['penerima']}";
// await _firestore
// .collection('resis')
// .doc(docId)
// .set(data); // BUKAN add()
// Get.snackbar(
// 'Berhasil',
// 'Data resi berhasil disimpan!',
// backgroundColor: const Color(0xFF4CAF50),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// } on FirebaseException catch (e) {
// /// HANDLE INTERNET PUTUS TENGAH JALAN
// if (e.code == 'network-request-failed') {
// Get.snackbar(
// 'Koneksi Terputus',
// 'Internet tidak stabil.',
// backgroundColor: const Color(0xFFFF9800),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// } else {
// Get.snackbar(
// 'Error Firebase',
// e.message ?? 'Terjadi kesalahan.',
// backgroundColor: const Color(0xFFF44336),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// }
// } catch (e) {
// Get.snackbar(
// 'Error',
// 'Gagal menyimpan data.',
// backgroundColor: const Color(0xFFF44336),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// } finally {
// /// aktifkan button kembali
// isSaving.value = false;
// }
// }
}

View File

@ -0,0 +1,163 @@
import 'package:get/get.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:intl/intl.dart';
class RiwayatBulananController extends GetxController {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
var isLoading = false.obs;
var monthlySummary = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
fetchMonthlySummary();
}
/// 🔹 Ambil ringkasan resi per bulan
Future<void> fetchMonthlySummary() async {
try {
isLoading.value = true;
monthlySummary.clear();
final user = _auth.currentUser;
if (user == null) return;
final userId = user.uid;
final snapshot = await _firestore
.collection('resis')
.where('store_id', isEqualTo: userId)
.get();
// 🔸 Kelompokkan data berdasarkan bulan & tahun
Map<String, Map<String, dynamic>> grouped = {};
for (var doc in snapshot.docs) {
final data = doc.data();
if (data['created_at'] == null) continue;
final createdAt = (data['created_at'] as Timestamp).toDate();
final monthKey = DateFormat('MMMM yyyy', 'id_ID').format(createdAt);
final pembayaran = (data['pembayaran'] ?? '').toString().toUpperCase();
final total = double.tryParse(data['total'].toString()) ?? 0;
if (!grouped.containsKey(monthKey)) {
grouped[monthKey] = {
'month': monthKey,
'rawDate': createdAt,
'total': 0,
'cod': 0,
'nonCod': 0,
'incomeCod': 0.0,
'incomeNonCod': 0.0,
};
}
grouped[monthKey]!['total'] += 1;
if (pembayaran == 'COD') {
grouped[monthKey]!['cod'] += 1;
grouped[monthKey]!['incomeCod'] += total;
} else {
grouped[monthKey]!['nonCod'] += 1;
grouped[monthKey]!['incomeNonCod'] += total;
}
}
// 🔸 Konversi ke List dan urutkan dari bulan terbaru
final sorted = grouped.values.toList()
..sort((a, b) => b['rawDate'].compareTo(a['rawDate']));
monthlySummary.assignAll(sorted);
} catch (e) {
print('Error fetchMonthlySummary: $e');
Get.snackbar('Error', 'Gagal mengambil data: $e');
monthlySummary.clear();
} finally {
isLoading.value = false;
}
}
/// 🔹 Ambil pendapatan per bulan (COD & Non COD)
Future<Map<String, dynamic>> getPendapatanBulanan(String monthLabel) async {
try {
final user = _auth.currentUser;
if (user == null) return {};
final parsedMonth = DateFormat('MMMM yyyy', 'id_ID').parse(monthLabel);
final start = DateTime(parsedMonth.year, parsedMonth.month, 1);
final end =
DateTime(parsedMonth.year, parsedMonth.month + 1, 0, 23, 59, 59);
final snapshot = await _firestore
.collection('resis')
.where('store_id', isEqualTo: user.uid)
.where('created_at', isGreaterThanOrEqualTo: start)
.where('created_at', isLessThanOrEqualTo: end)
.get();
double codIncome = 0;
double nonCodIncome = 0;
for (var doc in snapshot.docs) {
final data = doc.data();
final pembayaran = (data['pembayaran'] ?? '').toString().toUpperCase();
final total = double.tryParse(data['total'].toString()) ?? 0;
if (pembayaran == 'COD') {
codIncome += total;
} else {
nonCodIncome += total;
}
}
return {
'cod': codIncome,
'nonCod': nonCodIncome,
};
} catch (e) {
print('Error getPendapatanBulanan: $e');
return {};
}
}
/// 🔹 Ambil semua resi di bulan tertentu
Future<List<Map<String, dynamic>>> fetchResiByMonth(String monthLabel) async {
try {
final user = _auth.currentUser;
if (user == null) return [];
final parsedMonth = DateFormat('MMMM yyyy', 'id_ID').parse(monthLabel);
final start = DateTime(parsedMonth.year, parsedMonth.month, 1);
final end =
DateTime(parsedMonth.year, parsedMonth.month + 1, 0, 23, 59, 59);
final snapshot = await _firestore
.collection('resis')
.where('store_id', isEqualTo: user.uid)
.where('created_at', isGreaterThanOrEqualTo: start)
.where('created_at', isLessThanOrEqualTo: end)
.orderBy('created_at', descending: true)
.get();
return snapshot.docs.map((doc) {
final data = doc.data();
return {
'resi_id': doc.id,
'penerima': data['penerima'] ?? '-',
'barang': data['barang'] ?? '-',
'alamat': data['alamat'] ?? '-',
'pembayaran': data['pembayaran'] ?? '-',
'no_wa': data['no_wa'] ?? '-',
'total': data['total'] ?? 0,
'created_at': data['created_at'],
};
}).toList();
} catch (e) {
print('Error fetchResiByMonth: $e');
return [];
}
}
}

View File

@ -0,0 +1,568 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:intl/intl.dart';
class RiwayatController extends GetxController {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
List<Map<String, dynamic>> _wilayahData = [];
var monthlySummary = <Map<String, dynamic>>[].obs;
var isLoading = false.obs;
var dailySummary = <Map<String, dynamic>>[].obs;
var dailyDetail = <Map<String, dynamic>>[].obs;
/// 🔹 Ambil data ringkasan resi per hari
Future<void> fetchResiData({DateTime? start, DateTime? end}) async {
try {
isLoading.value = true;
final user = _auth.currentUser;
if (user == null) return;
final userId = user.uid;
final DateTime now = DateTime.now();
final DateTime startOfMonth = start ?? DateTime(now.year, now.month, 1);
final DateTime endOfMonth =
end ?? DateTime(now.year, now.month + 1, 0, 23, 59, 59);
final snapshot = await _firestore
.collection('resis')
.where('store_id', isEqualTo: userId)
.where('created_at', isGreaterThanOrEqualTo: startOfMonth)
.where('created_at', isLessThanOrEqualTo: endOfMonth)
.get();
final data = snapshot.docs.map((doc) {
final resi = doc.data();
final createdAt = (resi['created_at'] as Timestamp).toDate();
final tanggalKey = DateFormat('yyyy-MM-dd').format(createdAt);
return {
'id': doc.id,
'tanggalKey': tanggalKey,
'tanggalAsli': createdAt,
'pembayaran': resi['pembayaran'] ?? '',
'total': int.tryParse(resi['total'].toString()) ?? 0,
};
}).toList();
final Map<String, List<Map<String, dynamic>>> grouped = {};
for (var item in data) {
grouped.putIfAbsent(item['tanggalKey'], () => []);
grouped[item['tanggalKey']]!.add(item);
}
final List<Map<String, dynamic>> summary = [];
grouped.forEach((key, list) {
int total = 0, codCount = 0, nonCodCount = 0, income = 0;
for (var resi in list) {
total += 1;
final totalValue = num.tryParse(resi['total'].toString()) ?? 0;
income += totalValue.toInt();
if (resi['pembayaran'].toString().toUpperCase() == 'COD') {
codCount++;
} else {
nonCodCount++;
}
}
final date = DateTime.parse(key);
summary.add({
'day': DateFormat('EEEE', 'id_ID').format(date),
'date': DateFormat('d MMMM yyyy', 'id_ID').format(date),
'total': total,
'cod': codCount,
'nonCod': nonCodCount,
'income': income,
'rawDate': date,
});
});
summary.sort((a, b) => b['rawDate'].compareTo(a['rawDate']));
dailySummary.assignAll(summary);
} catch (e) {
print('Error fetching resi data: $e');
dailySummary.clear();
} finally {
isLoading.value = false;
}
}
/// 🔹 Ambil data resi + detail toko berdasarkan tanggal
Future<void> fetchResiByDate(DateTime selectedDate) async {
try {
isLoading.value = true;
final user = _auth.currentUser;
if (user == null) return;
final userId = user.uid;
final startOfDay =
DateTime(selectedDate.year, selectedDate.month, selectedDate.day);
final endOfDay =
DateTime(selectedDate.year, selectedDate.month, selectedDate.day, 23, 59, 59);
final snapshot = await _firestore
.collection('resis')
.where('store_id', isEqualTo: userId)
.where('created_at', isGreaterThanOrEqualTo: startOfDay)
.where('created_at', isLessThanOrEqualTo: endOfDay)
.get();
// 🔹 Ambil data resi & data toko
List<Map<String, dynamic>> results = [];
for (var doc in snapshot.docs) {
final data = doc.data();
final storeId = data['store_id'];
// ambil data toko
final storeSnapshot =
await _firestore.collection('stores').doc(storeId).get();
final storeData = storeSnapshot.data() ?? {};
results.add({
'id': doc.id,
'no_resi': data['no_resi'] ?? '-',
'penerima': data['penerima'] ?? '-',
'alamat': data['alamat'] ?? '-',
'kota': data['kota'] ?? '-',
'provinsi': data['provinsi'] ?? '-',
'barang': data['barang'] ?? '-',
'pembayaran': data['pembayaran'] ?? '-',
'total': data['total'] ?? 0,
'no_wa': data['no_wa'] ?? '-',
'created_at': (data['created_at'] as Timestamp).toDate(),
'store': {
'namaToko': storeData['namaToko'] ?? '-',
'alamat': storeData['alamat'] ?? '-',
'noHp': storeData['noHp'] ?? '-',
'noRegistrasi': storeData['noRegistrasi'] ?? '-',
'keterangan': storeData['keterangan'] ?? '-',
}
});
}
dailyDetail.assignAll(results);
} catch (e) {
print('Error fetching resi detail: $e');
dailyDetail.clear();
} finally {
isLoading.value = false;
}
}
/// 🔹 Ambil total pendapatan COD & Non COD per tanggal
Future<Map<String, dynamic>> getPendapatanHarian(DateTime date) async {
try {
final user = _auth.currentUser;
if (user == null) return {};
final startOfDay = DateTime(date.year, date.month, date.day);
final endOfDay = DateTime(date.year, date.month, date.day, 23, 59, 59);
final snapshot = await _firestore
.collection('resis')
.where('store_id', isEqualTo: user.uid)
.where('created_at', isGreaterThanOrEqualTo: startOfDay)
.where('created_at', isLessThanOrEqualTo: endOfDay)
.get();
double codIncome = 0;
double nonCodIncome = 0;
for (var doc in snapshot.docs) {
final data = doc.data();
final pembayaran = (data['pembayaran'] ?? '').toString().toUpperCase();
final total = double.tryParse(data['total'].toString()) ?? 0;
if (pembayaran == 'COD') {
codIncome += total;
} else {
nonCodIncome += total;
}
}
return {
'cod': codIncome,
'nonCod': nonCodIncome,
};
} catch (e) {
print('Error getPendapatanHarian: $e');
return {};
}
}
Map<String, String?> _extractWilayah(String alamat) {
if (alamat.isEmpty || _wilayahData.isEmpty) {
return {'kota': null, 'provinsi': null};
}
String alamatLower = alamat.toLowerCase();
String? provinsi;
String? kota;
// 🔹 Ambil bagian belakang alamat (lebih akurat)
List<String> words = alamatLower.split(RegExp(r'\s+'));
String lastPart = words.length > 5
? words.sublist(words.length - 5).join(' ')
: alamatLower;
// ==============================
// 🔹 STEP 1: DETEKSI PROVINSI DULU
// ==============================
for (var data in _wilayahData) {
final prov = data['provinsi']?.toString() ?? '';
final provLower = prov.toLowerCase();
if (provLower.isEmpty) continue;
if (lastPart.contains(provLower) ||
_similarity(lastPart, provLower) > 0.7 ||
(provLower.contains('jawa timur') && lastPart.contains('jatim')) ||
(provLower.contains('jawa tengah') && lastPart.contains('jateng')) ||
(provLower.contains('jawa barat') && lastPart.contains('jabar')) ||
(provLower.contains('yogyakarta') && lastPart.contains('diy'))) {
provinsi = prov;
break; // 🔥 STOP kalau sudah ketemu
}
}
// kalau provinsi tidak ketemu langsung return
if (provinsi == null) {
return {'kota': null, 'provinsi': null};
}
// ==============================
// 🔹 STEP 2: CARI KOTA DALAM PROVINSI TERSEBUT
// ==============================
final provData = _wilayahData.firstWhere(
(e) => e['provinsi'] == provinsi,
orElse: () => {},
);
final List kotaList = List.from(provData['kota'] ?? []);
double bestScore = 0;
for (var k in kotaList) {
final kotaNama = k.toString();
final kotaLower = kotaNama.toLowerCase();
final kotaBersih =
kotaLower.replaceAll(RegExp(r'^(kab\.?|kota)\s*'), '').trim();
double score;
if (alamatLower.contains(kotaBersih)) {
score = 0.95;
} else {
score = _similarity(lastPart, kotaBersih);
}
if (score > bestScore) {
bestScore = score;
kota = kotaNama;
}
}
return {
'kota': kota,
'provinsi': provinsi,
};
}
double _similarity(String a, String b) {
if (a.isEmpty || b.isEmpty) return 0.0;
final aWords = a.split(' ');
final bWords = b.split(' ');
int matches = 0;
for (var aw in aWords) {
for (var bw in bWords) {
if (aw.isNotEmpty && bw.isNotEmpty && aw == bw) matches++;
}
}
return matches / ((aWords.length + bWords.length) / 2);
}
/// 🔹 Update data resi berdasarkan ID
Future<void> updateResi(String id, Map<String, dynamic> updatedData) async {
try {
isLoading.value = true;
// Pastikan dokumen dengan ID tersebut ada
final docRef = _firestore.collection('resis').doc(id);
final snapshot = await docRef.get();
if (!snapshot.exists) {
Get.snackbar(
"Gagal",
"Data resi tidak ditemukan.",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: const Color(0xFFFFCDD2),
colorText: const Color(0xFFB71C1C),
);
return;
}
// 🔹 Update data di Firestore
await docRef.update(updatedData);
// 🔹 Refresh data setelah update
if (snapshot.data()?['created_at'] != null) {
final tanggal = (snapshot.data()!['created_at'] as Timestamp).toDate();
await fetchResiByDate(tanggal);
}
Get.snackbar(
"Berhasil",
"Data resi berhasil diperbarui.",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: const Color(0xFFC8E6C9),
colorText: const Color(0xFF1B5E20),
);
} catch (e) {
print("Error update resi: $e");
Get.snackbar(
"Error",
"Terjadi kesalahan saat memperbarui data.",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: const Color(0xFFFFCDD2),
colorText: const Color(0xFFB71C1C),
);
} finally {
isLoading.value = false;
}
}
// Future<void> updateResi(String id, Map<String, dynamic> updatedData) async {
// try {
// isLoading.value = true;
// final docRef = _firestore.collection('resis').doc(id);
// final snapshot = await docRef.get();
// if (!snapshot.exists) {
// Get.snackbar(
// "Gagal",
// "Data resi tidak ditemukan.",
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: const Color(0xFFFFCDD2),
// colorText: const Color(0xFFB71C1C),
// );
// return;
// }
// final oldData = snapshot.data()!;
// // ==============================
// // 🔥 VALIDASI FIELD KOSONG
// // ==============================
// bool isEmpty(String? val) => val == null || val.trim().isEmpty;
// List<String> missing = [];
// if (isEmpty(updatedData['penerima'])) missing.add('Penerima');
// if (isEmpty(updatedData['alamat'])) missing.add('Alamat');
// if (isEmpty(updatedData['no_wa'])) missing.add('No. WA');
// if (isEmpty(updatedData['barang'])) missing.add('Barang');
// if (isEmpty(updatedData['total'])) missing.add('Total');
// if (isEmpty(updatedData['pembayaran'])) missing.add('Pembayaran');
// if (missing.isNotEmpty) {
// Get.snackbar(
// 'Gagal Update',
// 'Field berikut kosong:\n${missing.join(', ')}',
// backgroundColor: const Color(0xFFFF9800),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// return;
// }
// // ==============================
// // 🔥 VALIDASI FORMAT
// // ==============================
// // No WA minimal 10 digit angka
// String noWa =
// updatedData['no_wa'].toString().replaceAll(RegExp(r'\D'), '');
// if (noWa.length < 10) {
// Get.snackbar(
// 'Gagal Update',
// 'No. WA tidak valid!',
// backgroundColor: const Color(0xFFFF9800),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// return;
// }
// // Total harus angka
// String total =
// updatedData['total'].toString().replaceAll(RegExp(r'\D'), '');
// if (total.isEmpty || int.tryParse(total) == null) {
// Get.snackbar(
// 'Gagal Update',
// 'Total harus berupa angka!',
// backgroundColor: const Color(0xFFFF9800),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// return;
// }
// // ==============================
// // 🔥 CEK APAKAH ALAMAT DIUBAH
// // ==============================
// final oldAlamat = oldData['alamat'] ?? '';
// final newAlamat = updatedData['alamat'] ?? '';
// // ==============================
// // 🔥 SELALU DETEKSI ULANG WILAYAH
// // ==============================
// final alamat = updatedData['alamat'].toString().trim();
// final wilayah = _extractWilayah(alamat);
// // Kalau gagal detect TOLAK
// if (wilayah['provinsi'] == null || wilayah['kota'] == null) {
// Get.snackbar(
// 'Gagal Update',
// 'Alamat tidak valid, provinsi/kota tidak terdeteksi!',
// backgroundColor: const Color(0xFFFF9800),
// colorText: Colors.white,
// snackPosition: SnackPosition.BOTTOM,
// );
// return;
// }
// // 🔥 SET ULANG
// updatedData['alamat'] = alamat;
// updatedData['kota'] = wilayah['kota'];
// updatedData['provinsi'] = wilayah['provinsi'];
// // ==============================
// // 🔥 CLEAN DATA
// // ==============================
// updatedData.updateAll((key, value) => value.toString().trim());
// debugPrint("FINAL UPDATE DATA: $updatedData");
// // ==============================
// // 🔹 UPDATE FIRESTORE
// // ==============================
// await docRef.update(updatedData);
// // 🔹 Refresh data
// if (snapshot.data()?['created_at'] != null) {
// final tanggal =
// (snapshot.data()!['created_at'] as Timestamp).toDate();
// await fetchResiByDate(tanggal);
// }
// Get.snackbar(
// "Berhasil",
// "Data resi berhasil diperbarui.",
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: const Color(0xFFC8E6C9),
// colorText: const Color(0xFF1B5E20),
// );
// } catch (e) {
// debugPrint("Error update resi: $e");
// Get.snackbar(
// "Error",
// "Terjadi kesalahan saat memperbarui data.",
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: const Color(0xFFFFCDD2),
// colorText: const Color(0xFFB71C1C),
// );
// } finally {
// isLoading.value = false;
// }
// }
Future<void> deleteRiwayat(String id) async {
try {
isLoading(true);
// Hapus dari Firestore
await _firestore.collection('riwayat_bulanan').doc(id).delete();
// Hapus juga dari list lokal agar tampilan otomatis ter-update
monthlySummary.removeWhere((item) => item['id'] == id);
Get.snackbar(
"Berhasil",
"Data berhasil dihapus",
backgroundColor: const Color(0xFFFFCDD2),
snackPosition: SnackPosition.BOTTOM,
);
} catch (e) {
Get.snackbar(
"Error",
"Gagal menghapus data: $e",
backgroundColor: const Color(0xFFFFE0B2),
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isLoading(false);
}
}
/// 🔹 Hapus data resi berdasarkan ID dokumen
Future<void> deleteResi(String id) async {
try {
isLoading(true);
// Cek apakah dokumen ada
final docRef = _firestore.collection('resis').doc(id);
final snapshot = await docRef.get();
if (!snapshot.exists) {
Get.snackbar(
"Gagal",
"Data tidak ditemukan atau sudah dihapus.",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: const Color(0xFFFFCDD2),
colorText: const Color(0xFFB71C1C),
);
return;
}
// 🔹 Hapus dari Firestore
await docRef.delete();
// 🔹 Hapus dari list lokal agar tampilan otomatis update
dailyDetail.removeWhere((item) => item['id'] == id);
dailySummary.removeWhere((item) => item['id'] == id);
Get.snackbar(
"Berhasil",
"Data berhasil dihapus.",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: const Color(0xFFFFEBEE),
colorText: const Color(0xFFB71C1C),
);
} catch (e) {
Get.snackbar(
"Error",
"Gagal menghapus data: $e",
snackPosition: SnackPosition.BOTTOM,
backgroundColor: const Color(0xFFFFE0B2),
colorText: const Color(0xFFBF360C),
);
} finally {
isLoading(false);
}
}
}

View File

@ -0,0 +1,52 @@
import 'package:get/get.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class StoreController extends GetxController {
final namaTokoC = ''.obs;
final noHpC = ''.obs;
final alamatC = ''.obs;
final noRegistrasiC = ''.obs;
final keteranganC = ''.obs;
var storeData = Rxn<Map<String, dynamic>>();
var isLoading = false.obs;
final uid = FirebaseAuth.instance.currentUser!.uid;
@override
void onInit() {
super.onInit();
getStoreData();
}
Future<void> getStoreData() async {
isLoading.value = true;
final doc = await FirebaseFirestore.instance.collection('stores').doc(uid).get();
if (doc.exists) {
storeData.value = doc.data();
// isi obs supaya textfield terisi
namaTokoC.value = storeData.value?['namaToko'] ?? '';
noHpC.value = storeData.value?['noHp'] ?? '';
alamatC.value = storeData.value?['alamat'] ?? '';
noRegistrasiC.value = storeData.value?['noRegistrasi'] ?? '';
keteranganC.value = storeData.value?['keterangan'] ?? '';
}
isLoading.value = false;
}
Future<void> saveStore() async {
isLoading.value = true;
final data = {
'namaToko': namaTokoC.value,
'noHp': noHpC.value,
'alamat': alamatC.value,
'noRegistrasi': noRegistrasiC.value,
'keterangan': keteranganC.value,
'updatedAt': DateTime.now(),
};
await FirebaseFirestore.instance.collection('stores').doc(uid).set(data);
storeData.value = data;
isLoading.value = false;
}
}

View File

@ -0,0 +1,80 @@
import 'package:get/get.dart';
import 'package:praresi/presentation/controllers/auth_controller.dart';
import 'package:praresi/presentation/controllers/riwayat_controller.dart';
import 'package:praresi/presentation/views/menu_akun/change_password_view.dart';
import 'package:praresi/presentation/views/menu_akun/data_pelanggan_view.dart';
import 'package:praresi/presentation/views/menu_akun/panduan_pengguna_view.dart';
import 'package:praresi/presentation/views/menu_akun/profile_view.dart';
import 'package:praresi/presentation/views/menu_akun/riwayat_pengiriman.dart';
import 'package:praresi/presentation/views/menu_akun/store_view.dart';
import 'package:praresi/presentation/views/menu_riwayat/detail_riwayat_view.dart';
import 'package:praresi/presentation/views/splash_view.dart';
import '../views/login_view.dart';
import '../views/register_view.dart';
import '../views/forgot_view.dart';
import '../views/main_navbar.dart'; // import baru
import 'app_routes.dart';
class AppPages {
static final routes = [
GetPage(
name: AppRoutes.login,
page: () => LoginView(),
binding: BindingsBuilder(() {
Get.put(AuthController());
}),
),
GetPage(
name: AppRoutes.register,
page: () => RegisterView(),
),
GetPage(
name: AppRoutes.forgot,
page: () => ForgotView(),
),
GetPage(
name: AppRoutes.home, // gunakan route 'home' untuk MainNavbar
page: () => const MainNavbar(),
),
GetPage(
name: AppRoutes.store, // gunakan route 'home' untuk MainNavbar
page: () => StoreView(),
),
GetPage(
name: AppRoutes.splash,
page: () => const SplashView(),
),
GetPage(
name: AppRoutes.changepassword,
page: () => ChangePasswordView(),
),
GetPage(
name: AppRoutes.detailRiwayat,
page: () => const DetailRiwayatView(),
binding: BindingsBuilder(() {
Get.put(RiwayatController(), permanent: true);
}),
),
GetPage(
name: AppRoutes.profile,
page: () => ProfileView(),
),
GetPage(
name: AppRoutes.datapelanggan,
page: () => DataPelangganView(),
),
GetPage(
name: AppRoutes.riwayatbulanan,
page: () => RiwayatBulananView(),
),
GetPage(
name: AppRoutes.panduanpengguna,
page: () => PanduanPenggunaPage(),
),
];
}

View File

@ -0,0 +1,17 @@
class AppRoutes {
static const login = '/login';
static const register = '/register';
static const forgot = '/forgot';
static const home = '/home';
static const store = '/store';
static const changepassword = '/changepassword';
static const splash = '/';
static const profile = '/profile';
static const datapelanggan = '/data_pelanggan';
static const riwayatbulanan = '/riwayat_Pengiriman';
static const panduanpengguna = '/panduan_pengguna';
static const detailRiwayat = '/detail_riwayat';
}

View File

@ -0,0 +1,254 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:get/get.dart';
class AkunView extends StatelessWidget {
const AkunView({super.key});
Future<Map<String, dynamic>?> _getUserData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return null;
final doc = await FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.get();
if (doc.exists) {
return doc.data();
} else {
return {"email": user.email, "name": "Tanpa Nama"};
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Scaffold(
body: Container(
width: size.width,
height: size.height,
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1976D2), Color(0xFFE3F2FD)], // biru putih
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: FutureBuilder<Map<String, dynamic>?>(
future: _getUserData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData) {
return const Center(child: Text("Tidak ada data user"));
}
final data = snapshot.data!;
final name = data['name'] ?? 'Tanpa Nama';
final email = data['email'] ?? '-';
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
child: Column(
children: [
// Card profil
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
)
],
),
child: Column(
children: [
const CircleAvatar(
radius: 40,
backgroundColor: Color(0xFF1976D2),
child: Icon(Icons.person,
size: 40, color: Colors.white),
),
const SizedBox(height: 10),
Text(
name,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
Text(
email,
style: const TextStyle(
fontSize: 16, color: Colors.grey),
),
],
),
),
const SizedBox(height: 30),
// Submenu sebagai button card
_menuButton(
icon: Icons.person, // 👤 ikon profil
title: "Profil",
onTap: () => Get.toNamed('/profile'),
),
_menuButton(
icon: Icons.storefront, // 🏪 ikon toko
title: "Toko",
onTap: () => Get.toNamed('/store'),
),
_menuButton(
icon: Icons.people, // 👥 ikon pelanggan
title: "Data Pelanggan",
onTap: () => Get.toNamed('/data_pelanggan'),
),
_menuButton(
icon: Icons.history, // 📦 ikon riwayat pengiriman
title: "Riwayat Pengiriman",
onTap: () => Get.toNamed('/riwayat_Pengiriman'),
),
_menuButton(
icon: Icons.lock, // 🔒 ikon ubah sandi
title: "Ubah Kata Sandi",
onTap: () => Get.toNamed('/changepassword'),
),
_menuButton(
icon: Icons.help_outline, // ikon untuk petunjuk
title: "Panduan Pengguna",
onTap: () => Get.toNamed('/panduan_pengguna'),
),
// _menuButton(
// icon: Icons.settings,
// title: "Pengaturan",
// onTap: () {},
// ),
// _menuButton(
// icon: Icons.help_outline,
// title: "Bantuan",
// onTap: () {},
// ),
const SizedBox(height: 30),
// Tombol logout
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
),
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: const Text("Konfirmasi"),
content: const Text(
"Apakah Anda yakin ingin keluar dari akun?",
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text("Tidak"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () => Navigator.pop(context, true),
child: const Text(
"Ya, Keluar",
style: TextStyle(color: Colors.white),
),
),
],
),
);
/// jika pilih YA
if (confirm == true) {
await FirebaseAuth.instance.signOut();
Navigator.of(context)
.pushReplacementNamed('/login');
}
},
icon: const Icon(Icons.logout, color: Colors.white),
label: const Text(
"Keluar",
style: TextStyle(color: Colors.white),
),
),
),
],
),
);
},
),
),
),
);
}
// Widget helper untuk button submenu
Widget _menuButton({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 15),
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
elevation: 3,
child: InkWell(
borderRadius: BorderRadius.circular(15),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(icon, color: const Color(0xFF1976D2)),
const SizedBox(width: 15),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,296 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:fl_chart/fl_chart.dart';
import '../controllers/analisis_controller.dart';
class AnalisisView extends StatelessWidget {
const AnalisisView({super.key});
@override
Widget build(BuildContext context) {
final AnalisisController controller = Get.put(AnalisisController());
return Scaffold(
extendBodyBehindAppBar: true,
body: Obx(() {
final monthlyData = controller.monthlyData;
final topCities = controller.topCities;
final selectedMonth = controller.selectedMonth.value;
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1976D2), Color(0xFFE3F2FD)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: Column(
children: [
const SizedBox(height: 12),
// 🔹 Grafik Pengiriman Bulanan
Container(
margin:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Row(
children: const [
Icon(Icons.bar_chart, color: Color(0xFF1976D2)),
SizedBox(width: 8),
Text(
"Pengiriman Bulanan",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
fontSize: 16,
),
),
],
),
const SizedBox(height: 12),
monthlyData.isEmpty
? const SizedBox(
height: 200,
child: Center(
child: Text(
"Belum ada data pengiriman",
style: TextStyle(color: Colors.black54),
),
),
)
: SizedBox(
height: 220,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
gridData: FlGridData(show: false),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
leftTitles: const AxisTitles(
sideTitles:
SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles:
SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles:
SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) {
final index = value.toInt();
if (index >= 0 &&
index < monthlyData.length) {
return Padding(
padding:
const EdgeInsets.only(top: 6),
child: Text(
monthlyData[index]['month'],
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
),
);
}
return const SizedBox.shrink();
},
),
),
),
barGroups:
monthlyData.asMap().entries.map((e) {
final pengiriman =
(e.value['pengiriman'] as num)
.toDouble();
return BarChartGroupData(
x: e.key,
barRods: [
BarChartRodData(
toY: pengiriman,
gradient: const LinearGradient(
colors: [
Color(0xFF1976D2),
Color(0xFF64B5F6),
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
),
width: 18,
borderRadius:
BorderRadius.circular(6),
),
],
);
}).toList(),
),
),
),
// 🔹 Tampilkan jumlah di atas batang
if (monthlyData.isNotEmpty) const SizedBox(height: 12),
if (monthlyData.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: monthlyData.map((e) {
return Text(
e['pengiriman'].toString(),
style: const TextStyle(
fontSize: 12,
color: Colors.black54,
fontWeight: FontWeight.w600,
),
);
}).toList(),
),
],
),
),
// 🔹 Top Kota Tujuan per Bulan
Expanded(
child: Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: const [
Icon(Icons.location_city,
color: Color(0xFF1976D2)),
SizedBox(width: 6),
Text(
"Top 10 Kota Tujuan",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
fontSize: 16,
),
),
],
),
// 🔹 Dropdown pilih bulan
DropdownButton<String>(
value: selectedMonth,
items: controller.monthlyData
.map((e) => DropdownMenuItem<String>(
value: e['month'],
child: Text(e['month']),
))
.toList(),
onChanged: (value) {
if (value != null) {
controller.setSelectedMonth(value);
}
},
underline: const SizedBox(),
style: const TextStyle(
color: Colors.black87, fontSize: 14),
icon: const Icon(Icons.arrow_drop_down,
color: Color(0xFF1976D2)),
),
],
),
),
const Divider(height: 1, color: Color(0xFFE0E0E0)),
Expanded(
child: topCities.isEmpty
? const Center(
child: Text(
"Tidak ada data untuk bulan ini",
style: TextStyle(color: Colors.black54),
),
)
: ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: 12),
itemCount: topCities.length,
separatorBuilder: (_, __) => Divider(
color: Colors.grey.shade200,
height: 1,
),
itemBuilder: (context, index) {
final city = topCities[index];
return ListTile(
contentPadding:
const EdgeInsets.symmetric(
horizontal: 8, vertical: 6),
leading: CircleAvatar(
backgroundColor: Colors.blue.shade50,
child: Text(
'${index + 1}',
style: const TextStyle(
color: Color(0xFF1976D2),
fontWeight: FontWeight.bold,
),
),
),
title: Text(
city['city'],
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
subtitle: Text(
"Pengiriman: ${city['transaksi']} kali\nPendapatan: ${controller.formatRupiah(city['pendapatan'])}",
style: const TextStyle(fontSize: 12),
),
// trailing: const Icon(
// Icons.chevron_right_rounded,
// color: Colors.grey,
// ),
);
},
),
),
],
),
),
),
const SizedBox(height: 16),
],
),
),
);
}),
);
}
}

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