Upload project M Posyandu Final

This commit is contained in:
zilillah08 2026-06-19 12:54:15 +07:00
commit 9f7b5ca4ad
220 changed files with 35026 additions and 0 deletions

47
.gitignore vendored Normal file
View File

@ -0,0 +1,47 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.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
*.log

45
.metadata Normal file
View File

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

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

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

16
README.md Normal file
View File

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

28
analysis_options.yaml Normal file
View File

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

13
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/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

44
android/app/build.gradle Normal file
View File

@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.example.posyandu_care"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.posyandu_care"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

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

View File

@ -0,0 +1,51 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="M_POSYANDU"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:allowBackup="false"
android:fullBackupContent="false">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

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

View File

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

View File

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

18
android/build.gradle Normal file
View File

@ -0,0 +1,18 @@
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 -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

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

25
android/settings.gradle Normal file
View File

@ -0,0 +1,25 @@
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
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.1" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
assets/images/logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
assets/images/logoo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/sounds/notif.mp3 Normal file

Binary file not shown.

34
ios/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

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

View File

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

View File

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

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

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Posyandu Care</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>posyandu_care</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.
}
}

196
lib/bidan/bidan_drawer.dart Normal file
View File

@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../layout/main_layout.dart';
class BidanDrawer extends StatefulWidget {
const BidanDrawer({super.key});
@override
State<BidanDrawer> createState() => _BidanDrawerState();
}
class _BidanDrawerState extends State<BidanDrawer> {
String? fotoUser;
String namaUser = "Bidan";
@override
void initState() {
super.initState();
_loadUserData();
}
Future<void> _loadUserData() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
fotoUser = prefs.getString('foto');
namaUser = prefs.getString('nama') ?? "Bidan Posyandu";
});
}
@override
Widget build(BuildContext context) {
return Drawer(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(
children: [
// ================= HEADER =================
Material(
color: MainLayout.mainColor,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(20),
),
child: InkWell(
onTap: () {
Navigator.pushReplacementNamed(context, '/profile-bidan');
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 37,
backgroundColor: Colors.blue.shade100,
backgroundImage: (fotoUser != null &&
fotoUser!.isNotEmpty)
? NetworkImage(
"http://ta.myhost.id/E31230549/mposyandu_api/uploads/$fotoUser")
: null,
child: (fotoUser == null || fotoUser!.isEmpty)
? const Icon(
Icons.person,
size: 40,
color: MainLayout.mainColor,
)
: null,
),
),
const SizedBox(height: 12),
Text(
namaUser,
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
"Lihat Profil",
style: GoogleFonts.poppins(
color: Colors.white70,
fontSize: 12,
),
),
],
),
),
),
),
// ================= MENU LIST =================
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
_drawerItem(context, Icons.home, "Home", '/dashboard-bidan'),
// ================= DROPDOWN JADWAL (TANPA GARIS) =================
Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
shape: const Border(),
collapsedShape: const Border(),
leading: const Icon(Icons.calendar_today,
color: MainLayout.mainColor),
title: Text(
"Jadwal",
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
iconColor: MainLayout.mainColor,
collapsedIconColor: MainLayout.mainColor,
childrenPadding: const EdgeInsets.only(left: 10),
children: [
_drawerItem(context, Icons.event_note, "Jadwal Posyandu",
'/jadwal-posyandu'),
// Link ke halaman Jadwal ANC yang baru dibuat
_drawerItem(context, Icons.medical_information,
"Jadwal Periksa ANC", '/jadwal-anc'),
],
),
),
_drawerItem(context, Icons.pregnant_woman,
"Pemeriksaan Kehamilan", '/periksa-kehamilan'),
_drawerItem(context, Icons.child_care, "Data Gizi Balita",
'/data-gizi-balita'),
_drawerItem(
context, Icons.medical_services, "Imunisasi", '/imunisasi'),
_drawerItem(context, Icons.menu_book, "Edukasi", '/edukasi'),
_drawerItem(context, Icons.description, "Laporan", '/laporan'),
],
),
),
const Divider(),
// ================= LOGOUT =================
ListTile(
leading: const Icon(Icons.logout, color: MainLayout.mainColor),
title: Text(
"Logout",
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
onTap: () => _logout(context),
),
const SizedBox(height: 10),
],
),
);
}
// ================= DRAWER ITEM WIDGET =================
Widget _drawerItem(
BuildContext context, IconData icon, String text, String route) {
return ListTile(
leading: Icon(icon, color: MainLayout.mainColor),
title: Text(
text,
style: GoogleFonts.poppins(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
onTap: () {
Navigator.pushReplacementNamed(context, route);
},
);
}
// ================= LOGOUT FUNCTION =================
Future<void> _logout(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
if (!context.mounted) return;
Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false);
}
}

View File

@ -0,0 +1,441 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
class EditGiziBalitaPage extends StatefulWidget {
final Map<String, dynamic> dataGizi;
const EditGiziBalitaPage({super.key, required this.dataGizi});
@override
State<EditGiziBalitaPage> createState() => _EditGiziBalitaPageState();
}
class _EditGiziBalitaPageState extends State<EditGiziBalitaPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController zbbController;
late TextEditingController ztbController;
late TextEditingController zbbtbController;
late TextEditingController tindakController;
late TextEditingController saranController;
// Variabel penampung teks rentang standar deviasi (SD)
String keteranganBBU = "-";
String keteranganTBU = "-";
String keteranganBBTB = "-";
// Tiga status gizi berbeda sesuai dengan database baru
String? _selectedStatusBBU;
String? _selectedStatusTBU;
String? _selectedStatusBBTB;
bool isLoading = false;
// List kategori indikator masing-masing gizi
final List<String> _listBBU = [
'Gizi Buruk',
'Gizi Kurang',
'Gizi Baik',
'Risiko Gizi Lebih'
];
final List<String> _listTBU = ['Sangat Pendek', 'Pendek', 'Normal', 'Tinggi'];
final List<String> _listBBTB = [
'Sangat Kurus',
'Kurus',
'Normal',
'Gemuk',
'Obesitas'
];
@override
void initState() {
super.initState();
zbbController = TextEditingController(
text: widget.dataGizi["zscore_bb_u"]?.toString() ?? "");
ztbController = TextEditingController(
text: widget.dataGizi["zscore_tb_u"]?.toString() ?? "");
zbbtbController = TextEditingController(
text: widget.dataGizi["zscore_bb_tb"]?.toString() ?? "");
tindakController =
TextEditingController(text: widget.dataGizi["tindak_lanjut"] ?? "");
saranController =
TextEditingController(text: widget.dataGizi["saran"] ?? "");
// Ambil rentang standar deviasi otomatis berdasarkan nilai Z-Score yang ada
double zBBU = double.tryParse(zbbController.text) ?? 0;
double zTBU = double.tryParse(ztbController.text) ?? 0;
double zBBTB = double.tryParse(zbbtbController.text) ?? 0;
keteranganBBU = _getSDStatus(zBBU);
keteranganTBU = _getSDStatus(zTBU);
keteranganBBTB = _getSDStatus(zBBTB);
// Set nilai awal dropdown berdasarkan data yang ada di database
if (_listBBU.contains(widget.dataGizi["status_bbu"])) {
_selectedStatusBBU = widget.dataGizi["status_bbu"];
}
if (_listTBU.contains(widget.dataGizi["status_tbu"])) {
_selectedStatusTBU = widget.dataGizi["status_tbu"];
}
if (_listBBTB.contains(widget.dataGizi["status_bbtb"])) {
_selectedStatusBBTB = widget.dataGizi["status_bbtb"];
}
}
// Fungsi penentu keterangan standar deviasi (Sama dengan kode tambah)
String _getSDStatus(double z) {
if (z < -3) return "Di bawah -3 SD";
if (z < -2) return "-3 SD s/d -2 SD";
if (z <= 1) return "-2 SD s/d +1 SD";
if (z <= 2) return "Di atas +1 SD s/d +2 SD";
return "Di atas +2 SD";
}
String formatTanggal(String? tgl) {
if (tgl == null || tgl == "-" || tgl.isEmpty) return "-";
try {
DateTime dt = DateTime.parse(tgl);
List<String> bulanIndo = [
"",
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
return "${dt.day} ${bulanIndo[dt.month]} ${dt.year}";
} catch (e) {
return tgl;
}
}
String hitungUsia(String? tglLahir) {
if (tglLahir == null || tglLahir.isEmpty) return "-";
try {
DateTime lahir = DateTime.parse(tglLahir);
DateTime sekarang = DateTime.now();
int bulan =
(sekarang.year - lahir.year) * 12 + sekarang.month - lahir.month;
return "$bulan Bulan";
} catch (e) {
return "-";
}
}
Future<void> updateData() async {
if (!_formKey.currentState!.validate()) return;
setState(() => isLoading = true);
try {
final response = await http.post(
Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/gizi_balita/update_gizi_balita.php"),
body: {
"id_gizi": widget.dataGizi["id_gizi"].toString(),
"zscore_bb_u": zbbController.text,
"zscore_tb_u": ztbController.text,
"zscore_bb_tb": zbbtbController.text,
"status_bbu": _selectedStatusBBU ?? "",
"status_tbu": _selectedStatusTBU ?? "",
"status_bbtb": _selectedStatusBBTB ?? "",
"tindak_lanjut": tindakController.text,
"saran": saranController.text,
},
);
final data = json.decode(response.body);
if (data["success"] == true) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil diupdate")));
Navigator.pop(context, true);
} else {
if (!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("Gagal update")));
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Error: $e")));
}
if (mounted) setState(() => isLoading = false);
}
@override
Widget build(BuildContext context) {
final usia = hitungUsia(widget.dataGizi["tanggal_lahir"]?.toString());
final tglPeriksa =
formatTanggal(widget.dataGizi["tanggal_pemeriksaan"]?.toString());
final catatan = widget.dataGizi["catatan"] ?? "-";
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: const Text(""),
backgroundColor: Colors.blue,
iconTheme: const IconThemeData(color: Colors.white),
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Form(
key: _formKey,
child: Column(
children: [
Text(
"Edit Data Gizi Balita",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 15),
// CARD INFO BALITA (BIRU)
Container(
width: double.infinity,
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.dataGizi["nama"] ?? "-",
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15)),
const Divider(color: Colors.white),
_rowInfo("Tgl Pemeriksaan", tglPeriksa),
_rowInfo("Usia Balita", usia),
_rowInfo("BB / TB / LK",
"${widget.dataGizi['bb']} kg / ${widget.dataGizi['tb']} cm / ${widget.dataGizi['lk']} cm"),
_rowInfo("Catatan Kader", catatan),
],
),
),
const SizedBox(height: 15),
// CARD INPUT (PUTIH)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Bagian Input Z-Score & Tampilan Standar Deviasi (Sama dengan Kode Tambah)
_buildSection(
"Z-Score BB/U",
zbbController,
keteranganBBU,
_listBBU,
_selectedStatusBBU, (v) {
setState(() => _selectedStatusBBU = v);
}),
_buildSection(
"Z-Score TB/U",
ztbController,
keteranganTBU,
_listTBU,
_selectedStatusTBU, (v) {
setState(() => _selectedStatusTBU = v);
}),
_buildSection(
"Z-Score BB/TB",
zbbtbController,
keteranganBBTB,
_listBBTB,
_selectedStatusBBTB, (v) {
setState(() => _selectedStatusBBTB = v);
}),
const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Divider(),
),
_buildLabel("Tindak Lanjut"),
_buildTextField(tindakController,
hint: "Masukkan rencana tindakan..."),
_buildLabel("Saran"),
_buildTextField(saranController,
hint: "Masukkan saran..."),
const SizedBox(height: 25),
// --- TOMBOL SIMPAN PERUBAHAN ---
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: isLoading ? null : updateData,
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Colors.blue, width: 2),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(
vertical: 15),
),
child: Text(
"Simpan Perubahan",
style: GoogleFonts.poppins(
color: Colors.blue,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
),
),
),
),
),
);
}
// Komponen pembangun struktur input + kotak Standar Deviasi + Dropdown
Widget _buildSection(String title, TextEditingController controller,
String sd, List<String> items, String? val, Function(String?) onChanged) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel(title),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
controller: controller,
readOnly: true,
decoration: _inputDeco(isFilled: true),
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.blue[900]),
),
),
const SizedBox(width: 10),
Expanded(
flex: 3,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.amber[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber[200]!)),
child: Text(
sd,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Colors.amber[900]),
),
),
),
],
),
const SizedBox(height: 10),
DropdownButtonFormField<String>(
value: val,
hint: Text("Pilih Status $title",
style: GoogleFonts.poppins(fontSize: 12)),
isExpanded: true,
items: items
.map((e) => DropdownMenuItem(
value: e,
child: Text(e, style: GoogleFonts.poppins(fontSize: 12))))
.toList(),
onChanged: onChanged,
decoration: _inputDeco(),
validator: (v) => v == null ? "Wajib dipilih" : null,
),
const SizedBox(height: 10),
],
);
}
Widget _rowInfo(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 110,
child: Text(label,
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600))),
const Text(": ", style: TextStyle(color: Colors.white)),
Expanded(
child: Text(value,
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500))),
],
),
);
}
Widget _buildLabel(String t) => Padding(
padding: const EdgeInsets.only(top: 10, bottom: 4),
child: Text(t,
style:
GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w600)));
Widget _buildTextField(TextEditingController c, {String? hint}) {
return TextFormField(
controller: c,
maxLines: 2,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
decoration: _inputDeco(hint: hint),
);
}
InputDecoration _inputDeco({String? hint, bool isFilled = false}) {
return InputDecoration(
hintText: hint,
hintStyle: GoogleFonts.poppins(fontSize: 11),
filled: true,
fillColor: isFilled ? Colors.blue[50] : Colors.grey[50],
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!)),
);
}
}

View File

@ -0,0 +1,476 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'edit_gizi_balita.dart';
class RiwayatGiziBalitaPage extends StatefulWidget {
final Map<String, dynamic> balita;
const RiwayatGiziBalitaPage({super.key, required this.balita});
@override
State<RiwayatGiziBalitaPage> createState() => _RiwayatGiziBalitaPageState();
}
class _RiwayatGiziBalitaPageState extends State<RiwayatGiziBalitaPage> {
List<Map<String, dynamic>> _riwayatGizi = [];
List<Map<String, dynamic>> _filteredRiwayatGizi = [];
bool _isLoading = true;
final TextEditingController _searchController = TextEditingController();
// Variabel Pagination
int _currentPage = 0;
final int _rowsPerPage = 5;
@override
void initState() {
super.initState();
fetchRiwayat();
}
// Logika mendapatkan data per halaman
List<Map<String, dynamic>> get _paginatedData {
if (_filteredRiwayatGizi.isEmpty) return [];
final start = _currentPage * _rowsPerPage;
final end = start + _rowsPerPage;
if (start >= _filteredRiwayatGizi.length) return [];
return _filteredRiwayatGizi.sublist(
start,
end > _filteredRiwayatGizi.length ? _filteredRiwayatGizi.length : end,
);
}
String formatTanggal(String? tgl) {
if (tgl == null || tgl == "-" || tgl.isEmpty) return "-";
try {
DateTime dt = DateTime.parse(tgl);
List<String> bulanIndo = [
"",
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
return "${dt.day} ${bulanIndo[dt.month]} ${dt.year}";
} catch (e) {
return tgl;
}
}
void _filterData(String query) {
setState(() {
_filteredRiwayatGizi = _riwayatGizi.where((data) {
final tanggal =
data["tanggal_pemeriksaan"]?.toString().toLowerCase() ?? "";
return tanggal.contains(query.toLowerCase());
}).toList();
_currentPage = 0; // Reset ke halaman pertama saat mencari
});
}
String hitungUsia(String? tglLahir) {
if (tglLahir == null || tglLahir == "-" || tglLahir.isEmpty) return "-";
try {
DateTime lahir = DateTime.parse(tglLahir);
DateTime sekarang = DateTime.now();
int bulan =
(sekarang.year - lahir.year) * 12 + sekarang.month - lahir.month;
return "$bulan Bulan";
} catch (e) {
return "-";
}
}
Future<void> fetchRiwayat() async {
setState(() => _isLoading = true);
try {
final idBalita = widget.balita['id'];
final response = await http.get(
Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/gizi_balita/get_riwayat_gizi_balita.php?id_balita=$idBalita"),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_riwayatGizi = List<Map<String, dynamic>>.from(data);
_filteredRiwayatGizi = _riwayatGizi;
_isLoading = false;
});
} else {
throw Exception("Gagal load data");
}
} catch (e) {
setState(() => _isLoading = false);
}
}
Future<void> hapusData(String idGizi) async {
try {
final response = await http.post(
Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/gizi_balita/hapus_gizi_balita.php"),
headers: {"Content-Type": "application/json"},
body: jsonEncode({"id_gizi": idGizi}),
);
final res = json.decode(response.body);
if (res["success"] == true) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil dihapus")));
fetchRiwayat();
}
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Error: $e")));
}
}
@override
Widget build(BuildContext context) {
final totalPages = _filteredRiwayatGizi.isEmpty
? 1
: (_filteredRiwayatGizi.length / _rowsPerPage).ceil();
return Scaffold(
backgroundColor: Colors.grey[50],
appBar: AppBar(
backgroundColor: Colors.blue,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
title: Text("",
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600)),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Center(
child: Text("Data Riwayat Gizi Balita",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black)),
),
const SizedBox(height: 15),
TextField(
controller: _searchController,
onChanged: _filterData,
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
hintText: "Cari berdasarkan tanggal (YYYY-MM-DD)...",
hintStyle: GoogleFonts.poppins(fontSize: 12),
prefixIcon: const Icon(Icons.search, size: 20),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
contentPadding: const EdgeInsets.symmetric(vertical: 8),
),
),
const SizedBox(height: 20),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredRiwayatGizi.isEmpty
? Center(
child: Text("Data tidak ditemukan",
style: GoogleFonts.poppins(
fontSize: 12, color: Colors.grey)))
: ListView.builder(
itemCount: _paginatedData.length,
itemBuilder: (context, index) {
final gizi = _paginatedData[index];
return _buildCardIdentik(gizi);
},
),
),
Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Halaman ${_currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(fontSize: 12),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _currentPage == 0
? null
: () => setState(() => _currentPage--),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage >= totalPages - 1
? null
: () => setState(() => _currentPage++),
),
],
),
],
),
),
],
),
),
);
}
Widget _buildCardIdentik(Map<String, dynamic> data) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 4))
],
),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(15),
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.balita["nama"]?.toString() ?? "-",
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14)),
const Divider(color: Colors.white54),
_rowWhite("Orang Tua",
widget.balita["nama_orang_tua"]?.toString() ?? "-"),
_rowWhite("Usia",
hitungUsia(widget.balita["tanggal_lahir"]?.toString())),
_rowWhite("Tgl Pemeriksaan",
formatTanggal(data["tanggal_pemeriksaan"]?.toString())),
_rowWhite("BB / TB / LK",
"${data['bb'] ?? '-'}kg / ${data['tb'] ?? '-'}cm / ${data['lk'] ?? '-'}cm"),
_rowWhite("Catatan Kader", data["catatan"]?.toString() ?? "-"),
],
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Detail Pemeriksaan",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600, fontSize: 12)),
const SizedBox(height: 8),
_rowGizi(
"Z-Score BB/U", data["zscore_bb_u"]?.toString() ?? "-"),
_rowGizi(
"Z-Score TB/U", data["zscore_tb_u"]?.toString() ?? "-"),
_rowGizi(
"Z-Score BB/TB", data["zscore_bb_tb"]?.toString() ?? "-"),
const Divider(),
_rowStatus("Status Gizi", data),
_rowGizi(
"Tindak Lanjut", data["tindak_lanjut"]?.toString() ?? "-"),
_rowGizi("Saran", data["saran"]?.toString() ?? "-"),
const SizedBox(height: 12),
Row(
children: [
_stadiumButton(Icons.edit, "Edit", Colors.orange, () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditGiziBalitaPage(
dataGizi: {
...data,
"nama": widget.balita["nama"],
"tanggal_lahir": widget.balita["tanggal_lahir"],
},
),
),
);
fetchRiwayat();
}),
const SizedBox(width: 8),
_stadiumButton(Icons.delete, "Hapus", Colors.red, () {
_showDeleteDialog(data["id_gizi"].toString());
}),
],
)
],
),
),
],
),
);
}
Widget _rowWhite(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
SizedBox(
width: 120,
child: Text(label,
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600))),
const Text(": ", style: TextStyle(color: Colors.white)),
Expanded(
child: Text(value,
style:
GoogleFonts.poppins(color: Colors.white, fontSize: 12))),
],
),
);
}
Widget _rowGizi(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
SizedBox(
width: 120,
child: Text(label,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey[800]))),
const Text(": "),
Expanded(
child: Text(value,
style:
GoogleFonts.poppins(fontSize: 12, color: Colors.black))),
],
),
);
}
Widget _rowStatus(String label, Map<String, dynamic> data) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.orange[50], borderRadius: BorderRadius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("$label :",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.orange[900])),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.only(left: 12),
child: Column(
children: [
_rowStatusDetail("BB / U", data["status_bbu"] ?? "-"),
_rowStatusDetail("TB / U", data["status_tbu"] ?? "-"),
_rowStatusDetail("BB / TB", data["status_bbtb"] ?? "-"),
],
),
),
],
),
);
}
Widget _rowStatusDetail(String indikator, String nilai) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
SizedBox(
width: 65,
child: Text(
indikator,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.orange[900]),
),
),
Text(
" : ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.orange[900]),
),
Expanded(
child: Text(
nilai,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange[900]),
),
),
],
),
);
}
Widget _stadiumButton(
IconData icon, String text, Color color, VoidCallback onPressed) {
return Expanded(
child: OutlinedButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: 14, color: color),
label: Text(text,
style: GoogleFonts.poppins(
fontSize: 11, color: color, fontWeight: FontWeight.bold)),
style: OutlinedButton.styleFrom(
side: BorderSide(color: color),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
);
}
void _showDeleteDialog(String id) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Hapus Data",
style:
GoogleFonts.poppins(fontWeight: FontWeight.bold, fontSize: 16)),
content: Text("Yakin ingin menghapus data pemeriksaan ini?",
style: GoogleFonts.poppins(fontSize: 13)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal")),
TextButton(
onPressed: () async {
Navigator.pop(context);
await hapusData(id);
},
child: const Text("Hapus", style: TextStyle(color: Colors.red)),
),
],
),
);
}
}

View File

@ -0,0 +1,453 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
class TambahGiziBalitaPage extends StatefulWidget {
final Map<String, dynamic> balita;
const TambahGiziBalitaPage({super.key, required this.balita});
@override
State<TambahGiziBalitaPage> createState() => _TambahGiziBalitaPageState();
}
class _TambahGiziBalitaPageState extends State<TambahGiziBalitaPage> {
final _formKey = GlobalKey<FormState>();
bool _isSaving = false;
bool _isLoadingPreview =
true; // Menahan transisi widget selama proses hitung API
// Controller text form input & field readonly
final TextEditingController _bbUController = TextEditingController();
final TextEditingController _tbUController = TextEditingController();
final TextEditingController _bbTbController = TextEditingController();
final TextEditingController _tindakLanjutController = TextEditingController();
final TextEditingController _saranController = TextEditingController();
// Status klasifikasi gizi text yang dihitung dinamis oleh server PHP
String? _selectedStatusBBU;
String? _selectedStatusTBU;
String? _selectedStatusBBTB;
String keteranganBBU = "Menghitung...";
String keteranganTBU = "Menghitung...";
String keteranganBBTB = "Menghitung...";
// Master list referensi dropdown status gizi penyeimbang data
final List<String> _listBBU = [
'Gizi Buruk',
'Gizi Kurang',
'Gizi Baik',
'Risiko Gizi Lebih'
];
final List<String> _listTBU = ['Sangat Pendek', 'Pendek', 'Normal', 'Tinggi'];
final List<String> _listBBTB = [
'Sangat Kurus',
'Kurus',
'Normal',
'Gemuk',
'Obesitas'
];
@override
void initState() {
super.initState();
_ambilPreviewKalkulasiServer();
}
// Helper pembersih string angka timbangan gizi anak di card atas
String formatAngka(dynamic value, String satuan) {
if (value == null ||
value.toString().trim().isEmpty ||
value.toString().toLowerCase() == "null") {
return "-";
}
return "${value.toString()} $satuan";
}
// =========================================================================
// Fungsi Pendukung Realtime Preview Kalkulasi Z-Score dari Database Server
// =========================================================================
Future<void> _ambilPreviewKalkulasiServer() async {
try {
final response = await http.post(
Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/gizi_balita/tambah_gizi_balita.php"),
body: {
"id_pemeriksaan": widget.balita["id_pemeriksaan"].toString(),
"id_balita": widget.balita["id"].toString(),
"mode": "preview" // Triger parameter pembeda request mode di PHP
},
);
final result = json.decode(response.body);
if (result['success'] == true) {
double zBBU =
double.tryParse(result['zscore_bb_u']?.toString() ?? '0.0') ?? 0.0;
double zTBU =
double.tryParse(result['zscore_tb_u']?.toString() ?? '0.0') ?? 0.0;
double zBBTB =
double.tryParse(result['zscore_bb_tb']?.toString() ?? '0.0') ?? 0.0;
_bbUController.text = zBBU.toStringAsFixed(2);
_tbUController.text = zTBU.toStringAsFixed(2);
_bbTbController.text = zBBTB.toStringAsFixed(2);
String serverBBU = result['status_bbu']?.toString() ?? 'Gizi Baik';
String serverTBU = result['status_tbu']?.toString() ?? 'Normal';
String serverBBTB = result['status_bbtb']?.toString() ?? 'Normal';
// Penyelaras kata kunci jika seandainya backend mengirimkan teks "Normal" pada kategori BB/U
if (serverBBU == "Normal") serverBBU = "Gizi Baik";
setState(() {
// Sistem pengaman asersi: hanya masukkan value ke dropdown jika string terdaftar di list widget
_selectedStatusBBU = _listBBU.contains(serverBBU) ? serverBBU : null;
_selectedStatusTBU = _listTBU.contains(serverTBU) ? serverTBU : null;
_selectedStatusBBTB =
_listBBTB.contains(serverBBTB) ? serverBBTB : null;
keteranganBBU = _getSDStatus(zBBU);
keteranganTBU = _getSDStatus(zTBU);
keteranganBBTB = _getSDStatus(zBBTB);
_isLoadingPreview =
false; // Mematikan putaran loading & memunculkan form komponen gizi
});
} else {
setState(() => _isLoadingPreview = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Info: ${result['message']}")));
}
}
} catch (e) {
setState(() {
keteranganBBU = "Gagal memuat";
keteranganTBU = "Gagal memuat";
keteranganBBTB = "Gagal memuat";
_isLoadingPreview = false;
});
}
}
// =========================================================================
// Perbaikan Inti: Logika Pemisah Angka Minus & Plus Terhadap Nilai Median
// =========================================================================
String _getSDStatus(double z) {
if (z < -3) return "Di bawah -3 SD";
if (z < -2) return "-3 SD s/d -2 SD";
// Jika nilai desimal minus (misal -0.07), langsung kunci ke rentang kiri median
if (z < 0) return "-2 SD s/d < Median";
// Jika nilai desimal nol atau positif kecil, masukkan ke rentang kanan median
if (z <= 1) return "Median s/d +1 SD";
if (z <= 2) return "Di atas +1 SD s/d +2 SD";
return "Di atas +2 SD";
}
// =========================================================================
// Fungsi Penyimpanan Data Gizi Transaksional Bersih ke Server API
// =========================================================================
Future<void> _simpanData() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isSaving = true);
try {
final response = await http.post(
Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/gizi_balita/tambah_gizi_balita.php"),
body: {
"id_pemeriksaan": widget.balita["id_pemeriksaan"].toString(),
"id_balita": widget.balita["id"].toString(),
"tindak_lanjut": _tindakLanjutController.text,
"saran": _saranController.text,
"status_bbu": _selectedStatusBBU,
"status_tbu": _selectedStatusTBU,
"status_bbtb": _selectedStatusBBTB,
},
);
final result = json.decode(response.body);
if (result['success'] == true) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data Berhasil Disimpan")));
Navigator.pop(context, true);
} else {
throw result['message'];
}
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Error: $e")));
} finally {
if (mounted) setState(() => _isSaving = false);
}
}
@override
Widget build(BuildContext context) {
final bb = formatAngka(widget.balita['bb'], "kg");
final tb = formatAngka(widget.balita['tb'], "cm");
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
backgroundColor: Colors.blue,
iconTheme: const IconThemeData(color: Colors.white),
elevation: 0,
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: Form(
key: _formKey,
child: Column(
children: [
// Judul Besar Halaman Tengah Atas
Text(
"Tambah Data Gizi Balita",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black),
),
const SizedBox(height: 15),
// Card Informasi Identitas Balita (Warna Biru)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.balita["nama"] ?? "-",
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15),
),
const SizedBox(height: 4),
const Divider(color: Colors.white54, thickness: 1),
const SizedBox(height: 4),
Text("BB / TB: $bb / $tb",
style: GoogleFonts.poppins(
color: Colors.white, fontSize: 13)),
],
),
),
const SizedBox(height: 15),
// Blok Container Putih Pembungkus Seluruh Form Gizi
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10)
],
),
child: _isLoadingPreview
? const Padding(
padding: EdgeInsets.all(30.0),
child: Center(child: CircularProgressIndicator()),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Seksi Z-Score BB/U
_buildSection(
"Z-Score BB/U",
_bbUController,
keteranganBBU,
_listBBU,
_selectedStatusBBU,
(v) =>
setState(() => _selectedStatusBBU = v)),
// 2. Seksi Z-Score TB/U
_buildSection(
"Z-Score TB/U",
_tbUController,
keteranganTBU,
_listTBU,
_selectedStatusTBU,
(v) =>
setState(() => _selectedStatusTBU = v)),
// 3. Seksi Z-Score BB/TB
_buildSection(
"Z-Score BB/TB",
_bbTbController,
keteranganBBTB,
_listBBTB,
_selectedStatusBBTB,
(v) =>
setState(() => _selectedStatusBBTB = v)),
const Divider(height: 30, thickness: 1),
// Input Field Catatan Tindak Lanjut
_buildLabel("Tindak Lanjut"),
_buildTextField(_tindakLanjutController,
"Rencana tindakan..."),
const SizedBox(height: 10),
// Input Field Catatan Saran
_buildLabel("Saran"),
_buildTextField(
_saranController, "Saran untuk orang tua..."),
const SizedBox(height: 25),
// Tombol Aksi Simpan Data Bergaya Outline (Putih-Biru Stadium)
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: _isSaving ? null : _simpanData,
style: OutlinedButton.styleFrom(
backgroundColor: Colors.white,
side: const BorderSide(
color: Colors.blue, width: 2),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(
vertical: 14)),
child: _isSaving
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.blue))
: Text("Simpan Data Gizi",
style: GoogleFonts.poppins(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 14)),
),
),
],
),
),
],
),
),
),
),
),
);
}
// Widget generator baris isian Z-score (Form, Label SD, dan Dropdown Kategori)
Widget _buildSection(String title, TextEditingController controller,
String sd, List<String> items, String? val, Function(String?) onChanged) {
// Penentu warna latar background dinamis teks keterangan rentang SD
Color bgKuningMuda =
sd.contains('atas') ? Colors.orange[50]! : Colors.amber[50]!;
Color borderKuning =
sd.contains('atas') ? Colors.orange[200]! : Colors.amber[200]!;
Color textKuningTua =
sd.contains('atas') ? Colors.orange[900]! : Colors.amber[900]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel(title),
Row(
children: [
// Sisi Kiri: Nilai Angka Z-Score (Read-Only)
Expanded(
flex: 2,
child: TextFormField(
controller: controller,
readOnly: true,
decoration: _inputDeco(isFilled: true),
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.bold))),
const SizedBox(width: 10),
// Sisi Kanan: Teks Status Deviasi (Warna Kuning/Orange Kotak)
Expanded(
flex: 3,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 11),
decoration: BoxDecoration(
color: bgKuningMuda,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: borderKuning)),
child: Text(sd,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.bold,
color: textKuningTua)))),
],
),
const SizedBox(height: 10),
// Dropdown Pemilihan Status Klasifikasi Dibawahnya
DropdownButtonFormField<String>(
value: val,
hint: Text("Pilih Status $title",
style:
GoogleFonts.poppins(fontSize: 12, color: Colors.grey[600])),
isExpanded: true,
items: items
.map((e) => DropdownMenuItem(
value: e,
child: Text(e, style: GoogleFonts.poppins(fontSize: 12))))
.toList(),
onChanged: onChanged,
decoration: _inputDeco(),
validator: (v) => v == null ? "Pilih salah satu status" : null,
),
const SizedBox(height: 10),
],
);
}
Widget _buildLabel(String t) => Padding(
padding: const EdgeInsets.only(top: 10, bottom: 6),
child: Text(t,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87)));
Widget _buildTextField(TextEditingController c, String h) => TextFormField(
controller: c,
maxLines: 2,
style: GoogleFonts.poppins(fontSize: 12),
decoration: _inputDeco(hint: h));
InputDecoration _inputDeco({String? hint, bool isFilled = false}) =>
InputDecoration(
hintText: hint,
hintStyle: GoogleFonts.poppins(fontSize: 11, color: Colors.grey[400]),
filled: true,
fillColor: isFilled ? Colors.blue[50] : Colors.white,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue)),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.red)),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.red, width: 2)),
);
}

View File

@ -0,0 +1,342 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
class EditEdukasiBalitaPage extends StatefulWidget {
final Map data;
const EditEdukasiBalitaPage({super.key, required this.data});
@override
State<EditEdukasiBalitaPage> createState() => _EditEdukasiBalitaPageState();
}
class _EditEdukasiBalitaPageState extends State<EditEdukasiBalitaPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _judulController;
late TextEditingController _deskripsiController;
bool isSubmitting = false;
File? _selectedImage;
Uint8List? _webImage;
final picker = ImagePicker();
// Sesuaikan URL jika sudah beralih ke hosting ta.myhost.id
final String urlEdit =
"http://ta.myhost.id/E31230549/mposyandu_api/edukasi_balita/edit_edukasi_balita.php";
@override
void initState() {
super.initState();
_judulController = TextEditingController(text: widget.data['judul']);
// Memproses deskripsi agar kode JSON Delta dikonversi menjadi teks biasa
String rawDeskripsi = widget.data['deskripsi'] ?? "";
_deskripsiController = TextEditingController(
text: _convertJsonToPlainText(rawDeskripsi),
);
}
// Fungsi untuk membersihkan format JSON Quill menjadi Plain Text
String _convertJsonToPlainText(String input) {
try {
// Jika input diawali dengan '[' berarti itu format JSON Delta
if (input.trim().startsWith('[')) {
List<dynamic> jsonDelta = jsonDecode(input);
String plainText = "";
for (var node in jsonDelta) {
if (node['insert'] != null) {
plainText += node['insert'];
}
}
return plainText;
}
} catch (e) {
debugPrint("Info: Deskripsi bukan format JSON, menggunakan teks asli.");
}
return input; // Kembalikan teks asli jika bukan JSON atau gagal decode
}
@override
void dispose() {
_judulController.dispose();
_deskripsiController.dispose();
super.dispose();
}
Future pickImage() async {
final picked = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
if (picked == null) return;
if (kIsWeb) {
_webImage = await picked.readAsBytes();
} else {
_selectedImage = File(picked.path);
}
setState(() {});
}
Future _updateData() async {
if (!_formKey.currentState!.validate()) return;
setState(() => isSubmitting = true);
try {
var request = http.MultipartRequest("POST", Uri.parse(urlEdit));
request.fields["id"] = widget.data["id"].toString();
request.fields["judul"] = _judulController.text;
request.fields["deskripsi"] = _deskripsiController.text;
if (_selectedImage != null) {
request.files.add(
await http.MultipartFile.fromPath("gambar", _selectedImage!.path),
);
} else if (_webImage != null) {
request.files.add(
http.MultipartFile.fromBytes(
"gambar",
_webImage!,
filename: "upload.jpg",
),
);
}
var response = await request.send();
var res = await http.Response.fromStream(response);
var data = jsonDecode(res.body);
if (data["status"] == "success") {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil diperbarui")),
);
Navigator.pop(context, true);
} else {
throw data["message"] ?? "Gagal memperbarui data";
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Terjadi kesalahan: $e")),
);
} finally {
if (mounted) setState(() => isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
title: Text("",
style: GoogleFonts.poppins(color: Colors.white, fontSize: 16)),
backgroundColor: Colors.blue,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.white),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 900),
child: Card(
color: Colors.white,
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
"Perbarui Edukasi Balita",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
_buildLabel("Gambar Edukasi"),
_buildImagePreview(),
const SizedBox(height: 30),
_buildLabel("Judul Edukasi"),
_buildTitleField(),
const SizedBox(height: 25),
_buildLabel("Deskripsi"),
_buildDescriptionField(),
const SizedBox(height: 35),
_buildSubmitButton(),
],
),
),
),
),
),
),
),
);
}
Widget _buildDescriptionField() {
return TextFormField(
controller: _deskripsiController,
maxLines: 12,
keyboardType: TextInputType.multiline,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: "Tulis isi materi di sini...",
filled: true,
fillColor: Colors.grey[50],
contentPadding: const EdgeInsets.all(15),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blue),
),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
Widget _buildImagePreview() {
return Center(
child: Column(
children: [
Container(
height: 200,
width: 350,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade200),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: _selectedImage != null
? Image.file(_selectedImage!, fit: BoxFit.cover)
: _webImage != null
? Image.memory(_webImage!, fit: BoxFit.cover)
: (widget.data["gambar"] != null
? Image.network(
"http://ta.myhost.id/E31230549/mposyandu_api/upload/edukasi/${widget.data['gambar']}",
fit: BoxFit.cover,
errorBuilder: (c, e, s) => const Icon(Icons.image,
size: 50, color: Colors.grey),
)
: const Icon(Icons.image,
size: 50, color: Colors.grey)),
),
),
const SizedBox(height: 12),
SizedBox(
height: 35,
child: OutlinedButton.icon(
onPressed: pickImage,
icon: const Icon(Icons.image, size: 16),
label: Text(
"Ganti Gambar",
style: GoogleFonts.poppins(fontSize: 12),
),
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
foregroundColor: Colors.blue,
side: BorderSide(color: Colors.grey.shade300),
),
),
),
],
),
);
}
Widget _buildLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
label,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
);
}
Widget _buildTitleField() {
return TextFormField(
controller: _judulController,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: "Masukkan judul",
filled: true,
fillColor: Colors.grey[50],
contentPadding:
const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blue),
),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
Widget _buildSubmitButton() {
return Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: 180,
child: ElevatedButton(
onPressed: isSubmitting ? null : _updateData,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: isSubmitting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2),
)
: Text("Simpan Perubahan",
style: GoogleFonts.poppins(fontWeight: FontWeight.bold)),
),
),
);
}
}

View File

@ -0,0 +1,313 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
class EditEdukasiBumilPage extends StatefulWidget {
final Map data;
const EditEdukasiBumilPage({super.key, required this.data});
@override
State<EditEdukasiBumilPage> createState() => _EditEdukasiBumilPageState();
}
class _EditEdukasiBumilPageState extends State<EditEdukasiBumilPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _judulController;
late TextEditingController _deskripsiController;
File? selectedImage;
Uint8List? webImage;
bool isSubmitting = false;
final picker = ImagePicker();
// Jika nanti sudah migrasi ke hosting, ganti localhost menjadi ta.myhost.id
final String urlEdit =
"http://ta.myhost.id/E31230549/mposyandu_api/edukasi_ibu_hamil/edit_edukasi_ibu_hamil.php";
@override
void initState() {
super.initState();
_judulController = TextEditingController(text: widget.data['judul']);
// Mengambil deskripsi dan membersihkannya dari format JSON Delta
String rawDeskripsi = widget.data['deskripsi'] ?? "";
_deskripsiController = TextEditingController(
text: _convertJsonToPlainText(rawDeskripsi),
);
}
// Fungsi pembantu untuk mengonversi format JSON Quill ke teks biasa
String _convertJsonToPlainText(String input) {
try {
if (input.trim().startsWith('[')) {
List<dynamic> jsonDelta = jsonDecode(input);
String plainText = "";
for (var node in jsonDelta) {
if (node['insert'] != null) {
plainText += node['insert'];
}
}
return plainText;
}
} catch (e) {
debugPrint("Info: Menggunakan teks asli karena bukan format JSON.");
}
return input;
}
@override
void dispose() {
_judulController.dispose();
_deskripsiController.dispose();
super.dispose();
}
Future pickImage() async {
final picked =
await picker.pickImage(source: ImageSource.gallery, imageQuality: 80);
if (picked == null) return;
if (kIsWeb) {
webImage = await picked.readAsBytes();
} else {
selectedImage = File(picked.path);
}
setState(() {});
}
Future _updateData() async {
if (!_formKey.currentState!.validate()) return;
setState(() => isSubmitting = true);
try {
var request = http.MultipartRequest("POST", Uri.parse(urlEdit));
request.fields["id"] = widget.data['id'].toString();
request.fields["judul"] = _judulController.text;
request.fields["deskripsi"] = _deskripsiController.text;
if (selectedImage != null) {
request.files.add(
await http.MultipartFile.fromPath("gambar", selectedImage!.path));
} else if (webImage != null) {
request.files.add(http.MultipartFile.fromBytes("gambar", webImage!,
filename: "upload.jpg"));
}
var response = await request.send();
var res = await http.Response.fromStream(response);
var data = jsonDecode(res.body);
if (data["status"] == "success") {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil diperbarui")),
);
Navigator.pop(context, true);
} else {
throw data["message"] ?? "Gagal memperbarui data";
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Error: $e")),
);
} finally {
if (mounted) setState(() => isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
title: Text("",
style: GoogleFonts.poppins(color: Colors.white, fontSize: 16)),
backgroundColor: Colors.blue,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.white),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 900),
child: Card(
color: Colors.white,
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
"Edit Edukasi Ibu Hamil",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
_buildLabel("Gambar Edukasi"),
_buildImageSection(),
const SizedBox(height: 25),
_buildLabel("Judul Edukasi"),
_buildTitleField(),
const SizedBox(height: 25),
_buildLabel("Deskripsi"),
_buildDescriptionField(),
const SizedBox(height: 35),
_buildSubmitButton(),
],
),
),
),
),
),
),
),
);
}
Widget _buildDescriptionField() {
return TextFormField(
controller: _deskripsiController,
maxLines: 12,
keyboardType: TextInputType.multiline,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: "Tulis isi materi di sini...",
filled: true,
fillColor: Colors.grey[50],
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blue),
),
contentPadding: const EdgeInsets.all(15),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
Widget _buildImageSection() {
return Center(
child: Column(
children: [
Container(
height: 200,
width: 350,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade200),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: selectedImage != null || webImage != null
? (kIsWeb
? Image.memory(webImage!, fit: BoxFit.cover)
: Image.file(selectedImage!, fit: BoxFit.cover))
: (widget.data["gambar"] != null
? Image.network(
"http://ta.myhost.id/E31230549/mposyandu_api/upload/edukasi/${widget.data['gambar']}",
fit: BoxFit.cover,
errorBuilder: (c, e, s) => const Icon(
Icons.broken_image,
size: 50,
color: Colors.grey),
)
: const Icon(Icons.image, size: 50, color: Colors.grey)),
),
),
const SizedBox(height: 10),
TextButton.icon(
onPressed: pickImage,
icon: const Icon(Icons.camera_alt),
label: Text("Ganti Gambar", style: GoogleFonts.poppins())),
],
),
);
}
Widget _buildTitleField() {
return TextFormField(
controller: _judulController,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: "Masukkan Judul",
filled: true,
fillColor: Colors.grey[50],
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blue),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
Widget _buildSubmitButton() {
return Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: 200,
height: 50,
child: ElevatedButton(
onPressed: isSubmitting ? null : _updateData,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
elevation: 2,
),
child: isSubmitting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: Text(
"Simpan Perubahan",
style: GoogleFonts.poppins(fontWeight: FontWeight.bold),
),
),
),
);
}
Widget _buildLabel(String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(text,
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold,
fontSize: 13,
color: Colors.black87)),
);
}
}

View File

@ -0,0 +1,315 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
class TambahEdukasiBalitaPage extends StatefulWidget {
const TambahEdukasiBalitaPage({super.key});
@override
State<TambahEdukasiBalitaPage> createState() =>
_TambahEdukasiBalitaPageState();
}
class _TambahEdukasiBalitaPageState extends State<TambahEdukasiBalitaPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _judulController = TextEditingController();
// Menggunakan TextEditingController biasa untuk deskripsi agar fungsi ENTER lancar
final TextEditingController _deskripsiController = TextEditingController();
File? _imageFile;
Uint8List? _webImage;
final ImagePicker _picker = ImagePicker();
bool isSubmitting = false;
final String urlTambah =
"http://ta.myhost.id/E31230549/mposyandu_api/edukasi_balita/tambah_edukasi_balita.php";
Future<void> _pickImage() async {
try {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
if (pickedFile == null) return;
if (kIsWeb) {
final bytes = await pickedFile.readAsBytes();
setState(() {
_webImage = bytes;
_imageFile = null;
});
} else {
setState(() {
_imageFile = File(pickedFile.path);
_webImage = null;
});
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Gagal memilih gambar: $e")),
);
}
}
Future<void> _simpanData() async {
if (!_formKey.currentState!.validate()) return;
if (_imageFile == null && _webImage == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Silakan pilih gambar terlebih dahulu")),
);
return;
}
setState(() => isSubmitting = true);
try {
var request = http.MultipartRequest('POST', Uri.parse(urlTambah));
request.fields['judul'] = _judulController.text;
// Kirim teks deskripsi apa adanya (mendukung pindah baris/newline)
request.fields['deskripsi'] = _deskripsiController.text;
if (kIsWeb) {
request.files.add(
http.MultipartFile.fromBytes(
'gambar',
_webImage!,
filename: "upload_web.jpg",
),
);
} else {
request.files.add(
await http.MultipartFile.fromPath(
'gambar',
_imageFile!.path,
),
);
}
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil disimpan")),
);
Navigator.pop(context);
} else {
throw data['message'];
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Terjadi kesalahan: $e")),
);
} finally {
if (mounted) setState(() => isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
title: const Text(""),
backgroundColor: Colors.blue,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 900),
child: Card(
elevation: 3,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
"Tambah Edukasi Balita",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
_buildLabel("Gambar Edukasi"),
const SizedBox(height: 8),
InkWell(
onTap: _pickImage,
borderRadius: BorderRadius.circular(10),
child: Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
),
child: _displayImage(),
),
),
const SizedBox(height: 25),
_buildLabel("Judul Edukasi"),
_buildTitleField(),
const SizedBox(height: 25),
_buildLabel("Deskripsi"),
// FIELD DESKRIPSI DENGAN FUNGSI ENTER (MULTILINE)
_buildDescriptionField(),
const SizedBox(height: 35),
_buildSubmitButton(),
],
),
),
),
),
),
),
),
);
}
Widget _buildDescriptionField() {
return TextFormField(
controller: _deskripsiController,
maxLines: 10, // Memberikan ruang untuk banyak baris
keyboardType:
TextInputType.multiline, // Mengaktifkan tombol ENTER di keyboard
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: "Tulis isi edukasi di sini...",
hintStyle: GoogleFonts.poppins(color: Colors.grey, fontSize: 13),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.all(15),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blue, width: 1.5),
),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
Widget _displayImage() {
if (_webImage != null || _imageFile != null) {
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: kIsWeb
? Image.memory(_webImage!, fit: BoxFit.cover)
: Image.file(_imageFile!, fit: BoxFit.cover),
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.add_a_photo, color: Colors.blue, size: 40),
const SizedBox(height: 8),
Text(
"Klik untuk pilih gambar",
style: GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
),
],
);
}
Widget _buildLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
label,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
);
}
Widget _buildTitleField() {
return TextFormField(
controller: _judulController,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: "Contoh: Pentingnya MPASI",
hintStyle: GoogleFonts.poppins(color: Colors.grey, fontSize: 13),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blue, width: 1.5),
),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
Widget _buildSubmitButton() {
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: isSubmitting ? null : _simpanData,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: isSubmitting
? const CircularProgressIndicator(color: Colors.white)
: Text(
"Simpan Edukasi",
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
}
}

View File

@ -0,0 +1,267 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
class TambahEdukasiBumilPage extends StatefulWidget {
const TambahEdukasiBumilPage({super.key});
@override
State<TambahEdukasiBumilPage> createState() => _TambahEdukasiBumilPageState();
}
class _TambahEdukasiBumilPageState extends State<TambahEdukasiBumilPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _judulController = TextEditingController();
// Menggunakan TextEditingController biasa untuk deskripsi
final TextEditingController _deskripsiController = TextEditingController();
bool isSubmitting = false;
File? selectedImage;
Uint8List? webImage;
final picker = ImagePicker();
final String urlTambah =
"http://ta.myhost.id/E31230549/mposyandu_api/edukasi_ibu_hamil/tambah_edukasi_ibu_hamil.php";
Future pickImage() async {
final picked = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
if (picked == null) return;
if (kIsWeb) {
webImage = await picked.readAsBytes();
} else {
selectedImage = File(picked.path);
}
setState(() {});
}
Future _simpanData() async {
if (!_formKey.currentState!.validate()) return;
if (selectedImage == null && webImage == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Silakan pilih gambar terlebih dahulu")),
);
return;
}
setState(() => isSubmitting = true);
try {
var request = http.MultipartRequest("POST", Uri.parse(urlTambah));
request.fields["judul"] = _judulController.text;
// Mengirim deskripsi sebagai teks biasa (mendukung enter/newline)
request.fields["deskripsi"] = _deskripsiController.text;
if (selectedImage != null) {
request.files.add(
await http.MultipartFile.fromPath("gambar", selectedImage!.path));
}
if (webImage != null) {
request.files.add(http.MultipartFile.fromBytes("gambar", webImage!,
filename: "upload.jpg"));
}
var response = await request.send();
var res = await http.Response.fromStream(response);
var data = jsonDecode(res.body);
if (data["status"] == "success") {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Edukasi berhasil ditambahkan")),
);
Navigator.pop(context);
}
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Error: $e")));
} finally {
if (mounted) setState(() => isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
title: const Text(""),
backgroundColor: Colors.blue,
elevation: 0,
centerTitle: true,
iconTheme: const IconThemeData(color: Colors.white),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 900),
child: Card(
elevation: 3,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
"Tambah Edukasi Ibu Hamil",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
_buildImageSection(),
const SizedBox(height: 30),
_buildLabel("Judul Edukasi"),
_buildTextField(
_judulController, "Contoh: Perawatan ibu hamil", 1),
const SizedBox(height: 25),
_buildLabel("Deskripsi (Gunakan Enter untuk baris baru)"),
// Field deskripsi yang mendukung ENTER
_buildTextField(_deskripsiController,
"Tulis isi edukasi di sini...", 10),
const SizedBox(height: 35),
_buildSubmitButton(),
],
),
),
),
),
),
),
),
);
}
// Widget TextField Universal (bisa satu baris atau banyak baris)
Widget _buildTextField(
TextEditingController controller, String hint, int maxLines) {
return TextFormField(
controller: controller,
maxLines: maxLines, // Jika > 1, maka fungsi ENTER aktif secara otomatis
keyboardType: maxLines > 1 ? TextInputType.multiline : TextInputType.text,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: hint,
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
// ... (Widget _buildImageSection, _buildSubmitButton, _buildLabel tetap sama)
// [Kode _buildImageSection dll disingkat karena sama dengan aslinya]
Widget _buildImageSection() {
return Center(
child: Column(
children: [
_buildLabel("Gambar Edukasi"),
InkWell(
onTap: pickImage,
child: Container(
height: 200,
width: 350,
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade300),
),
child: selectedImage != null || webImage != null
? ClipRRect(
borderRadius: BorderRadius.circular(10),
child: kIsWeb
? Image.memory(webImage!, fit: BoxFit.cover)
: Image.file(selectedImage!, fit: BoxFit.cover),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.add_a_photo,
size: 40, color: Colors.blue),
const SizedBox(height: 8),
Text(
"Klik untuk pilih gambar",
style: GoogleFonts.poppins(
fontSize: 12, color: Colors.grey),
),
],
),
),
),
],
),
);
}
Widget _buildSubmitButton() {
return SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: isSubmitting ? null : _simpanData,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: isSubmitting
? const CircularProgressIndicator(color: Colors.white)
: Text(
"Simpan Edukasi",
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
}
Widget _buildLabel(String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
text,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
);
}
}

View File

@ -0,0 +1,361 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
class DataImunisasiBalitaPage extends StatefulWidget {
final Map<String, dynamic>? balita;
const DataImunisasiBalitaPage({super.key, this.balita});
@override
State<DataImunisasiBalitaPage> createState() =>
_DataImunisasiBalitaPageState();
}
class _DataImunisasiBalitaPageState extends State<DataImunisasiBalitaPage> {
List<dynamic> riwayatData = [];
String _searchQuery = "";
bool isLoading = true;
int _currentPage = 0;
final int _rowsPerPage = 5;
final String urlRiwayat =
"http://ta.myhost.id/E31230549/mposyandu_api/imunisasi/get_imunisasi_balita.php";
@override
void initState() {
super.initState();
fetchRiwayat();
}
Future<void> fetchRiwayat() async {
try {
final idBalita = widget.balita?['id'];
final url =
idBalita == null ? urlRiwayat : "$urlRiwayat?id_balita=$idBalita";
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
riwayatData = data['data'] ?? [];
isLoading = false;
});
}
} catch (e) {
debugPrint("ERROR FETCH = $e");
setState(() => isLoading = false);
}
}
// Fungsi Hitung Usia
String hitungUsia(String? tanggalLahir) {
if (tanggalLahir == null || tanggalLahir.isEmpty || tanggalLahir == "-") {
return "Usia tidak diketahui";
}
try {
DateTime lahir = DateTime.parse(tanggalLahir);
DateTime sekarang = DateTime.now();
int tahun = sekarang.year - lahir.year;
int bulan = sekarang.month - lahir.month;
if (bulan < 0) {
tahun--;
bulan += 12;
}
return "$tahun Tahun $bulan Bulan";
} catch (e) {
return "Format tanggal salah";
}
}
List<dynamic> get _filteredData {
if (_searchQuery.isEmpty) return riwayatData;
return riwayatData.where((item) {
final namaBalita = item['nama'].toString().toLowerCase();
final query = _searchQuery.toLowerCase();
return namaBalita.contains(query);
}).toList();
}
List<dynamic> get _paginatedData {
final start = _currentPage * _rowsPerPage;
final end = start + _rowsPerPage;
if (start >= _filteredData.length) return [];
return _filteredData.sublist(
start,
end > _filteredData.length ? _filteredData.length : end,
);
}
String formatTanggal(String tanggal) {
if (tanggal.isEmpty || tanggal == "-") return "-";
try {
final parts = tanggal.split("-");
if (parts.length != 3) return tanggal;
final tahun = parts[0];
final bulan = int.parse(parts[1]);
final hari = parts[2];
const namaBulan = [
"",
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
return "$hari ${namaBulan[bulan]} $tahun";
} catch (e) {
return tanggal;
}
}
@override
Widget build(BuildContext context) {
final totalPages = (_filteredData.length / _rowsPerPage).ceil() == 0
? 1
: (_filteredData.length / _rowsPerPage).ceil();
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
title: Text("",
style: GoogleFonts.poppins(color: Colors.white, fontSize: 16)),
backgroundColor: Colors.blue,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
_header(),
_paginatedData.isEmpty
? Padding(
padding: const EdgeInsets.only(top: 50),
child: Text(
"Data tidak ditemukan",
style: GoogleFonts.poppins(fontSize: 12),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding:
const EdgeInsets.symmetric(horizontal: 16),
itemCount: _paginatedData.length,
itemBuilder: (context, index) =>
_card(_paginatedData[index]),
),
],
),
),
),
Container(
width: double.infinity,
color: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Halaman ${_currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500)),
Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _currentPage == 0
? null
: () => setState(() => _currentPage--),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage >= totalPages - 1
? null
: () => setState(() => _currentPage++),
),
],
),
],
),
),
],
),
);
}
Widget _header() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
"Data Imunisasi Balita",
style:
GoogleFonts.poppins(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 15),
TextField(
onChanged: (value) => setState(() {
_searchQuery = value;
_currentPage = 0;
}),
style: const TextStyle(fontSize: 12),
decoration: InputDecoration(
hintText: "Cari nama balita...",
hintStyle: const TextStyle(fontSize: 12),
prefixIcon: const Icon(Icons.search, size: 20),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey[300]!),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey[300]!),
),
contentPadding: const EdgeInsets.symmetric(vertical: 8),
),
),
],
),
);
}
Widget _card(Map item) {
String alamatLengkap =
"Dusun ${item['nama_dusun'] ?? '-'}, Desa ${item['nama_desa'] ?? '-'}, ${item['alamat_detail'] ?? '-'}";
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)
],
),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['nama'] ?? "Nama Balita",
style: GoogleFonts.poppins(
fontSize: 13,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
// USIA DI BAWAH NAMA
Text(
"Usia: ${hitungUsia(item['tanggal_lahir'])}",
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
// NAMA ORANG TUA (WARNA PUTIH)
Text(
"Nama Orang Tua : ${item['nama_ibu'] ?? '-'}(${item['nama_suami'] ?? '-'})",
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
),
),
// ALAMAT (WARNA PUTIH)
Text(
"Alamat : $alamatLengkap",
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.white,
fontStyle: FontStyle.normal,
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// POSISI TANGGAL DI ATAS IMUNISASI
detail("Tanggal Posyandu",
formatTanggal(item['tanggal_terakhir'] ?? "-")),
const SizedBox(height: 8),
detail("Jenis Imunisasi", item['daftar_imunisasi'] ?? "-"),
const SizedBox(height: 8),
detail("Status", item['status'] ?? "-"),
// BUTTON RIWAYAT SUDAH DIHAPUS
],
),
)
],
),
);
}
Widget detail(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
child: Text(
label,
style:
GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w500),
),
),
Text(" : ", style: GoogleFonts.poppins(fontSize: 12)),
Expanded(
child: Text(
value,
style: GoogleFonts.poppins(fontSize: 12),
),
)
],
);
}
}

View File

@ -0,0 +1,243 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
class EditImunisasiPage extends StatefulWidget {
final Map data; // Menampung data yang dikirim dari halaman list
const EditImunisasiPage({super.key, required this.data});
@override
State<EditImunisasiPage> createState() => _EditImunisasiPageState();
}
class _EditImunisasiPageState extends State<EditImunisasiPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _idController;
late TextEditingController _namaController;
late TextEditingController _usiaMinController;
late TextEditingController _usiaMaxController;
late TextEditingController _ketController;
bool isSubmitting = false;
final String urlEdit =
"http://ta.myhost.id/E31230549/mposyandu_api/imunisasi/edit_imunisasi.php";
@override
void initState() {
super.initState();
// Mengisi controller dengan data yang ada
_idController = TextEditingController(text: widget.data['id'].toString());
_namaController =
TextEditingController(text: widget.data['nama_imunisasi']);
_usiaMinController =
TextEditingController(text: widget.data['usia_min'].toString());
_usiaMaxController =
TextEditingController(text: widget.data['usia_max'].toString());
_ketController =
TextEditingController(text: widget.data['keterangan'] ?? "");
}
Future _updateData() async {
if (!_formKey.currentState!.validate()) return;
setState(() => isSubmitting = true);
try {
final response = await http.post(
Uri.parse(urlEdit),
body: {
"id": _idController.text, // Kirim ID sebagai kunci update
"nama_imunisasi": _namaController.text,
"usia_min": _usiaMinController.text,
"usia_max": _usiaMaxController.text,
"keterangan": _ketController.text,
},
);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(data['message']), backgroundColor: Colors.blue),
);
Navigator.pop(context, true); // Kembali dan beri sinyal sukses
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(data['message']), backgroundColor: Colors.red),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Terjadi kesalahan: $e"),
backgroundColor: Colors.red),
);
} finally {
if (mounted) setState(() => isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Colors.blue,
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
"",
style: GoogleFonts.poppins(
color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 850),
child: Card(
elevation: 3,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
"Edit Jenis Imunisasi",
style: GoogleFonts.poppins(
fontSize: 18, fontWeight: FontWeight.bold),
),
),
const Divider(height: 40),
_buildLabel("Nama Imunisasi"),
_buildTextField(
_namaController, "Masukkan nama imunisasi"),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel("Usia Min (Bulan)"),
_buildTextField(_usiaMinController, "0",
isNumber: true),
],
),
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel("Usia Max (Bulan)"),
_buildTextField(_usiaMaxController, "12",
isNumber: true),
],
),
),
],
),
const SizedBox(height: 20),
_buildLabel("Keterangan"),
_buildTextField(_ketController, "Keterangan tambahan...",
maxLines: 3),
const SizedBox(height: 35),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: isSubmitting ? null : _updateData,
style: ElevatedButton.styleFrom(
backgroundColor:
Colors.blue, // Warna orange untuk edit
padding: const EdgeInsets.symmetric(
horizontal: 35, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
child: isSubmitting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: Text(
"Simpan Perubahan",
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
],
),
],
),
),
),
),
),
),
),
);
}
Widget _buildLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
label,
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black87),
),
);
}
Widget _buildTextField(TextEditingController controller, String hint,
{bool isNumber = false, int maxLines = 1}) {
return TextFormField(
controller: controller,
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
maxLines: maxLines,
style: const TextStyle(fontSize: 12),
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(fontSize: 11, color: Colors.grey[400]),
filled: true,
fillColor: Colors.grey[50],
contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 15),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blue, width: 1.5),
),
errorStyle: const TextStyle(fontSize: 10),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
}

View File

@ -0,0 +1,305 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
class RiwayatDetailBalitaPage extends StatefulWidget {
final Map<String, dynamic> balita;
final String jenisImunisasi;
const RiwayatDetailBalitaPage({
super.key,
required this.balita,
required this.jenisImunisasi,
});
@override
State<RiwayatDetailBalitaPage> createState() =>
_RiwayatDetailBalitaPageState();
}
class _RiwayatDetailBalitaPageState extends State<RiwayatDetailBalitaPage> {
List<dynamic> listRiwayat = [];
String _query = "";
bool isLoading = true;
int _currentPage = 0;
final int _rowsPerPage = 5;
@override
void initState() {
super.initState();
fetchDetailRiwayat();
}
String formatTanggal(String tanggal) {
if (tanggal.isEmpty || tanggal == "-") return "-";
try {
final parts = tanggal.split("-");
if (parts.length != 3) return tanggal;
final tahun = parts[0];
final bulan = int.parse(parts[1]);
final hari = parts[2];
const namaBulan = [
"", "Januari", "Februari", "Maret", "April", "Mei", "Juni",
"Juli", "Agustus", "September", "Oktober", "November", "Desember"
];
return "$hari ${namaBulan[bulan]} $tahun";
} catch (e) {
return tanggal;
}
}
Future<void> fetchDetailRiwayat() async {
final String url =
"http://ta.myhost.id/E31230549/mposyandu_api/imunisasi/get_riwayat_detail.php?id_balita=${widget.balita['id']}";
try {
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (mounted) {
setState(() {
listRiwayat = data['data'] ?? [];
isLoading = false;
});
}
}
} catch (e) {
debugPrint("ERROR FETCHING DETAIL: $e");
if (mounted) {
setState(() => isLoading = false);
}
}
}
List<dynamic> get _filteredList {
if (_query.isEmpty) return listRiwayat;
return listRiwayat.where((item) {
// Filter berdasarkan daftar imunisasi yang sudah digabung
final nama = item['daftar_imunisasi']?.toString().toLowerCase() ?? "";
return nama.contains(_query.toLowerCase());
}).toList();
}
List<dynamic> get _paginatedData {
final start = _currentPage * _rowsPerPage;
final end = start + _rowsPerPage;
if (start >= _filteredList.length) return [];
return _filteredList.sublist(
start,
end > _filteredList.length ? _filteredList.length : end,
);
}
@override
Widget build(BuildContext context) {
final totalPages = (_filteredList.length / _rowsPerPage).ceil() == 0
? 1
: (_filteredList.length / _rowsPerPage).ceil();
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Colors.blue,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white, size: 18),
onPressed: () => Navigator.pop(context),
),
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
_buildSearchHeader(),
_paginatedData.isEmpty
? Padding(
padding: const EdgeInsets.only(top: 50),
child: Text(
"Data tidak ditemukan",
style: GoogleFonts.poppins(fontSize: 12),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding:
const EdgeInsets.symmetric(horizontal: 16),
itemCount: _paginatedData.length,
itemBuilder: (context, index) =>
_buildHistoryCard(_paginatedData[index]),
),
],
),
),
),
_buildPagination(totalPages),
],
),
);
}
Widget _buildSearchHeader() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Center(
child: Text(
"Riwayat Detail Balita",
style: GoogleFonts.poppins(
fontSize: 18, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 15),
TextField(
onChanged: (v) => setState(() {
_query = v;
_currentPage = 0;
}),
style: const TextStyle(fontSize: 12),
decoration: InputDecoration(
hintText: "Cari jenis imunisasi...",
hintStyle: const TextStyle(fontSize: 12),
prefixIcon: const Icon(Icons.search, size: 20),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey[300]!),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey[300]!),
),
contentPadding: const EdgeInsets.symmetric(vertical: 8),
),
),
],
),
);
}
Widget _buildHistoryCard(Map<String, dynamic> item) {
return Container(
margin: const EdgeInsets.only(bottom: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 5),
)
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Text(
widget.balita['nama_balita'] ?? "Nama Balita",
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
),
Padding(
padding: const EdgeInsets.all(15),
child: Column(
children: [
// Menggunakan 'daftar_imunisasi' yang berisi string gabungan (HB0, BCG, dll)
_buildInfoRow("Jenis Imunisasi",
item['daftar_imunisasi']?.toString() ?? "-"),
const SizedBox(height: 8),
_buildInfoRow(
"Tanggal Posyandu",
formatTanggal(
item['tanggal_pemberian']?.toString() ?? "-")),
const SizedBox(height: 8),
_buildInfoRow("Status", item['status']?.toString() ?? "-"),
],
),
)
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
child: Text(
label,
style:
GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w500),
),
),
Text(" : ", style: GoogleFonts.poppins(fontSize: 12)),
Expanded(
child: Text(
value,
style: GoogleFonts.poppins(fontSize: 12),
),
),
],
);
}
Widget _buildPagination(int totalPages) {
return Container(
width: double.infinity,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Halaman ${_currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500)),
Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _currentPage == 0
? null
: () => setState(() => _currentPage--),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage >= totalPages - 1
? null
: () => setState(() => _currentPage++),
),
],
),
],
),
);
}
}

View File

@ -0,0 +1,224 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
class TambahImunisasiPage extends StatefulWidget {
const TambahImunisasiPage({super.key});
@override
State<TambahImunisasiPage> createState() => _TambahImunisasiPageState();
}
class _TambahImunisasiPageState extends State<TambahImunisasiPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _namaController = TextEditingController();
final TextEditingController _usiaMinController = TextEditingController();
final TextEditingController _usiaMaxController = TextEditingController();
final TextEditingController _ketController = TextEditingController();
bool isSubmitting = false;
final String urlTambah =
"http://ta.myhost.id/E31230549/mposyandu_api/imunisasi/tambah_imunisasi.php";
Future _simpanData() async {
if (!_formKey.currentState!.validate()) return;
setState(() => isSubmitting = true);
try {
final response = await http.post(
Uri.parse(urlTambah),
body: {
"nama_imunisasi": _namaController.text,
"usia_min": _usiaMinController.text,
"usia_max": _usiaMaxController.text,
"keterangan": _ketController.text,
},
);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil disimpan!")),
);
Navigator.pop(context);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Terjadi kesalahan: $e")),
);
} finally {
if (mounted) setState(() => isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Colors.blue,
elevation: 0,
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
"",
style: GoogleFonts.poppins(
color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 850),
child: Card(
elevation: 3,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- JUDUL ---
Center(
child: Text(
"Tambah Jenis Imunisasi",
style: GoogleFonts.poppins(
fontSize: 18, fontWeight: FontWeight.bold),
),
),
const Divider(height: 40),
_buildLabel("Nama Imunisasi"),
_buildTextField(
_namaController, "Masukkan nama imunisasi"),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel("Usia Min (Bulan)"),
_buildTextField(_usiaMinController, "0",
isNumber: true),
],
),
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel("Usia Max (Bulan)"),
_buildTextField(_usiaMaxController, "12",
isNumber: true),
],
),
),
],
),
const SizedBox(height: 20),
_buildLabel("Keterangan"),
_buildTextField(_ketController,
"Contoh: Diberikan pada bayi baru lahir",
maxLines: 3),
const SizedBox(height: 35),
// --- TOMBOL SIMPAN (OUTLINED STADIUM - TANPA IKON) ---
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
width: 150, // Menentukan lebar agar proporsional
child: OutlinedButton(
onPressed: isSubmitting ? null : _simpanData,
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blue, width: 1.5),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: isSubmitting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.blue, strokeWidth: 2))
: Text(
"Simpan",
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.bold),
),
),
),
],
),
],
),
),
),
),
),
),
),
);
}
Widget _buildLabel(String label) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
label,
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black87),
),
);
}
Widget _buildTextField(TextEditingController controller, String hint,
{bool isNumber = false, int maxLines = 1}) {
return TextFormField(
controller: controller,
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
maxLines: maxLines,
style: const TextStyle(fontSize: 12),
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(fontSize: 11, color: Colors.grey[400]),
filled: true,
fillColor: Colors.grey[50],
contentPadding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 15),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.blue, width: 1.5),
),
errorStyle: const TextStyle(fontSize: 10),
),
validator: (value) =>
value == null || value.isEmpty ? "Wajib diisi" : null,
);
}
}

View File

@ -0,0 +1,417 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
const String baseUrl = "http://ta.myhost.id/E31230549/mposyandu_api";
class Kader {
final int id;
final String nama;
final int dusunId;
Kader({
required this.id,
required this.nama,
required this.dusunId,
});
}
class EditJadwalPage extends StatefulWidget {
final Map<String, dynamic> data;
const EditJadwalPage({
super.key,
required this.data,
});
@override
State<EditJadwalPage> createState() => _EditJadwalPageState();
}
class _EditJadwalPageState extends State<EditJadwalPage> {
late TextEditingController _tanggalController;
late TextEditingController _jamMulaiController;
late TextEditingController _jamSelesaiController;
late TextEditingController _lokasiController;
late TextEditingController _keteranganController;
List<dynamic> daftarDusun = [];
List<Kader> daftarKader = [];
List<int> dusunDipilih = [];
String dusunTeksAwal = "";
bool _isLoading = false;
@override
void initState() {
super.initState();
_tanggalController =
TextEditingController(text: widget.data['tanggal']?.toString() ?? "");
_jamMulaiController =
TextEditingController(text: widget.data['jam_mulai']?.toString() ?? "");
_jamSelesaiController = TextEditingController(
text: widget.data['jam_selesai']?.toString() ?? "");
_lokasiController =
TextEditingController(text: widget.data['lokasi']?.toString() ?? "");
_keteranganController = TextEditingController(
text: widget.data['keterangan']?.toString() ?? "");
dusunTeksAwal = widget.data['dusun']?.toString() ?? "-";
_parseInitialDusun();
WidgetsBinding.instance.addPostFrameCallback((_) {
_initialLoad();
});
}
void _parseInitialDusun() {
try {
var rawDusunIds = widget.data['dusun_ids'];
if (rawDusunIds != null && rawDusunIds.toString().isNotEmpty) {
dusunDipilih = rawDusunIds
.toString()
.split(',')
.map((e) => int.tryParse(e.trim()) ?? 0)
.where((e) => e > 0)
.toList();
}
} catch (e) {
debugPrint("Error parsing dusun: $e");
}
}
Future<void> _initialLoad() async {
await loadDusun();
if (dusunDipilih.isNotEmpty) {
await loadKaderByDusun();
}
}
Future<void> loadDusun() async {
try {
final res = await http.get(Uri.parse("$baseUrl/dusun/get_dusun.php"));
if (res.statusCode == 200) {
final responseData = jsonDecode(res.body);
setState(() {
daftarDusun = responseData['data'] ?? [];
});
}
} catch (e) {
debugPrint("Error load dusun: $e");
}
}
Future<void> loadKaderByDusun() async {
if (dusunDipilih.isEmpty) {
setState(() => daftarKader.clear());
return;
}
String ids = dusunDipilih.join(",");
try {
final res = await http.get(
Uri.parse("$baseUrl/petugas/get_kader_by_dusun.php?ids=$ids"),
);
if (res.statusCode == 200) {
final responseData = jsonDecode(res.body);
final List listData = responseData['data'] ?? [];
setState(() {
daftarKader = listData
.map((k) => Kader(
id: int.parse(k['id'].toString()),
nama: k['nama'].toString(),
dusunId: int.parse(k['dusun_id'].toString()),
))
.toList();
});
}
} catch (e) {
debugPrint("Error load kader: $e");
}
}
Future<void> _pickTanggal() async {
DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2024),
lastDate: DateTime(2030),
);
if (picked != null) {
setState(() {
_tanggalController.text =
"${picked.year}-${picked.month.toString().padLeft(2, '0')}-${picked.day.toString().padLeft(2, '0')}";
});
}
}
Future<void> _pickJam(TextEditingController controller) async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
setState(() {
controller.text =
"${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}";
});
}
}
Future<void> _updateJadwal() async {
if (_tanggalController.text.isEmpty || dusunDipilih.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Data tidak boleh kosong!",
style: GoogleFonts.poppins(fontSize: 12))),
);
return;
}
setState(() => _isLoading = true);
try {
final response = await http.post(
Uri.parse("$baseUrl/jadwal_posyandu/edit_jadwal.php"),
body: {
"id": widget.data['id'].toString(),
"bidan_id": widget.data['bidan_id']?.toString() ?? "",
"tanggal": _tanggalController.text,
"jam_mulai": _jamMulaiController.text,
"jam_selesai": _jamSelesaiController.text,
"lokasi": _lokasiController.text,
"keterangan": _keteranganController.text,
"dusun_ids": dusunDipilih.join(","),
},
);
final result = jsonDecode(response.body);
if (result['success']) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Jadwal Berhasil Diperbarui",
style: GoogleFonts.poppins(fontSize: 12))),
);
Navigator.pop(context, true);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content:
Text("Error: $e", style: GoogleFonts.poppins(fontSize: 12))));
}
setState(() => _isLoading = false);
}
InputDecoration _input(String label) {
return InputDecoration(
labelText: label,
labelStyle: GoogleFonts.poppins(fontSize: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("",
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600)),
backgroundColor: Colors.blue,
iconTheme: const IconThemeData(color: Colors.white),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 500),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(color: Colors.black12, blurRadius: 8)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text("Edit Data Jadwal",
style: GoogleFonts.poppins(
fontSize: 16, fontWeight: FontWeight.bold))),
const SizedBox(height: 15),
TextField(
controller: _tanggalController,
style: GoogleFonts.poppins(fontSize: 12),
readOnly: true,
onTap: _pickTanggal,
decoration: _input("Tanggal").copyWith(
suffixIcon: const Icon(Icons.calendar_today, size: 18),
),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: TextField(
controller: _jamMulaiController,
style: GoogleFonts.poppins(fontSize: 12),
readOnly: true,
onTap: () => _pickJam(_jamMulaiController),
decoration: _input("Jam Mulai").copyWith(
suffixIcon: const Icon(Icons.access_time, size: 18),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _jamSelesaiController,
style: GoogleFonts.poppins(fontSize: 12),
readOnly: true,
onTap: () => _pickJam(_jamSelesaiController),
decoration: _input("Jam Selesai").copyWith(
suffixIcon: const Icon(Icons.access_time, size: 18),
),
),
),
],
),
const SizedBox(height: 10),
TextField(
controller: _lokasiController,
style: GoogleFonts.poppins(fontSize: 12),
decoration: _input("Lokasi Posyandu")),
const SizedBox(height: 15),
Text("Dusun Yang Dilayani",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600, fontSize: 12)),
Container(
width: double.infinity,
margin: const EdgeInsets.only(top: 5, bottom: 10),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade100),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Dusun sebelumnya:",
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Colors.blue)),
Text(dusunTeksAwal,
style: GoogleFonts.poppins(
fontSize: 12, color: Colors.black87)),
const SizedBox(height: 4),
Text(
"*Silakan centang ulang di bawah jika ingin merubah pilihan dusun.",
style: GoogleFonts.poppins(
fontSize: 10,
fontStyle: FontStyle.italic,
color: Colors.grey)),
],
),
),
const Divider(),
daftarDusun.isEmpty
? Center(
child: Text("Memuat daftar dusun...",
style: GoogleFonts.poppins(
fontSize: 12, color: Colors.grey)))
: Column(
children: daftarDusun.map((d) {
int id = int.parse(d['id'].toString());
return CheckboxListTile(
visualDensity: VisualDensity.compact,
contentPadding: EdgeInsets.zero,
title: Text(d['nama_dusun'] ?? "",
style: GoogleFonts.poppins(fontSize: 12)),
value: dusunDipilih.contains(id),
onChanged: (val) {
setState(() {
if (val == true) {
if (!dusunDipilih.contains(id))
dusunDipilih.add(id);
} else {
dusunDipilih.remove(id);
}
});
loadKaderByDusun();
},
);
}).toList(),
),
const SizedBox(height: 10),
Text("Kader Otomatis:",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600, fontSize: 12)),
const SizedBox(height: 5),
daftarKader.isEmpty
? Text("-",
style: GoogleFonts.poppins(
color: Colors.grey, fontSize: 12))
: Wrap(
spacing: 5,
children: daftarKader
.map((k) => Chip(
label: Text(k.nama,
style: GoogleFonts.poppins(fontSize: 12)),
padding: EdgeInsets.zero,
backgroundColor: Colors.blue.shade50,
))
.toList(),
),
const SizedBox(height: 15),
TextField(
controller: _keteranganController,
style: GoogleFonts.poppins(fontSize: 12),
decoration: _input("Keterangan")),
const SizedBox(height: 25),
// --- TOMBOL SIMPAN PERUBAHAN (OUTLINED STADIUM) ---
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: _isLoading ? null : _updateJadwal,
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blue),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 14)),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.blue, strokeWidth: 2))
: Text(
"Simpan Perubahan",
style: GoogleFonts.poppins(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 12),
),
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,342 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
const String baseUrl = "http://ta.myhost.id/E31230549/mposyandu_api";
class RiwayatJadwalPosyanduPage extends StatefulWidget {
const RiwayatJadwalPosyanduPage({super.key});
@override
State<RiwayatJadwalPosyanduPage> createState() =>
_RiwayatJadwalPosyanduPageState();
}
class _RiwayatJadwalPosyanduPageState extends State<RiwayatJadwalPosyanduPage> {
List<Map<String, dynamic>> dataRiwayat = [];
List<Map<String, dynamic>> dataFilter = [];
bool isLoading = true;
final TextEditingController searchController = TextEditingController();
int _currentPage = 0;
final int _rowsPerPage = 5;
List<Map<String, dynamic>> get _paginatedData {
if (dataFilter.isEmpty) return [];
final start = _currentPage * _rowsPerPage;
final end = start + _rowsPerPage;
if (start >= dataFilter.length) return [];
return dataFilter.sublist(
start, end > dataFilter.length ? dataFilter.length : end);
}
@override
void initState() {
super.initState();
loadRiwayat();
}
Future<void> loadRiwayat() async {
try {
final res = await http.get(
Uri.parse("$baseUrl/jadwal_posyandu/get_all_jadwal.php"),
);
final data = jsonDecode(res.body);
if (data['success']) {
setState(() {
dataRiwayat = List<Map<String, dynamic>>.from(data['data']);
dataFilter = dataRiwayat;
isLoading = false;
});
}
} catch (e) {
debugPrint("Error riwayat : $e");
setState(() => isLoading = false);
}
}
String formatTanggal(String tanggal) {
try {
final parts = tanggal.split("-");
String tahun = parts[0];
int bulan = int.parse(parts[1]);
String hari = parts[2];
List<String> namaBulan = [
"",
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
return "$hari ${namaBulan[bulan]} $tahun";
} catch (e) {
return tanggal;
}
}
void searchData(String keyword) {
final key = keyword.toLowerCase();
setState(() {
dataFilter = dataRiwayat.where((item) {
return item['tanggal'].toString().toLowerCase().contains(key) ||
item['lokasi'].toString().toLowerCase().contains(key) ||
item['dusun'].toString().toLowerCase().contains(key);
}).toList();
_currentPage = 0;
});
}
Future<void> hapusJadwal(String id) async {
final res = await http.post(
Uri.parse("$baseUrl/jadwal_posyandu/hapus_jadwal_posyandu.php"),
body: {"id": id},
);
final data = jsonDecode(res.body);
if (data['success']) {
loadRiwayat();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Jadwal berhasil dihapus",
style: GoogleFonts.poppins(fontSize: 12))),
);
}
}
void konfirmasiHapus(String id) {
showDialog(
context: context,
builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Text("Hapus Jadwal",
style:
GoogleFonts.poppins(fontWeight: FontWeight.bold, fontSize: 14)),
content: Text("Apakah yakin ingin menghapus jadwal ini?",
style: GoogleFonts.poppins(fontSize: 12)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal", style: TextStyle(fontSize: 12))),
// Button Hapus di Dialog juga disesuaikan
OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.redAccent),
shape: const StadiumBorder(),
),
onPressed: () {
Navigator.pop(context);
hapusJadwal(id);
},
child: const Text("Hapus",
style: TextStyle(
color: Colors.redAccent,
fontSize: 12,
fontWeight: FontWeight.bold)),
)
],
),
);
}
Widget rowData(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 70,
child: Text(label,
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w600)),
),
Text(" : ", style: GoogleFonts.poppins(fontSize: 12)),
Expanded(
child: Text(value,
style: GoogleFonts.poppins(fontSize: 12), softWrap: true),
)
],
),
);
}
Widget cardJadwal(Map<String, dynamic> item) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 25, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 5,
offset: const Offset(0, 2)),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.event_note,
size: 16, color: Colors.blueAccent),
const SizedBox(width: 6),
Text("Data Jadwal Posyandu",
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold, fontSize: 13)),
],
),
const Divider(height: 16, thickness: 1),
rowData("Tanggal", formatTanggal(item['tanggal'] ?? "-")),
rowData("Jam", "${item['jam_mulai']} - ${item['jam_selesai']}"),
rowData("Lokasi", item['lokasi'] ?? "-"),
rowData("Dusun", item['dusun'] ?? "-"),
rowData("Kader", item['kader'] ?? "-"),
rowData("Ket", item['keterangan'] ?? "-"),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: SizedBox(
height: 32, // Sedikit disesuaikan untuk Outlined Style
// MENGUBAH GAYA BUTTON HAPUS DI SINI
child: OutlinedButton.icon(
onPressed: () => konfirmasiHapus(item['id'].toString()),
icon: const Icon(Icons.delete_outline,
size: 14, color: Colors.redAccent),
label: Text("Hapus",
style: GoogleFonts.poppins(
color: Colors.redAccent,
fontSize: 10,
fontWeight: FontWeight.bold)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.redAccent),
padding: const EdgeInsets.symmetric(horizontal: 12),
shape: const StadiumBorder(), // Membuat bentuk lonjong
),
),
),
)
],
),
),
);
}
@override
Widget build(BuildContext context) {
final totalPages =
dataFilter.isEmpty ? 1 : (dataFilter.length / _rowsPerPage).ceil();
return Scaffold(
backgroundColor: const Color(0xFFFDFDFD),
appBar: AppBar(
backgroundColor: Colors.blueAccent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: null,
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 12),
child: Center(
child: Text(
"Riwayat Jadwal Posyandu",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextField(
controller: searchController,
onChanged: searchData,
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
hintText: "Cari berdasarkan tanggal, lokasi, a...",
hintStyle:
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
prefixIcon:
const Icon(Icons.search, size: 20, color: Colors.grey),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.grey),
),
),
),
),
const SizedBox(height: 15),
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: dataFilter.isEmpty
? Center(
child: Text("Data tidak ditemukan",
style: GoogleFonts.poppins(fontSize: 12)))
: ListView.builder(
itemCount: _paginatedData.length,
itemBuilder: (context, index) {
return cardJadwal(_paginatedData[index]);
},
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Halaman ${_currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
),
Row(
children: [
IconButton(
iconSize: 20,
icon: const Icon(Icons.chevron_left),
onPressed: _currentPage == 0
? null
: () => setState(() => _currentPage--),
),
IconButton(
iconSize: 20,
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage >= totalPages - 1
? null
: () => setState(() => _currentPage++),
),
],
)
],
),
)
],
),
);
}
}

View File

@ -0,0 +1,384 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
class EditPemeriksaanKehamilanPage extends StatefulWidget {
final Map data;
final String nama;
const EditPemeriksaanKehamilanPage({
super.key,
required this.data,
required this.nama,
});
@override
State<EditPemeriksaanKehamilanPage> createState() =>
_EditPemeriksaanKehamilanPageState();
}
class _EditPemeriksaanKehamilanPageState
extends State<EditPemeriksaanKehamilanPage> {
final tanggalController = TextEditingController();
final displayTanggalController = TextEditingController();
final bbSebelumController = TextEditingController();
final beratController = TextEditingController();
final tinggiController = TextEditingController();
final lilaController = TextEditingController();
final tekananController = TextEditingController();
final fundusController = TextEditingController();
final djjController = TextEditingController();
final hbController = TextEditingController();
final keluhanController = TextEditingController();
final tindakanController = TextEditingController();
String? kakiBengkak;
String? statusGizi;
final String url =
"http://ta.myhost.id/E31230549/mposyandu_api/pemeriksaan_kehamilan/edit_pemeriksaan_kehamilan.php";
@override
void initState() {
super.initState();
tanggalController.text = widget.data["tanggal_pemeriksaan"] ?? "";
DateTime date = DateTime.parse(
widget.data["tanggal_pemeriksaan"] ?? DateTime.now().toString());
displayTanggalController.text = formatKeIndonesia(date);
bbSebelumController.text = widget.data["bb_sebelum_hamil"] ?? "";
beratController.text = widget.data["berat_badan"] ?? "";
tinggiController.text = widget.data["tinggi_badan"] ?? "";
lilaController.text = widget.data["LILA"] ?? "";
tekananController.text = widget.data["tekanan_darah"] ?? "";
fundusController.text = widget.data["tinggi_fundus"] ?? "";
djjController.text = widget.data["denyut_jantung_janin"] ?? "";
hbController.text = widget.data["hb"] ?? "";
keluhanController.text = widget.data["keluhan"] ?? "";
tindakanController.text = widget.data["tindakan"] ?? "";
kakiBengkak = widget.data["kaki_bengkak"];
statusGizi = widget.data["status_gizi"];
}
String formatKeIndonesia(DateTime date) {
List<String> bulan = [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
return "${date.day} ${bulan[date.month - 1]} ${date.year}";
}
Future<void> pilihTanggal() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.parse(tanggalController.text),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
tanggalController.text = DateFormat('yyyy-MM-dd').format(picked);
displayTanggalController.text = formatKeIndonesia(picked);
});
}
}
Future<void> updateData() async {
try {
final response = await http.post(
Uri.parse(url),
body: {
"id": widget.data["id"].toString(),
"tanggal_pemeriksaan": tanggalController.text,
"bb_sebelum_hamil": bbSebelumController.text,
"berat_badan": beratController.text,
"tinggi_badan": tinggiController.text,
"LILA": lilaController.text,
"status_gizi": statusGizi ?? "",
"tekanan_darah": tekananController.text,
"tinggi_fundus": fundusController.text,
"denyut_jantung_janin": djjController.text,
"hb": hbController.text,
"kaki_bengkak": kakiBengkak ?? "",
"keluhan": keluhanController.text,
"tindakan": tindakanController.text,
},
);
final res = jsonDecode(response.body);
if (res["success"] == true) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.green,
content: Text("Data berhasil diupdate",
style: GoogleFonts.poppins(color: Colors.white))),
);
Navigator.pop(context, true);
}
} catch (e) {
debugPrint(e.toString());
}
}
Widget input(String label, TextEditingController controller,
{bool readOnly = false,
VoidCallback? onTap,
TextInputType type = TextInputType.text}) {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
SizedBox(
width: 160,
child: Text(label,
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500)),
),
Text(": ", style: GoogleFonts.poppins()),
Expanded(
child: TextField(
controller: controller,
readOnly: readOnly,
onTap: onTap,
keyboardType: type,
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
)
],
),
);
}
Widget dropdownStatusGizi() {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
SizedBox(
width: 160,
child: Text("Status Gizi",
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500)),
),
Text(": ", style: GoogleFonts.poppins()),
Expanded(
child: DropdownButtonFormField<String>(
value: statusGizi,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
items: [
DropdownMenuItem(
value: "KEK (Kekurangan Energi Kronis)",
child: Text("KEK", style: GoogleFonts.poppins())),
DropdownMenuItem(
value: "Normal",
child: Text("Normal", style: GoogleFonts.poppins())),
],
onChanged: (value) {
setState(() {
statusGizi = value;
});
},
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
)
],
),
);
}
Widget dropdownKakiBengkak() {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
SizedBox(
width: 160,
child: Text("Kaki Bengkak",
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500)),
),
Text(": ", style: GoogleFonts.poppins()),
Expanded(
child: DropdownButtonFormField<String>(
value: kakiBengkak,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
items: [
DropdownMenuItem(
value: "Iya",
child: Text("Iya", style: GoogleFonts.poppins())),
DropdownMenuItem(
value: "Tidak",
child: Text("Tidak", style: GoogleFonts.poppins())),
],
onChanged: (value) {
setState(() {
kakiBengkak = value;
});
},
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
)
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.blue,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text("",
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600)),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Center(
child: Text(
"Edit Pemeriksaan Kehamilan",
style: GoogleFonts.poppins(
fontSize: 18, fontWeight: FontWeight.w600),
),
),
const SizedBox(height: 16),
/// CARD
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Card(
color: Colors.white,
elevation: 4,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
/// HEADER BIRU
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.vertical(
top: Radius.circular(16),
),
),
child: Text(
widget.nama,
style: GoogleFonts.poppins(
color: Colors.white, fontWeight: FontWeight.w600),
),
),
/// ISI FORM
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
input(
"Tanggal Pemeriksaan", displayTanggalController,
readOnly: true, onTap: pilihTanggal),
input("BB Sblm Hamil (kg)", bbSebelumController,
type: TextInputType.number),
input("Berat Badan (kg)", beratController,
type: TextInputType.number),
input("Tinggi Badan (cm)", tinggiController,
type: TextInputType.number),
input("LILA (cm)", lilaController,
type: TextInputType.number),
dropdownStatusGizi(),
input("Tekanan Darah", tekananController),
input("Tinggi Fundus (cm)", fundusController,
type: TextInputType.number),
input("Denyut Jantung Janin", djjController),
input("HB", hbController),
dropdownKakiBengkak(),
input("Keluhan", keluhanController),
input("Tindakan", tindakanController),
const SizedBox(height: 20),
// --- TOMBOL SIMPAN PERUBAHAN (OUTLINED STADIUM) ---
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: updateData,
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Colors.blue, width: 2),
shape: const StadiumBorder(),
padding:
const EdgeInsets.symmetric(vertical: 15),
),
child: Text(
"Simpan Perubahan",
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
)
],
),
)
],
),
),
),
)
],
),
),
);
}
}

View File

@ -0,0 +1,407 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'edit_pemeriksaan_kehamilan.dart';
class RiwayatPemeriksaanKehamilanPage extends StatefulWidget {
final String ibuHamilId;
final String nama;
const RiwayatPemeriksaanKehamilanPage({
super.key,
required this.ibuHamilId,
required this.nama,
});
@override
State<RiwayatPemeriksaanKehamilanPage> createState() =>
_RiwayatPemeriksaanKehamilanPageState();
}
class _RiwayatPemeriksaanKehamilanPageState
extends State<RiwayatPemeriksaanKehamilanPage> {
List<Map<String, dynamic>> _data = [];
List<Map<String, dynamic>> _dataFilter = [];
bool _loading = true;
final TextEditingController searchController = TextEditingController();
int _currentPage = 0;
final int _rowsPerPage = 5;
String _formatTanggalIndo(String? tanggal) {
if (tanggal == null || tanggal.isEmpty) return "-";
try {
DateTime dt = DateTime.parse(tanggal);
List<String> bulanIndo = [
"",
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
return "${dt.day.toString().padLeft(2, '0')} ${bulanIndo[dt.month]} ${dt.year}";
} catch (e) {
return tanggal;
}
}
@override
void initState() {
super.initState();
fetchRiwayat();
}
Future<void> fetchRiwayat() async {
setState(() => _loading = true);
final url = Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/pemeriksaan_kehamilan/get_riwayat_pemeriksaan.php?ibu_hamil_id=${widget.ibuHamilId}");
try {
final response = await http.get(url);
if (response.statusCode == 200) {
final jsonData = json.decode(response.body);
if (jsonData["success"] == true) {
List<Map<String, dynamic>> data =
List<Map<String, dynamic>>.from(jsonData["data"]);
data.sort((a, b) {
DateTime dateA =
DateTime.tryParse(a['tanggal_pemeriksaan'] ?? '') ??
DateTime(1900);
DateTime dateB =
DateTime.tryParse(b['tanggal_pemeriksaan'] ?? '') ??
DateTime(1900);
return dateB.compareTo(dateA);
});
setState(() {
_data = data;
_dataFilter = data;
});
}
}
} catch (e) {
debugPrint(e.toString());
}
setState(() => _loading = false);
}
void _filterData(String keyword) {
final key = keyword.toLowerCase();
setState(() {
_dataFilter = _data.where((item) {
return item['tanggal_pemeriksaan']
.toString()
.toLowerCase()
.contains(key) ||
item['keluhan'].toString().toLowerCase().contains(key) ||
item['tindakan'].toString().toLowerCase().contains(key);
}).toList();
_currentPage = 0;
});
}
List<Map<String, dynamic>> get _paginatedData {
if (_dataFilter.isEmpty) return [];
final start = _currentPage * _rowsPerPage;
final end = start + _rowsPerPage;
if (start >= _dataFilter.length) return [];
return _dataFilter.sublist(
start, end > _dataFilter.length ? _dataFilter.length : end);
}
Future<void> hapusData(String id) async {
final confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Text("Konfirmasi",
style:
GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.bold)),
content: Text("Yakin ingin menghapus data ini?",
style: GoogleFonts.poppins(fontSize: 12)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text("Batal",
style:
GoogleFonts.poppins(fontSize: 12, color: Colors.grey))),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text("Hapus",
style: GoogleFonts.poppins(fontSize: 12, color: Colors.red))),
],
),
);
if (confirm != true) return;
final url = Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/pemeriksaan_kehamilan/hapus_pemeriksaan.php");
try {
final response = await http.post(url, body: {"id": id});
final result = json.decode(response.body);
if (result["success"] == true) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Data berhasil dihapus",
style: GoogleFonts.poppins(fontSize: 12))),
);
fetchRiwayat();
}
} catch (e) {
debugPrint(e.toString());
}
}
@override
Widget build(BuildContext context) {
final totalPages =
_dataFilter.isEmpty ? 1 : (_dataFilter.length / _rowsPerPage).ceil();
return Scaffold(
backgroundColor: const Color(0xFFFDFDFD),
appBar: AppBar(
backgroundColor: Colors.blueAccent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context, true),
),
),
body: _loading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 12),
child: Center(
child: Text(
"Riwayat Pemeriksaan Kehamilan",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextField(
controller: searchController,
onChanged: _filterData,
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
hintText: "Cari berdasarkan tanggal, keluhan...",
hintStyle:
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.grey),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.grey),
),
),
),
),
const SizedBox(height: 15),
Expanded(
child: _dataFilter.isEmpty
? Center(
child: Text("Tidak ada riwayat pemeriksaan",
style: GoogleFonts.poppins(
fontSize: 12, color: Colors.grey)))
: ListView.builder(
itemCount: _paginatedData.length,
itemBuilder: (context, index) {
final data = _paginatedData[index];
return _buildCard(data);
},
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Halaman ${_currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(
fontSize: 12, color: Colors.black)),
Row(
children: [
IconButton(
iconSize: 20,
icon: const Icon(Icons.chevron_left),
onPressed: _currentPage == 0
? null
: () => setState(() => _currentPage--)),
IconButton(
iconSize: 20,
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage >= totalPages - 1
? null
: () => setState(() => _currentPage++)),
],
)
],
),
)
],
),
);
}
Widget _buildCard(Map<String, dynamic> data) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 25, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 5,
offset: const Offset(0, 2)),
],
),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.vertical(top: Radius.circular(14)),
),
child: Text(
widget.nama,
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_row("Tanggal",
_formatTanggalIndo(data["tanggal_pemeriksaan"])),
_row(
"BB Sblm Hamil", "${data["bb_sebelum_hamil"] ?? '-'} kg"),
_row("Berat Badan", "${data["berat_badan"] ?? '-'} kg"),
_row("Tinggi Badan", "${data["tinggi_badan"] ?? '-'} cm"),
_row("LILA", "${data["LILA"] ?? '-'} cm"),
_row("Status Gizi", data["status_gizi"]),
_row("Tekanan Darah", data["tekanan_darah"]),
_row("Tinggi Fundus", "${data["tinggi_fundus"] ?? '-'} cm"),
_row("DJJ", data["denyut_jantung_janin"]),
_row("HB", data["hb"]),
_row("Kaki Bengkak", data["kaki_bengkak"]),
_row("Keluhan", data["keluhan"]),
_row("Tindakan", data["tindakan"]),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 32,
child: OutlinedButton.icon(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EditPemeriksaanKehamilanPage(
data: data, nama: widget.nama),
),
);
if (result == true) fetchRiwayat();
},
icon: const Icon(Icons.edit,
size: 16, color: Colors.orange),
label: Text("Edit",
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.orange,
fontWeight: FontWeight.w500)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.orange),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.symmetric(horizontal: 16),
),
),
),
const SizedBox(width: 8),
SizedBox(
height: 32,
child: OutlinedButton.icon(
onPressed: () => hapusData(data["id"].toString()),
icon: const Icon(Icons.delete,
size: 16, color: Colors.redAccent),
label: Text("Hapus",
style: GoogleFonts.poppins(
fontSize: 12,
color: Colors.redAccent,
fontWeight: FontWeight.w500)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.redAccent),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.symmetric(horizontal: 16),
),
),
),
],
)
],
),
)
],
),
),
);
}
Widget _row(String label, dynamic value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 110, // Sedikit diperlebar agar label panjang tidak terpotong
child: Text(label,
style: GoogleFonts.poppins(
fontSize: 11, fontWeight: FontWeight.w600)),
),
Text(" : ", style: GoogleFonts.poppins(fontSize: 11)),
Expanded(
child: Text("${value ?? '-'}",
style: GoogleFonts.poppins(fontSize: 11)),
)
],
),
);
}
}

View File

@ -0,0 +1,416 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
class TambahPemeriksaanKehamilanPage extends StatefulWidget {
final String ibuHamilId;
final String nama;
const TambahPemeriksaanKehamilanPage({
super.key,
required this.ibuHamilId,
required this.nama,
});
@override
State<TambahPemeriksaanKehamilanPage> createState() =>
_TambahPemeriksaanKehamilanPageState();
}
class _TambahPemeriksaanKehamilanPageState
extends State<TambahPemeriksaanKehamilanPage> {
final tanggalController = TextEditingController();
final displayTanggalController = TextEditingController();
final bbSebelumController = TextEditingController(); // Baru
final beratController = TextEditingController();
final tinggiController = TextEditingController(); // Baru
final lilaController = TextEditingController(); // Baru
final tekananController = TextEditingController();
final fundusController = TextEditingController();
final djjController = TextEditingController();
final hbController = TextEditingController();
final keluhanController = TextEditingController();
final tindakanController = TextEditingController();
String? kakiBengkak;
String? statusGizi; // Baru
final String url =
"http://ta.myhost.id/E31230549/mposyandu_api/pemeriksaan_kehamilan/tambah_pemeriksaan_kehamilan.php";
@override
void initState() {
super.initState();
DateTime sekarang = DateTime.now();
tanggalController.text = DateFormat('yyyy-MM-dd').format(sekarang);
displayTanggalController.text = formatKeIndonesia(sekarang);
}
@override
void dispose() {
tanggalController.dispose();
displayTanggalController.dispose();
bbSebelumController.dispose();
beratController.dispose();
tinggiController.dispose();
lilaController.dispose();
tekananController.dispose();
fundusController.dispose();
djjController.dispose();
hbController.dispose();
keluhanController.dispose();
tindakanController.dispose();
super.dispose();
}
String formatKeIndonesia(DateTime date) {
List<String> bulan = [
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
return "${date.day} ${bulan[date.month - 1]} ${date.year}";
}
Future<void> pilihTanggal() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
tanggalController.text = DateFormat('yyyy-MM-dd').format(picked);
displayTanggalController.text = formatKeIndonesia(picked);
});
}
}
Future<void> simpanData() async {
if (kakiBengkak == null || statusGizi == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Silahkan lengkapi status Kaki Bengkak dan Status Gizi",
style: GoogleFonts.poppins(fontSize: 12),
),
),
);
return;
}
try {
final response = await http.post(
Uri.parse(url),
body: {
"ibu_hamil_id": widget.ibuHamilId,
"tanggal_pemeriksaan": tanggalController.text,
"bb_sebelum_hamil": bbSebelumController.text,
"berat_badan": beratController.text,
"tinggi_badan": tinggiController.text,
"LILA": lilaController.text,
"status_gizi": statusGizi ?? "",
"tekanan_darah": tekananController.text,
"tinggi_fundus": fundusController.text,
"denyut_jantung_janin": djjController.text,
"hb": hbController.text,
"kaki_bengkak": kakiBengkak ?? "",
"keluhan": keluhanController.text,
"tindakan": tindakanController.text,
},
);
final res = jsonDecode(response.body);
if (res["success"] == true) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Data berhasil disimpan",
style: GoogleFonts.poppins(fontSize: 12),
),
),
);
Navigator.pop(context, true);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
res["message"] ?? "Gagal menyimpan data",
style: GoogleFonts.poppins(fontSize: 12),
),
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Terjadi kesalahan koneksi",
style: GoogleFonts.poppins(fontSize: 12),
),
),
);
}
}
Widget input(String label, TextEditingController controller,
{bool readOnly = false,
VoidCallback? onTap,
TextInputType type = TextInputType.text}) {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
SizedBox(
width: 160,
child: Text(
label,
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500),
),
),
Text(" : ", style: GoogleFonts.poppins(fontSize: 12)),
Expanded(
child: TextField(
controller: controller,
readOnly: readOnly,
onTap: onTap,
keyboardType: type,
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
)
],
),
);
}
Widget dropdownGizi() {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
SizedBox(
width: 160,
child: Text(
"Status Gizi",
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500),
),
),
Text(" : ", style: GoogleFonts.poppins(fontSize: 12)),
Expanded(
child: DropdownButtonFormField<String>(
value: statusGizi,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
hint: Text("Pilih Status",
style: GoogleFonts.poppins(fontSize: 12)),
items: [
DropdownMenuItem(
value: "KEK (Kekurangan Energi Kronis)",
child:
Text("KEK", style: GoogleFonts.poppins(fontSize: 12))),
DropdownMenuItem(
value: "Normal",
child: Text("Normal",
style: GoogleFonts.poppins(fontSize: 12))),
],
onChanged: (value) => setState(() => statusGizi = value),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
)
],
),
);
}
Widget dropdownKakiBengkak() {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
SizedBox(
width: 160,
child: Text(
"Kaki Bengkak",
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500),
),
),
Text(" : ", style: GoogleFonts.poppins(fontSize: 12)),
Expanded(
child: DropdownButtonFormField<String>(
value: kakiBengkak,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
hint: Text("Pilih", style: GoogleFonts.poppins(fontSize: 12)),
items: [
DropdownMenuItem(
value: "Iya",
child:
Text("Iya", style: GoogleFonts.poppins(fontSize: 12))),
DropdownMenuItem(
value: "Tidak",
child: Text("Tidak",
style: GoogleFonts.poppins(fontSize: 12))),
],
onChanged: (value) => setState(() => kakiBengkak = value),
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
)
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("",
style: GoogleFonts.poppins(color: Colors.white, fontSize: 16)),
backgroundColor: Colors.blue,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Center(
child: Text(
"Tambah Pemeriksaan Kehamilan",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
),
const SizedBox(height: 16),
Center(
child: SizedBox(
width: 500,
child: Card(
color: Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.vertical(
top: Radius.circular(12),
),
),
child: Text(
widget.nama,
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
input(
"Tanggal Pemeriksaan",
displayTanggalController,
readOnly: true,
onTap: pilihTanggal,
),
input("BB Sebelum Hamil (kg)", bbSebelumController,
type: TextInputType.number),
input("Berat Badan Sekarang (kg)", beratController,
type: TextInputType.number),
input("Tinggi Badan (cm)", tinggiController,
type: TextInputType.number),
input("LILA (cm)", lilaController,
type: TextInputType.number),
dropdownGizi(),
input("Tekanan Darah", tekananController),
input("Tinggi Fundus (cm)", fundusController,
type: TextInputType.number),
input("Denyut Jantung Janin", djjController),
input("HB", hbController),
dropdownKakiBengkak(),
input("Keluhan", keluhanController),
input("Tindakan", tindakanController),
const SizedBox(height: 20),
Center(
child: SizedBox(
width: 200,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Colors.blue, width: 2),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(
vertical: 14),
),
onPressed: simpanData,
child: Text(
"Simpan",
style: GoogleFonts.poppins(
color: Colors.blue,
fontSize: 12,
fontWeight: FontWeight.bold),
),
),
),
),
],
),
)
],
),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,434 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import '../layout/main_layout.dart';
import 'bidan_drawer.dart';
import '../pages/login_page.dart';
import '../bidan/data_gizi_balita.dart';
import '../bidan/laporan.dart';
const String baseUrl = "http://ta.myhost.id/E31230549/mposyandu_api";
class DashboardBidanPage extends StatefulWidget {
const DashboardBidanPage({super.key});
@override
State<DashboardBidanPage> createState() => _DashboardBidanPageState();
}
class _DashboardBidanPageState extends State<DashboardBidanPage> {
int jumlahIbuHamil = 0;
int jumlahBalita = 0;
// Variabel penampung data grafik Ibu Hamil
double bumilKek = 0;
double bumilNormal = 0;
String namaUser = "";
final List<String> listBulan = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des'
];
@override
void initState() {
super.initState();
_checkLogin();
getDashboard();
}
Future<void> _checkLogin() async {
final prefs = await SharedPreferences.getInstance();
final isLogin = prefs.getBool('isLogin') ?? false;
if (!isLogin) {
if (!mounted) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const LoginPage(fromGuard: true)),
(route) => false,
);
} else {
setState(() {
namaUser = prefs.getString('nama') ?? "Bidan";
});
}
}
Future<void> getDashboard() async {
try {
final response =
await http.get(Uri.parse("$baseUrl/dashboard_bidan.php"));
final data = jsonDecode(response.body);
if (data["success"] == true) {
setState(() {
jumlahIbuHamil = int.parse(data["ibu_hamil"].toString());
jumlahBalita = int.parse(data["balita"].toString());
// Menangkap data kondisi pemeriksaan LILA terbaru dari JSON
bumilKek = double.parse((data["bumil_kek"] ?? 0).toString());
bumilNormal = double.parse((data["bumil_normal"] ?? 0).toString());
});
}
} catch (e) {
debugPrint("ERROR DASHBOARD: $e");
}
}
// Fungsi untuk mendeteksi klik pada diagram batang
void _handleChartTap(TapUpDetails details, Size size) {
const double paddingLeft = 30;
const double paddingBottom = 30;
final double chartWidth = size.width - paddingLeft;
final double chartHeight = size.height - paddingBottom;
double maxData = max(10, max(bumilNormal, bumilKek) + 5);
double groupWidth = chartWidth / listBulan.length;
double barWidth = groupWidth * 0.3;
int currentMonthIndex = DateTime.now().month - 1;
// Hitung posisi koordinat X pusat untuk bulan saat ini
double xCenter =
paddingLeft + (currentMonthIndex * groupWidth) + (groupWidth / 2);
// Hitung area Rect untuk Batang Normal
double barHeightNormal = (bumilNormal / maxData) * chartHeight;
Rect rectNormal = Rect.fromLTWH(xCenter - barWidth,
chartHeight - barHeightNormal, barWidth, barHeightNormal);
// Hitung area Rect untuk Batang KEK
double barHeightKek = (bumilKek / maxData) * chartHeight;
Rect rectKek = Rect.fromLTWH(
xCenter, chartHeight - barHeightKek, barWidth, barHeightKek);
// Ambil posisi lokal ketukan jari user
Offset tapPosition = details.localPosition;
// Cek apakah ketukan berada di dalam salah satu area batang
if (rectNormal.contains(tapPosition)) {
_showDetailDialog("Bumil Normal", bumilNormal.round());
} else if (rectKek.contains(tapPosition)) {
_showDetailDialog("Berisiko KEK", bumilKek.round());
}
}
// Fungsi memunculkan pop-up angka pasti
void _showDetailDialog(String kategori, int jumlah) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text(kategori,
style: GoogleFonts.poppins(fontWeight: FontWeight.bold)),
content: Text(
"Jumlah saat ini: $jumlah Ibu Hamil",
style: GoogleFonts.poppins(fontSize: 15),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("OK",
style:
TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
await SystemNavigator.pop();
},
child: Theme(
data: Theme.of(context).copyWith(
textTheme: GoogleFonts.poppinsTextTheme(Theme.of(context).textTheme),
dividerColor: Colors.transparent,
),
child: MainLayout(
title: "",
drawer: const BidanDrawer(),
body: _buildBody(),
),
),
);
}
Widget _buildBody() {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Selamat Datang Bidan $namaUser\n',
style: GoogleFonts.poppins(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.blue,
),
),
TextSpan(
text:
'Melayani dengan sepenuh hati untuk generasi tetap sehat ',
style:
GoogleFonts.poppins(fontSize: 13, color: Colors.black),
),
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(Icons.favorite, color: Colors.blue, size: 18),
),
],
),
),
const SizedBox(height: 15),
Center(
child: Image.asset(
'assets/images/logoo.webp',
width: 300,
height: 180,
fit: BoxFit.contain,
),
),
const SizedBox(height: 25),
Row(
children: [
_infoBox("$jumlahIbuHamil", "Ibu Hamil Aktif", Colors.pink),
const SizedBox(width: 10),
_infoBox("$jumlahBalita", "Balita Terdaftar", Colors.amber),
],
),
const SizedBox(height: 20),
Row(
children: [
_actionButton('Lihat Data Gizi', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DataGiziBalitaPage()));
}),
const SizedBox(width: 10),
_actionButton('Lihat Laporan', () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DataLaporanPage()));
}),
],
),
const SizedBox(height: 25),
Text(
'Statistik Risiko KEK Ibu Hamil Bulan Ini',
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 15),
Container(
padding: const EdgeInsets.fromLTRB(10, 25, 15, 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 2,
blurRadius: 10,
),
],
),
child: Column(
children: [
// Menggunakan LayoutBuilder agar size CustomPaint sinkron dengan pendeteksi klik
LayoutBuilder(
builder: (context, constraints) {
final size = Size(constraints.maxWidth, 250);
return GestureDetector(
onTapUp: (details) => _handleChartTap(details, size),
child: SizedBox(
height: size.height,
width: size.width,
child: CustomPaint(
painter: BarChartPainter(
normal: bumilNormal,
kek: bumilKek,
months: listBulan,
currentMonthIndex: DateTime.now().month - 1,
),
),
),
);
},
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_legendItem(Colors.teal, "Bumil Normal"),
const SizedBox(width: 25),
_legendItem(Colors.pink.shade400, "Berisiko KEK"),
],
),
],
),
),
const SizedBox(height: 20),
],
),
),
);
}
Widget _actionButton(String title, VoidCallback onPressed) {
return Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: onPressed,
child: Text(title, style: const TextStyle(color: Colors.white)),
),
);
}
Widget _legendItem(Color color, String text) {
return Row(
children: [
Container(
width: 14,
height: 14,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 8),
Text(text,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
],
);
}
Widget _infoBox(String value, String label, Color color) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.circular(12)),
child: Column(
children: [
Text(value,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold)),
const SizedBox(height: 5),
Text(label,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 12)),
],
),
),
);
}
}
class BarChartPainter extends CustomPainter {
final double normal;
final double kek;
final List<String> months;
final int currentMonthIndex;
BarChartPainter({
required this.normal,
required this.kek,
required this.months,
required this.currentMonthIndex,
});
@override
void paint(Canvas canvas, Size size) {
final paintGrid = Paint()
..color = Colors.grey.shade300
..strokeWidth = 1;
const double paddingLeft = 30;
const double paddingBottom = 30;
final double chartWidth = size.width - paddingLeft;
final double chartHeight = size.height - paddingBottom;
double maxData = max(10, max(normal, kek) + 5);
int segments = 5;
for (int i = 0; i <= segments; i++) {
double y = chartHeight - (i * chartHeight / segments);
canvas.drawLine(Offset(paddingLeft, y), Offset(size.width, y), paintGrid);
_drawText(canvas, Offset(5, y - 7),
(maxData / segments * i).round().toString(), 10, Colors.grey);
}
double groupWidth = chartWidth / months.length;
double barWidth = groupWidth * 0.3;
for (int i = 0; i < months.length; i++) {
double xCenter = paddingLeft + (i * groupWidth) + (groupWidth / 2);
_drawText(canvas, Offset(xCenter - 10, chartHeight + 10), months[i], 9,
Colors.black);
if (i == currentMonthIndex) {
double barHeightNormal = (normal / maxData) * chartHeight;
canvas.drawRect(
Rect.fromLTWH(xCenter - barWidth, chartHeight - barHeightNormal,
barWidth, barHeightNormal),
Paint()..color = Colors.teal,
);
double barHeightKek = (kek / maxData) * chartHeight;
canvas.drawRect(
Rect.fromLTWH(
xCenter, chartHeight - barHeightKek, barWidth, barHeightKek),
Paint()..color = Colors.pink.shade400,
);
}
}
}
void _drawText(
Canvas canvas, Offset offset, String text, double fontSize, Color color) {
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
color: color, fontSize: fontSize, fontWeight: FontWeight.bold)),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, offset);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@ -0,0 +1,531 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import '../layout/main_layout.dart';
import 'bidan_drawer.dart';
import '../bidan/crud_data_gizi/tambah_gizi_balita.dart';
import '../bidan/crud_data_gizi/riwayat_gizi_balita.dart';
import '../bidan/dashboard_bidan.dart';
class DataGiziBalitaPage extends StatefulWidget {
const DataGiziBalitaPage({super.key});
@override
State<DataGiziBalitaPage> createState() => _DataGiziBalitaPageState();
}
class _DataGiziBalitaPageState extends State<DataGiziBalitaPage> {
List<Map<String, dynamic>> _dataBalita = [];
bool _isLoading = true;
int _currentPage = 0;
final int _rowsPerPage = 5;
String _searchQuery = "";
@override
void initState() {
super.initState();
fetchData();
}
String formatAngka(dynamic value, String satuan) {
if (value == null ||
value.toString().trim().isEmpty ||
value.toString().toLowerCase() == "null") {
return "-";
}
return "${value.toString()}$satuan";
}
String formatTanggal(String? tgl) {
if (tgl == null || tgl == "-" || tgl.isEmpty) return "-";
try {
DateTime dt = DateTime.parse(tgl);
List<String> bulanIndo = [
"",
"Januari",
"Februari",
"Maret",
"April",
"Mei",
"Juni",
"Juli",
"Agustus",
"September",
"Oktober",
"November",
"Desember"
];
return "${dt.day} ${bulanIndo[dt.month]} ${dt.year}";
} catch (e) {
return tgl;
}
}
// PERBAIKAN: Menghitung usia berdasarkan Tanggal Pemeriksaan, bukan Tanggal Sekarang
String hitungUsia(String? tglLahir, String? tglPeriksa) {
if (tglLahir == null || tglLahir == "-" || tglLahir.isEmpty) return "-";
try {
DateTime lahir = DateTime.parse(tglLahir);
// Menggunakan tanggal pemeriksaan sebagai acuan utama, jika kosong baru pakai waktu sekarang
DateTime acuanPeriksa =
(tglPeriksa != null && tglPeriksa != "-" && tglPeriksa.isNotEmpty)
? DateTime.parse(tglPeriksa)
: DateTime.now();
// Hitung selisih bulan dasar
int bulan = (acuanPeriksa.year - lahir.year) * 12 +
acuanPeriksa.month -
lahir.month;
// Logika pembulatan hari: Jika sisa hari pada bulan berjalan > 15 hari, bulatkan ke atas (+1 bulan)
DateTime tanggalTargetUlangTahun =
DateTime(lahir.year, lahir.month + bulan, lahir.day);
Duration selisihHari = acuanPeriksa.difference(tanggalTargetUlangTahun);
if (selisihHari.inDays > 15) {
bulan += 1;
}
// Pengondisian jika hasil hitung bernilai negatif karena variasi tanggal
if (bulan < 0) bulan = 0;
return "$bulan Bulan";
} catch (e) {
return "-";
}
}
Future<void> fetchData() async {
try {
final response = await http.get(
Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/gizi_balita/get_gizi_balita.php"),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
_dataBalita = List<Map<String, dynamic>>.from(data);
_isLoading = false;
});
} else {
setState(() => _isLoading = false);
}
} catch (e) {
debugPrint("Error Fetch: $e");
setState(() => _isLoading = false);
}
}
List<Map<String, dynamic>> get _filteredData {
return _dataBalita
.where((item) => item["nama"]
.toString()
.toLowerCase()
.contains(_searchQuery.toLowerCase()))
.toList();
}
List<Map<String, dynamic>> get _paginatedData {
final start = _currentPage * _rowsPerPage;
final end = start + _rowsPerPage;
if (start >= _filteredData.length) return [];
return _filteredData.sublist(
start, end > _filteredData.length ? _filteredData.length : end);
}
@override
Widget build(BuildContext context) {
final totalPages = (_filteredData.length / _rowsPerPage).ceil() == 0
? 1
: (_filteredData.length / _rowsPerPage).ceil();
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const DashboardBidanPage()),
(route) => false);
},
child: MainLayout(
title: "",
drawer: const BidanDrawer(),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Center(
child: Text("Data Gizi Balita",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87)),
),
const SizedBox(height: 12),
TextField(
onChanged: (value) => setState(() {
_searchQuery = value;
_currentPage = 0;
}),
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
hintText: "Cari nama balita...",
hintStyle: GoogleFonts.poppins(fontSize: 12),
prefixIcon: const Icon(Icons.search, size: 20),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10)),
contentPadding: const EdgeInsets.symmetric(vertical: 8),
),
),
const SizedBox(height: 15),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredData.isEmpty
? Center(
child: Text("Data tidak tersedia",
style: GoogleFonts.poppins(fontSize: 12)))
: ListView.builder(
itemCount: _paginatedData.length,
itemBuilder: (context, index) {
final balita = _paginatedData[index];
final bb = formatAngka(balita["bb"], "kg");
final tb = formatAngka(balita["tb"], "cm");
final lk = formatAngka(balita["lk"], "cm");
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 8,
offset: Offset(0, 4))
],
),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(15),
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.vertical(
top: Radius.circular(12)),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
balita["nama"] ?? "-",
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight:
FontWeight.bold,
fontSize: 13)),
),
_badgeHadir(
balita["status_hadir"]),
],
),
const Divider(color: Colors.white54),
_rowWhite("Orang Tua",
balita["nama_orang_tua"] ?? "-"),
_rowWhite("Alamat",
balita["alamat"] ?? "-"),
// PERBAIKAN: Mengirimkan dua parameter (tanggal lahir & tanggal pemeriksaan)
_rowWhite(
"Usia",
hitungUsia(
balita["tanggal_lahir"],
balita[
"tanggal_pemeriksaan"])),
_rowWhite(
"Tgl Pemeriksaan",
formatTanggal(balita[
"tanggal_pemeriksaan"])),
_rowWhite("BB / TB / LK",
"$bb / $tb / $lk"),
_rowWhite("Catatan Kader",
balita["catatan"] ?? "-"),
],
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(15),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(12)),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text("Pemeriksaan Gizi Terakhir",
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 12)),
const SizedBox(height: 8),
_rowGizi("Z-Score BB/U",
balita["zscore_bb_u"] ?? "-"),
_rowGizi("Z-Score TB/U",
balita["zscore_tb_u"] ?? "-"),
_rowGizi("Z-Score BB/TB",
balita["zscore_bb_tb"] ?? "-"),
const Divider(),
_rowStatus("Status Gizi", balita),
_rowGizi("Tindak Lanjut",
balita["tindak_lanjut"] ?? "-"),
_rowGizi(
"Saran", balita["saran"] ?? "-"),
const SizedBox(height: 12),
Row(
children: [
_smallButton(Icons.add, "Input",
Colors.blue, () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
TambahGiziBalitaPage(
balita:
balita)));
fetchData();
}),
_smallButton(
Icons.history,
"Riwayat",
Colors.deepPurple, () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RiwayatGiziBalitaPage(
balita:
balita)));
fetchData();
}),
],
)
],
),
),
],
),
);
},
),
),
_buildPagination(totalPages),
],
),
),
),
);
}
Widget _badgeHadir(String? status) {
String text = status ?? "Belum Absen";
Color badgeColor;
if (text.toLowerCase() == "hadir") {
badgeColor = Colors.greenAccent[700]!;
} else if (text.toLowerCase() == "tidak hadir") {
badgeColor = Colors.redAccent;
} else {
badgeColor = Colors.orangeAccent;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.3),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white, width: 1),
),
child: Text(
text.toUpperCase(),
style: GoogleFonts.poppins(
fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white),
),
);
}
Widget _rowWhite(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(label,
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600))),
Text(" : ",
style: GoogleFonts.poppins(color: Colors.white, fontSize: 12)),
Expanded(
child: Text(value,
style:
GoogleFonts.poppins(color: Colors.white, fontSize: 12))),
],
),
);
}
Widget _rowGizi(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(label,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey[800]))),
const Text(" : ", style: TextStyle(fontSize: 12)),
Expanded(
child: Text(value,
style:
GoogleFonts.poppins(fontSize: 12, color: Colors.black))),
],
),
);
}
Widget _rowStatus(String label, Map<String, dynamic> balita) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.orange[50], borderRadius: BorderRadius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("$label :",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.orange[900])),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.only(left: 12),
child: Column(
children: [
_rowStatusDetail("BB / U", balita["status_bbu"] ?? "-"),
_rowStatusDetail("TB / U", balita["status_tbu"] ?? "-"),
_rowStatusDetail("BB / TB", balita["status_bbtb"] ?? "-"),
],
),
),
],
),
);
}
Widget _rowStatusDetail(String indikator, String nilai) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
SizedBox(
width: 65,
child: Text(
indikator,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.orange[900]),
),
),
Text(
" : ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.orange[900]),
),
Expanded(
child: Text(
nilai,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.orange[900]),
),
),
],
),
);
}
Widget _smallButton(
IconData icon, String text, Color color, VoidCallback onPressed) {
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: OutlinedButton.icon(
onPressed: onPressed,
icon: Icon(icon, size: 14, color: color),
label: Text(text,
style: GoogleFonts.poppins(
fontSize: 12, color: color, fontWeight: FontWeight.w500)),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8),
side: BorderSide(color: color.withOpacity(0.5)),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
),
);
}
Widget _buildPagination(int totalPages) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Halaman ${_currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(fontSize: 12)),
Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _currentPage == 0
? null
: () => setState(() => _currentPage--)),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage >= totalPages - 1
? null
: () => setState(() => _currentPage++)),
],
),
],
),
);
}
}

511
lib/bidan/edukasi.dart Normal file
View File

@ -0,0 +1,511 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_quill/flutter_quill.dart' as quill;
// Import Halaman CRUD
import '../bidan/crud_edukasi/tambah_edukasi_balita.dart';
import '../bidan/crud_edukasi/tambah_edukasi_ibu_hamil.dart';
import '../bidan/crud_edukasi/edit_edukasi_balita.dart';
import '../bidan/crud_edukasi/edit_edukasi_ibu_hamil.dart';
// Import Dashboard Bidan agar navigasi berfungsi
import '../bidan/dashboard_bidan.dart';
import '../layout/main_layout.dart';
import 'bidan_drawer.dart';
class DataEdukasiPage extends StatefulWidget {
const DataEdukasiPage({super.key});
@override
State<DataEdukasiPage> createState() => _DataEdukasiPageState();
}
class _DataEdukasiPageState extends State<DataEdukasiPage> {
// Base URL untuk API
final String baseUrlBalita =
"http://ta.myhost.id/E31230549/mposyandu_api/edukasi_balita/";
final String baseUrlIbu =
"http://ta.myhost.id/E31230549/mposyandu_api/edukasi_ibu_hamil/";
// Base URL untuk Gambar - Mengarah ke hosting bukan localhost
final String baseImageUrl =
"http://ta.myhost.id/E31230549/mposyandu_api/upload/edukasi/";
List balitaList = [];
List ibuList = [];
List filteredBalita = [];
List filteredIbu = [];
bool isLoading = true;
int _currentBalitaPage = 0;
int _currentIbuPage = 0;
final int _rowsPerPage = 5;
final searchBalitaController = TextEditingController();
final searchIbuController = TextEditingController();
@override
void initState() {
super.initState();
loadAllData();
searchBalitaController.addListener(() {
filterBalita(searchBalitaController.text);
});
searchIbuController.addListener(() {
filterIbu(searchIbuController.text);
});
}
Future loadAllData() async {
setState(() => isLoading = true);
await loadBalita();
await loadIbu();
setState(() => isLoading = false);
}
Future loadBalita() async {
try {
var response =
await http.get(Uri.parse("${baseUrlBalita}get_edukasi_balita.php"));
var data = jsonDecode(response.body);
if (data["success"]) {
setState(() {
balitaList = data["data"];
filteredBalita = balitaList;
});
}
} catch (e) {
debugPrint("Error Load Balita: $e");
}
}
Future loadIbu() async {
try {
var response =
await http.get(Uri.parse("${baseUrlIbu}get_edukasi_ibu_hamil.php"));
var data = jsonDecode(response.body);
if (data["success"]) {
setState(() {
ibuList = data["data"];
filteredIbu = ibuList;
});
}
} catch (e) {
debugPrint("Error Load Ibu: $e");
}
}
void filterBalita(String query) {
setState(() {
filteredBalita = balitaList
.where((item) =>
item["judul"].toLowerCase().contains(query.toLowerCase()))
.toList();
_currentBalitaPage = 0;
});
}
void filterIbu(String query) {
setState(() {
filteredIbu = ibuList
.where((item) =>
item["judul"].toLowerCase().contains(query.toLowerCase()))
.toList();
_currentIbuPage = 0;
});
}
Future<void> hapusData(String id, String type) async {
String url = type == "balita"
? "${baseUrlBalita}hapus_edukasi_balita.php"
: "${baseUrlIbu}hapus_edukasi_ibu_hamil.php";
try {
var response = await http.post(Uri.parse(url), body: {"id": id});
var data = jsonDecode(response.body);
if (data["status"] == "success") {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil dihapus")));
loadAllData();
}
} catch (e) {
debugPrint("Error Hapus: $e");
}
}
void confirmDelete(String id, String type) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text("Konfirmasi"),
content: const Text("Yakin ingin menghapus data ini?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal")),
TextButton(
onPressed: () {
Navigator.pop(context);
hapusData(id, type);
},
child: const Text("Hapus", style: TextStyle(color: Colors.red)),
),
],
),
);
}
Widget buildDescriptionCell(String? description) {
String cleanText = description ?? "";
if (cleanText.startsWith('[') && cleanText.endsWith(']')) {
try {
final List<dynamic> json = jsonDecode(cleanText);
final doc = quill.Document.fromJson(json);
cleanText = doc.toPlainText().trim();
} catch (e) {
debugPrint("Gagal parsing JSON deskripsi: $e");
}
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: SizedBox(
width: 300,
child: Text(
cleanText,
style: GoogleFonts.poppins(fontSize: 12, height: 1.5),
softWrap: true,
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
),
);
}
List<dynamic> getPaginatedData(List data, int currentPage) {
int start = currentPage * _rowsPerPage;
int end = start + _rowsPerPage;
if (start >= data.length) return [];
return data.sublist(start, end > data.length ? data.length : end);
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
// FIX FINAL: selalu kembali ke Dashboard Bidan
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => const DashboardBidanPage(),
),
(route) => false,
);
},
child: MainLayout(
title: "",
drawer: const BidanDrawer(),
body: isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: loadAllData,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
children: [
sectionBalita(),
const SizedBox(height: 40),
sectionIbu(),
],
),
),
),
),
);
}
Widget sectionBalita() {
int totalPages = (filteredBalita.length / _rowsPerPage).ceil();
if (totalPages == 0) totalPages = 1;
return Column(
children: [
title("Edukasi Balita"),
const SizedBox(height: 15),
searchAndAddRow(
controller: searchBalitaController,
hint: "Cari Judul Edukasi Balita...",
onAdd: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const TambahEdukasiBalitaPage()));
loadBalita();
},
),
const SizedBox(height: 20),
tableData(
data: getPaginatedData(filteredBalita, _currentBalitaPage),
type: "balita",
),
paginationControls(_currentBalitaPage, totalPages,
(i) => setState(() => _currentBalitaPage = i)),
],
);
}
Widget sectionIbu() {
int totalPages = (filteredIbu.length / _rowsPerPage).ceil();
if (totalPages == 0) totalPages = 1;
return Column(
children: [
title("Edukasi Ibu Hamil"),
const SizedBox(height: 15),
searchAndAddRow(
controller: searchIbuController,
hint: "Cari Judul Edukasi Ibu...",
onAdd: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const TambahEdukasiBumilPage()));
loadIbu();
},
),
const SizedBox(height: 20),
tableData(
data: getPaginatedData(filteredIbu, _currentIbuPage),
type: "ibu",
),
paginationControls(_currentIbuPage, totalPages,
(i) => setState(() => _currentIbuPage = i)),
],
);
}
Widget searchAndAddRow({
required TextEditingController controller,
required String hint,
required VoidCallback onAdd,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
searchField(controller, hint),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: addBtn(onAdd),
),
],
),
);
}
Widget tableData({required List data, required String type}) {
return Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 1000),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: tableCard(
headers: const ["Gambar", "Judul", "Deskripsi", "Aksi"],
rows: data
.map((e) => [
imagePreview(e),
SizedBox(
width: 120,
child: Text(e["judul"] ?? "",
style: GoogleFonts.poppins(fontSize: 13))),
buildDescriptionCell(e["deskripsi"]),
actionButtons(e, type)
])
.toList(),
),
),
);
}
Widget imagePreview(Map<String, dynamic> item) {
String? fileName = item["gambar"];
if (fileName == null || fileName.isEmpty) {
return const Icon(Icons.image_not_supported,
color: Colors.grey, size: 40);
}
String fullUrl = "$baseImageUrl$fileName";
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
fullUrl,
width: 60,
height: 60,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 60,
height: 60,
color: Colors.grey.shade200,
child: const Icon(Icons.broken_image, color: Colors.red, size: 30),
);
},
),
);
}
Widget title(String text) {
return Text(text,
style: GoogleFonts.poppins(fontSize: 18, fontWeight: FontWeight.bold));
}
Widget searchField(TextEditingController controller, String hint) {
return TextField(
controller: controller,
style: GoogleFonts.poppins(fontSize: 13),
decoration: InputDecoration(
hintText: hint,
prefixIcon: const Icon(Icons.search, size: 20),
contentPadding: const EdgeInsets.symmetric(vertical: 15),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: Colors.white,
),
);
}
Widget addBtn(VoidCallback onTap) {
return SizedBox(
height: 45,
child: OutlinedButton.icon(
onPressed: onTap,
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blue),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
icon: const Icon(Icons.add, size: 18, color: Colors.blue),
label: Text("Tambah",
style: GoogleFonts.poppins(
fontSize: 13, color: Colors.blue, fontWeight: FontWeight.bold)),
),
);
}
Widget tableCard({
required List<String> headers,
required List<List<dynamic>> rows,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(15),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
headingRowColor: MaterialStateProperty.all(Colors.blue),
dataRowMaxHeight: 100,
dataRowMinHeight: 70,
columnSpacing: 15,
columns: headers
.map((h) => DataColumn(
label: Text(h,
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13))))
.toList(),
rows: rows
.map((row) => DataRow(
cells: row.map((cell) {
if (cell is Widget) return DataCell(cell);
return DataCell(Text(cell.toString(),
style: GoogleFonts.poppins(fontSize: 12)));
}).toList()))
.toList(),
),
),
),
);
}
Widget paginationControls(
int currentPage, int totalPages, Function(int) onPageChanged) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Halaman ${currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black54),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: currentPage == 0
? null
: () => onPageChanged(currentPage - 1),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: currentPage >= totalPages - 1
? null
: () => onPageChanged(currentPage + 1),
),
],
),
],
),
);
}
Widget actionButtons(Map<String, dynamic> data, String type) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.orange, size: 18),
onPressed: () async {
if (type == "balita") {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EditEdukasiBalitaPage(data: data)));
loadBalita();
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EditEdukasiBumilPage(data: data)));
loadIbu();
}
}),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red, size: 18),
onPressed: () => confirmDelete(data["id"].toString(), type)),
],
);
}
}

433
lib/bidan/imunisasi.dart Normal file
View File

@ -0,0 +1,433 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:http/http.dart' as http;
import '../layout/main_layout.dart';
import 'bidan_drawer.dart';
// Import file crud
import '../bidan/crud_imunisasi/tambah_imunisasi.dart';
import '../bidan/crud_imunisasi/edit_imunisasi.dart';
import '../bidan/crud_imunisasi/data_imunisasi_balita.dart';
// Import Dashboard Bidan agar navigasi berfungsi
import '../bidan/dashboard_bidan.dart';
class DataImunisasiPage extends StatefulWidget {
const DataImunisasiPage({super.key});
@override
State<DataImunisasiPage> createState() => _DataImunisasiPageState();
}
class _DataImunisasiPageState extends State<DataImunisasiPage> {
List masterImunisasi = [];
String _searchQuery = "";
bool isLoading = true;
// Variabel Pagination
int _currentPage = 0;
final int _rowsPerPage = 10;
// Endpoint API
final String baseMaster =
"http://ta.myhost.id/E31230549/mposyandu_api/imunisasi/get_master_imunisasi.php";
final String urlDelete =
"http://ta.myhost.id/E31230549/mposyandu_api/imunisasi/hapus_imunisasi.php";
@override
void initState() {
super.initState();
fetchMasterData();
}
Future fetchMasterData() async {
try {
final resMaster = await http.get(Uri.parse(baseMaster));
if (mounted) {
setState(() {
masterImunisasi = jsonDecode(resMaster.body)["data"] ?? [];
isLoading = false;
});
}
} catch (e) {
if (mounted) setState(() => isLoading = false);
}
}
Future _deleteData(String id) async {
try {
final response = await http.post(
Uri.parse(urlDelete),
body: {"id": id},
);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(data['message'] ?? "Data berhasil dihapus"),
backgroundColor: Colors.blue,
),
);
fetchMasterData();
} else {
throw data['message'] ?? "Gagal menghapus data";
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Kesalahan: $e"), backgroundColor: Colors.red),
);
}
}
void _showDeleteDialog(Map item) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Text("Konfirmasi Hapus",
style:
GoogleFonts.poppins(fontWeight: FontWeight.bold, fontSize: 16)),
content: Text(
"Apakah Anda yakin ingin menghapus '${item['nama_imunisasi']}'?",
style: GoogleFonts.poppins(fontSize: 13)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child:
Text("Batal", style: GoogleFonts.poppins(color: Colors.grey)),
),
OutlinedButton(
onPressed: () {
Navigator.pop(context);
_deleteData(item['id'].toString());
},
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
shape: const StadiumBorder(),
),
child: Text("Hapus",
style: GoogleFonts.poppins(
color: Colors.red, fontWeight: FontWeight.bold)),
),
],
),
);
}
List get _filteredMaster {
return masterImunisasi.where((item) {
final nama = item["nama_imunisasi"].toString().toLowerCase();
final ket = item["keterangan"].toString().toLowerCase();
final query = _searchQuery.toLowerCase();
return nama.contains(query) || ket.contains(query);
}).toList();
}
List get _paginatedData {
final start = _currentPage * _rowsPerPage;
final end = start + _rowsPerPage;
if (start >= _filteredMaster.length) return [];
return _filteredMaster.sublist(
start,
end > _filteredMaster.length ? _filteredMaster.length : end,
);
}
@override
Widget build(BuildContext context) {
final totalPages = (_filteredMaster.length / _rowsPerPage).ceil() == 0
? 1
: (_filteredMaster.length / _rowsPerPage).ceil();
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const DashboardBidanPage()),
(route) => false,
);
},
child: MainLayout(
title: "",
drawer: const BidanDrawer(),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Text(
"Data Jenis Imunisasi",
style: GoogleFonts.poppins(
fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 15),
// --- INPUT CARI ---
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 850),
child: TextField(
onChanged: (value) => setState(() {
_searchQuery = value;
_currentPage = 0;
}),
style: GoogleFonts.poppins(fontSize: 12),
decoration: InputDecoration(
hintText: "Cari nama atau keterangan...",
hintStyle: GoogleFonts.poppins(fontSize: 12),
prefixIcon:
const Icon(Icons.search, size: 20),
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide:
BorderSide(color: Colors.grey[300]!),
),
contentPadding:
const EdgeInsets.symmetric(vertical: 8),
),
),
),
),
const SizedBox(height: 20),
// --- TABEL DENGAN DATATABLE ---
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 850),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
headingRowColor:
MaterialStateProperty.all(Colors.blue),
columnSpacing: 25,
columns: [
_buildDataColumn("Nama Imunisasi"),
_buildDataColumn("Min"),
_buildDataColumn("Max"),
_buildDataColumn("Keterangan"),
_buildDataColumn("Aksi"),
],
rows: _paginatedData.isEmpty
? [
DataRow(cells: [
DataCell(Text(
"Data tidak ditemukan",
style: GoogleFonts.poppins(
fontSize: 12))),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: _paginatedData.map((item) {
return DataRow(cells: [
DataCell(Text(
item["nama_imunisasi"] ?? "-",
style: GoogleFonts.poppins(
fontSize: 12))),
DataCell(Text(
"${item["usia_min"]} Bln",
style: GoogleFonts.poppins(
fontSize: 12))),
DataCell(Text(
"${item["usia_max"]} Bln",
style: GoogleFonts.poppins(
fontSize: 12))),
DataCell(
Container(
constraints:
const BoxConstraints(
maxWidth: 180),
child: Text(
item["keterangan"] ?? "-",
style: GoogleFonts.poppins(
fontSize: 12),
overflow:
TextOverflow.ellipsis,
maxLines: 2,
),
),
),
DataCell(
Row(
children: [
IconButton(
icon: const Icon(
Icons.edit,
color: Colors.orange,
size: 20),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
EditImunisasiPage(
data: item),
),
).then((value) {
if (value == true)
fetchMasterData();
});
},
),
IconButton(
icon: const Icon(
Icons.delete,
color: Colors.red,
size: 20),
onPressed: () =>
_showDeleteDialog(
item),
),
],
),
),
]);
}).toList(),
),
),
),
),
),
const SizedBox(height: 25),
// --- BUTTON ACTION ---
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 850),
child: Row(
children: [
Expanded(
child: _actionBtnLonjong(
Icons.add, "Tambah Jenis", Colors.blue,
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const TambahImunisasiPage(),
),
).then((_) => fetchMasterData());
}),
),
const SizedBox(width: 12),
Expanded(
child: _actionBtnLonjong(
Icons.visibility,
"Imunisasi Balita",
Colors.deepPurple, () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const DataImunisasiBalitaPage(),
),
);
}),
),
],
),
),
),
],
),
),
),
// --- PAGINATION ---
Container(
width: double.infinity,
color: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Halaman ${_currentPage + 1} dari $totalPages",
style: GoogleFonts.poppins(
fontSize: 12, fontWeight: FontWeight.w500)),
Row(
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: _currentPage == 0
? null
: () => setState(() => _currentPage--),
),
IconButton(
icon: const Icon(Icons.chevron_right),
onPressed: _currentPage >= totalPages - 1
? null
: () => setState(() => _currentPage++),
),
],
),
],
),
),
],
),
),
);
}
DataColumn _buildDataColumn(String label) {
return DataColumn(
label: Text(
label,
style: GoogleFonts.poppins(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
);
}
Widget _actionBtnLonjong(
IconData icon, String label, Color color, VoidCallback onTap) {
return OutlinedButton.icon(
onPressed: onTap,
icon: Icon(icon, size: 18, color: color),
style: OutlinedButton.styleFrom(
side: BorderSide(color: color, width: 1.5),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 14),
),
label: Text(
label,
style: GoogleFonts.poppins(
fontSize: 12, color: color, fontWeight: FontWeight.bold),
),
);
}
}

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