Repository: ryanaidilp/sistem_absensi_pegawai_app Branch: master Commit: 896c2201a003 Files: 107 Total size: 466.7 KB Directory structure: gitextract_q81wuefv/ ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── banuacoders/ │ │ │ │ └── siap/ │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ ├── gradient_background.xml │ │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ └── values/ │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── settings.gradle │ └── settings_aar.gradle ├── assets/ │ └── flare/ │ ├── documents.flr │ ├── empty.flr │ ├── failure.flr │ ├── not_found.flr │ ├── qrcode.flr │ └── success.flr ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ └── Runner.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── lib/ │ ├── main.dart │ ├── models/ │ │ ├── absent_permission.dart │ │ ├── employee.dart │ │ ├── holiday.dart │ │ ├── location.dart │ │ ├── notification.dart │ │ ├── outstation.dart │ │ ├── paid_leave.dart │ │ ├── presence.dart │ │ ├── report/ │ │ │ ├── absent_report.dart │ │ │ ├── daily.dart │ │ │ ├── monthly.dart │ │ │ └── yearly.dart │ │ └── user.dart │ ├── network/ │ │ ├── api.dart │ │ └── api_service.dart │ ├── repositories/ │ │ └── data_repository.dart │ ├── screen/ │ │ ├── application_screen.dart │ │ ├── bottom_nav_screen.dart │ │ ├── change_absent_permission_photo_screen.dart │ │ ├── change_outstation_photo_screen.dart │ │ ├── change_paid_leave_photo_screen.dart │ │ ├── change_pass_screen.dart │ │ ├── create_notification_screen.dart │ │ ├── create_outstation_screen.dart │ │ ├── create_paid_leave_screen.dart │ │ ├── create_permission_screen.dart │ │ ├── employee_attendance_screen.dart │ │ ├── employee_list_screen.dart │ │ ├── employee_outstation.dart │ │ ├── employee_paid_leave_screen.dart │ │ ├── employee_permission.dart │ │ ├── forgot_pass_screen.dart │ │ ├── home_screen.dart │ │ ├── image_detail_screen.dart │ │ ├── login_screen.dart │ │ ├── notification_list_screen.dart │ │ ├── on_boarding_screen.dart │ │ ├── outstation_list_screen.dart │ │ ├── paid_leave_list_screen.dart │ │ ├── permission_list_screen.dart │ │ ├── presence_screen.dart │ │ ├── regulation_screen.dart │ │ ├── report_screen.dart │ │ └── splash_screen.dart │ ├── utils/ │ │ ├── app_const.dart │ │ ├── extensions.dart │ │ ├── file_util.dart │ │ └── view_util.dart │ └── widgets/ │ ├── employee_presence_card_widget.dart │ ├── employee_proposal_info_widget.dart │ ├── employee_proposal_widget.dart │ ├── image_placeholder_widget.dart │ ├── next_presence_empty_card_widget.dart │ ├── statistics_card_widget.dart │ └── user_info_card_widget.dart ├── privacy-policy.md ├── pubspec.yaml └── test/ └── widget_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # Environment Variable .env # 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 .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json ================================================ FILE: .metadata ================================================ # 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: 8874f21e79d7ec66d0457c7ab338348e31b17f1d channel: stable project_type: app ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: README.md ================================================

SiAP

Aplikasi Android untuk manajemen data presensi pegawai di lingkungan kantor pemerintahan Kecamatan Balaesang. Aplikasi ini dibangun dengan [Flutter](https://flutter.dev) [![SIAP](https://i.ibb.co/Xz9Dppd/thumb.png)](https://play.google.com/store/apps/details?id=com.banuacoders.siap) ## About Aplikasi ini dibangun untuk mengatasi permasalahan pencatatan absensi pegawai di lingkungan kantor pemerintahan Kecamatan Balaesang. Pencatatan kehadiran pegawai di kantor pemerintahan Kecamatan Balaesang selama ini masih dilakukan secara manual yaitu dengan memberi paraf pada absensi. Permasalahan timbul saat sebagian besar pegawai tidak jujur dalam mengisi absen tersebut, ada yang titip ke teman untuk diparaf namanya, ada yang langsung isi absen sampai beberapa hari ke depan, ada yang mengisi absen diluar waktunya, dsb. Dengan adanya sistem ini, diharapkan bisa membantu mengatasi permasalahan-permasalahan yang telah disebutkan. ## License **SiAP** is open-sourced software licensed under the [GPL v2.0](https://www.gnu.org/licenses/gpl-2.0.html). ## Konfigurasi *Clone* repository back-end aplikasinya [disini](https://github.com/ryanaidilp/sistem_absensi_pegawai). Buat file **.env** pada root folder aplikasi ini lalu tambahkan variabel berikut ```dotenv BASE_URL=BASE URL ONLINE LOCAL_URL=LOCAL_URL #Jika kamu ingin menyambungkian ke server di localhost ONE_SIGNAL_APP_ID=APP ID UNTUK ONE SIGNAL ADMIN_PHONE_NUMBER=Nomor handphone admin server ``` Isikan variabel sesuai dengan konfigurasi anda. Untuk mendapatkan `ONE_SIGNAL_APP_ID`, buat akun di [One Signal](https://app.onesignal.com) lalu ikuti petunjuk cara untuk mendapatkan `APP_KEY` melalui dokumentasi resmi One Signal. Pastikan `ONE_SIGNAL_APP_ID` pada aplikasi ini sama dengan yang digunakan di aplikasi [backend](https://github.com/ryanaidilp/sistem_absensi_pegawai) ## Screenshoot ![Splash Screen](https://i.ibb.co/p4n5K3D/splash-screen.gif)![Login Screen](https://i.ibb.co/5TmZYTT/login-screen.gif)![Home Screen](https://i.ibb.co/qyJB11s/home-screen.gif)![Presence Screen](https://i.ibb.co/SVGrL5r/presence-screen.gif)![Presence Process Screen](https://i.ibb.co/6H8YmB9/presence-process-screen.gif)![Employee List Screen](https://i.ibb.co/4dGZXR3/employee-list-screen.gif)![Statistics Screen](https://i.ibb.co/5n2gGzc/statistics-screen.gif)![Statistics Screen](https://i.ibb.co/RYtT7gH/statistics-screen-2.gif)![Absent Permissions Screen](https://i.ibb.co/XSFKdX6/absent-permission-screen.gif)![Outstations Screen](https://i.ibb.co/VqSvMQF/outstation-screen.gif)![Paid Leaves Screen](https://i.ibb.co/zV6m0C8/paid-leave-screen.gif)![Employee Presence Screen](https://i.ibb.co/KXMyMq6/employee-presence-list.gif)![Employee Presence List Screen](https://i.ibb.co/bN1cBnJ/employee-presence-screen-2.gif)![Notifications List & Create Notification Screen](https://i.ibb.co/NtH4pbT/notification-list-screen.gif) ================================================ FILE: analysis_options.yaml ================================================ include: package:lint/analysis_options.yaml ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties ================================================ FILE: android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply plugin: 'com.onesignal.androidsdk.onesignal-gradle-plugin' apply from: project(':flutter_config').projectDir.getPath() + "/dotenv.gradle" def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if(keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } android { compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' } lintOptions { disable 'InvalidPackage' } defaultConfig { applicationId "com.banuacoders.siap" minSdkVersion 24 targetSdkVersion 30 versionCode 46 versionName '5.0.3' } signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } ================================================ FILE: android/app/proguard-rules.pro ================================================ -keep class com.banuacoders.siap.BuildConfig{ *; } ## Flutter wrapper -keep class io.flutter.app.** { *; } -keep class io.flutter.plugin.** { *; } -keep class io.flutter.util.** { *; } -keep class io.flutter.view.** { *; } -keep class io.flutter.** { *; } -keep class io.flutter.plugins.** { *; } -dontwarn io.flutter.embedding.** ## Gson rules # Gson uses generic type information stored in a class file when working with fields. Proguard # removes such information by default, so configure it to keep all of it. -keepattributes Signature # For using GSON @Expose annotation -keepattributes *Annotation* # Gson specific classes -dontwarn sun.misc.** #-keep class com.google.gson.stream.** { *; } # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) -keep class * extends com.google.gson.TypeAdapter -keep class * implements com.google.gson.TypeAdapterFactory -keep class * implements com.google.gson.JsonSerializer -keep class * implements com.google.gson.JsonDeserializer # Prevent R8 from leaving Data object members always null -keepclassmembers,allowobfuscation class * { @com.google.gson.annotations.SerializedName ; } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/com/banuacoders/siap/MainActivity.kt ================================================ package com.banuacoders.siap import io.flutter.embedding.android.FlutterActivity import android.os.Build import android.view.ViewTreeObserver import android.view.WindowManager class MainActivity: FlutterActivity() ================================================ FILE: android/app/src/main/res/drawable/gradient_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: android/app/src/main/res/values/colors.xml ================================================ #0C2979 #71A7F8 ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.3.50' repositories { google() jcenter() maven { url 'https://plugins.gradle.org/m2/' } } dependencies { classpath 'com.android.tools.build:gradle:3.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'gradle.plugin.com.onesignal:onesignal-gradle-plugin:[0.12.9, 0.99.99]' } } allprojects { repositories { google() jcenter() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true android.enableR8=true ================================================ FILE: android/settings.gradle ================================================ include ':app' def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" ================================================ FILE: android/settings_aar.gradle ================================================ include ':app' ================================================ FILE: ios/.gitignore ================================================ *.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/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 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 9.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include "Generated.xcconfig" ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { var flutter_native_splash = 1 UIApplication.shared.isStatusBarHidden = false GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "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" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "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" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # 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. ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName spo_balaesang CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance UIStatusBarHidden ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile 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 = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 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 = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 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 = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 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 = { LastUpgradeCheck = 1020; ORGANIZATIONNAME = ""; TargetAttributes = { 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 */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 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; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); 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; 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 */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.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; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.banuacoders.siapesang; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 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; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.banuacoders.siapesang; 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; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.banuacoders.siapesang; 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 */ 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 */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: lib/main.dart ================================================ import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_config/flutter_config.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/network/api.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/splash_screen.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart' as tz; import 'network/api_service.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); Future main() async { await initializeDateFormatting(); WidgetsFlutterBinding.ensureInitialized(); await FlutterConfig.loadEnvVariables(); Intl.defaultLocale = 'id_ID'; OneSignal.shared.setLogLevel(OSLogLevel.verbose, OSLogLevel.none); OneSignal.shared.setLocationShared(true); OneSignal.shared.init(FlutterConfig.get("ONE_SIGNAL_APP_ID").toString(), iOSSettings: { OSiOSSettings.autoPrompt: false, OSiOSSettings.inAppLaunchUrl: false }); OneSignal.shared .setInFocusDisplayType(OSNotificationDisplayType.notification); const initializedSettingsAndroid = AndroidInitializationSettings('ic_stat_onesignal_default'); const initializationSettings = InitializationSettings(android: initializedSettingsAndroid); await flutterLocalNotificationsPlugin.initialize(initializationSettings); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) .then((_) { runApp(MyApp()); }); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { @override Widget build(BuildContext context) { return Provider( create: (_) => DataRepository(apiService: ApiService(api: API())), child: GetMaterialApp( debugShowCheckedModeBanner: false, title: 'SIAP Balaesang', theme: ThemeData( primarySwatch: Colors.blue, backgroundColor: Colors.white, scaffoldBackgroundColor: Colors.grey[100], appBarTheme: const AppBarTheme( brightness: Brightness.dark, )), home: SplashScreen()), ); } } Future scheduleAlarm( DateTime scheduledNotificationDateTime, String body) async { final androidPlatformChannelSpecifics = AndroidNotificationDetails( 'alarm_id', 'alarm_id', 'Channel alarm', icon: 'ic_stat_onesignal_default', enableLights: true, priority: Priority.high, importance: Importance.max, vibrationPattern: Int64List.fromList([0, 1000, 5000, 2000])); tz.initializeTimeZones(); tz.setLocalLocation(tz.getLocation('Asia/Makassar')); final scheduleTime = tz.TZDateTime.from( scheduledNotificationDateTime, tz.getLocation('Asia/Makassar')); final platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics); await flutterLocalNotificationsPlugin.zonedSchedule( Random().nextInt(int.parse(pow(2, 31).toString())), 'Pengingat', body, scheduleTime, platformChannelSpecifics, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, androidAllowWhileIdle: true, ); } ================================================ FILE: lib/models/absent_permission.dart ================================================ import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class AbsentPermission { const AbsentPermission( {this.id, this.title, this.dueDate, this.startDate, this.description, this.photo, this.approvalStatus, this.isApproved, this.user}); final int id; final String title; final String description; final bool isApproved; final String photo; final String approvalStatus; final DateTime dueDate; final DateTime startDate; final User user; factory AbsentPermission.fromJson(Map json) { return AbsentPermission( id: json[absentPermissionIdField] as int, title: json[absentPermissionTitleField] as String, description: json[absentPermissionDescriptionField] as String, isApproved: json[absentPermissionIsApprovedField] as bool, photo: json[absentPermissionPhotoField] as String, approvalStatus: json[approvalStatusField] as String, dueDate: DateTime.parse(json[absentPermissionDueDateField].toString()), startDate: DateTime.parse(json[absentPermissionStartDateField].toString()), user: json[absentPermissionUserField] != null ? User.fromJson( json[absentPermissionUserField] as Map) : null); } } ================================================ FILE: lib/models/employee.dart ================================================ import 'package:spo_balaesang/models/presence.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class Employee { const Employee( {this.id, this.nip, this.name, this.phone, this.gender, this.department, this.status, this.position, this.presences, this.rank, this.group}); final int id; final String nip; final String name; final String phone; final String gender; final String department; final String status; final String position; final String rank; final String group; final List presences; factory Employee.fromJson(Map json) { return Employee( id: json[userIdField] as int, nip: json[userNipField] as String, name: json[userNameField] as String, phone: json[userPhoneField] as String, gender: json[userGenderField] as String, department: json[userDepartmentField] as String, rank: json[userRankField] as String, group: json[userGroupField] as String, status: json[userStatusField] as String, position: json[userPositionField] as String, presences: json[userPresencesField] != null ? (json[userPresencesField] as List) .map((json) => Presence.fromJson(json as Map)) .toList() : null); } Map toJson() => { userIdField: id, userNameField: name, userNipField: nip, userPhoneField: phone, userGenderField: gender, userDepartmentField: department, userRankField: rank, userGroupField: group, userStatusField: status, userPositionField: position }; } ================================================ FILE: lib/models/holiday.dart ================================================ import 'package:spo_balaesang/utils/app_const.dart'; class Holiday { const Holiday({this.date, this.name, this.description}); final DateTime date; final String name; final String description; factory Holiday.fromJson(Map json) => Holiday( date: json == null ? DateTime.now() : DateTime.parse(json[holidayDateField].toString()), name: json[holidayNameField] as String, description: json[holidayDescriptionField] as String); } ================================================ FILE: lib/models/location.dart ================================================ import 'package:spo_balaesang/utils/app_const.dart'; class Location { const Location({this.latitude, this.longitude, this.address}); final double latitude; final double longitude; final String address; factory Location.fromJson(Map json) { return Location( latitude: double.parse(json[locationLatitudeField].toString()), longitude: double.parse(json[locationLongitudeField].toString()), address: json[locationAddressField] as String); } Map toJson() => { locationLatitudeField: latitude, locationLongitudeField: longitude, locationAddressField: address }; } ================================================ FILE: lib/models/notification.dart ================================================ import 'package:spo_balaesang/utils/app_const.dart'; class UserNotification { const UserNotification( {this.id, this.notifiableId, this.notifiableType, this.data, this.isRead}); final String id; final int notifiableId; final String notifiableType; final Map data; final bool isRead; factory UserNotification.fromJson(Map json) { return UserNotification( id: json[notificationIdField] as String, notifiableId: json[notificationNotifiableIdField] as int, notifiableType: json[notificationNotifiableTypeField] as String, data: json[jsonDataField] as Map, isRead: json[notificationIsReadField] as bool); } } ================================================ FILE: lib/models/outstation.dart ================================================ import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class Outstation { const Outstation( {this.id, this.title, this.description, this.isApproved, this.photo, this.approvalStatus, this.dueDate, this.startDate, this.user}); final int id; final String title; final String description; final bool isApproved; final String photo; final String approvalStatus; final DateTime dueDate; final DateTime startDate; final User user; factory Outstation.fromJson(Map json) { return Outstation( id: json[outstationIdField] as int, title: json[outstationTitleField] as String, description: json[outstationDescriptionField] as String, isApproved: json[outstationIsApprovedField] as bool, photo: json[outstationPhotoField] as String, approvalStatus: json[approvalStatusField] as String, dueDate: DateTime.parse(json[outstationDueDateField].toString()), startDate: DateTime.parse(json[outstationStartDateField].toString()), user: json[outstationUserField] != null ? User.fromJson(json[outstationUserField] as Map) : null); } } ================================================ FILE: lib/models/paid_leave.dart ================================================ import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class PaidLeave { const PaidLeave( {this.title, this.id, this.category, this.photo, this.approvalStatus, this.description, this.startDate, this.dueDate, this.isApproved, this.user}); final int id; final String title; final String category; final String description; final bool isApproved; final String approvalStatus; final DateTime startDate; final DateTime dueDate; final String photo; final User user; factory PaidLeave.fromJson(Map json) => PaidLeave( id: json[paidLeaveIdField] as int, title: json[paidLeaveTitleField] as String, category: json[paidLeaveCategoryField] as String, description: json[paidLeaveDescriptionField] as String, isApproved: json[paidLeaveIsApprovedField] as bool, approvalStatus: json[approvalStatusField] as String, startDate: DateTime.parse(json[paidLeaveStartDateField].toString()), dueDate: DateTime.parse(json[paidLeaveDueDateField].toString()), photo: json[paidLeavePhotoField] as String, user: json[paidLeaveUserField] != null ? User.fromJson(json[paidLeaveUserField] as Map) : null); } ================================================ FILE: lib/models/presence.dart ================================================ import 'package:spo_balaesang/models/location.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class Presence { const Presence( {this.id, this.date, this.codeType, this.status, this.attendTime, this.location, this.photo, this.startTime, this.endTime}); final int id; final DateTime date; final String codeType; final String status; final String attendTime; final DateTime startTime; final DateTime endTime; final Location location; final String photo; factory Presence.fromJson(Map json) { return Presence( id: json[userIdField] as int, date: DateTime.parse(json[presenceDateField].toString()), codeType: json[presenceCodeTypeField] as String, status: json[presenceStatusField] as String, attendTime: json[presenceAttendTimeField] as String, location: Location.fromJson( json[presenceLocationField] as Map), photo: json[presencePhotoField] as String, startTime: DateTime.parse(json[presenceStartTimeField].toString()), endTime: DateTime.parse(json[presenceEndTimeField].toString()), ); } } ================================================ FILE: lib/models/report/absent_report.dart ================================================ import 'package:spo_balaesang/models/holiday.dart'; import 'package:spo_balaesang/models/report/daily.dart'; import 'package:spo_balaesang/models/report/monthly.dart'; import 'package:spo_balaesang/models/report/yearly.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class AbsentReport { const AbsentReport( {this.daily, this.monthly, this.yearly, this.holidays, this.totalWorkDay}); final List daily; final Monthly monthly; final Yearly yearly; final List holidays; final int totalWorkDay; factory AbsentReport.fromJson(Map json) => AbsentReport( daily: (json[absentReportDailyField] as List) .map((item) => Daily.fromJson(item as Map)) .toList(), monthly: Monthly.fromJson( json[absentReportMonthlyField] as Map), yearly: Yearly.fromJson( json[absentReportYearlyField] as Map), holidays: (json[absentReportHolidaysField] as List) .map((item) => Holiday.fromJson(item as Map)) .toList(), totalWorkDay: json[reportTotalWorkDayField] as int); } ================================================ FILE: lib/models/report/daily.dart ================================================ import 'package:spo_balaesang/models/location.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class Daily { const Daily({this.date, this.attendancePercentage, this.attendances}); final DateTime date; final double attendancePercentage; final List attendances; factory Daily.fromJson(Map json) { final List _presences = json[dailyPresencesField] as List; return Daily( date: DateTime.parse(json[dailyDateField].toString()), attendancePercentage: double.parse(json[reportAttendancePercentageFieldField].toString()), attendances: _presences .map((json) => DailyData.fromJson(json as Map)) .toList(), ); } } class DailyData { const DailyData( {this.id, this.date, this.location, this.attendType, this.attendTime, this.attendStatus, this.startTime, this.endTime, this.address, this.photo}); final int id; final String date; final Location location; final String attendType; final String attendTime; final String attendStatus; final DateTime startTime; final DateTime endTime; final String address; final String photo; factory DailyData.fromJson(Map json) => DailyData( id: json[userIdField] as int, date: json[presenceDateField].toString(), location: Location.fromJson( json[presenceLocationField] as Map), attendTime: json[dailyDataAttendTimeField].toString(), attendType: json[dailyDataAttendTypeField].toString(), attendStatus: json[dailyDataAttendStatusField].toString(), startTime: DateTime.parse(json[presenceStartTimeField].toString()), endTime: DateTime.parse(json[presenceEndTimeField].toString()), address: json[locationAddressField] as String, photo: json[presencePhotoField] as String); Map toMap() => { userIdField: id, dailyDataAttendTypeField: attendType, dailyDataAttendTimeField: attendTime, dailyDataAttendStatusField: attendStatus, presenceStartTimeField: startTime.toString(), locationAddressField: address, presencePhotoField: photo }; Map toPresenceJson() => { userIdField: id, presenceDateField: date, presenceLocationField: location.toJson(), presenceCodeTypeField: attendType, presenceStatusField: attendStatus, presenceAttendTimeField: attendTime, presenceStartTimeField: startTime.toString(), presenceEndTimeField: endTime.toString(), presencePhotoField: photo }; } ================================================ FILE: lib/models/report/monthly.dart ================================================ import 'package:spo_balaesang/utils/app_const.dart'; class Monthly { const Monthly( {this.lateCount, this.attendancePercentage, this.leaveEarlyCount, this.notMorningParadeCount, this.earlyLunchBreakCount, this.notComeAfterLunchBreakCount}); final double attendancePercentage; final int lateCount; final int leaveEarlyCount; final int notMorningParadeCount; final int earlyLunchBreakCount; final int notComeAfterLunchBreakCount; factory Monthly.fromJson(Map json) => Monthly( attendancePercentage: double.parse(json[reportAttendancePercentageFieldField].toString()), lateCount: json[reportLateCountField] as int, leaveEarlyCount: json[reportLeaveEarlyFieldCountField] as int, notMorningParadeCount: json[reportNotMorningParadeCountField] as int, earlyLunchBreakCount: json[reportEarlyLunchBreakCountField] as int, notComeAfterLunchBreakCount: json[reportNotComeAfterLunchBreakCountField] as int); } ================================================ FILE: lib/models/report/yearly.dart ================================================ import 'package:spo_balaesang/utils/app_const.dart'; class Yearly { const Yearly({ this.attendancePercentage, this.outstation, this.absent, this.lateCount, this.absentPermission, this.leaveEarlyCount, this.earlyLunchBreakCount, this.notComeAfterLunchBreakCount, this.notMorningParadeCount, this.annualLeave, this.importantReasonLeave, this.sickLeave, this.maternityLeave, this.outOfLiabilityLeave, }); final double attendancePercentage; final int lateCount; final int leaveEarlyCount; final int notMorningParadeCount; final int earlyLunchBreakCount; final int notComeAfterLunchBreakCount; final Map absentPermission; final Map outstation; final Map absent; final Map annualLeave; final Map importantReasonLeave; final Map sickLeave; final Map maternityLeave; final Map outOfLiabilityLeave; factory Yearly.fromJson(Map json) { return Yearly( lateCount: json[reportLateCountField] as int, attendancePercentage: double.parse(json[reportAttendancePercentageFieldField].toString()), absent: json[yearlyAbsentField] as Map, absentPermission: json[yearlyAbsentPermissionField] as Map, outstation: json[yearlyOutstationField] as Map, leaveEarlyCount: json[reportLeaveEarlyFieldCountField] as int, notMorningParadeCount: json[reportNotMorningParadeCountField] as int, earlyLunchBreakCount: json[reportEarlyLunchBreakCountField] as int, notComeAfterLunchBreakCount: json[reportNotComeAfterLunchBreakCountField] as int, annualLeave: json[reportAnnualLeaveField] as Map, importantReasonLeave: json[reportImportantReasonLeaveField] as Map, sickLeave: json[reportSickLeaveField] as Map, maternityLeave: json[reportMaternityLeaveField] as Map, outOfLiabilityLeave: json[reportOutOfLiabilityLeaveField] as Map); } } ================================================ FILE: lib/models/user.dart ================================================ import 'package:spo_balaesang/models/holiday.dart'; import 'package:spo_balaesang/models/presence.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class User { const User( {this.id, this.nip, this.name, this.phone, this.gender, this.department, this.status, this.position, this.unreadNotification, this.token, this.nextPresence, this.presences, this.holiday, this.isWeekend, this.rank, this.group}); final int id; final String nip; final String name; final String phone; final String gender; final String department; final String status; final String position; final int unreadNotification; final String token; final Presence nextPresence; final List presences; final Holiday holiday; final bool isWeekend; final String rank; final String group; factory User.fromJson(Map json) { return User( id: json[userIdField] as int, nip: json[userNipField] as String, name: json[userNameField] as String, phone: json[userPhoneField] as String, gender: json[userGenderField] as String, department: json[userDepartmentField] as String, status: json[userStatusField] as String, position: json[userPositionField] as String, unreadNotification: json[userUnreadNotificationsCountField] as int, holiday: json[userIsHolidayField] == null ? null : Holiday.fromJson( json[userIsHolidayField] as Map), isWeekend: json[userIsWeekendField] as bool, token: json[userTokenField] as String, nextPresence: json[userNextPresenceField] != null ? Presence.fromJson(json[userNextPresenceField][jsonDataField] as Map) : null, presences: ((json[userPresencesField] != null) && (json[userPresencesField] as List).isNotEmpty) ? (json[userPresencesField] as List) .map((json) => Presence.fromJson(json as Map)) .toList() : [], rank: json[userRankField] as String, group: json[userGroupField] as String); } } ================================================ FILE: lib/network/api.dart ================================================ import 'package:flutter_config/flutter_config.dart'; enum Endpoint { login, logout, changePass, users, presence, my, permission, employeePermission, approvePermission, outstation, employeeOutstation, approveOutstation, notifications, readNotifications, deleteNotifications, sendNotifications, statistics, paidLeave, employeePaidLeave, approvePaidLeave, cancelAttendance, changePermissionPhoto, changeOutstationPhoto, changePaidLeavePhoto, } class API { final String host = FlutterConfig.get("BASE_URL").toString(); String endpointUri(Endpoint endpoint) => '$host/api/${_paths[endpoint]}'; static final Map _paths = { Endpoint.login: 'login', Endpoint.logout: 'logout', Endpoint.changePass: 'change_password', Endpoint.users: 'user', Endpoint.presence: 'presence', Endpoint.my: 'my', Endpoint.permission: 'permission', Endpoint.employeePermission: 'permission/all', Endpoint.approvePermission: 'permission/approve', Endpoint.outstation: 'outstation', Endpoint.employeeOutstation: 'outstation/all', Endpoint.approveOutstation: 'outstation/approve', Endpoint.notifications: 'notifications', Endpoint.readNotifications: 'notifications/read', Endpoint.deleteNotifications: 'notifications/delete', Endpoint.sendNotifications: 'notifications/send', Endpoint.statistics: 'statistics', Endpoint.paidLeave: 'paid-leave', Endpoint.employeePaidLeave: 'paid-leave/all', Endpoint.approvePaidLeave: 'paid-leave/approve', Endpoint.cancelAttendance: 'presence/cancel', Endpoint.changePermissionPhoto: 'permission/picture', Endpoint.changeOutstationPhoto: 'outstation/picture', Endpoint.changePaidLeavePhoto: 'paid-leave/picture', }; } ================================================ FILE: lib/network/api_service.dart ================================================ import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spo_balaesang/network/api.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class ApiService { ApiService({@required this.api}); final API api; String token = ''; Future _getToken() async { final SharedPreferences localStorage = await SharedPreferences.getInstance(); token = jsonDecode(localStorage.getString(prefsTokenKey)) as String; } Future> getEndpointData( {@required Endpoint endpoint, Map query}) async { final url = api.endpointUri(endpoint); await _getToken(); Uri uri = Uri.parse(url); if (query != null) { uri = uri.replace(queryParameters: query); } final response = await http.get(uri, headers: _setHeaders()); if (response.statusCode == 200) { final Map data = json.decode(response.body) as Map; if (data['success'] as bool) { return data; } } throw response; } Future postEndpointWithoutToken( {@required Endpoint endpoint, Map data}) async { final url = api.endpointUri(endpoint); final Uri uri = Uri.parse(url); final response = await http.post(uri, body: data); return response; } Future postEndpointWithToken( {@required Endpoint endpoint, Map data}) async { await _getToken(); final url = api.endpointUri(endpoint); final Uri uri = Uri.parse(url); final response = http.post(uri, body: jsonEncode(data), headers: _setHeaders()); return response; } Map _setHeaders() => { 'Content-type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer $token' }; } ================================================ FILE: lib/repositories/data_repository.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:http/http.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spo_balaesang/models/employee.dart'; import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/network/api.dart'; import 'package:spo_balaesang/network/api_service.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class DataRepository { DataRepository({@required this.apiService}); final ApiService apiService; Future> getAllEmployee() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); List employee; try { final Map _data = await apiService.getEndpointData(endpoint: Endpoint.users); final List _result = _data['data'] as List; if (prefs.containsKey(prefsEmployeeKey)) { prefs.remove(prefsEmployeeKey); prefs.reload(); } prefs.setString(prefsEmployeeKey, jsonEncode(_data['data'])); employee = _result .map( (dynamic json) => Employee.fromJson(json as Map)) .toList(); } on SocketException { final Map data = jsonDecode(prefs.getString(prefsEmployeeKey)) as Map; final List _data = data['data'] as List; employee = _data .map( (dynamic json) => Employee.fromJson(json as Map)) .toList(); } catch (e) {} return employee; } Future getMyData() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); User user; try { final Map _data = await apiService.getEndpointData(endpoint: Endpoint.my); prefs.remove(prefsUserKey); prefs.reload(); prefs.setString(prefsUserKey, jsonEncode(_data['data'])); user = User.fromJson(_data['data'] as Map); } on SocketException { return null; } catch (e) {} return user; } Future> logout() async { Map response; try { final Map _data = await apiService.getEndpointData(endpoint: Endpoint.logout); response = _data; } catch (e) {} return response; } Future login(Map data) async { Response response; try { response = await apiService.postEndpointWithoutToken( endpoint: Endpoint.login, data: data); } catch (e) {} return response; } Future changePass(Map data) async { Response response; try { response = await apiService.postEndpointWithToken( endpoint: Endpoint.changePass, data: data); } catch (e) {} return response; } Future> permission(Map data) async { Map result; try { final response = await apiService.postEndpointWithToken( endpoint: Endpoint.permission, data: data); result = jsonDecode(response.body) as Map; } catch (e) {} return result; } Future> getAllPermissions(DateTime date) async { Map data; try { data = await apiService.getEndpointData( endpoint: Endpoint.permission, query: {'date': date.toString()}); } catch (e) {} return data; } Future> getAllEmployeePermissions(DateTime date) async { Map data; try { data = await apiService.getEndpointData( endpoint: Endpoint.employeePermission, query: {'date': date.toString()}); } catch (e) {} return data; } Future approvePermission(Map data) async { Response response; try { response = await apiService.postEndpointWithToken( endpoint: Endpoint.approvePermission, data: data); } catch (e) {} return response; } Future> changePermissionPhoto( Map data) async { Map result; try { final response = await apiService.postEndpointWithToken( endpoint: Endpoint.changePermissionPhoto, data: data); result = jsonDecode(response.body) as Map; } catch (e) {} return result; } Future> outstation(Map data) async { Map result; try { final response = await apiService.postEndpointWithToken( endpoint: Endpoint.outstation, data: data); result = jsonDecode(response.body) as Map; } catch (e) {} return result; } Future> getAllOutstation(DateTime date) async { Map data; try { data = await apiService.getEndpointData( endpoint: Endpoint.outstation, query: {'date': date.toString()}); } catch (e) {} return data; } Future> getAllEmployeeOutstation(DateTime date) async { Map data; try { data = await apiService.getEndpointData( endpoint: Endpoint.employeeOutstation, query: {'date': date.toString()}); } catch (e) {} return data; } Future approveOutstation(Map data) async { Response response; try { response = await apiService.postEndpointWithToken( endpoint: Endpoint.approveOutstation, data: data); } catch (e) {} return response; } Future> changeOutstationPhoto( Map data) async { Map result; try { final response = await apiService.postEndpointWithToken( endpoint: Endpoint.changeOutstationPhoto, data: data); result = jsonDecode(response.body) as Map; } catch (e) {} return result; } Future> getAllNotifications() async { Map data; try { data = await apiService.getEndpointData(endpoint: Endpoint.notifications); } catch (e) {} return data; } Future> getStatistics(DateTime date, [int userId]) async { Map data; final Map queries = { 'year': date.year.toString(), 'month': date.month.toString(), }; if (userId != null) { queries['user_id'] = userId.toString(); } try { data = await apiService.getEndpointData( endpoint: Endpoint.statistics, query: queries); } catch (e) {} return data; } Future readNotification(Map data) async { Response response; try { response = await apiService.postEndpointWithToken( endpoint: Endpoint.notifications, data: data); } catch (e) {} return response; } Future> readAllNotifications() async { Map data; try { data = await apiService.getEndpointData( endpoint: Endpoint.readNotifications); } catch (e) {} return data; } Future> deleteAllNotifications() async { Map data; try { data = await apiService.getEndpointData( endpoint: Endpoint.deleteNotifications); } catch (e) {} return data; } Future> sendNotification( Map data) async { Map result; try { final response = await apiService.postEndpointWithToken( endpoint: Endpoint.sendNotifications, data: data); result = jsonDecode(response.body) as Map; } catch (e) {} return result; } Future presence(Map data) async { Response response; try { response = await apiService.postEndpointWithToken( endpoint: Endpoint.presence, data: data); } catch (e) {} return response; } Future> getAllPaidLeave(DateTime date) async { Map data; try { data = await apiService.getEndpointData( endpoint: Endpoint.paidLeave, query: {'date': date.toString()}); } catch (e) {} return data; } Future> getAllEmployeePaidLeave(DateTime date) async { Map data; try { data = await apiService.getEndpointData( endpoint: Endpoint.employeePaidLeave, query: {'date': date.toString()}); } catch (e) {} return data; } Future> changePaidLeavePhoto( Map data) async { Map result; try { final response = await apiService.postEndpointWithToken( endpoint: Endpoint.changePaidLeavePhoto, data: data); result = jsonDecode(response.body) as Map; } catch (e) {} return result; } Future approvePaidLeave(Map data) async { Response response; try { response = await apiService.postEndpointWithToken( endpoint: Endpoint.approvePaidLeave, data: data); } catch (e) {} return response; } Future> paidLeave(Map data) async { Map result; try { final response = await apiService.postEndpointWithToken( endpoint: Endpoint.paidLeave, data: data); result = jsonDecode(response.body) as Map; } catch (e) {} return result; } Future cancelAttendance(Map data) async { Response response; try { response = await apiService.postEndpointWithToken( endpoint: Endpoint.cancelAttendance, data: data); } catch (e) {} return response; } } ================================================ FILE: lib/screen/application_screen.dart ================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spo_balaesang/main.dart'; import 'package:spo_balaesang/models/presence.dart'; import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/change_pass_screen.dart'; import 'package:spo_balaesang/screen/employee_attendance_screen.dart'; import 'package:spo_balaesang/screen/employee_outstation.dart'; import 'package:spo_balaesang/screen/employee_paid_leave_screen.dart'; import 'package:spo_balaesang/screen/employee_permission.dart'; import 'package:spo_balaesang/screen/forgot_pass_screen.dart'; import 'package:spo_balaesang/screen/login_screen.dart'; import 'package:spo_balaesang/screen/outstation_list_screen.dart'; import 'package:spo_balaesang/screen/paid_leave_list_screen.dart'; import 'package:spo_balaesang/screen/permission_list_screen.dart'; import 'package:spo_balaesang/screen/regulation_screen.dart'; import 'package:spo_balaesang/screen/report_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; class ApplicationScreen extends StatefulWidget { @override _ApplicationScreenState createState() => _ApplicationScreenState(); } class _ApplicationScreenState extends State { User user; bool _isAlarmActive = false; List _presences; Future loadData() async { final sp = await SharedPreferences.getInstance(); final _data = sp.get(prefsUserKey); bool _alarm = false; if (sp.containsKey(prefsAlarmKey)) { _alarm = sp.get(prefsAlarmKey) as bool; } else { sp.setBool(prefsAlarmKey, _alarm); } final Map _json = jsonDecode(_data.toString()) as Map; if (_alarm) { await flutterLocalNotificationsPlugin.cancelAll(); } setState(() { user = User.fromJson(_json); _presences = user.presences; _isAlarmActive = _alarm; if (_isAlarmActive) { _presences.forEach(_setAlarm); } }); } Future logout() async { try { Get.defaultDialog( title: 'Keluar', content: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: const [ Icon( Icons.dangerous, color: Colors.red, size: 72, ), sizedBoxH10, Text('Apakah anda yakin ingin keluar dari aplikasi?'), ], ), ), actions: [ TextButton( onPressed: () async { Get.back(); final ProgressDialog pd = ProgressDialog(context, isDismissible: false); pd.show(); final dataRepo = Provider.of(context, listen: false); final Map _response = await dataRepo.logout(); if (_response['success'] as bool) { final SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.remove(prefsTokenKey); prefs.remove(prefsUserKey); prefs.remove(prefsAlarmKey); pd.hide(); OneSignal.shared.removeExternalUserId(); Get.off(() => LoginScreen()); } }, child: const Text('Ya', style: TextStyle( color: Colors.blueAccent, ))), TextButton( onPressed: () { Get.back(); }, child: const Text('Tidak', style: TextStyle( color: Colors.blueAccent, ))), ]); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } } Future _handleSelected(bool value) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.reload(); prefs.setBool(prefsAlarmKey, value); setState(() { _isAlarmActive = value; }); if (value) { _presences.forEach(_setAlarm); showAlertDialog('success', 'Sukses', 'Berhasil mengaktifkan alarm!', dismissible: true); } else { await flutterLocalNotificationsPlugin.cancelAll(); showAlertDialog('success', 'Sukses', 'Berhasil menonaktifkan alarm!', dismissible: true); } } void _setAlarm(Presence presence) { const dur10Min = Duration(minutes: 10); const dur1Day = Duration(days: 1); if (presence.startTime.isAfter(DateTime.now())) { scheduleAlarm(presence.startTime.subtract(dur10Min), '${presence.codeType} akan dimulai dalam 10 menit!'); } else { if (presence.startTime.weekday < DateTime.friday) { scheduleAlarm(presence.startTime.add(dur1Day), '${presence.codeType} akan dimulai dalam 10 menit!'); } } if (presence.endTime.isAfter(DateTime.now())) { scheduleAlarm(presence.endTime.subtract(dur10Min), '${presence.codeType} akan selesai dalam 10 menit!'); } else { if (presence.endTime.weekday < DateTime.friday) { scheduleAlarm(presence.endTime.add(dur1Day), '${presence.codeType} akan selesai dalam 10 menit!'); } } } @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } @override void initState() { super.initState(); loadData(); } Widget _buildStakeholderMenu() { if (user?.position != 'Camat') { return sizedBox; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Atasan', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 18.0, color: Colors.blueAccent), ), dividerT1, sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => EmployeeAttendanceScreen()); }, child: const ListTile( dense: false, leading: Icon( Icons.playlist_add_check_rounded, color: Colors.green, size: 32.0, ), title: Text( 'Presensi Pegawai', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Lihat & konfirmasi kehadiran pegawai', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => EmployeePermissionScreen()); }, child: const ListTile( leading: Icon( Icons.playlist_add_check_rounded, color: Colors.green, size: 32.0, ), title: Text( 'Persetujuan Izin', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Setujui Izin yang diajukan', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => EmployeeOutstationScreen()); }, child: const ListTile( leading: Icon( Icons.playlist_add_check_rounded, color: Colors.green, size: 32.0, ), title: Text( 'Persetujuan Dinas Luar', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Setujui Dinas Luar yang diajukan', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => EmployeePaidLeaveScreen()); }, child: const ListTile( leading: Icon( Icons.playlist_add_check_rounded, color: Colors.green, size: 32.0, ), title: Text( 'Persetujuan Cuti', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Setujui Cuti yang diajukan', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH30, ], ); } Widget _buildCutiSection() { if (user?.status == 'Honorer') { return sizedBox; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => PaidLeaveListScreen()); }, child: const ListTile( leading: Icon( Icons.card_giftcard_rounded, color: Colors.red, size: 32.0, ), title: Text( 'Cuti', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Pengajuan dan riwayat Cuti', style: TextStyle(color: Colors.black87), ), ), ), ), ], ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[200], appBar: AppBar( backgroundColor: Colors.blueAccent, leading: Image.asset('assets/logo/logo.png'), leadingWidth: Get.width * 0.25, title: const Text('Aplikasi'), ), body: SingleChildScrollView( child: Container( margin: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pengaturan & Personalisasi', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 18.0, color: Colors.blueAccent), ), dividerT1, sizedBoxH10, Card( elevation: 2.0, child: SwitchListTile( onChanged: _handleSelected, activeColor: Colors.blueAccent, value: _isAlarmActive, secondary: const Icon( Icons.alarm, color: Colors.indigo, size: 32.0, ), title: const Text( 'Aktifkan Alarm Absensi', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: const Text( 'Pengingat waktu absen', style: TextStyle(color: Colors.black87), ), ), ), sizedBoxH30, const Text( 'Presensi', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 18.0, color: Colors.blueAccent), ), dividerT1, sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => ReportScreen(user: user)); }, child: const ListTile( leading: Icon( Icons.bar_chart_rounded, color: Colors.deepOrangeAccent, size: 32.0, ), title: Text( 'Statistik', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Lihat statistik presensi anda', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => PermissionListScreen()); }, child: const ListTile( leading: Icon( Icons.calendar_today_rounded, color: Colors.purple, size: 32.0, ), title: Text( 'Izin', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Pengajuan dan riwayat Izin', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => OutstationListScreen()); }, child: ListTile( leading: Icon( Icons.card_travel_rounded, color: Colors.yellow[800], size: 32.0, ), title: const Text( 'Dinas Luar', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: const Text( 'Pengajuan dan riwayat Dinas Luar', style: TextStyle(color: Colors.black87), ), ), ), ), _buildCutiSection(), sizedBoxH30, _buildStakeholderMenu(), const Text( 'Bantuan', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 18.0, color: Colors.blueAccent), ), dividerT1, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => ForgotPassScreen()); }, child: const ListTile( leading: Icon( Icons.warning, color: Colors.pink, size: 32.0, ), title: Text( 'Lapor', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Lapor kendala & pelanggaran', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => RegulationScreen()); }, child: ListTile( leading: Icon( Icons.room_preferences, color: Colors.lime[800], size: 32.0, ), title: const Text( 'Rujukan', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: const Text( 'Daftar aturan yang menjadi rujukan SiAP Balaesang', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH30, const Text( 'Akun', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 18.0, color: Colors.blueAccent), ), dividerT1, Card( elevation: 2.0, child: InkWell( onTap: () { Get.to(() => ChangePasswordScreen()); }, child: const ListTile( leading: Icon( Icons.lock_outline, color: Colors.blueAccent, size: 32.0, ), title: Text( 'Password', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( 'Ubah password akun', style: TextStyle(color: Colors.black87), ), ), ), ), sizedBoxH10, Card( elevation: 2.0, child: InkWell( onTap: () { logout(); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: ListTile( leading: Icon( Icons.exit_to_app_rounded, color: Colors.red[800], size: 32.0, ), title: const Text( 'Keluar Dari Aplikasi', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: const Text( 'Keluar dari aplikasi dan menghapus sesi saat ini. Gunakan ini jika aplikasi terasa berat atau anda ingin mengganti akun.', style: TextStyle(color: Colors.black87), ), ), ), ), ), ], ), ), ), ); } } ================================================ FILE: lib/screen/bottom_nav_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:spo_balaesang/screen/application_screen.dart'; import 'package:spo_balaesang/screen/home_screen.dart'; class BottomNavScreen extends StatefulWidget { @override _BottomNavScreenState createState() => _BottomNavScreenState(); } class _BottomNavScreenState extends State { final List _screens = [HomeScreen(), ApplicationScreen()]; int _currentIndex = 0; final Map _bottomNavItems = { 'Beranda': Icons.home_filled, 'Aplikasi': Icons.apps_rounded, }; @override Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _currentIndex, children: _screens, ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) => setState(() => _currentIndex = index), type: BottomNavigationBarType.fixed, backgroundColor: Colors.white, showUnselectedLabels: true, selectedItemColor: Colors.blueAccent, selectedLabelStyle: const TextStyle( color: Colors.blueAccent, fontWeight: FontWeight.bold), unselectedLabelStyle: const TextStyle( color: Colors.grey, fontWeight: FontWeight.normal, ), unselectedItemColor: Colors.grey, items: _bottomNavItems .map((key, value) => MapEntry( key, BottomNavigationBarItem( label: key, icon: Icon(value as IconData), ), )) .values .toList(), )); } } ================================================ FILE: lib/screen/change_absent_permission_photo_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/absent_permission.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/file_util.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_info_widget.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; class ChangePermissionPhotoScreen extends StatefulWidget { const ChangePermissionPhotoScreen({this.permission}); final AbsentPermission permission; @override _ChangePermissionPhotoScreenState createState() => _ChangePermissionPhotoScreenState(); } class _ChangePermissionPhotoScreenState extends State { String _base64Image; String _fileName; File _tmpFile; AbsentPermission _permission; Future _openCamera() async { final picture = await ImagePicker().getImage(source: ImageSource.camera); final file = await compressAndGetFile(File(picture.path), '/storage/emulated/0/Android/data/com.banuacoders.siap/files/Pictures/images.jpg'); setState(() { _tmpFile = file; _base64Image = base64Encode(_tmpFile.readAsBytesSync()); _fileName = _tmpFile.path.split('/').last; }); await file.delete(recursive: true); } Future _uploadData(AbsentPermission permission) async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final dataRepo = Provider.of(context, listen: false); final Map data = { 'photo': _base64Image, 'file_name': _fileName, 'permission_id': permission.id }; final Map _res = await dataRepo.changePermissionPhoto(data); if (_res['success'] as bool) { pd.hide(); showAlertDialog('success', "Sukses", _res['message'].toString(), dismissible: false); Timer( const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } Widget _showImage() { if (_base64Image == null) { return InkWell( onTap: () { Get.to(() => ImageDetailScreen( imageUrl: _permission.photo, tag: _permission.id.toString(), )); }, child: Hero( tag: _permission.id.toString(), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: CachedNetworkImage( placeholder: (_, __) => const ImagePlaceholderWidget( label: 'Memuat Foto', child: SpinKitFadingCircle( size: 25.0, color: Colors.blueAccent, ), ), imageUrl: _permission.photo, fit: BoxFit.cover, errorWidget: (_, __, ___) => const ImagePlaceholderWidget( label: 'Gagal memuat foto!', child: Icon( Icons.image_not_supported_rounded, color: Colors.grey, ), ), width: Get.width, height: 250.0, ), ), ), ); } final Uint8List bytes = base64Decode(_base64Image); return InkWell( onTap: () { Get.to(() => ImageDetailScreen( bytes: bytes, tag: 'image', )); }, child: Hero( tag: 'image', child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Image.memory( bytes, width: Get.width, height: 250, fit: BoxFit.cover, ), ), ), ); } @override void initState() { _permission = widget.permission; super.initState(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Perbarui Lampiran'), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pastikan gembar yang akan dikirim adalah salah satu dari memo absen, SPPD, atau surat keterangan dokter. ' + 'Pastikan juga surat sudah ditandatangani oleh pihak yang berwenang disertai dengan cap resmi.'), sizedBoxH20, EmployeeProposalInfoWidget( title: _permission.title, startDate: _permission.startDate, dueDate: _permission.dueDate, label: 'Izin', ), sizedBoxH20, Row( children: const [ Text('Foto Surat Izin'), SizedBox(width: 5.0), Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Lampirkan foto surat izin atau surat lainnya seperti surat keterangan dokter, dsb.', style: TextStyle(color: Colors.grey), ), const Text( '*tekan untuk memperbesar', style: TextStyle( fontSize: 12.0, color: Colors.black87, fontStyle: FontStyle.italic), ), sizedBoxH20, _showImage(), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: _openCamera, child: const Text('Ubah Foto'), ), ), sizedBoxH20, SizedBox( width: Get.width, height: 40, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.green, onPrimary: Colors.white, ), onPressed: () { _uploadData(_permission); }, child: const Text('Kirim'), ), ), sizedBoxH20, ], ), ), ), ); } } ================================================ FILE: lib/screen/change_outstation_photo_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/outstation.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/file_util.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_info_widget.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; class ChangeOutstationPhotoScreen extends StatefulWidget { const ChangeOutstationPhotoScreen({this.outstation}); final Outstation outstation; @override _ChangeOutstationPhotoScreenState createState() => _ChangeOutstationPhotoScreenState(); } class _ChangeOutstationPhotoScreenState extends State { String _base64Image; String _fileName; File _tmpFile; Outstation _outstation; Future _openCamera() async { final picture = await ImagePicker().getImage(source: ImageSource.camera); final file = await compressAndGetFile(File(picture.path), '/storage/emulated/0/Android/data/com.banuacoders.siap/files/Pictures/images.jpg'); setState(() { _tmpFile = file; _base64Image = base64Encode(_tmpFile.readAsBytesSync()); _fileName = _tmpFile.path.split('/').last; }); await file.delete(recursive: true); } Future _uploadData(Outstation outstation) async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final dataRepo = Provider.of(context, listen: false); final Map data = { 'photo': _base64Image, 'file_name': _fileName, 'outstation_id': outstation.id }; final Map _res = await dataRepo.changeOutstationPhoto(data); if (_res['success'] as bool) { pd.hide(); showAlertDialog('success', "Sukses", _res['message'].toString(), dismissible: false); Timer( const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } Widget _showImage() { if (_base64Image == null) { return InkWell( onTap: () { Get.to(() => ImageDetailScreen( imageUrl: _outstation.photo, tag: _outstation.id.toString(), )); }, child: Hero( tag: _outstation.id.toString(), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: CachedNetworkImage( placeholder: (_, __) => const ImagePlaceholderWidget( label: 'Memuat Foto', child: SpinKitFadingCircle( size: 25.0, color: Colors.blueAccent, ), ), imageUrl: _outstation.photo, fit: BoxFit.cover, errorWidget: (_, __, ___) => const ImagePlaceholderWidget( label: 'Gagal memuat foto!', child: Icon( Icons.image_not_supported_rounded, color: Colors.grey, ), ), width: Get.width, height: 250.0, ), ), ), ); } final Uint8List bytes = base64Decode(_base64Image); return InkWell( onTap: () { Get.to(() => ImageDetailScreen( bytes: bytes, tag: 'image', )); }, child: Hero( tag: 'image', child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Image.memory( bytes, width: Get.width, height: 250, fit: BoxFit.cover, ), ), ), ); } @override void initState() { _outstation = widget.outstation; super.initState(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Perbarui Lampiran'), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pastikan gembar yang akan dikirim adalah salah satu dari memo absen, SPPD, atau surat keterangan dokter. ' + 'Pastikan juga surat sudah ditandatangani oleh pihak yang berwenang disertai dengan cap resmi.'), sizedBoxH20, EmployeeProposalInfoWidget( title: _outstation.title, startDate: _outstation.startDate, dueDate: _outstation.dueDate, label: 'Dinas Luar', ), sizedBoxH20, Row( children: const [ Text('Foto Surat Tugas'), SizedBox(width: 5.0), Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Lampirkan foto SPPD atau surat lainnya seperti memo absen, dsb.', style: TextStyle(color: Colors.grey), ), const Text( '*tekan untuk memperbesar', style: TextStyle( fontSize: 12.0, color: Colors.black87, fontStyle: FontStyle.italic), ), sizedBoxH20, _showImage(), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: _openCamera, child: const Text('Ubah Foto'), ), ), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.green, onPrimary: Colors.white, ), onPressed: () { _uploadData(_outstation); }, child: const Text('Kirim'), ), ), sizedBoxH20, ], ), ), ), ); } } ================================================ FILE: lib/screen/change_paid_leave_photo_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/paid_leave.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/file_util.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_info_widget.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; class ChangePaidLeavePhotoScreen extends StatefulWidget { const ChangePaidLeavePhotoScreen({this.paidLeave}); final PaidLeave paidLeave; @override _ChangePaidLeavePhotoScreenState createState() => _ChangePaidLeavePhotoScreenState(); } class _ChangePaidLeavePhotoScreenState extends State { String _base64Image; String _fileName; File _tmpFile; PaidLeave _paidLeave; Future _openCamera() async { final picture = await ImagePicker().getImage(source: ImageSource.camera); final file = await compressAndGetFile(File(picture.path), '/storage/emulated/0/Android/data/com.banuacoders.siap/files/Pictures/images.jpg'); setState(() { _tmpFile = file; _base64Image = base64Encode(_tmpFile.readAsBytesSync()); _fileName = _tmpFile.path.split('/').last; }); await file.delete(recursive: true); } Future _uploadData(PaidLeave paidLeave) async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final dataRepo = Provider.of(context, listen: false); final Map data = { 'photo': _base64Image, 'file_name': _fileName, 'paid_leave_id': paidLeave.id }; final Map _res = await dataRepo.changePaidLeavePhoto(data); if (_res['success'] as bool) { pd.hide(); showAlertDialog('success', "Sukses", _res['message'].toString(), dismissible: false); Timer( const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } Widget _showImage() { if (_base64Image == null) { return InkWell( onTap: () { Get.to(() => ImageDetailScreen( imageUrl: _paidLeave.photo, tag: _paidLeave.id.toString(), )); }, child: Hero( tag: _paidLeave.id.toString(), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: CachedNetworkImage( placeholder: (_, __) => const ImagePlaceholderWidget( label: 'Memuat Foto', child: SpinKitFadingCircle( size: 25.0, color: Colors.blueAccent, ), ), imageUrl: _paidLeave.photo, fit: BoxFit.cover, errorWidget: (_, __, ___) => const ImagePlaceholderWidget( label: 'Gagal memuat foto!', child: Icon( Icons.image_not_supported_rounded, color: Colors.grey, ), ), width: Get.width, height: 250.0, ), ), ), ); } final Uint8List bytes = base64Decode(_base64Image); return InkWell( onTap: () { Get.to(() => ImageDetailScreen( bytes: bytes, tag: 'image', )); }, child: Hero( tag: 'image', child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Image.memory( bytes, width: Get.width, height: 250, fit: BoxFit.cover, ), ), ), ); } @override void initState() { _paidLeave = widget.paidLeave; super.initState(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Perbarui Lampiran'), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pastikan gembar yang akan dikirim adalah surat pengajuan cuti atau surat keterangan dokter ' + 'yang sudah ditandatangani oleh pihak berwenang disertai dengan cap resmi.'), sizedBoxH20, EmployeeProposalInfoWidget( title: _paidLeave.title, startDate: _paidLeave.startDate, dueDate: _paidLeave.dueDate, label: _paidLeave.category, ), sizedBoxH20, Row( children: const [ Text('Foto Surat Pengajuan'), SizedBox(width: 5.0), Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Lampirkan foto surat pengajuan atau surat lainnya seperti surat keterangan dokter, dsb.', style: TextStyle(color: Colors.grey), ), const Text( '*tekan untuk memperbesar', style: TextStyle( fontSize: 12.0, color: Colors.black87, fontStyle: FontStyle.italic), ), sizedBoxH20, _showImage(), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: _openCamera, child: const Text('Ubah Foto'), ), ), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.green, onPrimary: Colors.white, ), onPressed: () { _uploadData(_paidLeave); }, child: const Text('Kirim'), ), ), sizedBoxH20, ], ), ), ), ); } } ================================================ FILE: lib/screen/change_pass_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/utils/view_util.dart'; class ChangePasswordScreen extends StatefulWidget { @override _ChangePasswordScreenState createState() => _ChangePasswordScreenState(); } class _ChangePasswordScreenState extends State { final TextEditingController _oldPassCtrl = TextEditingController(); final TextEditingController _newPassCtrl = TextEditingController(); final TextEditingController _newPass2Ctrl = TextEditingController(); final GlobalKey _formKey = GlobalKey(); bool isLoading = false; bool isOldPassVisible = false; bool isNewPassVisible = false; @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Ubah Password'), ), body: Padding( padding: const EdgeInsets.all(8.0), child: Stack( children: [ SizedBox( height: Get.height, child: Form( key: _formKey, child: ListView( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Password Lama', style: TextStyle(fontSize: 12.0)), TextFormField( controller: _oldPassCtrl, validator: (String value) { return value.isEmpty ? 'Password lama tidak boleh kosong!' : null; }, obscureText: !isNewPassVisible, decoration: InputDecoration( suffixIcon: IconButton( icon: Icon(isNewPassVisible ? Icons.visibility_off : Icons.visibility), onPressed: () { setState(() { isNewPassVisible = !isNewPassVisible; }); }), hintText: 'Password Lama'), ), ], ), const SizedBox(height: 20.0), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Password Baru', style: TextStyle(fontSize: 12.0)), TextFormField( controller: _newPassCtrl, validator: (String value) { if (value.isEmpty) { return 'Password baru tidak boleh kosong!'; } if (value == _oldPassCtrl.text) { return 'Password baru tidak boleh sama dengan Password Lama!'; } return null; }, obscureText: !isOldPassVisible, decoration: InputDecoration( suffixIcon: IconButton( icon: Icon(isOldPassVisible ? Icons.visibility_off : Icons.visibility), onPressed: () { setState(() { isOldPassVisible = !isOldPassVisible; }); }), hintText: 'Password Baru'), ), ], ), const SizedBox(height: 20.0), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Ulangi Password Baru', style: TextStyle(fontSize: 12.0)), TextFormField( controller: _newPass2Ctrl, validator: (String value) { if (value.isEmpty) { return 'Konfirmasi Password baru tidak boleh kosong!'; } if (value != _newPassCtrl.text) { return 'Kolom ini harus sama dengan password baru!'; } return null; }, obscureText: true, decoration: const InputDecoration( hintText: 'Konfirmasi Password Baru'), ), ], ) ], )), ), Positioned( left: 0, right: 0, bottom: MediaQuery.of(context).viewInsets.bottom, child: ElevatedButton( style: ElevatedButton.styleFrom( onPrimary: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0)), primary: Colors.blueAccent, ), onPressed: () async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); pd.show(); try { final dataRepo = Provider.of(context, listen: false); final Map data = { 'old_pass': _oldPassCtrl.value.text, 'new_pass': _newPassCtrl.value.text, 'new_pass_conf': _newPass2Ctrl.value.text }; final http.Response response = await dataRepo.changePass(data); final Map _res = jsonDecode(response.body) as Map; if (response.statusCode == 200) { pd.hide(); showAlertDialog( 'success', "Sukses", _res['message'].toString(), dismissible: false); Timer(const Duration(seconds: 1), () => Get.off(() => BottomNavScreen())); } else { pd.hide(); showErrorDialog(_res); } } catch (e) { pd.hide(); showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } }, child: isLoading ? const SizedBox( height: 30.0, width: 30.0, child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : const Text('SIMPAN'), ), ) ], ), ), ); } } ================================================ FILE: lib/screen/create_notification_screen.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; class CreateNotificationScreen extends StatefulWidget { @override _CreateNotificationScreenState createState() => _CreateNotificationScreenState(); } class _CreateNotificationScreenState extends State { final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); Future _sendNotification() async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final dataRepo = Provider.of(context, listen: false); final Map data = { 'title': _titleController.value.text, 'content': _descriptionController.value.text, }; final Map _res = await dataRepo.sendNotification(data); if (_res['success'] as bool) { pd.hide(); showAlertDialog('success', "Sukses", _res['message'].toString(), dismissible: false); Timer( const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Buat Pemberitahuan'), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: const [ Text('Judul Pemberitahuan'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), TextFormField( keyboardType: TextInputType.text, controller: _titleController, decoration: const InputDecoration( alignLabelWithHint: true, hintText: 'Judul', focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), labelStyle: TextStyle(color: Colors.grey)), ), sizedBoxH20, Row( children: const [ Text('Isi Pemberitahuan'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), TextFormField( keyboardType: TextInputType.multiline, minLines: 1, maxLines: 5, controller: _descriptionController, decoration: const InputDecoration( alignLabelWithHint: true, hintText: 'Deskripsi', focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), labelStyle: TextStyle(color: Colors.grey)), ), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: _sendNotification, child: const Text('Kirim'), ), ), sizedBoxH20, ], ), ), ), ); } } ================================================ FILE: lib/screen/create_outstation_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/file_util.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; import 'package:table_calendar/table_calendar.dart'; class CreateOutstationScreen extends StatefulWidget { @override _CreateOutstationScreenState createState() => _CreateOutstationScreenState(); } class _CreateOutstationScreenState extends State { String _base64Image; String _fileName; File _tmpFile; DateTime _dueDate = DateTime.now(); DateTime _startDate = DateTime.now(); bool _isDateChange = false; final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); Future _openCamera() async { final picture = await ImagePicker().getImage(source: ImageSource.camera); final file = await compressAndGetFile(File(picture.path), '/storage/emulated/0/Android/data/com.banuacoders.siap/files/Pictures/images.jpg'); setState(() { _tmpFile = file; _base64Image = base64Encode(_tmpFile.readAsBytesSync()); _fileName = _tmpFile.path.split('/').last; }); await file.delete(recursive: true); } Future _uploadData() async { if (!_isDateChange) { showAlertDialog('failed', 'Pelanggaran', 'Pilih tanggal terlebih dahulu!', dismissible: true); } else { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final dataRepo = Provider.of(context, listen: false); final Map data = { 'title': _titleController.value.text, 'description': _descriptionController.value.text, 'photo': _base64Image, 'due_date': _dueDate.toIso8601String(), 'start_date': _startDate.toIso8601String(), 'file_name': _fileName }; final Map _res = await dataRepo.outstation(data); if (_res['success'] as bool) { pd.hide(); showAlertDialog('success', "Sukses", _res['message'].toString(), dismissible: false); Timer(const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } } Widget _showImage() { if (_base64Image == null) { return const ImagePlaceholderWidget( label: 'Ambil Foto', child: Icon( Icons.camera_alt_rounded, color: Colors.grey, ), ); } final Uint8List bytes = base64Decode(_base64Image); return InkWell( onTap: () { Get.to(() => ImageDetailScreen( bytes: bytes, tag: 'image', )); }, child: Hero( tag: 'image', child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Image.memory( bytes, width: Get.width, height: 250, fit: BoxFit.cover, ), ), ), ); } Future _selectDueDate() async { Get.defaultDialog( title: 'Pilih Tanggal Selesai', content: SizedBox( width: Get.width * 0.9, height: Get.height * 0.4, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, shouldFillViewport: true, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime.now().subtract(const Duration(days: 7)), focusedDay: _dueDate, selectedDayPredicate: (day) { return isSameDay(_dueDate, day); }, lastDay: DateTime(DateTime.now().year + 5), locale: 'in_ID', onDaySelected: (day, focusedDay) { Get.back(); setState(() { if (!_isDateChange) { _isDateChange = true; } _dueDate = day; if (_dueDate.isBefore(_startDate)) { _startDate = day; } }); }, ), )); } Future _selectStartDate() async { Get.defaultDialog( title: 'Pilih Tanggal Mulai', content: SizedBox( width: Get.width * 0.9, height: Get.height * 0.4, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), shouldFillViewport: true, calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime.now().subtract(const Duration(days: 7)), focusedDay: _startDate, lastDay: DateTime(DateTime.now().year + 5), locale: 'in_ID', selectedDayPredicate: (day) { return isSameDay(_startDate, day); }, onDaySelected: (day, focusedDay) { Get.back(); setState(() { if (!_isDateChange) { _isDateChange = true; } _startDate = day; if (_startDate.isAfter(_dueDate)) { _dueDate = day; } }); }, ), )); } @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Ajukan Dinas Luar'), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pastikan data yang dikirim sudah benar. Anda tidak dapat mengubah dokumen setelah dikirim. Jika terjadi kesalahan, hubungi administrator sistem.'), sizedBoxH20, Row( children: const [ Text('Judul Surat'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), TextFormField( keyboardType: TextInputType.text, controller: _titleController, decoration: const InputDecoration( alignLabelWithHint: true, hintText: 'Judul', focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), labelStyle: TextStyle(color: Colors.grey)), ), sizedBoxH20, Row( children: const [ Text('Deskripsi'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Jelaskan secara singkat tentang Dinas Luar yang diajukan!', style: TextStyle(color: Colors.grey), ), TextFormField( keyboardType: TextInputType.multiline, minLines: 1, maxLines: 5, controller: _descriptionController, decoration: const InputDecoration( alignLabelWithHint: true, hintText: 'Deskripsi', focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), labelStyle: TextStyle(color: Colors.grey)), ), sizedBoxH20, Row( children: const [ Text('Tanggal Mulai'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Pilih kapan tugas Dinas Luar mulai!', style: TextStyle(color: Colors.grey), ), Row( children: [ Expanded( child: Text( DateFormat('EEEE, d MMMM y').format(_startDate), style: const TextStyle(fontWeight: FontWeight.w600), ), ), IconButton( icon: const Icon( Icons.calendar_today_rounded, color: Colors.blueAccent, ), onPressed: () { _selectStartDate(); }) ], ), sizedBoxH20, Row( children: const [ Text('Tanggal Selesai'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Pilih sampai kapan Dinas Luar yang diajukan berlaku!', style: TextStyle(color: Colors.grey), ), Row( children: [ Expanded( child: Text( DateFormat('EEEE, d MMMM y').format(_dueDate), style: const TextStyle(fontWeight: FontWeight.w600), ), ), IconButton( icon: const Icon( Icons.calendar_today_rounded, color: Colors.blueAccent, ), onPressed: () { _selectDueDate(); }) ], ), sizedBoxH20, Row( children: const [ Text('Foto Surat Tugas'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Lampirkan foto surat tugas atau Surat Perintah Perjalanan Dinas (SPPD).', style: TextStyle(color: Colors.grey), ), const Text( '*tekan untuk memperbesar', style: TextStyle( fontSize: 12.0, color: Colors.black87, fontStyle: FontStyle.italic), ), sizedBoxH20, _showImage(), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: _openCamera, child: Text(_base64Image == null ? 'Ambil Foto' : 'Ubah Foto'), ), ), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.green, onPrimary: Colors.white, ), onPressed: _uploadData, child: const Text('Kirim'), ), ), sizedBoxH20, ], ), ), ), ); } } ================================================ FILE: lib/screen/create_paid_leave_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/file_util.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; import 'package:table_calendar/table_calendar.dart'; class CreatePaidLeaveScreen extends StatefulWidget { @override _CreatePaidLeaveScreenState createState() => _CreatePaidLeaveScreenState(); } class _CreatePaidLeaveScreenState extends State { String _base64Image; String _fileName; File _tmpFile; DateTime _startDate = DateTime.now(); DateTime _dueDate = DateTime.now(); final Map _categories = paidLeaveCategories; int _category = 1; bool _isDateChange = false; final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); Future _openCamera() async { final picture = await ImagePicker().getImage(source: ImageSource.camera); final file = await compressAndGetFile(File(picture.path), '/storage/emulated/0/Android/data/com.banuacoders.siap/files/Pictures/images.jpg'); setState(() { _tmpFile = file; _base64Image = base64Encode(_tmpFile.readAsBytesSync()); _fileName = _tmpFile.path.split('/').last; }); await file.delete(recursive: true); } Widget _showImage() { if (_base64Image == null) { return const ImagePlaceholderWidget( label: 'Ambil Foto', child: Icon( Icons.camera_alt_rounded, color: Colors.grey, ), ); } final Uint8List bytes = base64Decode(_base64Image); return InkWell( onTap: () { Get.to(() => ImageDetailScreen( bytes: bytes, tag: 'image', )); }, child: Hero( tag: 'image', child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Image.memory( bytes, width: Get.width, height: 250, fit: BoxFit.cover, ), ), ), ); } Future _uploadData() async { if (!_isDateChange) { showAlertDialog('failed', 'Pelanggaran', 'Pilih tanggal terlebih dahulu!', dismissible: true); } else { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final dataRepo = Provider.of(context, listen: false); final Map data = { 'category': _category, 'title': _titleController.value.text, 'description': _descriptionController.value.text, 'photo': _base64Image, 'due_date': _dueDate.toIso8601String(), 'start_date': _startDate.toIso8601String(), 'file_name': _fileName }; final Map _res = await dataRepo.paidLeave(data); if (_res['success'] as bool) { pd.hide(); showAlertDialog('success', "Sukses", _res['message'].toString(), dismissible: false); Timer(const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } } void _selectDueDate() { Get.defaultDialog( title: 'Pilih Tanggal Selesai', content: SizedBox( width: Get.width * 0.9, height: Get.height * 0.4, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), shouldFillViewport: true, calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime.now().subtract(const Duration(days: 7)), focusedDay: _dueDate, lastDay: DateTime.now().add(const Duration(days: 180)), locale: 'in_ID', selectedDayPredicate: (day) { return isSameDay(_dueDate, day); }, onDaySelected: (day, focusedDay) { Get.back(); setState(() { if (!_isDateChange) { _isDateChange = true; } _dueDate = day; if (_dueDate.isBefore(_startDate)) { _startDate = day; } }); }, ), )); } void _selectStartDate() { Get.defaultDialog( title: 'Pilih Tanggal Mulai', content: SizedBox( width: Get.width * 0.9, height: Get.height * 0.4, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), shouldFillViewport: true, calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime.now().subtract(const Duration(days: 7)), focusedDay: _startDate, lastDay: DateTime.now().add(const Duration(days: 180)), locale: 'in_ID', selectedDayPredicate: (day) { return isSameDay(_startDate, day); }, onDaySelected: (day, focusedDay) { Get.back(); setState(() { if (!_isDateChange) { _isDateChange = true; } _startDate = day; if (_startDate.isAfter(_dueDate)) { _dueDate = day; } }); }, ), )); } @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Ajukan Cuti'), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pastikan data yang dikirim sudah benar. Anda tidak dapat mengubah dokumen setelah dikirim. Jika terjadi kesalahan, hubungi administrator sistem.'), sizedBoxH20, Row( children: const [ Text('Subjek Cuti'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Ringkasan dari kegiatan cuti anda.', style: TextStyle(color: Colors.grey), ), TextFormField( keyboardType: TextInputType.text, controller: _titleController, decoration: const InputDecoration( alignLabelWithHint: true, hintText: 'Judul/Subjek', focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), labelStyle: TextStyle(color: Colors.grey)), ), sizedBoxH20, Row( children: const [ Text('Jenis Cuti'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), DropdownButton( isExpanded: true, elevation: 10, value: _category, items: _categories.entries .map((option) => DropdownMenuItem( value: option.value, child: Text(option.key), )) .toList(), onChanged: (value) { setState(() { _category = int.parse(value.toString()); }); }), sizedBoxH20, Row( children: const [ Text('Deskripsi'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Jelaskan secara singkat tentang cuti yang diajukan!', style: TextStyle(color: Colors.grey), ), TextFormField( keyboardType: TextInputType.multiline, minLines: 1, maxLines: 5, controller: _descriptionController, decoration: const InputDecoration( alignLabelWithHint: true, hintText: 'Deskripsi', focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), labelStyle: TextStyle(color: Colors.grey)), ), sizedBoxH20, Row( children: const [ Text('Tanggal Mulai'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Pilih kapan anda mulai cuti!', style: TextStyle(color: Colors.grey), ), Row( children: [ Expanded( child: Text( DateFormat('EEEE, d MMMM y').format(_startDate), style: const TextStyle( fontWeight: FontWeight.w600, ), ), ), IconButton( icon: const Icon( Icons.calendar_today_rounded, color: Colors.blueAccent, ), onPressed: _selectStartDate) ], ), sizedBoxH20, Row( children: const [ Text('Tanggal Selesai'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Pilih sampai kapan anda cuti!', style: TextStyle(color: Colors.grey), ), Row( children: [ Expanded( child: Text( DateFormat('EEEE, d MMMM y').format(_dueDate), style: const TextStyle(fontWeight: FontWeight.w600), ), ), IconButton( icon: const Icon( Icons.calendar_today_rounded, color: Colors.blueAccent, ), onPressed: _selectDueDate) ], ), sizedBoxH20, Row( children: const [ Text('Foto Surat Pengajuan'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Lampirkan foto surat pengajuan cuti seperti surat keterangan sakit dsb.', style: TextStyle(color: Colors.grey), ), const Text( '*tekan untuk memperbesar', style: TextStyle( fontSize: 12.0, color: Colors.black87, fontStyle: FontStyle.italic), ), sizedBoxH20, _showImage(), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: _openCamera, child: Text(_base64Image == null ? 'Ambil Foto' : 'Ubah Foto'), ), ), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.green, onPrimary: Colors.white, ), onPressed: _uploadData, child: const Text('Kirim'), ), ), sizedBoxH20, ], ), ), ), ); } } ================================================ FILE: lib/screen/create_permission_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/file_util.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; import 'package:table_calendar/table_calendar.dart'; class CreatePermissionScreen extends StatefulWidget { @override _CreatePermissionScreenState createState() => _CreatePermissionScreenState(); } class _CreatePermissionScreenState extends State { String _base64Image; String _fileName; File _tmpFile; DateTime _dueDate = DateTime.now(); DateTime _startDate = DateTime.now(); bool _isDateChange = false; final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); Future _openCamera() async { final picture = await ImagePicker().getImage(source: ImageSource.camera); final file = await compressAndGetFile(File(picture.path), '/storage/emulated/0/Android/data/com.banuacoders.siap/files/Pictures/images.jpg'); setState(() { _tmpFile = file; _base64Image = base64Encode(_tmpFile.readAsBytesSync()); _fileName = _tmpFile.path.split('/').last; }); await file.delete(recursive: true); } Future _uploadData() async { if (!_isDateChange) { showAlertDialog('failed', 'Pelanggaran', 'Pilih tanggal terlebih dahulu!', dismissible: true); } else { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final dataRepo = Provider.of(context, listen: false); final Map data = { 'title': _titleController.value.text, 'description': _descriptionController.value.text, 'photo': _base64Image, 'due_date': _dueDate.toIso8601String(), 'start_date': _startDate.toIso8601String(), 'file_name': _fileName }; final Map _res = await dataRepo.permission(data); if (_res['success'] as bool) { pd.hide(); showAlertDialog('success', "Sukses", _res['message'].toString(), dismissible: false); Timer(const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } } Widget _showImage() { if (_base64Image == null) { return const ImagePlaceholderWidget( label: 'Ambil Foto', child: Icon( Icons.camera_alt_rounded, color: Colors.grey, ), ); } final Uint8List bytes = base64Decode(_base64Image); return InkWell( onTap: () { Get.to(() => ImageDetailScreen( bytes: bytes, tag: 'image', )); }, child: Hero( tag: 'image', child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Image.memory( bytes, width: Get.width, height: 250, fit: BoxFit.cover, ), ), ), ); } Future _selectDueDate() async { Get.defaultDialog( title: 'Pilih Tanggal Selesai', content: SizedBox( width: Get.width * 0.9, height: Get.height * 0.4, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), shouldFillViewport: true, calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime.now().subtract(const Duration(days: 7)), focusedDay: _dueDate, lastDay: DateTime.now().add(const Duration(days: 180)), locale: 'in_ID', selectedDayPredicate: (day) { return isSameDay(_dueDate, day); }, onDaySelected: (day, focusedDay) { Get.back(); setState(() { if (!_isDateChange) { _isDateChange = true; } _dueDate = day; if (_dueDate.isBefore(_startDate)) { _startDate = day; } }); }, ), )); } Future _selectStartDate() async { Get.defaultDialog( title: 'Pilih Tanggal Mulai', content: SizedBox( height: Get.height * 0.4, width: Get.width * 0.9, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), shouldFillViewport: true, calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime.now().subtract(const Duration(days: 7)), focusedDay: _startDate, lastDay: DateTime.now().add(const Duration(days: 180)), locale: 'in_ID', selectedDayPredicate: (day) { return isSameDay(_startDate, day); }, onDaySelected: (day, focusedDay) { Get.back(); setState(() { if (!_isDateChange) { _isDateChange = true; } _startDate = day; if (_startDate.isAfter(_dueDate)) { _dueDate = day; } }); }, ), )); } @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Buat Izin'), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pastikan data yang dikirim sudah benar. Anda tidak dapat mengubah dokumen setelah dikirim. Jika terjadi kesalahan, hubungi administrator sistem.'), sizedBoxH20, Row( children: const [ Text('Judul Surat'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Mis: Izin Sakit, dsb.', style: TextStyle(color: Colors.grey), ), TextFormField( keyboardType: TextInputType.text, controller: _titleController, decoration: const InputDecoration( alignLabelWithHint: true, hintText: 'Judul', focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), labelStyle: TextStyle(color: Colors.grey)), ), sizedBoxH20, Row( children: const [ Text('Deskripsi'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Jelaskan secara singkat tentang izin yang diajukan!', style: TextStyle(color: Colors.grey), ), TextFormField( keyboardType: TextInputType.multiline, minLines: 1, maxLines: 5, controller: _descriptionController, decoration: const InputDecoration( alignLabelWithHint: true, hintText: 'Deskripsi', focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), labelStyle: TextStyle(color: Colors.grey)), ), sizedBoxH20, Row( children: const [ Text('Tanggal Izin'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Pilih kapan izin mulai berlaku!', style: TextStyle(color: Colors.grey), ), Row( children: [ Expanded( child: Text( DateFormat('EEEE, d MMMM y').format(_startDate), style: const TextStyle(fontWeight: FontWeight.w600), ), ), IconButton( icon: const Icon( Icons.calendar_today_rounded, color: Colors.blueAccent, ), onPressed: () { _selectStartDate(); }) ], ), sizedBoxH20, Row( children: const [ Text('Tanggal Kadaluarsa'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Pilih sampai kapan izin yang diajukan berlaku!', style: TextStyle(color: Colors.grey), ), Row( children: [ Expanded( child: Text( DateFormat('EEEE, d MMMM y').format(_dueDate), style: const TextStyle(fontWeight: FontWeight.w600), ), ), IconButton( icon: const Icon( Icons.calendar_today_rounded, color: Colors.blueAccent, ), onPressed: () { _selectDueDate(); }) ], ), sizedBoxH20, Row( children: const [ Text('Foto Surat Izin'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Lampirkan foto surat izin atau surat lainnya seperti surat keterangan sakit dsb.', style: TextStyle(color: Colors.grey), ), const Text( '*tekan untuk memperbesar', style: TextStyle( fontSize: 12.0, color: Colors.black87, fontStyle: FontStyle.italic), ), sizedBoxH20, _showImage(), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: _openCamera, child: Text(_base64Image == null ? 'Ambil Foto' : 'Ubah Foto'), ), ), sizedBoxH20, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.green, onPrimary: Colors.white, ), onPressed: _uploadData, child: const Text('Kirim'), ), ), sizedBoxH20, ], ), ), ), ); } } ================================================ FILE: lib/screen/employee_attendance_screen.dart ================================================ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:search_page/search_page.dart'; import 'package:spo_balaesang/models/employee.dart'; import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/report_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class EmployeeAttendanceScreen extends StatefulWidget { @override _EmployeeAttendanceScreenState createState() => _EmployeeAttendanceScreenState(); } class _EmployeeAttendanceScreenState extends State { List _employees; bool _isLoading = false; @override void setState(void Function() fn) { if (mounted) super.setState(fn); } void _setLoading(bool isLoading) { setState(() { _isLoading = isLoading; }); } Future loadData() async { try { _setLoading(true); final dataRepo = Provider.of(context, listen: false); final List _jsonEmployees = await dataRepo.getAllEmployee(); setState(() { _employees = _jsonEmployees; }); } catch (e) {} finally { _setLoading(false); } } @override void initState() { loadData(); super.initState(); } Widget _buildPnsSection(Employee employee) { if (employee.status != 'PNS') { return sizedBox; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Golongan', style: TextStyle(color: Colors.grey[600]), ), Text(employee.group ?? '') ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'NIP', style: TextStyle(color: Colors.grey[600]), ), Text(employee.nip ?? '') ], ), ], ); } Widget _buildEmployeeCard(Employee employee) { return Container( width: Get.width, margin: const EdgeInsets.only(bottom: 8.0), child: Card( elevation: 2.0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: Padding( padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( employee.name ?? '', style: const TextStyle( fontWeight: FontWeight.w600, ), ), dividerT1, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Jabatan', style: TextStyle(color: Colors.grey[600]), ), Text(employee.position ?? '') ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Bagian ', style: TextStyle(color: Colors.grey[600]), ), Text(employee.department ?? '') ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Status', style: TextStyle(color: Colors.grey[600]), ), Text(employee.status ?? '') ], ), _buildPnsSection(employee), sizedBoxH4, dividerT1, Center( child: SizedBox( width: Get.width * 0.9, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), onPrimary: Colors.white, primary: Colors.blueAccent, ), onPressed: () { final User user = User.fromJson(employee.toJson()); Get.to(() => ReportScreen(user: user, isApproval: true)); }, child: const Text('Lihat data kehadiran'), ), ), ) ], ), ), ), ); } Widget _buildEmptyWidget() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Data tidak ditemukan :(') ], ), ); } Widget _buildContent() { if (_isLoading) { return const SizedBox( child: Center( child: SpinKitCircle( size: 45, color: Colors.blueAccent, )), ); } if (_employees == null || _employees.isEmpty) { return SizedBox( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/failure.flr', animation: 'failure', ), ), const Text('Gagal memuat data pegawai!'), sizedBoxH20, ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white), onPressed: loadData, child: const Text('Coba Lagi'), ) ]), ), ); } return ListView.builder( itemBuilder: (context, index) => _buildEmployeeCard(_employees[index]), itemCount: _employees?.length ?? 0, ); } Future _onSearchButtonPressed() async { if (_employees == null || _employees.isEmpty) { return null; } return showSearch( context: context, delegate: SearchPage( searchLabel: 'Cari Pegawai', barTheme: ThemeData( appBarTheme: const AppBarTheme( brightness: Brightness.dark, color: Colors.blueAccent, ), ), showItemsOnEmpty: true, failure: _buildEmptyWidget(), builder: (Employee employee) => Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: _buildEmployeeCard(employee), ), filter: (Employee employee) => [ employee.name, employee.status, employee.nip, employee.department ], items: _employees)); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Presensi Pegawai'), actions: [ IconButton( onPressed: _onSearchButtonPressed, icon: const Icon(Icons.search_rounded)) ], ), body: Padding( padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8), child: _buildContent(), ), ); } } ================================================ FILE: lib/screen/employee_list_screen.dart ================================================ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:search_page/search_page.dart'; import 'package:spo_balaesang/models/employee.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/extensions.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:timeline_tile/timeline_tile.dart'; import 'package:url_launcher/url_launcher.dart'; class EmployeeListScreen extends StatefulWidget { const EmployeeListScreen({this.employees}); final List employees; @override State createState() => _EmployeeListScreenState(); } class _EmployeeListScreenState extends State { List employees; List _isExpanded = []; // ignore: prefer_final_fields @override void initState() { super.initState(); employees = widget.employees; _isExpanded .addAll(List.generate(employees.length, (index) => false).toList()); } Widget _buildPnsInfoSection(Employee employee) { if (employee.status == 'Honorer') { return sizedBox; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Golongan", style: labelTextStyle, ), Text(employee.group ?? '') ], ), sizedBoxH2, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Pangkat", style: labelTextStyle, ), Text(employee.rank ?? '') ], ), sizedBoxH2, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "NIP", style: labelTextStyle, ), Text(employee.nip ?? '') ], ), sizedBoxH2, ], ); } Widget _buildPresenceSection(int index) { if (employees[index].presences.isEmpty) { return sizedBox; } return Column( children: [ dividerT1, ExpansionPanelList( animationDuration: const Duration(milliseconds: 500), elevation: 0, expandedHeaderPadding: const EdgeInsets.all(1.0), expansionCallback: (i, isOpen) => setState(() => {_isExpanded[index] = !isOpen}), children: [ ExpansionPanel( canTapOnHeader: true, isExpanded: _isExpanded[index], headerBuilder: (_, isOpen) { return Row( children: [ sizedBoxW12, const Text( 'Kehadiran : ', style: TextStyle(color: Colors.blueGrey), ), Text( DateFormat("EEEE, d MMMM y").format(DateTime.now()), style: const TextStyle(fontSize: 12.0), ) ], ); }, body: Column( children: employees[index].presences.map((presence) { final int _index = employees[index].presences.indexOf(presence); String status = presence.status; if (presence.status == 'Terlambat') { final duration = calculateLateTime( presence.startTime, presence.attendTime); status = '${presence.status} $duration'; } final isFirst = _index == 0; final isLast = _index == 3; final isOnGoing = presence.startTime.isOnGoing(); final statusColor = isOnGoing ? Colors.grey : checkStatusColor(presence.status); final afterLineColor = isOnGoing ? Colors.grey[300] : statusColor; var beforeLineColor = Colors.grey[300]; if (!isFirst) { final _presence = employees[index].presences[_index - 1]; beforeLineColor = _presence.startTime.isFinished() ? checkStatusColor(_presence.status) : Colors.grey[300]; } final indicatorIcon = _checkIconData(presence.status); return TimelineTile( isFirst: isFirst, isLast: isLast, startChild: Center( child: Text( presence.attendTime.isEmpty ? "--:--:--" : presence.attendTime, style: TextStyle( color: isOnGoing ? Colors.grey : Colors.black), ), ), alignment: TimelineAlign.manual, lineXY: 0.2, endChild: ListTile( dense: true, title: Text(presence.codeType, style: TextStyle( fontWeight: FontWeight.w500, color: isOnGoing ? Colors.grey : Colors.black)), subtitle: Text(status, style: TextStyle(color: statusColor)), trailing: Text( formatPercentage( checkPresencePercentage(presence.status)), style: TextStyle(color: statusColor)), ), indicatorStyle: IndicatorStyle( color: afterLineColor, width: isOnGoing ? 20 : 23, iconStyle: IconStyle( color: Colors.white, iconData: indicatorIcon, fontSize: 16, ), ), afterLineStyle: LineStyle(color: afterLineColor), beforeLineStyle: LineStyle(color: beforeLineColor), ); }).toList(), )) ], ), ], ); } IconData _checkIconData(String status) { switch (status) { case 'Terlambat': return Icons.thumb_down; case 'Tidak Hadir': return Icons.close_rounded; case 'Tepat Waktu': return Icons.check_rounded; default: return Icons.calendar_today_rounded; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Pegawai'), actions: [ IconButton( onPressed: () => showSearch( context: context, delegate: SearchPage( searchLabel: 'Cari Pegawai', searchStyle: const TextStyle(color: Colors.white), barTheme: ThemeData( appBarTheme: const AppBarTheme( color: Colors.blueAccent, brightness: Brightness.dark, ), ), showItemsOnEmpty: true, failure: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Data tidak ditemukan :(') ], ), ), builder: (Employee employee) => Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: _buildEmployeeCardSection(employee), ), filter: (Employee employee) => [ employee.name, employee.status, employee.department, employee.nip, ], items: employees, )), icon: const Icon(Icons.search_rounded)) ], ), body: Padding( padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8), child: ListView.builder( itemBuilder: (_, index) => _buildEmployeeCardSection(employees[index]), itemCount: employees.length, ), ), ); } Widget _buildEmployeeCardSection(Employee employee) { final int index = employees.indexOf(employee); return Container( margin: const EdgeInsets.only(bottom: 16), child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 3.0, child: Padding( padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( employee.name, style: const TextStyle(fontWeight: FontWeight.w600), ), dividerT1, Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Jabatan", style: labelTextStyle, ), Text(employee.position) ], ), sizedBoxH2, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Bagian", style: labelTextStyle, ), Text(employee.department) ], ), sizedBoxH2, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Status", style: labelTextStyle, ), Text(employee.status) ], ), sizedBoxH2, _buildPnsInfoSection(employee) ], ), dividerT1, Center( child: Wrap( spacing: 8.0, children: [ IconButton( onPressed: () { launch('tel:${trimPhoneNumber(employee.phone)}'); }, color: Colors.blueAccent, icon: const Icon(Icons.phone), tooltip: 'Hubungi via Telpon', ), IconButton( onPressed: () async { final whatsappUrl = "whatsapp://send?phone=${trimPhoneNumber(employee.phone)}"; await canLaunch(whatsappUrl) ? launch(whatsappUrl) : Get.defaultDialog( title: 'Gagal', content: const Text('WhatsApp tidak ditemukan!'), ); }, color: Colors.green[600], icon: const FaIcon(FontAwesomeIcons.whatsapp), tooltip: 'Hubungi via WA', ), IconButton( onPressed: () async { final smsUrl = "smsto:${trimPhoneNumber(employee.phone)}"; await canLaunch(smsUrl) ? launch(smsUrl) : Get.defaultDialog( title: 'Gagal', content: const Text('Aplikasi SMS tidak ditemukan!'), ); }, color: Colors.red[800], icon: const FaIcon(FontAwesomeIcons.mailBulk), tooltip: 'Hubungi via SMS', ), ], ), ), _buildPresenceSection(index) ], ), ), ), ); } } ================================================ FILE: lib/screen/employee_outstation.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/outstation.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_widget.dart'; import 'package:table_calendar/table_calendar.dart'; class EmployeeOutstationScreen extends StatefulWidget { @override _EmployeeOutstationScreenState createState() => _EmployeeOutstationScreenState(); } class _EmployeeOutstationScreenState extends State { List _outstations = []; bool _isLoading = false; final TextEditingController _reasonController = TextEditingController(); final TextEditingController _nameController = TextEditingController(); List _filteredOutstation = []; Set choices = {'Semua', 'Disetujui', 'Belum Disetujui'}; String _selectedChoice = 'Semua'; DateTime _selectedDate = DateTime.now(); @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } Future _fetchOutstationData() async { try { setState(() { _isLoading = true; }); final dataRepo = Provider.of(context, listen: false); final Map _result = await dataRepo.getAllEmployeeOutstation(_selectedDate); final List outstations = _result['data'] as List; final _data = outstations .map((json) => Outstation.fromJson(json as Map)) .toList(); setState(() { _outstations = _data; _filteredOutstation = _outstations; }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { setState(() { _isLoading = false; }); } } void _rejectOutstation(Outstation outstation) { Get.defaultDialog( title: 'Alasan Pembatalan!', content: Flexible( child: Container( padding: const EdgeInsets.all(8), width: Get.width * 0.9, child: TextFormField( controller: _reasonController, decoration: const InputDecoration( labelText: 'Alasan', focusColor: Colors.blueAccent, focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent))), ), ), ), confirm: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: () { Get.back(); _sendData(outstation, false); }, child: const Text('OK'), )); } // ignore: always_declare_return_types _approveOutstation(Outstation outstation) { _sendData(outstation, true); } SizedBox _cancelButton(String label, Outstation outstation) { return SizedBox( width: Get.width, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.red, onPrimary: Colors.white, ), onPressed: () { _rejectOutstation(outstation); }, child: Text(label), ), ); } SizedBox _approveButton(Outstation outstation) { return SizedBox( width: Get.width, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: () { _approveOutstation(outstation); }, child: const Text('Setujui'), ), ); } Widget _buildButtonSection(Outstation outstation) { switch (outstation.approvalStatus) { case 'Menunggu Persetujuan': return Column( children: [ _approveButton(outstation), _cancelButton('Tolak', outstation) ], ); case 'Disetujui': return _cancelButton('Batal Setujui', outstation); case 'Ditolak': return _approveButton(outstation); default: return sizedBox; } } Future _sendData(Outstation outstation, bool isApproved) async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); pd.show(); try { final dataRepo = Provider.of(context, listen: false); final Map data = { 'user_id': outstation.user.id, 'is_approved': isApproved, 'outstation_id': outstation.id, 'reason': _reasonController.value.text }; final http.Response response = await dataRepo.approveOutstation(data); final Map _res = jsonDecode(response.body) as Map; if (response.statusCode == 200) { pd.hide(); showAlertDialog("success", "Sukses", _res['message'].toString(), dismissible: false); _fetchOutstationData(); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { pd.hide(); showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } } @override void dispose() { _reasonController.dispose(); _nameController.dispose(); super.dispose(); } @override void initState() { super.initState(); _fetchOutstationData(); } Widget _buildBody() { if (_isLoading) { return SizedBox( height: Get.height * 0.8, child: const Center( child: SpinKitFadingFour( size: 45, color: Colors.blueAccent, )), ); } if (_filteredOutstation.isEmpty) { return SizedBox( height: Get.height * 0.6, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Belum ada Dinas Luar yang diajukan!') ]), ), ); } return Column( children: _filteredOutstation.map((outstation) { final DateTime dueDate = outstation.dueDate; final DateTime startDate = outstation.startDate; return EmployeeProposalWidget( isApprovalCard: true, employeeName: outstation.user.name, button: _buildButtonSection(outstation), photo: outstation.photo, heroTag: outstation.id.toString(), isApproved: outstation.isApproved, approvalStatus: outstation.approvalStatus, startDate: startDate, dueDate: dueDate, description: outstation.description, title: outstation.title, ); }).toList(), ); } List _setFilter(String value) { if (value == 'Disetujui') { return _outstations .where((element) => element.isApproved == true) .toList(); } if (value == 'Belum Disetujui') { return _outstations .where((element) => element.isApproved == false) .toList(); } return _outstations; } void _selectDate() { Get.defaultDialog( title: 'Pilih Tanggal', content: Flexible( child: SizedBox( height: Get.height * 0.4, width: Get.width * 0.9, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, shouldFillViewport: true, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime(2021), focusedDay: _selectedDate, selectedDayPredicate: (day) { return isSameDay(_selectedDate, day); }, lastDay: DateTime(DateTime.now().year + 5), locale: 'in_ID', onDaySelected: (day, focusedDay) { Get.back(); setState(() { _selectedDate = day; _fetchOutstationData(); }); }, ), ), )); } void _searchByName(String value) { setState(() { if (value.isNotEmpty) { if (_filteredOutstation.isNotEmpty) { _filteredOutstation = _filteredOutstation .where((element) => element.user.name.toLowerCase().contains(value.toLowerCase())) .toList(); } } else { _filteredOutstation = _setFilter(_selectedChoice); } }); } Widget _buildLabelSection() { if (_selectedChoice == 'Tanggal') { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Hasil : ${_filteredOutstation.length} dinas luar'), Text( 'Tanggal : ${DateFormat.yMMMMEEEEd('id_ID').format(_selectedDate)}') ], ); } return Text('Hasil : ${_filteredOutstation.length} dinas luar'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Daftar Dinas Luar Pegawai'), ), body: Container( padding: const EdgeInsets.all(8), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( controller: _nameController, onChanged: _searchByName, decoration: const InputDecoration( prefixIcon: Icon(Icons.search), labelText: 'Cari dengan nama pegawai'), ), sizedBoxH10, Row( children: [ const Expanded( child: Text( 'Filter : ', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0), ), ), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), border: Border.all(color: Colors.grey[600])), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: _selectedChoice, items: choices .map( (choice) => DropdownMenuItem( value: choice, child: Text( choice, ), ), ) .toList(), onChanged: (value) { setState(() { _selectedChoice = value.toString(); _filteredOutstation = _setFilter(value.toString()); if (_nameController.value.text.isNotEmpty) { _searchByName(_nameController.value.text); } }); }), ), ), ) ], ), const Text( 'Pilih Tahun & Bulan : ', ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( DateFormat.yMMMMEEEEd('id_ID').format(_selectedDate), style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.w600, ), ), IconButton( icon: Icon( Icons.calendar_today_rounded, color: Colors.blueAccent[400], ), onPressed: () { _selectDate(); }) ], ), dividerT1, sizedBoxH4, _buildLabelSection(), sizedBoxH8, _buildBody(), ], ), ), ), ); } } ================================================ FILE: lib/screen/employee_paid_leave_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/paid_leave.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_widget.dart'; import 'package:table_calendar/table_calendar.dart'; class EmployeePaidLeaveScreen extends StatefulWidget { @override _EmployeePaidLeaveScreenState createState() => _EmployeePaidLeaveScreenState(); } class _EmployeePaidLeaveScreenState extends State { List _paidLeaves = []; bool _isLoading = false; final TextEditingController _reasonController = TextEditingController(); final TextEditingController _nameController = TextEditingController(); List _filteredPaidLeave = []; Set choices = {'Semua', 'Disetujui', 'Belum Disetujui'}; String _selectedChoice = 'Semua'; DateTime _selectedDate = DateTime.now(); Future _fetchPaidLeaveData() async { try { setState(() { _isLoading = true; }); final dataRepo = Provider.of(context, listen: false); final Map _result = await dataRepo.getAllEmployeePaidLeave(_selectedDate); final List paidLeaves = _result['data'] as List; final List _data = paidLeaves .map((json) => PaidLeave.fromJson(json as Map)) .toList(); setState(() { _paidLeaves = _data; _filteredPaidLeave = _data; }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { setState(() { _isLoading = false; }); } } void _approvePaidLeave(PaidLeave paidLeave) { _sendData(paidLeave, true); } Future _sendData(PaidLeave paidLeave, bool isApproved) async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); pd.show(); try { final dataRepo = Provider.of(context, listen: false); final Map data = { 'user_id': paidLeave.user.id, 'is_approved': isApproved, 'paid_leave_id': paidLeave.id, 'reason': _reasonController.value.text }; final http.Response response = await dataRepo.approvePaidLeave(data); final Map _res = jsonDecode(response.body) as Map; if (response.statusCode == 200) { pd.hide(); showAlertDialog("success", "Sukses", _res['message'].toString(), dismissible: true); _fetchPaidLeaveData(); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { pd.hide(); showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } } @override void dispose() { _reasonController.dispose(); _nameController.dispose(); super.dispose(); } @override void initState() { _fetchPaidLeaveData(); super.initState(); } @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } Widget _buildBody() { if (_isLoading) { return SizedBox( height: Get.height * 0.8, child: const Center( child: SpinKitFadingGrid( size: 45, color: Colors.blueAccent, )), ); } if (_paidLeaves.isEmpty) { return SizedBox( height: Get.height * 0.6, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Belum ada Cuti yang diajukan!') ]), ), ); } return Column( children: _filteredPaidLeave .map((paidLeave) => _buildPaidLeaveItem(paidLeave)) .toList(), ); } void _rejectPaidLeave(PaidLeave paidLeave) { Get.defaultDialog( title: 'Alasan Pembatalan!', content: Flexible( child: Container( padding: const EdgeInsets.all(8), width: Get.width * 0.9, child: TextFormField( controller: _reasonController, decoration: const InputDecoration( labelText: 'Alasan', focusColor: Colors.blueAccent, focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent))), ), ), ), confirm: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: () { Get.back(); _sendData(paidLeave, false); }, child: const Text('OK'), )); } SizedBox _cancelButton(String label, PaidLeave paidLeave) { return SizedBox( width: Get.width, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.red, onPrimary: Colors.white, ), onPressed: () { _rejectPaidLeave(paidLeave); }, child: Text(label), ), ); } SizedBox _approveButton(PaidLeave paidLeave) { return SizedBox( width: Get.width, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: () { _approvePaidLeave(paidLeave); }, child: const Text('Setujui'), ), ); } Widget _buildButtonSection(PaidLeave paidLeave) { switch (paidLeave.approvalStatus) { case 'Menunggu Persetujuan': return Column( children: [ _approveButton(paidLeave), _cancelButton('Tolak', paidLeave) ], ); case 'Disetujui': return _cancelButton('Batal Setujui', paidLeave); case 'Ditolak': return _approveButton(paidLeave); default: return sizedBox; } } Widget _buildPaidLeaveItem(PaidLeave paidLeave) { final startDate = paidLeave.startDate; final dueDate = paidLeave.dueDate; return EmployeeProposalWidget( isApprovalCard: true, isPaidLeave: true, description: paidLeave.description, dueDate: dueDate, category: paidLeave.category, startDate: startDate, heroTag: paidLeave.id.toString(), photo: paidLeave.photo, approvalStatus: paidLeave.approvalStatus, employeeName: paidLeave.user.name, button: _buildButtonSection(paidLeave), isApproved: paidLeave.isApproved, title: paidLeave.title, ); } List _setFilter(String value) { if (value == 'Disetujui') { return _paidLeaves .where((element) => element.isApproved == true) .toList(); } if (value == 'Belum Disetujui') { return _paidLeaves .where((element) => element.isApproved == false) .toList(); } return _paidLeaves; } void _selectDate() { Get.defaultDialog( title: 'Pilih Tanggal', content: Flexible( child: SizedBox( height: Get.height * 0.4, width: Get.width * 0.9, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, shouldFillViewport: true, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime(2021), focusedDay: _selectedDate, selectedDayPredicate: (day) { return isSameDay(_selectedDate, day); }, lastDay: DateTime(DateTime.now().year + 5), locale: 'in_ID', onDaySelected: (day, focusedDay) { Get.back(); setState(() { _selectedDate = day; _fetchPaidLeaveData(); }); }, ), ), )); } void _searchByName(String value) { setState(() { if (value.isNotEmpty) { if (_filteredPaidLeave.isNotEmpty) { _filteredPaidLeave = _filteredPaidLeave .where((element) => element.user.name.toLowerCase().contains(value.toLowerCase())) .toList(); } } else { _filteredPaidLeave = _setFilter(_selectedChoice); } }); } Widget _buildLabelSection() { if (_selectedChoice == 'Tanggal') { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Hasil : ${_filteredPaidLeave.length} cuti'), Text( 'Tanggal : ${DateFormat.yMMMMEEEEd('id_ID').format(_selectedDate)}') ], ); } return Text('Hasil : ${_filteredPaidLeave.length} cuti'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Daftar Cuti Pegawai'), ), body: Container( padding: const EdgeInsets.all(8), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( controller: _nameController, decoration: const InputDecoration( prefixIcon: Icon(Icons.search), labelText: 'Cari dengan nama pegawai', ), onChanged: _searchByName, ), sizedBoxH10, Row( children: [ const Expanded( child: Text( 'Filter : ', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0), ), ), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), border: Border.all(color: Colors.grey[600])), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: _selectedChoice, items: choices .map( (choice) => DropdownMenuItem( value: choice, child: Text( choice, ), ), ) .toList(), onChanged: (value) { setState(() { _selectedChoice = value.toString(); _filteredPaidLeave = _setFilter(value.toString()); if (_nameController.value.text.isNotEmpty) { _searchByName(_nameController.value.text); } }); }), ), ), ) ], ), const Text( 'Pilih Tahun & Bulan : ', ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( DateFormat.yMMMMEEEEd('id_ID').format(_selectedDate), style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.w600, ), ), IconButton( icon: Icon( Icons.calendar_today_rounded, color: Colors.blueAccent[400], ), onPressed: () { _selectDate(); }) ], ), dividerT1, sizedBoxH4, _buildLabelSection(), sizedBoxH8, _buildBody(), ], ), ), ), ); } } ================================================ FILE: lib/screen/employee_permission.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/absent_permission.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_widget.dart'; import 'package:table_calendar/table_calendar.dart'; class EmployeePermissionScreen extends StatefulWidget { @override _EmployeePermissionScreenState createState() => _EmployeePermissionScreenState(); } class _EmployeePermissionScreenState extends State { List _permissions = []; bool _isLoading = false; final TextEditingController _reasonController = TextEditingController(); final TextEditingController _nameController = TextEditingController(); List _filteredPermission = []; Set choices = {'Semua', 'Disetujui', 'Belum Disetujui'}; String _selectedChoice = 'Semua'; DateTime _selectedDate = DateTime.now(); @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } Future _fetchPermissionData() async { try { setState(() { _isLoading = true; }); final dataRepo = Provider.of(context, listen: false); final Map _result = await dataRepo.getAllEmployeePermissions(_selectedDate); final List permissions = _result['data'] as List; final List _data = permissions .map( (json) => AbsentPermission.fromJson(json as Map)) .toList(); setState(() { _permissions = _data; _filteredPermission = _data; }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { _isLoading = false; } } void _rejectPermission(AbsentPermission permission) { Get.defaultDialog( title: 'Alasan Pembatalan!', content: Flexible( child: Container( padding: const EdgeInsets.all(8), width: Get.width * 0.9, child: TextFormField( decoration: const InputDecoration( labelText: 'Alasan', focusColor: Colors.blueAccent, focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent))), controller: _reasonController, ), ), ), confirm: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: () { Get.back(); _sendData(permission, false); }, child: const Text('OK'), )); } void _approvePermission(AbsentPermission permission) { _sendData(permission, true); } SizedBox _cancelButton(String label, AbsentPermission permission) { return SizedBox( width: Get.width, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.red, onPrimary: Colors.white, ), onPressed: () { _rejectPermission(permission); }, child: Text(label), ), ); } SizedBox _approveButton(AbsentPermission permission) { return SizedBox( width: Get.width, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: () { _approvePermission(permission); }, child: const Text('Setujui'), ), ); } Widget _buildButtonSection(AbsentPermission permission) { switch (permission.approvalStatus) { case 'Menunggu Persetujuan': return Column( children: [ _approveButton(permission), _cancelButton('Tolak', permission) ], ); case 'Disetujui': return _cancelButton('Batal Setujui', permission); case 'Ditolak': return _approveButton(permission); default: return sizedBox; } } Future _sendData(AbsentPermission permission, bool isApproved) async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); pd.show(); try { final dataRepo = Provider.of(context, listen: false); final Map data = { 'user_id': permission.user.id, 'is_approved': isApproved, 'permission_id': permission.id, 'reason': _reasonController.value.text }; final http.Response response = await dataRepo.approvePermission(data); final Map _res = jsonDecode(response.body) as Map; if (response.statusCode == 200) { pd.hide(); showAlertDialog("success", "Sukses", _res['message'].toString(), dismissible: false); _fetchPermissionData(); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { pd.hide(); showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } } @override void dispose() { _reasonController.dispose(); _nameController.dispose(); super.dispose(); } @override void initState() { _fetchPermissionData(); super.initState(); } Widget _buildBody() { if (_isLoading) { return SizedBox( height: Get.height * 0.8, child: const Center( child: SpinKitFadingCircle( size: 45, color: Colors.blueAccent, )), ); } if (_filteredPermission.isEmpty) { return SizedBox( height: Get.height * 0.6, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Belum ada izin yang diajukan!') ]), ), ); } return Column( children: _filteredPermission.map((permission) { final DateTime dueDate = permission.dueDate; final DateTime startDate = permission.startDate; return EmployeeProposalWidget( title: permission.title, description: permission.description, startDate: startDate, dueDate: dueDate, employeeName: permission.user.name, isApproved: permission.isApproved, approvalStatus: permission.approvalStatus, heroTag: permission.id.toString(), photo: permission.photo, isApprovalCard: true, button: _buildButtonSection(permission), ); }).toList(), ); } List _setFilter(String value) { if (value == 'Disetujui') { return _permissions .where((element) => element.isApproved == true) .toList(); } if (value == 'Belum Disetujui') { return _permissions .where((element) => element.isApproved == false) .toList(); } return _permissions; } void _selectDate() { Get.defaultDialog( title: 'Pilih Tanggal', content: Flexible( child: SizedBox( height: Get.height * 0.4, width: Get.width * 0.9, child: TableCalendar( availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarStyle: const CalendarStyle( weekendTextStyle: TextStyle(color: Colors.red), ), calendarBuilders: const CalendarBuilders( dowBuilder: dowBuilder, ), calendarFormat: CalendarFormat.month, availableGestures: AvailableGestures.horizontalSwipe, shouldFillViewport: true, headerStyle: const HeaderStyle(titleCentered: true), startingDayOfWeek: StartingDayOfWeek.monday, firstDay: DateTime(2021), focusedDay: _selectedDate, selectedDayPredicate: (day) { return isSameDay(_selectedDate, day); }, lastDay: DateTime(DateTime.now().year + 5), locale: 'in_ID', onDaySelected: (day, focusedDay) { Get.back(); setState(() { _selectedDate = day; _fetchPermissionData(); }); }, ), ), )); } void _searchByName(String value) { setState(() { if (value.isNotEmpty) { if (_filteredPermission.isNotEmpty) { _filteredPermission = _filteredPermission .where((element) => element.user.name.toLowerCase().contains(value.toLowerCase())) .toList(); } } else { _filteredPermission = _setFilter(_selectedChoice); } }); } Widget _buildLabelSection() { if (_selectedChoice == 'Tanggal') { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Hasil : ${_filteredPermission.length} izin'), Text( 'Tanggal : ${DateFormat.yMMMMEEEEd('id_ID').format(_selectedDate)}') ], ); } return Text('Hasil : ${_filteredPermission.length} izin'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Daftar Izin Pegawai'), ), body: Container( padding: const EdgeInsets.all(8), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( controller: _nameController, decoration: const InputDecoration( prefixIcon: Icon(Icons.search), labelText: 'Cari dengan nama pegawai', ), onChanged: _searchByName, ), sizedBoxH10, Row( children: [ const Expanded( child: Text( 'Filter : ', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0), ), ), Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), border: Border.all(color: Colors.grey[600])), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, value: _selectedChoice, items: choices .map( (choice) => DropdownMenuItem( value: choice, child: Text( choice, ), ), ) .toList(), onChanged: (value) { setState(() { _selectedChoice = value.toString(); _filteredPermission = _setFilter(value.toString()); if (_nameController.value.text.isNotEmpty) { _searchByName(_nameController.value.text); } }); }), ), ), ) ], ), sizedBoxH10, const Text( 'Pilih Tahun & Bulan : ', ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( DateFormat.yMMMMEEEEd('id_ID').format(_selectedDate), style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.w600, ), ), IconButton( icon: Icon( Icons.calendar_today_rounded, color: Colors.blueAccent[400], ), onPressed: () { _selectDate(); }) ], ), dividerT1, sizedBoxH4, _buildLabelSection(), sizedBoxH8, _buildBody(), ], ), ), ), ); } } ================================================ FILE: lib/screen/forgot_pass_screen.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_config/flutter_config.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:url_launcher/url_launcher.dart'; class ForgotPassScreen extends StatelessWidget { final double getSmallDiameter = Get.width * 2 / 3; final double getBigDiameter = Get.width * 7 / 8; final String adminPhoneNumber = FlutterConfig.get("ADMIN_PHONE_NUMBER").toString(); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[200], body: Stack( alignment: Alignment.center, children: [ Positioned( right: -getSmallDiameter / 3, top: -getSmallDiameter / 3, child: Container( width: getSmallDiameter, height: getSmallDiameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [Colors.lightBlue[200], Colors.blueAccent], begin: Alignment.topCenter, end: Alignment.bottomCenter), ), ), ), Positioned( left: -getBigDiameter / 4, top: -getBigDiameter / 4, child: Container( width: getBigDiameter, height: getBigDiameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [Colors.blueAccent[700], Colors.blueAccent], begin: Alignment.topCenter, end: Alignment.bottomCenter), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'assets/logo/logo.png', width: 200, ), const Text( 'Sistem Absensi Pegawai', style: TextStyle(fontSize: 12.0, color: Colors.white), ), ], ), ), ), Align( alignment: Alignment.bottomCenter, child: ListView( children: [ ClipRRect( borderRadius: BorderRadius.circular(6), child: Container( margin: EdgeInsets.fromLTRB(5.0, Get.height * 0.35, 5.0, 10), padding: const EdgeInsets.fromLTRB(10, 0, 10, 25), child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), elevation: 6.0, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ sizedBoxH16, const Text( 'Fajrian Aidil Pratama', style: TextStyle( fontSize: 18.0, fontWeight: FontWeight.w600, ), ), const Text( 'Administrator\nFounder of @BanuaCoders', style: TextStyle(color: Colors.grey), textAlign: TextAlign.center, ), sizedBoxH2, const Divider( color: Colors.black26, thickness: 1, ), sizedBoxH2, Text( 'Tekan tombol dibawah untuk menghubungi administrator sistem dan melaporkan masalah anda', textAlign: TextAlign.center, style: TextStyle( color: Colors.grey[700], ), ), Wrap( spacing: 8.0, children: [ IconButton( onPressed: () { launch('tel:$adminPhoneNumber'); }, color: Colors.blueAccent, icon: const Icon(Icons.phone), tooltip: 'Hubungi via Telpon', ), IconButton( onPressed: () async { final String whatsappUrl = 'whatsapp://send?phone=$adminPhoneNumber'; await canLaunch(whatsappUrl) ? launch(whatsappUrl) : Get.defaultDialog( title: 'Gagal', content: const Text( 'WhatsApp tidak ditemukan!')); }, color: Colors.green[600], icon: const FaIcon(FontAwesomeIcons.whatsapp), tooltip: 'Hubungi via WA', ), ], ) ], ), ), ), ), ), ], ), ), ], ), ); } } ================================================ FILE: lib/screen/home_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_countdown_timer/countdown_timer_controller.dart'; import 'package:flutter_countdown_timer/flutter_countdown_timer.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shimmer/shimmer.dart'; import 'package:spo_balaesang/models/employee.dart'; import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/employee_list_screen.dart'; import 'package:spo_balaesang/screen/notification_list_screen.dart'; import 'package:spo_balaesang/screen/presence_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_presence_card_widget.dart'; import 'package:spo_balaesang/widgets/next_presence_empty_card_widget.dart'; class HomeScreen extends StatefulWidget { @override _HomeScreenState createState() => _HomeScreenState(); } class _HomeScreenState extends State { List _images = []; CountdownTimerController _countdownController; User user; List _users; bool isLoading = false; double _percentage = 0; final RefreshController _refreshController = RefreshController(); @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } Widget _buildImageStack() { if (_images.isNotEmpty) { final widgets = _images .sublist(0, 5) .map( (e) => Padding( padding: const EdgeInsets.only(right: 4.0, top: 4.0), child: ClipRRect( borderRadius: BorderRadius.circular(100), child: CachedNetworkImage( imageUrl: e, placeholder: (_, __) => Shimmer.fromColors( baseColor: Colors.grey[400], highlightColor: Colors.white, child: Padding( padding: const EdgeInsets.only(right: 4.0, top: 4.0), child: ClipRRect( borderRadius: BorderRadius.circular(100), child: Material( color: Colors.grey[300], child: InkWell( onTap: () {}, splashColor: Colors.white, borderRadius: BorderRadius.circular(100), child: Padding( padding: const EdgeInsets.all(16.0), child: Icon( Icons.add, color: Colors.grey[300], ), ), ), ), ), ), ), height: 55.0, ), ), ), ) .toList(); widgets.add(Padding( padding: const EdgeInsets.only(right: 4.0, top: 4.0), child: ClipRRect( borderRadius: BorderRadius.circular(100), child: Material( color: Colors.grey[300], child: InkWell( onTap: () { Get.to(() => EmployeeListScreen(employees: _users)); }, splashColor: Colors.white, borderRadius: BorderRadius.circular(100), child: const Padding( padding: EdgeInsets.all(16.0), child: Icon( Icons.add, color: Colors.grey, ), ), ), ), ), )); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: widgets, ), ); } return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: List.generate( 7, (index) => Shimmer.fromColors( baseColor: Colors.grey[400], highlightColor: Colors.white, child: Padding( padding: const EdgeInsets.only(right: 4.0, top: 4.0), child: ClipRRect( borderRadius: BorderRadius.circular(100), child: Material( color: Colors.grey[300], child: InkWell( onTap: () {}, splashColor: Colors.white, borderRadius: BorderRadius.circular(100), child: Padding( padding: const EdgeInsets.all(16.0), child: Icon( Icons.add, color: Colors.grey[300], ), ), ), ), ), ), )), ), ); } Future _getAllEmployee({ProgressDialog pd}) async { try { final dataRepo = Provider.of(context, listen: false); final List users = await dataRepo.getAllEmployee(); setState(() { _users = users; _images = users .map((e) => "https://ui-avatars.com/api/?name=${e.name.replaceAll(' ', '+')}&size=248" .toString()) .toList(); if (pd != null && pd.isShowing()) pd?.hide(); }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } } Future _getUser() async { try { setState(() { isLoading = true; }); final dataRepo = Provider.of(context, listen: false); User _user = await dataRepo.getMyData(); if (_user == null) { showAlertDialog( 'failed', 'Kesalahan', 'Pastikan anda terhubung ke internet lalu tekan tombol refresh.', dismissible: true, ); final SharedPreferences prefs = await SharedPreferences.getInstance(); final data = jsonDecode(prefs.getString(prefsUserKey)); _user = User.fromJson(data as Map); } OneSignal.shared.setExternalUserId(_user.id.toString()); setState(() { user = _user; _countAttendancePercentage(); }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { if (mounted) { setState(() { isLoading = false; _countdownController = CountdownTimerController( onEnd: () { _getUser(); }, endTime: checkTime()); }); } } } void _countAttendancePercentage() { double sum = 0; if (user == null) { return; } if (user.presences == null) { return; } if (user.presences.isEmpty) { _percentage = 0; } // ignore: avoid_function_literals_in_foreach_calls user.presences.forEach((presence) { switch (presence.status) { case 'Tepat Waktu': case 'Dinas Luar': case 'Cuti Tahunan': sum += 25; break; case 'Cuti Bersalin': case 'Cuti Sakit': case 'Cuti Alasan Penting': sum += 24.375; break; case 'Terlambat': sum += 6.25; break; case 'Izin': sum += 12.5; break; default: sum += 0; break; } }); _percentage = sum; } Widget _buildShimmerSection(double width, double height) { return Shimmer.fromColors( baseColor: Colors.grey[300], highlightColor: Colors.white, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), color: Colors.blueAccent), width: width, height: height, ), ); } Widget _buildUserNameSection() { if (isLoading) { return _buildShimmerSection(200, 20); } if (user == null) { const Text( 'Gagal memuat data', style: TextStyle( color: Colors.white, fontSize: 20.0, fontWeight: FontWeight.w600), ); } final nipSection = user.status == 'PNS' ? Text( 'NIP : ${user.nip}', style: const TextStyle( color: Colors.white, fontSize: 12.0, ), ) : sizedBox; return Column( children: [ Text( user.name ?? '', style: const TextStyle( color: Colors.white, fontSize: 20.0, fontWeight: FontWeight.w600), ), sizedBoxH5, nipSection ], ); } Widget _buildPositionSection() { if (isLoading) { return Column( children: [sizedBoxH10, _buildShimmerSection(60, 15)], ); } if (user != null) { final text = user.position == 'Camat' || user.position == 'Sekcam' ? user.position : "${user.position} - ${user.department}"; return AutoSizeText( '($text)', style: const TextStyle(color: Colors.white), maxFontSize: 14.0, ); } return const Text( 'Coba untuk memuat kembali data!', style: TextStyle( color: Colors.white, fontSize: 20.0, fontWeight: FontWeight.w600), ); } List _buildPresenceSection() { if (isLoading) { return [ _buildShimmerSection(Get.width, 250), sizedBoxH10, _buildShimmerSection(Get.width, 250), sizedBoxH10, _buildShimmerSection(Get.width, 250), sizedBoxH10, _buildShimmerSection(Get.width, 250), sizedBoxH10, ]; } if (user != null && user.presences.isNotEmpty) { return user.presences.map((presence) { final Color color = checkStatusColor(presence.status); String status = presence.status ?? ''; if (presence.status == 'Terlambat') { final String duration = calculateLateTime(presence.startTime, presence.attendTime); status = '${presence.status} $duration'; } return EmployeePresenceCardWidget( photo: presence.photo, heroTag: presence.id.toString(), status: status, color: color, address: presence.location.address, attendTime: presence.attendTime, point: formatPercentage(checkPresencePercentage(presence.status)), presenceType: presence.codeType, ); }).toList(); } return [ Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ SizedBox( width: 150, height: 150, child: FlareActor( 'assets/flare/empty.flr', animation: 'empty', ), ), Text('Tidak ada presensi hari ini!') ]) ]; } int checkTime() { if (user != null && user.nextPresence != null && user.nextPresence.startTime.isAfter(DateTime.now())) { return user?.nextPresence?.startTime?.millisecondsSinceEpoch; } if (user != null && user.nextPresence != null && user.nextPresence.endTime.isAfter(DateTime.now())) { return user?.nextPresence?.endTime?.millisecondsSinceEpoch; } return 0; } String _checkTimeLabel() { if (user != null && user.nextPresence != null && user.nextPresence.startTime.isAfter(DateTime.now())) { return "MULAI DALAM :"; } return "SELESAI DALAM :"; } Widget _checkStatusIcon(String status) { switch (status) { case 'Tepat Waktu': return Column( children: [ Icon(Icons.check, color: checkStatusColor(status), size: 54), const Text( 'Hadir', style: TextStyle(color: Colors.blueGrey), ) ], ); case 'Tidak Hadir': case 'Terlambat': return Column( children: [ Icon(Icons.warning, color: checkStatusColor(status), size: 54), Text( status, style: const TextStyle(color: Colors.blueGrey), ) ], ); case 'Dinas Luar': case 'Cuti Tahunan': case 'Cuti Bersalin': case 'Cuti Alasan Penting': case 'Cuti Sakit': case 'Izin': return Column( children: [ Icon(Icons.calendar_today, size: 54, color: checkStatusColor(status)), Text( status, style: const TextStyle(color: Colors.blueGrey), ) ], ); default: return Column( children: [ Icon(Icons.warning, color: checkStatusColor(status), size: 54), Text( status, style: const TextStyle(color: Colors.blueGrey), ) ], ); } } Widget _buildStatusSection() { if (user.nextPresence.attendTime.isNotEmpty || user.nextPresence.startTime.isAfter(DateTime.now())) { return _checkStatusIcon(user.nextPresence.status); } return Center( child: ClipRRect( borderRadius: BorderRadius.circular(20.0), child: Card( color: Colors.green[300], child: InkWell( onTap: () { Get.to(() => PresenceScreen(user: user)) .then((value) => _getUser()); }, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: const [ Icon( Icons.qr_code_rounded, size: 84, color: Colors.white, ), Text( 'Mulai Absen', style: TextStyle( color: Colors.white, ), ), ], ), ), ), ), ), ); } String _checkPresenceStatus(double percentage) { if (percentage >= 25 && percentage < 50) { return 'Buruk'; } else if (percentage >= 50 && percentage < 75) { return 'Cukup Baik'; } else if (percentage >= 75 && percentage < 85) { return 'Baik'; } else if (percentage >= 85 && percentage <= 100) { return 'Sangat Baik'; } return 'Sangat Buruk'; } Color _checkPresenceStatusColor(double percentage) { if (percentage >= 25 && percentage < 50) { return Colors.red; } else if (percentage >= 50 && percentage < 75) { return Colors.orange; } else if (percentage >= 75 && percentage < 85) { return Colors.blueAccent; } else if (percentage >= 85 && percentage <= 100) { return Colors.green; } return Colors.red[800]; } Widget _buildCountdownSection() { if (_countdownController == null) { return const Text('Memuat Timer...'); } return CountdownTimer( controller: _countdownController, endWidget: const AutoSizeText( 'Semua absen hari ini telah selesai', maxFontSize: 12.0, minFontSize: 10.0, ), ); } Widget _buildTimerSection() { if (isLoading) { return _buildShimmerSection(Get.width, 180); } if (user?.nextPresence != null) { String status = user.nextPresence.status; double fontSize = 14; if (status == 'Terlambat') { final duration = calculateLateTime( user.nextPresence.startTime, user.nextPresence.attendTime); status += ' $duration'; fontSize = 12; } return Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 4.0, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( DateFormat.EEEE().format(user.nextPresence.date), style: const TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(width: 5.0), const Text('|'), const SizedBox(width: 5.0), Text( DateFormat.yMMMd().format(user.nextPresence.date), ), const SizedBox(width: 5.0), const Text('|'), const SizedBox(width: 5.0), Text( user.nextPresence.attendTime.isEmpty ? '-' : '${user.nextPresence.attendTime} WITA', ), ], ), const Divider( thickness: 1.0, color: Colors.black26, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'SKEMA ABSENSI :', style: TextStyle(fontWeight: FontWeight.bold), ), sizedBoxH2, Text(user.nextPresence.codeType), sizedBoxH10, const Text( 'JADWAL ABSENSI :', style: TextStyle(fontWeight: FontWeight.bold), ), sizedBoxH2, Row( children: [ Text( DateFormat('HH:mm') .format(user.nextPresence.startTime), ), const Text(' - '), Text( DateFormat('HH:mm') .format(user.nextPresence.endTime), ) ], ), sizedBoxH10, const Text( 'STATUS KEHADIRAN :', style: TextStyle(fontWeight: FontWeight.bold), ), sizedBoxH2, Text( status, style: TextStyle( fontSize: fontSize, color: checkStatusColor(user.nextPresence.status)), ), sizedBoxH10, Text( _checkTimeLabel(), style: const TextStyle(fontWeight: FontWeight.bold), ), sizedBoxH2, _buildCountdownSection(), ], ), Expanded(child: _buildStatusSection()), ], ), ], ), ), ); } if (user?.holiday != null) { return NextPresenceEmptyCardWidget( color: checkStatusColor('Izin'), topLabel: 'Libur', firstLabel: 'JENIS LIBUR', firstContent: 'Libur Nasional', secondLabel: 'NAMA LIBUR', secondContent: user.holiday.name, thirdLabel: 'STATUS KEHADIRAN', thirdContent: 'Libur', fourthLabel: 'CATATAN', fourthContent: '-', trailingLabel: 'Libur Nasional', trailingTop: Icon(Icons.calendar_today_rounded, color: checkStatusColor('Izin'), size: 72), ); } if (user.isWeekend) { return NextPresenceEmptyCardWidget( color: checkStatusColor('Izin'), topLabel: 'Akhir Pekan', firstLabel: 'JENIS LIBUR', firstContent: 'Akhir Pekan', secondLabel: 'NAMA LIBUR', secondContent: 'Akhir Pekan', thirdLabel: 'STATUS KEHADIRAN', thirdContent: 'Libur', fourthLabel: 'CATATAN', fourthContent: 'Tidak Ada Presensi Hari Ini', trailingLabel: 'AKHIR PEKAN', trailingTop: Icon(Icons.calendar_today_rounded, color: checkStatusColor('Izin'), size: 72), ); } return NextPresenceEmptyCardWidget( color: _checkPresenceStatusColor(_percentage), topLabel: 'Selesai', firstLabel: 'SKEMA ABSENSI', firstContent: '-', secondLabel: 'JADWAL ABSENSI', secondContent: '-', thirdLabel: 'STATUS KEHADIRAN', thirdContent: _checkPresenceStatus(_percentage), fourthLabel: 'CATATAN', fourthContent: 'Semua Presensi Sudah Selesai', trailingLabel: 'KEHADIRAN', trailingTop: Text( formatPercentage(_percentage), style: TextStyle( fontSize: 32, color: _checkPresenceStatusColor(_percentage), ), ), ); } Widget _buildPNSHonorerSection() { final pns = _countUserByStatus('PNS'); final honorer = _countUserByStatus('Honorer'); return Text( '$pns PNS/$honorer Honorer', style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 12.0, color: Colors.grey), ); } int _countUserByStatus(String status) { if (_users == null) { return 0; } return _users.where((element) => element.status == status).length + (user?.status == status ? 1 : 0); } @override void dispose() { _countdownController.dispose(); super.dispose(); } @override void initState() { super.initState(); Future.wait([_getUser(), _getAllEmployee()]); } Widget _buildUnreadNotificationCount() { if (user != null && user.unreadNotification > 0) { return Positioned( right: 8, top: 6, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(50)), child: Text( user?.unreadNotification.toString(), style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold), ), ), ); } return sizedBox; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], appBar: AppBar( backgroundColor: Colors.blueAccent, elevation: 0.0, actions: [ Stack( children: [ IconButton( icon: Icon( (user != null && user.unreadNotification > 0) ? Icons.notifications_active_rounded : Icons.notifications_none_rounded, color: Colors.white, ), onPressed: () { Get.to(() => NotificationListScreen()) .then((value) => _getUser()); }, ), _buildUnreadNotificationCount(), ], ), ], leadingWidth: Get.width * 0.25, leading: Image.asset( 'assets/logo/logo.png', ), ), body: SmartRefresher( header: const MaterialClassicHeader(), controller: _refreshController, onRefresh: () async { final ProgressDialog pd = ProgressDialog(context); final showing = await pd.show(); try { await Future.wait([_getUser(), _getAllEmployee(pd: pd)]); } catch (e) { _refreshController.refreshFailed(); showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { _refreshController.refreshCompleted(); if (showing) { pd.hide(); } } }, child: CustomScrollView( physics: const ClampingScrollPhysics(), slivers: [_buildHeader(), _buildNextPresence()], ), ), ); } SliverToBoxAdapter _buildHeader() { return SliverToBoxAdapter( child: Stack( children: [ Container( padding: EdgeInsets.only( left: 20.0, right: 20.0, bottom: Get.height * 0.13, top: 5.0), decoration: const BoxDecoration( boxShadow: [ BoxShadow( color: Colors.black26, offset: Offset(0, 5), blurRadius: 10.0, ) ], color: Colors.blueAccent, borderRadius: BorderRadius.only( bottomLeft: Radius.circular(40.0), bottomRight: Radius.circular(40.0))), child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ const Text( 'Selamat Datang', style: TextStyle(fontSize: 12.0, color: Colors.white), ), sizedBoxH10, _buildUserNameSection(), _buildPositionSection(), ], ) ], ), ), ), Align( alignment: Alignment.bottomCenter, child: Container( decoration: const BoxDecoration( boxShadow: [ BoxShadow( color: Colors.black26, offset: Offset(0, 3), blurRadius: 15.0, ) ], ), margin: EdgeInsets.only( top: Get.height * 0.15, left: 20.0, right: 20.0, ), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: SizedBox( width: Get.width, child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${_users == null ? 0 : (_users.length + 1)} Pegawai', style: const TextStyle( fontWeight: FontWeight.w600), ), sizedBoxH10, _buildPNSHonorerSection() ], ), sizedBoxH10, Padding( padding: const EdgeInsets.all(8.0), child: _buildImageStack(), ) ], ), ), ), ), ), ), ), ], ), ); } SliverToBoxAdapter _buildNextPresence() { return SliverToBoxAdapter( child: Container( margin: const EdgeInsets.only(top: 12.0), padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Absen Selanjutnya', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0, ), ), Center( child: _buildTimerSection(), ), sizedBoxH30, const Text( 'Absen Hari Ini', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0, ), ), Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: _buildPresenceSection(), ), ) ], ), ), ); } } ================================================ FILE: lib/screen/image_detail_screen.dart ================================================ import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; class ImageDetailScreen extends StatefulWidget { const ImageDetailScreen({this.imageUrl, this.bytes, @required this.tag}); final String imageUrl; final Uint8List bytes; final String tag; @override _ImageDetailScreenState createState() => _ImageDetailScreenState(); } class _ImageDetailScreenState extends State { @override void initState() { super.initState(); } @override void dispose() { super.dispose(); } @override Widget build(BuildContext context) { final provider = widget.imageUrl == null ? MemoryImage(widget.bytes) : CachedNetworkImageProvider(widget.imageUrl); return Scaffold( body: Center( child: PhotoView( heroAttributes: PhotoViewHeroAttributes( tag: widget.tag, transitionOnUserGestures: true), maxScale: 5.0, imageProvider: provider as ImageProvider)), ); } } ================================================ FILE: lib/screen/login_screen.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:device_info/device_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/forgot_pass_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; class LoginScreen extends StatefulWidget { @override _LoginScreenState createState() => _LoginScreenState(); } class _LoginScreenState extends State { double getSmallDiameter = Get.width * 2 / 3; double getBigDiameter = Get.size.width * 7 / 8; bool _isLoading = false; final TextEditingController _phoneController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); bool isPasswordVisible = false; Widget _buildPhoneForm() { return TextFormField( validator: (String value) { if (value.isEmpty) { return 'Nomor telpon tidak boleh kosong!'; } return null; }, controller: _phoneController, keyboardType: TextInputType.phone, decoration: InputDecoration( focusedBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), prefixIcon: Icon( Icons.phone_android, color: Colors.blueAccent[700], ), labelText: 'Nomor Telpon', labelStyle: TextStyle(color: Colors.blueAccent[200])), style: TextStyle(color: Colors.blueAccent[700]), ); } Widget _buildPasswordForm() { return TextFormField( validator: (String value) { if (value.isEmpty) { return 'Kata sandi tidak boleh kosong!'; } return null; }, obscureText: !isPasswordVisible, controller: _passwordController, keyboardType: TextInputType.text, decoration: InputDecoration( focusedBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), suffixIcon: GestureDetector( onTap: () { setState(() { isPasswordVisible = !isPasswordVisible; }); }, child: Icon( isPasswordVisible ? Icons.visibility_off : Icons.visibility, color: Colors.blueAccent[700], ), ), prefixIcon: Icon( Icons.lock_outline, color: Colors.blueAccent[700], ), labelText: 'Password', labelStyle: TextStyle(color: Colors.blueAccent[200])), style: TextStyle(color: Colors.blueAccent[700]), ); } Future getDeviceInfo() async { final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); String model; if (Platform.isAndroid) { final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; model = androidInfo.model; } return model; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[200], body: Stack( children: [ Positioned( right: -getSmallDiameter / 3, top: -getSmallDiameter / 3, child: Container( width: getSmallDiameter, height: getSmallDiameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [Colors.lightBlue[200], Colors.blueAccent], begin: Alignment.topCenter, end: Alignment.bottomCenter), ), ), ), Positioned( left: -getBigDiameter / 4, top: -getBigDiameter / 4, child: Container( width: getBigDiameter, height: getBigDiameter, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: [Colors.blueAccent[700], Colors.blueAccent], begin: Alignment.topCenter, end: Alignment.bottomCenter), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'assets/logo/logo.png', width: 200, ), const Text( 'Sistem Absensi Pegawai', style: TextStyle(fontSize: 12.0, color: Colors.white), ), ], ), ), ), Align( alignment: Alignment.bottomCenter, child: ListView( children: [ ClipRRect( borderRadius: BorderRadius.circular(6), child: Container( margin: const EdgeInsets.fromLTRB(5.0, 350, 5.0, 10), padding: const EdgeInsets.fromLTRB(10, 0, 10, 25), child: Card( elevation: 6.0, child: Column( children: [ _buildPhoneForm(), _buildPasswordForm() ], ), ), ), ), Align( alignment: Alignment.centerRight, child: Container( margin: const EdgeInsets.only(right: 20.0, bottom: 20.0), child: InkWell( onTap: () { Get.to(() => ForgotPassScreen(), fullscreenDialog: true); }, child: Text( 'Lupa Password?', style: TextStyle(color: Colors.blue[800]), ), ), ), ), Container( margin: const EdgeInsets.fromLTRB(20, 0, 20, 30), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: MediaQuery.of(context).size.width * 0.5, height: 40.0, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), gradient: LinearGradient( colors: [ Colors.lightBlue[700], Colors.lightBlue[900] ], begin: Alignment.topLeft, end: Alignment.bottomRight), ), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(10.0), child: InkWell( borderRadius: BorderRadius.circular(10.0), onTap: _isLoading ? null : () async { final SharedPreferences prefs = await SharedPreferences.getInstance(); setState(() { _isLoading = true; }); final String _deviceName = await getDeviceInfo(); final Map data = { 'phone': _phoneController.value.text, 'password': _passwordController.value.text, 'device_name': _deviceName }; try { final dataRepository = Provider.of(context, listen: false); final http.Response response = await dataRepository.login(data); final Map result = jsonDecode(response.body) as Map; if (response.statusCode == 200) { prefs.setString( prefsTokenKey, jsonEncode(result[jsonDataField] [prefsTokenKey])); prefs.setString( prefsUserKey, jsonEncode( result[jsonDataField])); OneSignal.shared.setExternalUserId( result[jsonDataField][userIdField] .toString()); Get.off(() => BottomNavScreen()); } else { showErrorDialog(result); } } on SocketException catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': [e.message] } }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { setState(() { _isLoading = false; }); } }, child: Center( child: _isLoading ? const SizedBox( height: 30.0, width: 30.0, child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation( Colors.white), ), ) : const Text( 'MASUK', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600), ), ), ), ), ), ) ], ), ), Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ Text( 'v5.0.3', style: TextStyle( color: Colors.grey, fontSize: 12.0, fontWeight: FontWeight.w500), ), Text( 'Sistem Absensi Pegawai Online by Banua Coders ', style: TextStyle( color: Colors.grey, fontSize: 12.0, fontWeight: FontWeight.w500), ) ], ), ], ), ), ], ), ); } } ================================================ FILE: lib/screen/notification_list_screen.dart ================================================ import 'dart:convert'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spo_balaesang/models/notification.dart'; import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/create_notification_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; class NotificationListScreen extends StatefulWidget { @override _NotificationListScreenState createState() => _NotificationListScreenState(); } class _NotificationListScreenState extends State { List notifications = []; bool _isLoading = false; DataRepository dataRepo; Set choices = {'Tandai Semua Dibaca', 'Hapus Semua'}; User _user; Future getUser() async { final sp = await SharedPreferences.getInstance(); final _data = sp.get(prefsUserKey); final Map _json = jsonDecode(_data.toString()) as Map; setState(() { _user = User.fromJson(_json); }); } @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } Future _fetchNotificationsData() async { try { setState(() { _isLoading = true; }); final Map _result = await dataRepo.getAllNotifications(); final List _notifications = _result['data'] as List; final List _data = _notifications .map( (json) => UserNotification.fromJson(json as Map)) .toList(); setState(() { notifications = _data; }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { setState(() { _isLoading = false; }); } } Future _readNotification(String id) async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final Map data = {'notification_id': id}; final http.Response response = await dataRepo.readNotification(data); final Map _res = jsonDecode(response.body) as Map; if (response.statusCode == 200) { pd.hide(); showAlertDialog("success", "Sukses", _res['message'].toString(), dismissible: true); _fetchNotificationsData(); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } Future _readAllNotifications() async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final Map _res = await dataRepo.readAllNotifications(); if (_res['success'] as bool) { pd.hide(); showAlertDialog("success", "Sukses", _res['message'].toString(), dismissible: true); _fetchNotificationsData(); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } Future _deleteAllNotifications() async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final Map _res = await dataRepo.deleteAllNotifications(); if (_res['success'] as bool) { pd.hide(); showAlertDialog("success", "Sukses", _res['message'].toString(), dismissible: true); _fetchNotificationsData(); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } Widget _buildMarker(bool isRead) { return isRead ? const SizedBox() : Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(50.0), color: Colors.red, ), padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 1.0), child: const Text( '', style: TextStyle(fontSize: 10.0), ), ); } Widget _buildBody() { if (_isLoading) { return const Center( child: SpinKitChasingDots( size: 45, color: Colors.blueAccent, )); } if (notifications.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Belum ada pemberitahuan!') ]), ); } return Padding( padding: const EdgeInsets.all(8.0), child: ListView.builder( itemBuilder: (_, index) { final UserNotification notification = notifications[index]; return Container( margin: const EdgeInsets.only(bottom: 8.0), child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), elevation: 2.0, child: InkWell( onTap: () { _readNotification(notification.id); }, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( '${notification.data['heading']}', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ), _buildMarker(notification.isRead) ], ), dividerT1, Text(notification.data['body'].toString()) ], )), ), ), ); }, itemCount: notifications.length, ), ); } List _buildActionMenu() { return notifications.isNotEmpty ? [ PopupMenuButton( itemBuilder: (BuildContext context) { return choices .map((String choice) => PopupMenuItem( value: choice, child: Text(choice), )) .toList(); }, onSelected: (value) { if (value == choices.first) { _readAllNotifications(); } if (value == choices.last) { _deleteAllNotifications(); } }, offset: const Offset(0, 100), ) ] : []; } @override void initState() { super.initState(); dataRepo = Provider.of(context, listen: false); getUser(); _fetchNotificationsData(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( actions: _buildActionMenu(), title: const Text( 'Pemberitahuan', style: TextStyle(color: Colors.white), ), backgroundColor: Colors.blueAccent, iconTheme: const IconThemeData(color: Colors.white), ), body: _buildBody(), floatingActionButton: _user?.position == 'Camat' ? FloatingActionButton( onPressed: () { Get.to(() => CreateNotificationScreen()); }, backgroundColor: Colors.blueAccent, child: const Icon(Icons.add), ) : null, ); } } ================================================ FILE: lib/screen/on_boarding_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:introduction_screen/introduction_screen.dart'; import 'package:spo_balaesang/utils/view_util.dart'; class OnBoardingScreen extends StatelessWidget { const OnBoardingScreen({this.page}); final Widget page; @override Widget build(BuildContext context) { return Scaffold( body: IntroductionScreen( pages: onBoardingScreens, showSkipButton: true, skip: const Text("Skip"), next: const Icon( Icons.navigate_next, color: Colors.blueAccent, size: 32, ), onSkip: () { Get.off(() => page); }, done: const Text("Selesai", style: TextStyle(fontWeight: FontWeight.w600)), onDone: () { Get.off(() => page); }, ), ); } } ================================================ FILE: lib/screen/outstation_list_screen.dart ================================================ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cupertino_datetime_picker/flutter_cupertino_datetime_picker.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/outstation.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/change_outstation_photo_screen.dart'; import 'package:spo_balaesang/screen/create_outstation_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_widget.dart'; class OutstationListScreen extends StatefulWidget { @override _OutstationListScreenState createState() => _OutstationListScreenState(); } class _OutstationListScreenState extends State { List _outstations = []; bool _isLoading = false; DateTime _date; @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } Future _fetchOutstationData() async { try { setState(() { _isLoading = true; }); final dataRepo = Provider.of(context, listen: false); final Map _result = await dataRepo.getAllOutstation(_date); final List outstations = _result['data'] as List; final List _data = outstations .map((json) => Outstation.fromJson(json as Map)) .toList(); setState(() { _outstations = _data; }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { setState(() { _isLoading = false; }); } } @override void initState() { super.initState(); _date = DateTime.now(); _fetchOutstationData(); } Future _selectDate(BuildContext context) async { DatePicker.showDatePicker(context, initialDateTime: _date, minDateTime: DateTime(2021), maxDateTime: DateTime(DateTime.now().year + 5), onConfirm: (picked, _) { if (picked != null) { setState(() { _date = picked; }); _fetchOutstationData(); } }, locale: DateTimePickerLocale.id, dateFormat: 'MMMM-y'); } Widget _buildBody() { if (_isLoading) { return SizedBox( height: Get.height * 0.7, child: const Center( child: SpinKitFadingFour( size: 45, color: Colors.blueAccent, )), ); } if (_outstations.isEmpty) { return SizedBox( height: Get.height * 0.7, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Belum ada Dinas Luar yang diajukan!') ]), ), ); } return Column( children: _outstations.map((Outstation outstation) { final DateTime dueDate = outstation.dueDate; final DateTime startDate = outstation.startDate; return EmployeeProposalWidget( title: outstation.title, description: outstation.description, dueDate: dueDate, startDate: startDate, approvalStatus: outstation.approvalStatus, isApproved: outstation.isApproved, heroTag: outstation.id.toString(), photo: outstation.photo, updateWidget: ChangeOutstationPhotoScreen(outstation: outstation), ); }).toList(), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Daftar Dinas Luar'), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.blueAccent, onPressed: () { Get.to(() => CreateOutstationScreen()); }, child: const Icon(Icons.add), ), body: Container( height: Get.height, padding: const EdgeInsets.all(8.0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pilih Tahun & Bulan : ', ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( DateFormat.yMMMM('id_ID').format(_date), style: const TextStyle( fontSize: 22.0, fontWeight: FontWeight.w600, ), ), IconButton( icon: Icon( Icons.calendar_today_rounded, color: Colors.blueAccent[400], ), onPressed: () { _selectDate(context); }) ], ), sizedBoxH4, dividerT1, sizedBoxH4, _buildBody() ], ), ), )); } } ================================================ FILE: lib/screen/paid_leave_list_screen.dart ================================================ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cupertino_datetime_picker/flutter_cupertino_datetime_picker.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/paid_leave.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/change_paid_leave_photo_screen.dart'; import 'package:spo_balaesang/screen/create_paid_leave_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_widget.dart'; class PaidLeaveListScreen extends StatefulWidget { @override _PaidLeaveListScreenState createState() => _PaidLeaveListScreenState(); } class _PaidLeaveListScreenState extends State { List _paidLeaves; bool _isLoading = false; DateTime _date; @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } Future _fetchPaidLeaveData() async { try { setState(() { _isLoading = true; }); final DataRepository dataRepo = Provider.of(context, listen: false); final Map _result = await dataRepo.getAllPaidLeave(_date); final List paidLeaves = _result['data'] as List; final List _data = paidLeaves .map((json) => PaidLeave.fromJson(json as Map)) .toList(); setState(() { _paidLeaves = _data; }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } finally { _isLoading = false; } } @override void initState() { _date = DateTime.now(); _fetchPaidLeaveData(); super.initState(); } Future _selectDate(BuildContext context) async { DatePicker.showDatePicker(context, initialDateTime: _date, minDateTime: DateTime(2021), maxDateTime: DateTime(DateTime.now().year + 5), onConfirm: (picked, _) { if (picked != null) { setState(() { _date = picked; }); _fetchPaidLeaveData(); } }, locale: DateTimePickerLocale.id, dateFormat: 'MMMM-y'); } Widget _buildBody() { if (_isLoading) { return SizedBox( height: Get.height * 0.7, child: const Center( child: SpinKitFadingGrid( size: 45, color: Colors.blueAccent, )), ); } if (_paidLeaves.isEmpty) { return SizedBox( height: Get.height * 0.7, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Belum ada Cuti yang diajukan!') ]), ), ); } return Column( children: _paidLeaves .map((PaidLeave paidLeave) => _buildPaidLeaveItem(paidLeave)) .toList(), ); } Widget _buildPaidLeaveItem(PaidLeave paidLeave) { final startDate = paidLeave.startDate; final dueDate = paidLeave.dueDate; return EmployeeProposalWidget( title: paidLeave.title, description: paidLeave.description, startDate: startDate, dueDate: dueDate, approvalStatus: paidLeave.approvalStatus, photo: paidLeave.photo, isApproved: paidLeave.isApproved, isPaidLeave: true, updateWidget: ChangePaidLeavePhotoScreen(paidLeave: paidLeave), heroTag: paidLeave.id.toString(), category: paidLeave.category, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Daftar Cuti'), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.blueAccent, onPressed: () { Get.to(() => CreatePaidLeaveScreen()); }, child: const Icon(Icons.add), ), body: Container( height: Get.height, padding: const EdgeInsets.all(8.0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pilih Tahun & Bulan : ', ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( DateFormat.yMMMM('id_ID').format(_date), style: const TextStyle( fontSize: 22.0, fontWeight: FontWeight.w600, ), ), IconButton( icon: Icon( Icons.calendar_today_rounded, color: Colors.blueAccent[400], ), onPressed: () { _selectDate(context); }) ], ), sizedBoxH4, dividerT1, sizedBoxH4, _buildBody() ], ), ), ), ); } } ================================================ FILE: lib/screen/permission_list_screen.dart ================================================ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cupertino_datetime_picker/flutter_cupertino_datetime_picker.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/absent_permission.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/change_absent_permission_photo_screen.dart'; import 'package:spo_balaesang/screen/create_permission_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_proposal_widget.dart'; class PermissionListScreen extends StatefulWidget { @override _PermissionListScreenState createState() => _PermissionListScreenState(); } class _PermissionListScreenState extends State { List _permissions = []; bool _isLoading = false; DateTime _date; @override void setState(void Function() fn) { if (mounted) { super.setState(fn); } } Future _fetchPermissionData() async { try { setState(() { _isLoading = true; }); final dataRepo = Provider.of(context, listen: false); final Map _result = await dataRepo.getAllPermissions(_date); final List permissions = _result['data'] as List; final List _data = permissions .map( (json) => AbsentPermission.fromJson(json as Map)) .toList(); setState(() { _permissions = _data; _isLoading = false; }); } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } } @override void initState() { super.initState(); _date = DateTime.now(); _fetchPermissionData(); } Future _selectDate(BuildContext context) async { DatePicker.showDatePicker(context, initialDateTime: _date, minDateTime: DateTime(2021), maxDateTime: DateTime(DateTime.now().year + 5), onConfirm: (picked, _) { if (picked != null) { setState(() { _date = picked; }); _fetchPermissionData(); } }, locale: DateTimePickerLocale.id, dateFormat: 'MMMM-y'); } Widget _buildBody() { if (_isLoading) { return SizedBox( height: Get.height * 0.7, child: const Center( child: SpinKitFadingFour( size: 45, color: Colors.blueAccent, )), ); } if (_permissions.isEmpty) { return SizedBox( height: Get.height * 0.7, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: Get.width * 0.5, height: Get.height * 0.3, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Belum ada izin yang diajukan!') ]), ), ); } return Column( children: _permissions.map((AbsentPermission permission) { final DateTime dueDate = permission.dueDate; final DateTime startDate = permission.startDate; return EmployeeProposalWidget( photo: permission.photo, heroTag: permission.id.toString(), isApproved: permission.isApproved, startDate: startDate, dueDate: dueDate, approvalStatus: permission.approvalStatus, description: permission.description, title: permission.title, updateWidget: ChangePermissionPhotoScreen(permission: permission), ); }).toList(), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Daftar Izin'), ), floatingActionButton: FloatingActionButton( backgroundColor: Colors.blueAccent, onPressed: () { Get.to(() => CreatePermissionScreen()); }, child: const Icon(Icons.add), ), body: Container( height: Get.height, padding: const EdgeInsets.all(8.0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Pilih Tahun & Bulan : ', ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( DateFormat.yMMMM('id_ID').format(_date), style: const TextStyle( fontSize: 22.0, fontWeight: FontWeight.w600, ), ), IconButton( icon: Icon( Icons.calendar_today_rounded, color: Colors.blueAccent[400], ), onPressed: () { _selectDate(context); }) ], ), sizedBoxH4, dividerT1, sizedBoxH4, _buildBody() ], ), ), )); } } ================================================ FILE: lib/screen/presence_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:android_intent/android_intent.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:location/location.dart' as location; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/file_util.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; import 'package:spo_balaesang/widgets/user_info_card_widget.dart'; class PresenceScreen extends StatefulWidget { const PresenceScreen({this.user}); final User user; @override _PresenceScreenState createState() => _PresenceScreenState(); } class _PresenceScreenState extends State { String _base64Image; String _fileName; File _tmpFile; String _address = ""; double _latitude = 0; double _longitude = 0; String _code; User _user; final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); QRViewController controller; Future _openCamera() async { if (_address == null || _address.isEmpty) { showAlertDialog('failure', 'Lokasi tidak ditemukan.', 'Pastikan anda sudah menyalakan akses lokasi dan mengizinkan aplikasi untuk mengakses lokasi anda', dismissible: false); } else { final picture = await ImagePicker().getImage(source: ImageSource.camera); final file = await compressAndGetFile(File(picture.path), '/storage/emulated/0/Android/data/com.banuacoders.siap/files/Pictures/images.jpg'); setState(() { _tmpFile = file; _base64Image = base64Encode(_tmpFile.readAsBytesSync()); _fileName = _tmpFile.path.split('/').last; }); await file.delete(recursive: true); } } void _onQRViewCreated(QRViewController controller) { this.controller = controller; controller.scannedDataStream.listen((scanData) { setState(() { _code = scanData.code; }); if (_address != null || _address.isNotEmpty) { this.controller?.pauseCamera(); _uploadData(); } else { showAlertDialog('failure', 'Gagal', 'Lokasi tidak ditemukan.\nPastikan lokasi sudah diaktifkan!', dismissible: false); } }); } Future _uploadData() async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); try { pd.show(); final dataRepo = Provider.of(context, listen: false); final Map data = { 'code': _code, 'latitude': _latitude, 'longitude': _longitude, 'address': _address, 'photo': _base64Image, 'file_name': _fileName }; final http.Response response = await dataRepo.presence(data); final Map _res = jsonDecode(response.body) as Map; if (response.statusCode == 200) { pd.hide(); showAlertDialog("success", "Sukses", _res['message'].toString(), dismissible: false); Timer( const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { controller?.resumeCamera(); if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); pd.hide(); } } Future _getUserLocation() async { if (_address.isNotEmpty) { setState(() { _address = null; }); } if (!(await Geolocator.isLocationServiceEnabled())) { final bool isLocationServiceEnable = await location.Location().requestService(); if (!isLocationServiceEnable) { const AndroidIntent intent = AndroidIntent(action: 'android.settings.LOCATION_SOURCE_SETTINGS'); intent.launch(); } } final Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.bestForNavigation); final address = await placemarkFromCoordinates( position.latitude, position.longitude, localeIdentifier: 'id'); setState(() { _latitude = position.latitude; _longitude = position.longitude; _address = "${address.first.name}, ${address.first.street}, ${address.first.locality}, ${address.first.subAdministrativeArea}, ${address.first.administrativeArea} ${address.first.postalCode}"; }); } Widget _showImage() { if (_base64Image == null) { return const ImagePlaceholderWidget( label: 'Ambil Foto', child: Icon( Icons.camera_alt_rounded, color: Colors.grey, ), ); } final Uint8List bytes = base64Decode(_base64Image); return InkWell( onTap: () { Get.to(() => ImageDetailScreen( bytes: bytes, tag: 'image', )); }, child: Hero( tag: 'image', child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Image.memory( bytes, width: Get.width, height: 250, fit: BoxFit.cover, ), ), ), ); } Widget _buildQrScanner() { if (_address == null || _base64Image == null) { return const ImagePlaceholderWidget( label: 'Scan Kode Absen', child: Icon( Icons.qr_code_rounded, color: Colors.grey, ), ); } return SizedBox( height: 300.0, child: ClipRRect( borderRadius: BorderRadius.circular(20.0), child: QRView( key: qrKey, onQRViewCreated: _onQRViewCreated, overlay: QrScannerOverlayShape( borderRadius: 10, borderLength: 30, borderWidth: 10, cutOutSize: 300, ), ), )); } Widget _buildPlaceholderQR() { if (_address == null || _base64Image == null) { return const Text( 'Scanner akan aktif setelah lokasi berhasil dideteksi dan anda telah mengambil foto', style: TextStyle(color: Colors.grey), ); } return const Text( 'Arahkan kamera ke layar komputer', style: TextStyle(color: Colors.grey), ); } @override void initState() { super.initState(); _user = widget.user; _getUserLocation(); } @override void dispose() { controller?.dispose(); super.dispose(); } Widget _buildLocationSection() { List _children; if (_address == null || _address.isEmpty) { _children = [ sizedBoxH6, Row( children: const [ Text( 'Memuat lokasi..', style: TextStyle(color: Colors.grey), ), sizedBoxW6, SpinKitCircle( color: Colors.blueAccent, size: 18.0, ) ], ) ]; } else { _children = [ sizedBoxH6, Text( _address, style: const TextStyle(color: Colors.grey), ), sizedBoxH10, SizedBox( width: Get.width, height: 40.0, child: ElevatedButton( onPressed: _getUserLocation, style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent[200], onPrimary: Colors.white, ), child: const Text('Muat Ulang Lokasi'), ), ) ]; } return Column( children: _children, ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Presensi'), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.start, children: [ UserInfoCardWidget( department: _user.department, name: _user.name, position: _user.position, status: _user.status, rank: _user?.rank, group: _user?.group, nip: _user?.nip, ), sizedBoxH6, const Text( '*) : Pastikan data pegawai sesuai sebelum melakukan presensi.', style: TextStyle(color: Colors.grey), ), sizedBoxH20, Row( children: const [ Text('Scanner'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), _buildPlaceholderQR(), sizedBoxH10, _buildQrScanner(), sizedBoxH20, Row( children: const [ Text('Lokasi'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), _buildLocationSection(), sizedBoxH20, Row( children: const [ Text('Foto Diri'), sizedBoxW5, Text( '*', style: TextStyle(color: Colors.red), ), ], ), const Text( 'Ambil foto selfie anda sebagai bukti bahwa anda melakukan presensi di kantor tanpa diwakili orang lain. Pastikan wajah terlihat jelas.', style: TextStyle(color: Colors.grey), ), sizedBoxH10, const Text( '*): Absen akan dibatalkan jika foto tidak sesuai dengan ketentuan. Tekan untuk memperbesar.', style: TextStyle( fontSize: 10.0, color: Colors.red, fontStyle: FontStyle.italic), ), sizedBoxH20, _showImage(), sizedBoxH10, Align( alignment: Alignment.bottomRight, child: SizedBox( width: Get.width, height: 40, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: _openCamera, child: Text(_base64Image == null ? 'Ambil Foto' : 'Ubah Foto'), ), ), ), ], ), ), ), ); } } ================================================ FILE: lib/screen/regulation_screen.dart ================================================ import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class RegulationScreen extends StatelessWidget { final List> _regulations = [ { 'title': 'PP Nomor 11 Tahun 2017', 'description': 'Tentang Manajemen PNS', 'link': 'https://peraturan.bpk.go.id/Home/Download/40728/PP%2011%20TAHUN%202017.pdf' }, { 'title': 'Permenpan No. 6/2018', 'description': 'Tentang Hari Kerja Dan Jam Kerja PNS', 'link': 'https://peraturan.bpk.go.id/Home/Download/123285/Permenpan%20no%206%20Tahun%202018.pdf', }, { 'title': 'PP Nomor 53 Tahun 2010', 'description': 'Tentang Disiplin PNS', 'link': 'https://peraturan.bpk.go.id/Home/Download/36030/PP%2053%20Tahun%202010.pdf' }, { 'title': 'Contoh Surat Izin', 'description': 'Beberapa contoh surat izin resmi sebagai referensi', 'link': 'https://www.suratresmi.id/contoh-surat-izin-kerja/' }, { 'title': 'Contoh Surat Izin Sakit', 'description': 'Referensi saat membuat surat izin sakit', 'link': 'https://www.ashadin.com/2019/05/surat-izin-sakit-formal-pns.html' } ]; Widget _buildBody() { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: _regulations .map((regulation) => Container( margin: const EdgeInsets.only(bottom: 4.0), child: Card( elevation: 4.0, child: ListTile( onTap: () { launch(regulation['link'].toString()); }, title: Text( regulation['title'].toString(), style: const TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text(regulation['description'].toString()), trailing: IconButton( icon: const Icon(Icons.chevron_right), onPressed: () { launch(regulation['link'].toString()); }, ), dense: true, ), ), )) .toList(), ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Daftar Rujukan'), backgroundColor: Colors.blueAccent, ), body: _buildBody(), ); } } ================================================ FILE: lib/screen/report_screen.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_cupertino_datetime_picker/flutter_cupertino_datetime_picker.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:progress_dialog/progress_dialog.dart'; import 'package:provider/provider.dart'; import 'package:spo_balaesang/models/holiday.dart'; import 'package:spo_balaesang/models/presence.dart'; import 'package:spo_balaesang/models/report/absent_report.dart'; import 'package:spo_balaesang/models/report/daily.dart'; import 'package:spo_balaesang/models/user.dart'; import 'package:spo_balaesang/repositories/data_repository.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/extensions.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/employee_presence_card_widget.dart'; import 'package:spo_balaesang/widgets/statistics_card_widget.dart'; import 'package:spo_balaesang/widgets/user_info_card_widget.dart'; import 'package:table_calendar/table_calendar.dart'; import 'bottom_nav_screen.dart'; class ReportScreen extends StatefulWidget { const ReportScreen({this.user, this.isApproval = false}); final User user; final bool isApproval; @override _ReportScreenState createState() => _ReportScreenState(); } class _ReportScreenState extends State { AbsentReport _absentReport; bool _isLoading = false; bool _isApproval; DataRepository _dataRepo; DateTime _year; DateTime _selectedDate = DateTime.now(); final Map> _events = {}; List _selectedEvents; List _selectedHolidays; final Map> _holidays = {}; User _user; final TextEditingController _salaryController = TextEditingController(); double _salary = 0; final TextEditingController _reasonController = TextEditingController(); Future _fetchReportData() async { try { setState(() { _isLoading = true; }); final Map _result = await _dataRepo.getStatistics(_year, _user.id); final AbsentReport absentReport = AbsentReport.fromJson(_result['data'] as Map); setState(() { _absentReport = absentReport; _events.addEntries(absentReport.daily .map((daily) => MapEntry(daily.date, daily.attendances))); _holidays.addEntries(absentReport.holidays .map((holiday) => MapEntry(holiday.date, [holiday]))); if (_events.entries.last.key.isSameDate(DateTime.now())) { _selectedEvents = _events.entries.last.value as List; } else { _selectedEvents = []; } // _selectedHolidays = _getHolidayForDay(DateTime.now()) as List; }); } catch (e) { // } finally { setState(() { _isLoading = false; }); } } void _onDaySelected(DateTime day, DateTime focusedDay) { final List _eventsData = _getEventsForDay(day); final List _holidaysData = _getHolidayForDay(day); setState(() { if (_eventsData is List) { _selectedEvents = _eventsData; } else { _selectedEvents = []; } if (_holidaysData is List) { _selectedHolidays = _holidaysData; } else { _selectedHolidays = []; } _selectedDate = day; }); } Color _checkAttendancePercentageColor(double percentage) { if (percentage >= 25 && percentage < 50) { return Colors.yellow[800]; } if (percentage >= 50 && percentage < 70) { return Colors.indigo; } if (percentage >= 70 && percentage <= 80) { return Colors.blueAccent; } if (percentage >= 80 && percentage <= 100) { return Colors.green[600]; } return Colors.red[800]; } Widget _buildUserInfoSection() { return UserInfoCardWidget( name: _user?.name, status: _user?.status, group: _user?.group, rank: _user?.rank, department: _user?.department, nip: _user?.nip, position: _user?.position, ); } Widget _buildStatisticSection(AbsentReport report, DateTime year) { return StatisticCard( report: report, year: year, status: _user?.status, ); } Widget _buildSalaryCalculator() { final String date = DateFormat.yMMMM('id_ID').format(_year); String label = 'Kalkulator Gaji : $date'; String _prefix = 'Gaji'; if (_user?.status == 'PNS') { label = 'Kalkulator Tunjangan : $date'; _prefix = 'Tunjangan'; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.w600, ), ), const Divider(), Card( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( inputFormatters: [FilteringTextInputFormatter.digitsOnly], controller: _salaryController, decoration: InputDecoration( labelStyle: const TextStyle(color: Colors.blueAccent), border: const UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent)), prefixIcon: const Icon( Icons.money_rounded, color: Colors.blueAccent, ), labelText: 'Jumlah $_prefix Bulanan', ), keyboardType: TextInputType.number, onChanged: (value) { if (value.isNotEmpty) { setState(() { _salary = double.parse(value); }); } }, ), sizedBoxH2, const Divider(color: Colors.black26), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Kehadiran : '), Text( formatPercentage( _absentReport.monthly.attendancePercentage), style: const TextStyle(fontWeight: FontWeight.w600), ) ], ), sizedBoxH6, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Gaji Bulanan : '), Text( formatCurrency(_salary), style: const TextStyle(fontWeight: FontWeight.w600), ) ], ), sizedBoxH6, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Potongan : '), Text( formatCurrency(_countSalaryCuts()), style: const TextStyle(fontWeight: FontWeight.w600), ) ], ), sizedBoxH6, const Divider(color: Colors.black26), sizedBoxH6, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Total : '), Text( formatCurrency(_countTotalSalary()), style: const TextStyle(fontWeight: FontWeight.w600), ) ], ), ], ), ), ), const Divider(), sizedBoxH10, ], ); } Widget _buildBody() { if (_isLoading) { return SizedBox( height: Get.height * 0.7, child: const Center( child: SpinKitFadingCircle( size: 45, color: Colors.blueAccent, ), ), ); } if (_absentReport == null) { return Center( child: Column( children: [ SizedBox( width: Get.width * 0.6, height: 200, child: const FlareActor( 'assets/flare/failure.flr', animation: 'failure', fit: BoxFit.cover, ), ), const Text('Gagal memuat data'), const SizedBox(height: 20.0), ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6.0), ), primary: Colors.blueAccent, onPrimary: Colors.white), onPressed: _fetchReportData, child: const Text('Coba Lagi'), ) ], ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildStatisticSection(_absentReport, _year), const Divider(), sizedBoxH10, _buildSalaryCalculator(), const Text( 'Kalender Presensi : ', style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w600, ), ), sizedBoxH10, const Divider(), _buildTableCalendar(), const Divider(), Text( 'Presensi ${DateFormat.yMMMMEEEEd('id_ID').format(_selectedDate)} : ', style: const TextStyle( fontSize: 14.0, fontWeight: FontWeight.w600, ), ), sizedBoxH10, AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: _buildEventList()) ], ); } double _countTotalSalary() { return _salary * _absentReport.monthly.attendancePercentage / 100; } double _countSalaryCuts() { return _salary - _countTotalSalary(); } double _countAttendancePercentage(List presences) { double sum = 0; if (presences == null) { return 0; } if (presences.isEmpty) { return 0; } // ignore: avoid_function_literals_in_foreach_calls presences.forEach((presence) { switch (presence.attendStatus) { case 'Tepat Waktu': case 'Dinas Luar': case 'Cuti Tahunan': sum += 25; break; case 'Cuti Alasan Penting': case 'Cuti Sakit': case 'Cuti Bersalin': sum += 24.375; break; case 'Terlambat': sum += 6.25; break; case 'Izin': sum += 12.5; break; default: sum += 0; break; } }); return sum; } Widget _buildTableCalendar() { return Center( child: Card( elevation: 2.0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), child: SizedBox( child: TableCalendar( startingDayOfWeek: StartingDayOfWeek.monday, availableCalendarFormats: const { CalendarFormat.month: '1 bulan', }, calendarFormat: CalendarFormat.month, firstDay: DateTime(2021), lastDay: DateTime(DateTime.now().year + 5), focusedDay: _selectedDate, selectedDayPredicate: (day) { return isSameDay(_selectedDate, day); }, headerStyle: const HeaderStyle(titleCentered: true), onDaySelected: _onDaySelected, availableGestures: AvailableGestures.horizontalSwipe, calendarBuilders: CalendarBuilders( defaultBuilder: (_, date, focusedDay) { return holidayBuilder(date, isNotEmpty: _getHolidayForDay(date).isNotEmpty); }, dowBuilder: dowBuilder, markerBuilder: (context, date, events) { if (events.isNotEmpty) { return Positioned( bottom: 1, child: _buildEventsMarker(date, events), ); } return const SizedBox(); }, ), eventLoader: _getEventsForDay, ), ), ), ); } List _getEventsForDay(DateTime day) { List _event; try { _event = _events.entries .singleWhere((element) => isSameDay(day, element.key)) .value; } catch (e) { printError(info: e.toString()); } return _event ?? []; } List _getHolidayForDay(DateTime day) { List _holiday; try { _holiday = _holidays.entries .firstWhere((element) => isSameDay(day, element.key)) .value; } catch (e) { printError(info: e.toString()); } return _holiday ?? []; } Widget _buildEventsMarker(DateTime date, List events) { return AnimatedContainer( duration: const Duration(milliseconds: 300), width: 32.0, height: 16.0, decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: date.isSameDate(_selectedDate) ? _checkAttendancePercentageColor( _countAttendancePercentage(events as List)) : date.isToday() ? date.isSameDate(_selectedDate) ? _checkAttendancePercentageColor( _countAttendancePercentage(events as List)) : Colors.white : Colors.white, ), child: Center( child: Text( formatPercentage(_countAttendancePercentage(events as List) .toPrecision(0)), style: TextStyle( color: isSameDay(_selectedDate, date) ? Colors.white : _checkAttendancePercentageColor( _countAttendancePercentage(events as List)), fontSize: 10.0, fontWeight: FontWeight.bold, ), ), ), ); } Widget _buildEventList() { if (_selectedEvents == null) { return Center( child: Column( children: [ SizedBox( width: Get.width * 0.6, height: 300, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Gagal memuat data'), const SizedBox(height: 20.0), ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white), onPressed: _fetchReportData, child: const Text('Coba Lagi'), ) ], ), ); } if (_selectedEvents.isEmpty) { if (_selectedHolidays != null && _selectedHolidays.isNotEmpty) { return Column( children: _selectedHolidays .map((Holiday holiday) => Card( margin: const EdgeInsets.symmetric(vertical: 6.0), child: ListTile( title: Text( 'Libur Nasional : ${holiday.name}', style: const TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text( holiday.description, maxLines: 3, overflow: TextOverflow.ellipsis, ), ), )) .toList(), ); } return Center( child: Column( children: [ SizedBox( width: Get.width * 0.6, height: 300, child: const FlareActor( 'assets/flare/not_found.flr', animation: 'empty', ), ), const Text('Tidak ada presensi'), const SizedBox(height: 20.0), ], ), ); } return Column( children: _selectedEvents.map((event) { final Color color = checkStatusColor(event.attendStatus); String status = event.attendStatus ?? ''; if (event.attendStatus == 'Terlambat') { final duration = calculateLateTime(event.startTime, event.attendTime); status = '${event.attendStatus} $duration'; } return EmployeePresenceCardWidget( isApprovalCard: _isApproval, photo: event.photo, heroTag: event.photo, status: status, color: color, address: event.address, attendTime: event.attendTime, point: formatPercentage(checkPresencePercentage(event.attendStatus)), presenceType: event.attendType, buttonWidget: SizedBox( width: Get.width * 0.9, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6)), primary: Colors.red[600], onPrimary: Colors.white, ), onPressed: () { _cancelAttendance(Presence.fromJson(event.toPresenceJson())); }, child: const Text('Batalkan'), ), ), ); }).toList(), ); } void _cancelAttendance(Presence presence) { Get.defaultDialog( title: 'Alasan Pembatalan!', content: Flexible( child: Container( padding: const EdgeInsets.all(8), width: Get.width * 0.9, child: TextFormField( controller: _reasonController, decoration: const InputDecoration( labelText: 'Alasan', focusColor: Colors.blueAccent, focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueAccent))), ), ), ), confirm: ElevatedButton( style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), primary: Colors.blueAccent, onPrimary: Colors.white, ), onPressed: () { Get.back(); _sendData(presence); }, child: const Text('OK'), )); } @override void initState() { _year = DateTime.now().year == 2021 ? DateTime.now() : DateTime.utc(2021); _dataRepo = Provider.of(context, listen: false); super.initState(); _user = widget.user; _isApproval = widget.isApproval; _fetchReportData(); } @override void dispose() { _reasonController.dispose(); super.dispose(); } Future _selectYear(BuildContext context) async { DatePicker.showDatePicker(context, initialDateTime: _year, minDateTime: DateTime(2021), maxDateTime: DateTime(DateTime.now().year + 5), onConfirm: (picked, _) { if (picked != null) { setState(() { _year = picked; }); _fetchReportData(); } }, locale: DateTimePickerLocale.id, dateFormat: 'MMMM-y'); } Future _sendData(Presence presence) async { final ProgressDialog pd = ProgressDialog(context, isDismissible: false); pd.show(); try { final dataRepo = Provider.of(context, listen: false); final Map data = { 'presence_id': presence.id, 'reason': _reasonController.value.text }; final http.Response response = await dataRepo.cancelAttendance(data); final Map _res = jsonDecode(response.body) as Map; if (response.statusCode == 200) { pd.hide(); showAlertDialog("success", "Sukses", _res['message'].toString(), dismissible: false); Timer( const Duration(seconds: 5), () => Get.off(() => BottomNavScreen())); } else { if (pd.isShowing()) pd.hide(); showErrorDialog(_res); } } catch (e) { pd.hide(); showErrorDialog({ 'message': 'Kesalahan', 'errors': { 'exception': ['Terjadi kesalahan!'] } }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.blueAccent, title: const Text('Statistik'), ), body: SingleChildScrollView( child: Container( width: Get.width, padding: const EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildUserInfoSection(), dividerT1, const Text( 'Pilih Tahun & Bulan : ', ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( DateFormat.yMMMM('id_ID').format(_year), style: const TextStyle( fontSize: 22.0, fontWeight: FontWeight.w600, ), ), IconButton( icon: Icon( Icons.calendar_today_rounded, color: Colors.blueAccent[400], ), onPressed: () { _selectYear(context); }) ], ), dividerT1, _buildBody(), ], ), ), ), ); } } ================================================ FILE: lib/screen/splash_screen.dart ================================================ import 'dart:async'; import 'package:android_intent/android_intent.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:location/location.dart' as location; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spo_balaesang/screen/bottom_nav_screen.dart'; import 'package:spo_balaesang/screen/login_screen.dart'; import 'package:spo_balaesang/screen/on_boarding_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class SplashScreen extends StatefulWidget { @override _SplashScreenState createState() => _SplashScreenState(); } class _SplashScreenState extends State { bool _isLoggedIn = false; bool _isFirstSeen; Future _askPermission() async { try { final PermissionStatus locationPerms = await Permission.locationWhenInUse.status; if (locationPerms != PermissionStatus.granted) { await Permission.locationWhenInUse.request(); } final PermissionStatus cameraPerms = await Permission.camera.status; if (cameraPerms != PermissionStatus.granted) { await Permission.camera.request(); } final PermissionStatus storagePerms = await Permission.storage.status; if (storagePerms != PermissionStatus.granted) { await Permission.storage.request(); } final PermissionStatus phonePerms = await Permission.phone.status; if (phonePerms != PermissionStatus.granted) { await Permission.phone.request(); } final PermissionStatus notificationPerms = await Permission.notification.status; if (notificationPerms != PermissionStatus.granted) { await Permission.notification.request(); } } catch (e) { debugPrint(e.toString()); } } Future checkIsFirstSeen() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); if (prefs.containsKey(prefsSeenKey)) { setState(() { _isFirstSeen = true; }); } else { prefs.setBool(prefsSeenKey, true); setState(() { _isFirstSeen = false; }); } } Future _checkIfLoggedIn() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); final String token = prefs.getString(prefsTokenKey); if (token != null) { setState(() { _isLoggedIn = true; }); } } void navigationPage() { final Widget _page = _isLoggedIn ? BottomNavScreen() : LoginScreen(); if (_isFirstSeen) { Get.off(() => _page, transition: Transition.rightToLeftWithFade, curve: Curves.easeIn, duration: const Duration(milliseconds: 500)); } else { Get.off(() => OnBoardingScreen(page: _page)); } } Future _loadWidget() async { const Duration _duration = Duration(seconds: 5); return Timer(_duration, navigationPage); } Future _checkGps() async { if (!(await Geolocator.isLocationServiceEnabled())) { Get.defaultDialog( title: 'Perhatian', content: const Center( child: Text( 'Tidak dapat mendeteksi lokasi saat ini! Pastikan GPS sudah aktif dan coba lagi!', textAlign: TextAlign.center, ), ), actions: [ TextButton( onPressed: () async { final _serviceEnabled = await location.Location().requestService(); if (!_serviceEnabled) { const AndroidIntent intent = AndroidIntent( action: 'android.settings.LOCATION_SOURCE_SETTINGS'); intent.launch(); } Get.back(); }, child: const Text('OK', style: TextStyle( color: Colors.blueAccent, )), ), TextButton( onPressed: () { Get.back(); }, child: const Text('TIDAK', style: TextStyle( color: Colors.blueAccent, )), ), ], ); } } @override void initState() { _checkIfLoggedIn(); checkIsFirstSeen(); super.initState(); _askPermission(); _checkGps(); _loadWidget(); } @override Widget build(BuildContext context) { return Container( height: Get.height, width: Get.width, decoration: const BoxDecoration( gradient: LinearGradient(colors: [ Color(0xFF6B8EEF), Color(0xFF0C2979), ], begin: Alignment.topCenter, end: Alignment.bottomCenter)), child: Scaffold( backgroundColor: Colors.transparent, body: Stack( fit: StackFit.expand, children: [ Align( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'assets/launcher/icon.png', width: Get.width * 0.3, ), sizedBoxH4, const Text( 'SiAP Balaesang', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700), ) ], ), ), Align( alignment: Alignment.bottomCenter, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ const SpinKitFadingCircle( color: Colors.white, size: 35.0, ), const SizedBox(height: 10.0), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: const [ Spacer(), Text('SiAP Balaesang', style: TextStyle( color: Colors.white, fontWeight: FontWeight.w700)), SizedBox(width: 2.0), Text('v5.0.3', style: TextStyle(color: Colors.white)), Spacer() ], ), const SizedBox(height: 10.0), Container( width: Get.width * 0.6, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), child: IntrinsicHeight( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Spacer(), Image.asset( 'assets/logo/banuacoders.png', width: Get.width * 0.25, fit: BoxFit.fitHeight, ), const Spacer(), verticalDiv, const Spacer(), Image.asset( 'assets/logo/balaesang.png', width: Get.width * 0.25, fit: BoxFit.scaleDown, ), const Spacer(), ], ), ), ), const SizedBox(height: 30.0), ], ), ), ], ), ), ); } } ================================================ FILE: lib/utils/app_const.dart ================================================ // Shared Preferences Keys import 'package:flutter/material.dart'; const String prefsTokenKey = 'token'; const String prefsUserKey = 'user'; const String prefsEmployeeKey = 'employee'; const String prefsAlarmKey = 'alarm'; const String prefsSeenKey = 'seen'; // General const String jsonDataField = 'data'; const String approvalStatusField = 'approval_status'; // User/Employee const String userIdField = 'id'; const String userNipField = 'nip'; const String userNameField = 'name'; const String userPhoneField = 'phone'; const String userGenderField = 'gender'; const String userDepartmentField = 'department'; const String userStatusField = 'status'; const String userPositionField = 'position'; const String userUnreadNotificationsCountField = 'unread_notifications'; const String userIsHolidayField = 'holiday'; const String userIsWeekendField = 'is_weekend'; const String userTokenField = 'token'; const String userNextPresenceField = 'next_presence'; const String userPresencesField = 'presence'; const String userRankField = 'rank'; const String userGroupField = 'group'; // Presence const String presenceDateField = 'date'; const String presenceCodeTypeField = 'code_type'; const String presenceStatusField = 'status'; const String presenceAttendTimeField = 'attend_time'; const String presenceLocationField = 'location'; const String presencePhotoField = 'photo'; const String presenceStartTimeField = 'start_time'; const String presenceEndTimeField = 'end_time'; // Outstation/Permission const String outstationIdField = 'id'; const String outstationTitleField = 'title'; const String outstationDescriptionField = 'description'; const String outstationIsApprovedField = 'is_approved'; const String outstationPhotoField = 'photo'; const String outstationDueDateField = 'due_date'; const String outstationStartDateField = 'start_date'; const String outstationUserField = 'user'; // Paid Leave const String paidLeaveIdField = 'id'; const String paidLeaveTitleField = 'title'; const String paidLeaveCategoryField = 'category'; const String paidLeaveDescriptionField = 'description'; const String paidLeaveIsApprovedField = 'is_approved'; const String paidLeavePhotoField = 'photo'; const String paidLeaveDueDateField = 'due_date'; const String paidLeaveStartDateField = 'start_date'; const String paidLeaveUserField = 'user'; // Absent Permission const String absentPermissionIdField = 'id'; const String absentPermissionTitleField = 'title'; const String absentPermissionDescriptionField = 'description'; const String absentPermissionIsApprovedField = 'is_approved'; const String absentPermissionPhotoField = 'photo'; const String absentPermissionDueDateField = 'due_date'; const String absentPermissionStartDateField = 'start_date'; const String absentPermissionUserField = 'user'; // Notification const String notificationIdField = 'id'; const String notificationNotifiableIdField = 'notifiable_id'; const String notificationNotifiableTypeField = 'notifiable_type'; const String notificationIsReadField = 'is_read'; // Location const String locationLatitudeField = 'latitude'; const String locationLongitudeField = 'longitude'; const String locationAddressField = 'address'; // Absent Report const String reportAttendancePercentageFieldField = 'attendance_percentage'; const String reportDayField = 'day'; const String reportLimitField = 'limit'; const String reportLateCountField = 'late_count'; const String reportLeaveEarlyFieldCountField = 'leave_early_count'; const String reportNotMorningParadeCountField = 'not_morning_parade_count'; const String reportEarlyLunchBreakCountField = 'early_lunch_break_count'; const String reportNotComeAfterLunchBreakCountField = 'not_come_after_lunch_break_count'; const String reportTotalWorkDayField = 'total_work_day'; const String reportAnnualLeaveField = 'annual_leave'; const String reportImportantReasonLeaveField = 'important_reason_leave'; const String reportSickLeaveField = 'sick_leave'; const String reportMaternityLeaveField = 'maternity_leave'; const String reportOutOfLiabilityLeaveField = 'out_of_liability_leave'; const String absentReportPercentageField = 'percentage'; const String absentReportYearlyField = 'yearly'; const String absentReportMonthlyField = 'monthly'; const String absentReportDailyField = 'daily'; const String absentReportHolidaysField = 'holidays'; // DailyData (Report) const String dailyDateField = 'date'; const String dailyPresencesField = 'attendances'; const String dailyDataAttendTimeField = 'attend_time'; const String dailyDataAttendTypeField = 'absent_type'; const String dailyDataAttendStatusField = 'attend_status'; // Yearly const String yearlyAbsentField = 'absent'; const String yearlyAbsentPermissionField = 'absent_permission'; const String yearlyOutstationField = 'outstation'; // Holiday const String holidayDateField = 'date'; const String holidayNameField = 'name'; const String holidayDescriptionField = 'description'; const Map paidLeaveCategories = { 'Cuti Tahunan (97.5%)': 1, 'Cuti Alasan Penting (97.5%)': 2, 'Cuti Bersalin (97.5%)': 3, 'Cuti Sakit (97.5%)': 4, 'Cuti Diluar Tanggungan (0%)': 5 }; const sizedBox = SizedBox(); const sizedBoxW2 = SizedBox(width: 2); const sizedBoxW4 = SizedBox(width: 4); const sizedBoxW5 = SizedBox(width: 5); const sizedBoxW6 = SizedBox(width: 6); const sizedBoxW8 = SizedBox(width: 8); const sizedBoxW10 = SizedBox(width: 10); const sizedBoxW12 = SizedBox(width: 12); const sizedBoxW16 = SizedBox(width: 16); const sizedBoxW20 = SizedBox(width: 20); const sizedBoxW30 = SizedBox(width: 30); const sizedBoxH2 = SizedBox(height: 2); const sizedBoxH4 = SizedBox(height: 4); const sizedBoxH5 = SizedBox(height: 5); const sizedBoxH6 = SizedBox(height: 6); const sizedBoxH8 = SizedBox(height: 8); const sizedBoxH10 = SizedBox(height: 10); const sizedBoxH12 = SizedBox(height: 12); const sizedBoxH16 = SizedBox(height: 16); const sizedBoxH20 = SizedBox(height: 20); const sizedBoxH30 = SizedBox(height: 30); const dividerT1 = Divider(thickness: 1); const dividerT2 = Divider(thickness: 2); const verticalDiv = VerticalDivider( thickness: 1, color: Colors.black, ); ================================================ FILE: lib/utils/extensions.dart ================================================ extension DateOnlyCompare on DateTime { bool isSameDate(DateTime other) { return year == other.year && month == other.month && day == other.day; } bool isToday() { final now = DateTime.now(); final diff = now.difference(this).inDays; return diff == 0 && now.day == day; } bool isWeekend() { return weekday == 6 || weekday == 7; } bool isFinished() { return isBefore(DateTime.now()); } bool isOnGoing() { return isAfter(DateTime.now()); } } ================================================ FILE: lib/utils/file_util.dart ================================================ import 'dart:io'; import 'package:flutter_image_compress/flutter_image_compress.dart'; Future compressAndGetFile(File file, String targetPath) async { final result = await FlutterImageCompress.compressAndGetFile( file.absolute.path, targetPath, quality: 60, ); return result; } ================================================ FILE: lib/utils/view_util.dart ================================================ import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:introduction_screen/introduction_screen.dart'; import 'extensions.dart'; Future showAlertDialog(String type, String title, String content, {bool dismissible}) async { final List actions = type == 'success' ? [] : [ TextButton( onPressed: () { Get.back(); }, child: const Text( 'OK', style: TextStyle( color: Colors.blueAccent, ), ), ) ]; final color = type == 'success' ? Colors.green : Colors.red; final icon = type == 'success' ? Icons.check_circle : Icons.dangerous; return Get.defaultDialog( title: title, content: Column( children: [ Icon( icon, color: color, size: 72, ), const SizedBox(height: 10.0), Center( child: Text( content, textAlign: TextAlign.center, )), ], ), actions: actions, barrierDismissible: dismissible); } Future showErrorDialog(Map json) { return Get.defaultDialog( title: 'Gagal', content: Column( children: [ const Icon( Icons.dangerous, color: Colors.red, size: 72.0, ), const SizedBox(height: 10.0), Column( mainAxisSize: MainAxisSize.min, children: [ Text( json['message'].toString(), textAlign: TextAlign.center, ), const SizedBox(height: 10.0), Column( children: (json['errors'] as Map) .entries .map((e) => Text(e.value[0].toString())) .toList()), ], ), ], ), cancel: TextButton( onPressed: () { Get.back(); }, child: const Text( 'OK', style: TextStyle( color: Colors.blueAccent, ), ), ), ); } Color percentageLabelColor(double percentage) { if (percentage < 50) { return Colors.black87; } return Colors.white; } Color checkStatusColor(String status) { switch (status) { case 'Tidak Hadir': return Colors.red[800]; case 'Tepat Waktu': return Colors.green; case 'Terlambat': return Colors.orange; case 'Dinas Luar': case 'Izin': return Colors.blueAccent; case 'Cuti Tahunan': case 'Cuti Bersalin': case 'Cuti Sakit': case 'Cuti Alasan Penting': return Colors.pink; default: return Colors.red[800]; } } double checkPresencePercentage(String status) { switch (status) { case 'Tepat Waktu': case 'Dinas Luar': case 'Cuti Tahunan': return 100; case 'Cuti Bersalin': case 'Cuti Sakit': case 'Cuti Alasan Penting': return 97.5; case 'Terlambat': return 25; break; case 'Izin': return 50; default: return 0; } } List onBoardingScreens = [ PageViewModel( title: "QR Code", body: "Lakukan presensi cukup dengan scan QR Code", image: const Center( child: FlareActor( 'assets/flare/qrcode.flr', animation: 'scan', )), decoration: const PageDecoration( titleTextStyle: TextStyle( fontSize: 20.0, fontWeight: FontWeight.w700, color: Colors.blueAccent), bodyTextStyle: TextStyle(fontSize: 12.0, fontWeight: FontWeight.normal)), ), PageViewModel( title: "Izin, Cuti, & Dinas Luar", body: "Ajukan Izin, Cuti, dan Dinas Luar melalui aplikasi.", image: const Center( child: FlareActor( 'assets/flare/documents.flr', animation: 'document', )), decoration: const PageDecoration( titleTextStyle: TextStyle( fontSize: 20.0, fontWeight: FontWeight.w700, color: Colors.blueAccent), bodyTextStyle: TextStyle(fontSize: 12.0, fontWeight: FontWeight.normal)), ) ]; String calculateLateTime(DateTime startTime, String attendTime) { const dur = Duration(minutes: 30); final attendDate = '${startTime.year.toString()}-${startTime.month.toString().padLeft(2, '0')}-${startTime.day.toString().padLeft(2, '0')}'; final diff = DateTime.parse('$attendDate $attendTime').difference(startTime.add(dur)); var duration = diff.inMinutes; if (duration == 0) { duration = diff.inSeconds; return '$duration detik'; } if (duration > 59) { duration = diff.inHours; return '$duration jam'; } return '$duration menit'; } final TextStyle labelTextStyle = TextStyle(color: Colors.grey[600]); String formatPercentage(double percentage) { return '${NumberFormat.decimalPattern('id_ID').format(percentage)}%'; } String formatCurrency(double salary) { return NumberFormat.currency(locale: 'id_ID', symbol: 'Rp. ').format(salary); } String trimPhoneNumber(String phoneNumber) { final phone = phoneNumber.replaceAll(' ', ''); return '62${phone.substring(1, phone.length)}'; } Widget dowBuilder(BuildContext context, DateTime date) { TextStyle _style = const TextStyle(color: Colors.black); if (date.isWeekend()) { _style = const TextStyle(color: Colors.red); } final text = DateFormat.E().format(date); return Center( child: Text( text, style: _style, ), ); } Widget holidayBuilder(DateTime date, {bool isNotEmpty}) { TextStyle _style = const TextStyle(color: Colors.black); if (isNotEmpty || date.isWeekend()) { _style = const TextStyle(color: Colors.red); } final text = DateFormat.d().format(date); return Center( child: Text( text, style: _style, ), ); } ================================================ FILE: lib/widgets/employee_presence_card_widget.dart ================================================ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; class EmployeePresenceCardWidget extends StatelessWidget { const EmployeePresenceCardWidget( {this.presenceType, this.attendTime, this.status, this.address, this.photo, this.heroTag, this.isApprovalCard = false, this.buttonWidget, this.point, this.color}); final String presenceType; final String point; final String attendTime; final String status; final String address; final String photo; final String heroTag; final bool isApprovalCard; final Widget buttonWidget; final Color color; Widget _buildCancelButton(String status) { if ((status != 'Tepat Waktu' && !status.contains('Terlambat')) || !isApprovalCard) { return const SizedBox(); } return Column( children: [const Divider(), buttonWidget], ); } Widget _showImage(String photo) { if (photo.isEmpty) { return const ImagePlaceholderWidget( label: 'Tidak ada foto!', child: Icon( Icons.image_not_supported_rounded, color: Colors.grey, ), ); } return InkWell( onTap: () { Get.to(() => ImageDetailScreen( tag: heroTag, imageUrl: photo, )); }, child: Hero( tag: heroTag, child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: CachedNetworkImage( placeholder: (_, __) => const ImagePlaceholderWidget( label: 'Memuat Foto', child: SpinKitFadingCircle( size: 25.0, color: Colors.blueAccent, ), ), imageUrl: photo, fit: BoxFit.cover, errorWidget: (_, __, ___) => const ImagePlaceholderWidget( label: 'Gagal memuat foto', child: Icon( Icons.image_not_supported_rounded, color: Colors.grey, ), ), width: Get.width, height: 250.0, ), ), ), ); } String checkAddressLabel() { if (int.parse(point.substring(0, point.length - 1)) == 0 && address.isNotEmpty) { return 'Alasan Pembatalan'; } return 'Lokasi'; } IconData checkAddressIcon() { if (int.parse(point.substring(0, point.length - 1)) == 0 && address.isNotEmpty) { return Icons.notes_rounded; } return Icons.location_on_rounded; } @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(top: 16.0), width: Get.width, child: Card( elevation: 2.0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( presenceType, style: const TextStyle(fontWeight: FontWeight.w600), ), const Divider(thickness: 1.0), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Jam Absen', style: labelTextStyle, ), Text( attendTime.isEmpty ? '-' : attendTime, ) ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Poin Kehadiran', style: labelTextStyle, ), Text( point, style: TextStyle( color: color, ), ) ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Status Kehadiran', style: labelTextStyle, ), Text( status, style: TextStyle( color: color, ), ) ], ), sizedBoxH4, dividerT1, Row( children: [ Icon( checkAddressIcon(), color: Colors.grey[600], size: 20.0, ), sizedBoxW4, Text(checkAddressLabel(), style: labelTextStyle) ], ), sizedBoxH4, AutoSizeText( address.isEmpty ? '-' : address, maxLines: 3, minFontSize: 10.0, maxFontSize: 12.0, textAlign: TextAlign.justify, overflow: TextOverflow.ellipsis, ), dividerT1, Row( children: [ Icon( Icons.photo, color: Colors.grey[600], size: 20.0, ), sizedBoxW4, Text('Foto Wajah', style: labelTextStyle) ], ), sizedBoxH4, _showImage(photo), const SizedBox(height: 8.0), _buildCancelButton(status) ], ), ), ), ); } } ================================================ FILE: lib/widgets/employee_proposal_info_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; class EmployeeProposalInfoWidget extends StatelessWidget { const EmployeeProposalInfoWidget( {this.title, this.label, this.startDate, this.dueDate}); final String label; final DateTime startDate; final DateTime dueDate; final String title; @override Widget build(BuildContext context) { return Card( elevation: 4.0, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Text( 'Informasi $label', style: labelTextStyle.copyWith( fontWeight: FontWeight.w600, ), ), dividerT1, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Judul ', style: labelTextStyle), sizedBoxW5, Text(title), ], ), sizedBoxH5, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Tanggal Mulai ', style: labelTextStyle), sizedBoxW5, Text(DateFormat.yMMMMEEEEd('id_ID').format(startDate)), ], ), sizedBoxH5, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Tanggal Selesai ', style: labelTextStyle), sizedBoxW5, Text(DateFormat.yMMMMEEEEd('id_ID').format(dueDate)), ], ), ], ), ), ); } } ================================================ FILE: lib/widgets/employee_proposal_widget.dart ================================================ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:spo_balaesang/screen/image_detail_screen.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; import 'package:spo_balaesang/widgets/image_placeholder_widget.dart'; class EmployeeProposalWidget extends StatelessWidget { const EmployeeProposalWidget( {this.title, this.isApproved, this.employeeName, this.approvalStatus, this.isApprovalCard = false, this.isPaidLeave = false, this.startDate, this.dueDate, this.description, this.category, this.photo, this.updateWidget, this.heroTag, this.button}); final String title; final bool isApproved; final String employeeName; final String approvalStatus; final bool isApprovalCard; final bool isPaidLeave; final DateTime startDate; final DateTime dueDate; final String description; final String category; final String photo; final String heroTag; final Widget button; final Widget updateWidget; Color _checkStatusColor(String status) { switch (status) { case 'Disetujui': return Colors.green; case 'Menunggu Persetujuan': return Colors.deepOrange; default: return Colors.red[800]; } } Widget _buildEmployeeNameSection() { if (isApprovalCard) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ sizedBoxH4, Row( children: [ Text( 'Diajukan oleh : ', style: labelTextStyle.copyWith(fontSize: 12.0), ), Text( employeeName, style: const TextStyle( fontSize: 12.0, fontWeight: FontWeight.w600, ), ), ], ), sizedBoxH5, ], ); } return sizedBoxH5; } Widget _buildCategorySection() { if (isPaidLeave) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ sizedBoxH4, Row( children: [ Text( 'Kategori : ', style: labelTextStyle.copyWith(fontSize: 12.0), ), Text( category, style: const TextStyle(fontSize: 12.0), ), ], ), ], ); } return sizedBox; } Widget _buildButtonSection() { if (isApprovalCard) { return Column( children: [ dividerT1, button, ], ); } return sizedBox; } @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 16.0), child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 4.0, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0), ), sizedBoxH5, dividerT1, sizedBoxH5, Row( children: [ Text( 'Status : ', style: labelTextStyle.copyWith(fontSize: 12.0), ), Text( approvalStatus, style: TextStyle( fontSize: 12.0, color: _checkStatusColor(approvalStatus)), ), ], ), _buildCategorySection(), _buildEmployeeNameSection(), dividerT1, sizedBoxH5, const Text( 'Masa Berlaku : ', style: TextStyle(fontSize: 12.0, color: Colors.grey), ), sizedBoxH5, Row( children: [ const Icon( Icons.calendar_today_rounded, size: 16.0, ), sizedBoxW5, Text( '${startDate.day}/${startDate.month}/${startDate.year} - ${dueDate.day}/${dueDate.month}/${dueDate.year}', style: const TextStyle(fontSize: 12.0), ), ], ), sizedBoxH10, const Text( 'Deskripsi : ', style: TextStyle(fontSize: 12.0, color: Colors.grey), ), AutoSizeText( description, maxFontSize: 12.0, minFontSize: 10.0, maxLines: 4, overflow: TextOverflow.ellipsis, ), sizedBoxH10, const Text( 'Lampiran : ', style: TextStyle(fontSize: 12.0, color: Colors.grey), ), const Text( '*tekan untuk memperbesar', style: TextStyle( fontSize: 12.0, color: Colors.black87, fontStyle: FontStyle.italic), ), sizedBoxH5, InkWell( onLongPress: () { if (updateWidget != null) { Get.to(() => updateWidget); } }, onTap: () { Get.to(() => ImageDetailScreen( imageUrl: photo, tag: heroTag, )); }, child: Hero( tag: heroTag, child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: CachedNetworkImage( placeholder: (_, __) => const ImagePlaceholderWidget( label: 'Memuat Foto', child: SpinKitFadingCircle( size: 25.0, color: Colors.blueAccent, ), ), imageUrl: photo, fit: BoxFit.cover, errorWidget: (_, __, ___) => const ImagePlaceholderWidget( label: 'Gagal memuat foto!', child: Icon( Icons.image_not_supported_rounded, color: Colors.grey, ), ), width: Get.width, height: 250.0, ), ), ), ), _buildButtonSection() ], )), ), ); } } ================================================ FILE: lib/widgets/image_placeholder_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class ImagePlaceholderWidget extends StatelessWidget { const ImagePlaceholderWidget({this.label, this.child}); final String label; final Widget child; @override Widget build(BuildContext context) { return Container( width: Get.width, height: 250, decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(20.0), border: Border.all(color: Colors.grey[400]), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ child, sizedBoxH5, Text( label, style: const TextStyle(color: Colors.grey), ) ], )); } } ================================================ FILE: lib/widgets/next_presence_empty_card_widget.dart ================================================ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class NextPresenceEmptyCardWidget extends StatelessWidget { const NextPresenceEmptyCardWidget( {this.topLabel, this.trailingLabel, this.firstLabel, this.firstContent, this.secondLabel, this.secondContent, this.thirdLabel, this.thirdContent, this.fourthLabel, this.fourthContent, this.color, this.trailingTop}); final String topLabel; final String trailingLabel; final String firstLabel; final String firstContent; final String secondLabel; final String secondContent; final String thirdLabel; final String thirdContent; final String fourthLabel; final String fourthContent; final Color color; final Widget trailingTop; @override Widget build(BuildContext context) { return Card( elevation: 4.0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: Padding( padding: const EdgeInsets.all(8.0), child: Center( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( DateFormat.EEEE().format(DateTime.now()), style: const TextStyle(fontWeight: FontWeight.w600), ), sizedBoxW5, const Text('|'), sizedBoxW5, Text( DateFormat.yMMMd().format(DateTime.now()), ), sizedBoxW5, const Text('|'), sizedBoxW5, Text(topLabel), ], ), const Divider( thickness: 1.0, color: Colors.black26, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '$firstLabel :', style: const TextStyle(fontWeight: FontWeight.bold), ), Text(firstContent), sizedBoxH10, Text( '$secondLabel :', style: const TextStyle(fontWeight: FontWeight.bold), ), sizedBoxH2, Text(secondContent), sizedBoxH10, Text( '$thirdLabel :', style: const TextStyle(fontWeight: FontWeight.bold), ), sizedBoxH2, Text( thirdContent, style: TextStyle(color: color), ), sizedBoxH10, Text( '$fourthLabel :', style: const TextStyle(fontWeight: FontWeight.bold), ), sizedBoxH2, AutoSizeText( fourthContent, minFontSize: 10.0, maxFontSize: 12.0, ) ], ), Expanded( child: Column(children: [ trailingTop, sizedBoxH2, Text( trailingLabel, style: const TextStyle( fontWeight: FontWeight.bold, color: Colors.blueGrey, ), ) ]), ) ], ), ], ), ), ), ); } } ================================================ FILE: lib/widgets/statistics_card_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; import 'package:spo_balaesang/models/report/absent_report.dart'; import 'package:spo_balaesang/utils/app_const.dart'; import 'package:spo_balaesang/utils/view_util.dart'; class StatisticCard extends StatelessWidget { const StatisticCard({this.report, this.year, this.status}); final AbsentReport report; final DateTime year; final String status; Color _checkAttendancePercentageColor(double percentage) { if (percentage >= 25 && percentage < 50) { return Colors.yellow[800]; } if (percentage >= 50 && percentage < 70) { return Colors.indigo; } if (percentage >= 70 && percentage <= 80) { return Colors.blueAccent; } if (percentage >= 80 && percentage <= 100) { return Colors.green[600]; } return Colors.red[800]; } Widget _buildCircularPercentage( double percentage, String header, String suffix, String type) { return CircularPercentIndicator( radius: Get.width * 0.3, linearGradient: LinearGradient(colors: [ _checkAttendancePercentageColor(percentage).withOpacity(1), _checkAttendancePercentageColor(percentage).withOpacity(0.5) ]), animation: true, header: Text('${suffix[0].toUpperCase()}${suffix.substring(1)}'), animationDuration: 1000, percent: percentage / 100, lineWidth: 10.0, circularStrokeCap: CircularStrokeCap.round, backgroundColor: _checkAttendancePercentageColor(percentage).withOpacity(0.2), center: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( type, style: const TextStyle( fontSize: 10.0, color: Colors.black87, ), ), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '$percentage', style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 24.0, ), ), const Text( '%', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14.0, ), ) ], ), Text( '/$suffix', style: const TextStyle(color: Colors.grey, fontSize: 12.0), ) ], ), footer: Text( header.toUpperCase(), style: const TextStyle( fontWeight: FontWeight.w600, ), ), ); } Widget _buildLinearPercentage(double percentage, String label, Widget footer, Color color, double width) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ sizedBoxW2, Text( label, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0, ), ), ], ), sizedBoxH6, LinearPercentIndicator( progressColor: color, animationDuration: 1000, percent: percentage / 100, width: width, animation: true, lineHeight: 15.0, backgroundColor: color.withOpacity(0.15), center: Text( '$percentage%', style: TextStyle( fontSize: 12.0, color: percentageLabelColor(percentage)), ), ), sizedBoxH6, Row( children: [sizedBoxW2, footer], ) ], ); } Widget _buildPaidLeaveSection() { if (status == 'Honorer') { return sizedBox; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Divider(color: Colors.black26), Text( 'Cuti', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0, color: Colors.grey[600], ), ), sizedBoxH4, _buildLinearPercentage( double.parse(report .yearly.outOfLiabilityLeave[absentReportPercentageField] .toString()), 'Diluar Tanggungan', Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ sizedBoxW6, Text( report.yearly.outOfLiabilityLeave[reportDayField].toString(), style: TextStyle( color: Colors.grey[800], ), ), Text( '/${report.yearly.outOfLiabilityLeave[reportLimitField]} hari', style: const TextStyle(color: Colors.grey, fontSize: 12.0), ) ], ), Colors.red[800], Get.width * 0.85, ), sizedBoxH20, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildLinearPercentage( double.parse(report .yearly.annualLeave[absentReportPercentageField] .toString()), 'Tahunan', Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.annualLeave[reportDayField].toString(), style: TextStyle( color: Colors.grey[800], ), ), Text( '/${report.yearly.annualLeave[reportLimitField]} hari', style: const TextStyle(color: Colors.grey, fontSize: 12.0), ) ], ), Colors.indigo[300], Get.width * 0.4), _buildLinearPercentage( double.parse(report .yearly.importantReasonLeave[absentReportPercentageField] .toString()), 'Alasan Penting', Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.importantReasonLeave[reportDayField] .toString(), style: TextStyle( color: Colors.grey[800], ), ), Text( '/${report.yearly.importantReasonLeave[reportLimitField]} hari', style: const TextStyle(color: Colors.grey, fontSize: 12.0), ) ], ), Colors.pink[400], Get.width * 0.4), ], ), sizedBoxH20, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildLinearPercentage( double.parse(report .yearly.sickLeave[absentReportPercentageField] .toString()), 'Sakit', Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.sickLeave[reportDayField].toString(), style: TextStyle( color: Colors.grey[800], ), ), Text( '/${report.yearly.sickLeave[reportLimitField]} hari', style: const TextStyle(color: Colors.grey, fontSize: 12.0), ) ], ), Colors.green[300], Get.width * 0.4), _buildLinearPercentage( double.parse(report .yearly.maternityLeave[absentReportPercentageField] .toString()), 'Bersalin', Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.maternityLeave[reportDayField].toString(), style: TextStyle( color: Colors.grey[800], ), ), Text( '/${report.yearly.maternityLeave[reportLimitField]} hari', style: const TextStyle(color: Colors.grey, fontSize: 12.0), ) ], ), Colors.orange[300], Get.width * 0.4), ], ), ], ); } @override Widget build(BuildContext context) { // ignore: sized_box_for_whitespace return Container( width: Get.width, child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 2.0, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Statistik', style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w600, ), ), const Divider(color: Colors.black38), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildCircularPercentage( report.monthly.attendancePercentage, DateFormat.MMMM('id_ID').format(year), 'bulan', 'Kehadiran', ), _buildCircularPercentage( report.yearly.attendancePercentage, '${year.year}', 'tahun', 'Kehadiran', ), ], ), const Divider(color: Colors.black26), _buildLinearPercentage( double.parse(report.yearly.absent[absentReportPercentageField] .toString()), 'Alpa', Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ sizedBoxW6, Text( report.yearly.absent[reportDayField].toString(), style: TextStyle( color: Colors.grey[800], ), ), Text( '/${report.yearly.absent[reportLimitField]} hari', style: const TextStyle(color: Colors.grey, fontSize: 12.0), ) ], ), Colors.red[800], Get.width * 0.85, ), sizedBoxH20, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildLinearPercentage( double.parse(report .yearly.absentPermission[absentReportPercentageField] .toString()), 'Izin', Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.absentPermission[reportDayField] .toString(), style: TextStyle( color: Colors.grey[800], ), ), Text( '/${report.totalWorkDay} hari kerja', style: const TextStyle( color: Colors.grey, fontSize: 12.0), ) ], ), Colors.blueAccent[400], Get.width * 0.4), _buildLinearPercentage( double.parse(report .yearly.outstation[absentReportPercentageField] .toString()), 'Dinas Luar', Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.outstation[reportDayField].toString(), style: TextStyle( color: Colors.grey[800], ), ), Text( '/${report.totalWorkDay} hari kerja', style: const TextStyle( color: Colors.grey, fontSize: 12.0), ) ], ), Colors.deepOrange[600], Get.width * 0.4), ], ), _buildPaidLeaveSection(), sizedBoxH6, const Divider(color: Colors.black26), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Tidak Apel Pagi', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14.0, ), ), sizedBoxH8, Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.monthly.notMorningParadeCount.toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/bulan', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), const SizedBox(height: 40.0, child: verticalDiv), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.notMorningParadeCount.toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/tahun', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), ], ) ], ), sizedBoxH6, const Divider(color: Colors.black26), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Terlambat', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14.0, ), ), sizedBoxH8, Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.monthly.lateCount.toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/bulan', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), const SizedBox(height: 40.0, child: verticalDiv), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.lateCount.toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/tahun', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), ], ) ], ), sizedBoxH6, const Divider(color: Colors.black26), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Istrahat Sebelum Waktunya', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14.0, ), ), sizedBoxH8, Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.monthly.earlyLunchBreakCount.toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/bulan', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), const SizedBox(height: 40.0, child: verticalDiv), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.earlyLunchBreakCount.toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/tahun', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), ], ) ], ), sizedBoxH6, const Divider(color: Colors.black26), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Tidak Masuk Siang', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14.0, ), ), sizedBoxH8, Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.monthly.notComeAfterLunchBreakCount .toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/bulan', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), const SizedBox(height: 40.0, child: verticalDiv), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.notComeAfterLunchBreakCount .toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/tahun', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), ], ) ], ), sizedBoxH6, const Divider(color: Colors.black26), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Pulang Cepat', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14.0, ), ), sizedBoxH8, Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.monthly.leaveEarlyCount.toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/bulan', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), const SizedBox(height: 40.0, child: verticalDiv), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( report.yearly.leaveEarlyCount.toString(), style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 32.0, ), ), const Text( ' kali/tahun', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], ), ], ) ], ), ], ), ), ), ); } } ================================================ FILE: lib/widgets/user_info_card_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:spo_balaesang/utils/app_const.dart'; class UserInfoCardWidget extends StatelessWidget { const UserInfoCardWidget( {this.status, this.name, this.position, this.rank, this.group, this.nip, this.department}); final String status; final String name; final String position; final String rank; final String group; final String nip; final String department; Widget _buildPnsInfoSection() { if (status == 'Honorer') { return sizedBox; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Golongan', style: TextStyle(color: Colors.grey[700]), ), Text( group ?? '', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Pangkat', style: TextStyle(color: Colors.grey[700]), ), Text( rank?.toUpperCase() ?? '', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'NIP', style: TextStyle(color: Colors.grey[700]), ), Text( nip ?? '', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ], ), sizedBoxH4, ], ); } @override Widget build(BuildContext context) { return Card( elevation: 4.0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Informasi Pegawai : ', style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w600, color: Colors.grey[700]), ), const Divider(color: Colors.black38), Center( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Nama', style: TextStyle(color: Colors.grey[700]), ), Text( name ?? '', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Jabatan', style: TextStyle(color: Colors.grey[700]), ), Text( position ?? '', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Bagian', style: TextStyle(color: Colors.grey[700]), ), Text( department ?? '', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ], ), sizedBoxH4, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Status', style: TextStyle(color: Colors.grey[700]), ), Text( status ?? '', style: const TextStyle( fontWeight: FontWeight.w600, ), ), ], ), sizedBoxH4, _buildPnsInfoSection(), ], ), ), ], ), ), ); } } ================================================ FILE: privacy-policy.md ================================================ PRIVACY NOTICE Last updated November 12, 2020 Thank you for choosing to be part of our community at Banua Coders ("Company", "we", "us", "our"). We are committed to protecting your personal information and your right to privacy. If you have any questions or concerns about this privacy notice, or our practices with regards to your personal information, please contact us at admin@banuacoders.com. When you use our mobile application, as the case may be (the "App") and more generally, use any of our services (the "Services", which include the App), we appreciate that you are trusting us with your personal information. We take your privacy very seriously. In this privacy notice, we seek to explain to you in the clearest way possible what information we collect, how we use it and what rights you have in relation to it. We hope you take some time to read through it carefully, as it is important. If there are any terms in this privacy notice that you do not agree with, please discontinue use of our Services immediately. This privacy notice applies to all information collected through our Services (which, as described above, includes our App), as well as, any related services, sales, marketing or events. Please read this privacy notice carefully as it will help you understand what we do with the information that we collect. TABLE OF CONTENTS 1. WHAT INFORMATION DO WE COLLECT? 2. HOW DO WE USE YOUR INFORMATION? 3. WILL YOUR INFORMATION BE SHARED WITH ANYONE? 4. HOW LONG DO WE KEEP YOUR INFORMATION? 5. HOW DO WE KEEP YOUR INFORMATION SAFE? 6. DO WE COLLECT INFORMATION FROM MINORS? 7. WHAT ARE YOUR PRIVACY RIGHTS? 8. CONTROLS FOR DO-NOT-TRACK FEATURES 9. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? 10. DO WE MAKE UPDATES TO THIS NOTICE? 11. HOW CAN YOU CONTACT US ABOUT THIS NOTICE? 12. HOW CAN YOU REVIEW, UPDATE OR DELETE THE DATA WE COLLECT FROM YOU? 1. WHAT INFORMATION DO WE COLLECT? Personal information you disclose to us In Short: We collect personal information that you provide to us. We collect personal information that you voluntarily provide to us when you express an interest in obtaining information about us or our products and Services, when you participate in activities on the App (such as by posting messages in our online forums or entering competitions, contests or giveaways) or otherwise when you contact us. The personal information that we collect depends on the context of your interactions with us and the App, the choices you make and the products and features you use. The personal information we collect may include the following: Personal Information Provided by You. We collect names; phone numbers; email addresses; job titles; passwords; and other similar information. All personal information that you provide to us must be true, complete and accurate, and you must notify us of any changes to such personal information. Information collected through our App In Short: We collect information regarding your geo-location, mobile device, when you use our App. If you use our App, we also collect the following information: Geo-Location Information. We may request access or permission to and track location-based information from your mobile device, either continuously or while you are using our App, to provide certain location-based services. If you wish to change our access or permissions, you may do so in your device's settings. Mobile Device Access. We may request access or permission to certain features from your mobile device, including your mobile device's camera, storage, and other features. If you wish to change our access or permissions, you may do so in your device's settings. Mobile Device Data. We automatically collect device information (such as your mobile device ID, model and manufacturer), operating system, version information and system configuration information, device and application identification numbers, browser type and version, hardware model Internet service provider and/or mobile carrier, and Internet Protocol (IP) address (or proxy server). If you are using our App, we may also collect information about the phone network associated with your mobile device, your mobile device’s operating system or platform, the type of mobile device you use, your mobile device’s unique device ID and information about the features of our App you accessed. This information is primarily needed to maintain the security and operation of our App, for troubleshooting and for our internal analytics and reporting purposes. Information collected from other sources In Short: We may collect limited data from public databases, marketing partners, and other outside sources. In order to enhance our ability to provide relevant marketing, offers and services to you and update our records, we may obtain information about you from other sources, such as public databases, joint marketing partners, affiliate programs, data providers, as well as from other third parties. This information includes mailing addresses, job titles, email addresses, phone numbers, intent data (or user behavior data), Internet Protocol (IP) addresses, social media profiles, social media URLs and custom profiles, for purposes of targeted advertising and event promotion. 2. HOW DO WE USE YOUR INFORMATION? In Short: We process your information for purposes based on legitimate business interests, the fulfillment of our contract with you, compliance with our legal obligations, and/or your consent. We use personal information collected via our App for a variety of business purposes described below. We process your personal information for these purposes in reliance on our legitimate business interests, in order to enter into or perform a contract with you, with your consent, and/or for compliance with our legal obligations. We indicate the specific processing grounds we rely on next to each purpose listed below. We use the information we collect or receive: To facilitate account creation and logon process. If you choose to link your account with us to a third-party account (such as your Google or Facebook account), we use the information you allowed us to collect from those third parties to facilitate account creation and logon process for the performance of the contract. To post testimonials. We post testimonials on our App that may contain personal information. Prior to posting a testimonial, we will obtain your consent to use your name and the content of the testimonial. If you wish to update, or delete your testimonial, please contact us at admin@banuacoders.com and be sure to include your name, testimonial location, and contact information. Request feedback. We may use your information to request feedback and to contact you about your use of our App. To enable user-to-user communications. We may use your information in order to enable user-to-user communications with each user's consent. To manage user accounts. We may use your information for the purposes of managing our account and keeping it in working order. To send administrative information to you. We may use your personal information to send you product, service and new feature information and/or information about changes to our terms, conditions, and policies. To protect our Services. We may use your information as part of our efforts to keep our App safe and secure (for example, for fraud monitoring and prevention). To enforce our terms, conditions and policies for business purposes, to comply with legal and regulatory requirements or in connection with our contract. To respond to legal requests and prevent harm. If we receive a subpoena or other legal request, we may need to inspect the data we hold to determine how to respond. 3. WILL YOUR INFORMATION BE SHARED WITH ANYONE? In Short: We only share information with your consent, to comply with laws, to provide you with services, to protect your rights, or to fulfill business obligations. We may process or share your data that we hold based on the following legal basis: Consent: We may process your data if you have given us specific consent to use your personal information for a specific purpose. Legitimate Interests: We may process your data when it is reasonably necessary to achieve our legitimate business interests. Performance of a Contract: Where we have entered into a contract with you, we may process your personal information to fulfill the terms of our contract. Legal Obligations: We may disclose your information where we are legally required to do so in order to comply with applicable law, governmental requests, a judicial proceeding, court order, or legal process, such as in response to a court order or a subpoena (including in response to public authorities to meet national security or law enforcement requirements). Vital Interests: We may disclose your information where we believe it is necessary to investigate, prevent, or take action regarding potential violations of our policies, suspected fraud, situations involving potential threats to the safety of any person and illegal activities, or as evidence in litigation in which we are involved. More specifically, we may need to process your data or share your personal information in the following situations: Business Transfers. We may share or transfer your information in connection with, or during negotiations of, any merger, sale of company assets, financing, or acquisition of all or a portion of our business to another company. Other Users. When you share personal information or otherwise interact with public areas of the App, such personal information may be viewed by all users and may be publicly made available outside the App in perpetuity. Similarly, other users will be able to view descriptions of your activity, communicate with you within our App, and view your profile. 4. HOW LONG DO WE KEEP YOUR INFORMATION? In Short: We keep your information for as long as necessary to fulfill the purposes outlined in this privacy notice unless otherwise required by law. We will only keep your personal information for as long as it is necessary for the purposes set out in this privacy notice, unless a longer retention period is required or permitted by law (such as tax, accounting or other legal requirements). No purpose in this notice will require us keeping your personal information for longer than 1 year. When we have no ongoing legitimate business need to process your personal information, we will either delete or anonymize such information, or, if this is not possible (for example, because your personal information has been stored in backup archives), then we will securely store your personal information and isolate it from any further processing until deletion is possible. 5. HOW DO WE KEEP YOUR INFORMATION SAFE? In Short: We aim to protect your personal information through a system of organizational and technical security measures. We have implemented appropriate technical and organizational security measures designed to protect the security of any personal information we process. However, despite our safeguards and efforts to secure your information, no electronic transmission over the Internet or information storage technology can be guaranteed to be 100% secure, so we cannot promise or guarantee that hackers, cybercriminals, or other unauthorized third parties will not be able to defeat our security, and improperly collect, access, steal, or modify your information. Although we will do our best to protect your personal information, transmission of personal information to and from our App is at your own risk. You should only access the App within a secure environment. 6. DO WE COLLECT INFORMATION FROM MINORS? In Short: We do not knowingly collect data from or market to children under 18 years of age. We do not knowingly solicit data from or market to children under 18 years of age. By using the App, you represent that you are at least 18 or that you are the parent or guardian of such a minor and consent to such minor dependent’s use of the App. If we learn that personal information from users less than 18 years of age has been collected, we will deactivate the account and take reasonable measures to promptly delete such data from our records. If you become aware of any data we may have collected from children under age 18, please contact us at admin@banuacoders.com. 7. WHAT ARE YOUR PRIVACY RIGHTS? In Short: You may review, change, or terminate your account at any time. If you are a resident in the European Economic Area and you believe we are unlawfully processing your personal information, you also have the right to complain to your local data protection supervisory authority. You can find their contact details here: http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm. If you are a resident in Switzerland, the contact details for the data protection authorities are available here: https://www.edoeb.admin.ch/edoeb/en/home.html. 8. CONTROLS FOR DO-NOT-TRACK FEATURES Most web browsers and some mobile operating systems and mobile applications include a Do-Not-Track ("DNT") feature or setting you can activate to signal your privacy preference not to have data about your online browsing activities monitored and collected. At this stage no uniform technology standard for recognizing and implementing DNT signals has been finalized. As such, we do not currently respond to DNT browser signals or any other mechanism that automatically communicates your choice not to be tracked online. If a standard for online tracking is adopted that we must follow in the future, we will inform you about that practice in a revised version of this privacy notice. 9. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? In Short: Yes, if you are a resident of California, you are granted specific rights regarding access to your personal information. California Civil Code Section 1798.83, also known as the "Shine The Light" law, permits our users who are California residents to request and obtain from us, once a year and free of charge, information about categories of personal information (if any) we disclosed to third parties for direct marketing purposes and the names and addresses of all third parties with which we shared personal information in the immediately preceding calendar year. If you are a California resident and would like to make such a request, please submit your request in writing to us using the contact information provided below. If you are under 18 years of age, reside in California, and have a registered account with the App, you have the right to request removal of unwanted data that you publicly post on the App. To request removal of such data, please contact us using the contact information provided below, and include the email address associated with your account and a statement that you reside in California. We will make sure the data is not publicly displayed on the App, but please be aware that the data may not be completely or comprehensively removed from all our systems (e.g. backups, etc.). 10. DO WE MAKE UPDATES TO THIS NOTICE? In Short: Yes, we will update this notice as necessary to stay compliant with relevant laws. We may update this privacy notice from time to time. The updated version will be indicated by an updated "Revised" date and the updated version will be effective as soon as it is accessible. If we make material changes to this privacy notice, we may notify you either by prominently posting a notice of such changes or by directly sending you a notification. We encourage you to review this privacy notice frequently to be informed of how we are protecting your information. 11. HOW CAN YOU CONTACT US ABOUT THIS NOTICE? If you have questions or comments about this notice, you may email us at admin@banuacoders.com or by post to: Banua Coders Jl. Poros Palu - Toli-Toli, Dusun V Moluid, Desa Tambu, Kec. Balaesang, Kab. Donggala, Sulawesi Tengah Donggala, Sulawesi Tengah 94355 Indonesia 12. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU? Based on the applicable laws of your country, you may have the right to request access to the personal information we collect from you, change that information, or delete it in some circumstances. To request to review, update, or delete your personal information, please submit a request form by clicking here. We will respond to your request within 30 days. This privacy policy was created using Termly’s Privacy Policy Generator. ================================================ FILE: pubspec.yaml ================================================ name: spo_balaesang version: 2.0.0+1 publish_to: none description: Sistem presensi online kantor pemerintahan kecamatan balaesang. environment: sdk: '>=2.7.0 <3.0.0' dependencies: font_awesome_flutter: ^9.0.0 flutter_cupertino_datetime_picker: ^2.0.1 percent_indicator: ^3.0.1 table_calendar: ^3.0.0 cupertino_icons: ^1.0.3 http: ^0.13.1 provider: ^5.0.0 intl: ^0.17.0 url_launcher: ^6.0.4 shared_preferences: ^2.0.5 permission_handler: ^7.1.0 slugify: ^2.0.0 device_info: ^2.0.1 auto_size_text: ^3.0.0-nullsafety.0 geolocator: ^7.0.3 android_intent: ^2.0.0 app_settings: ^4.1.0 flutter_config: any camera: ^0.8.1 path_provider: ^2.0.1 path: ^1.8.0 flutter_image_compress: ^1.0.0 qr_code_scanner: ^0.4.0 flutter_countdown_timer: ^4.0.2 image_picker: ^0.7.4 geocoding: ^2.0.0 progress_dialog: ^1.2.4 shimmer: ^2.0.0-nullsafety.0 get: ^4.1.1 flare_flutter: ^3.0.0 introduction_screen: ^2.1.0 flutter_launcher_icons: ^0.9.0 onesignal_flutter: ^2.6.2 flutter_local_notifications: ^5.0.0 cached_network_image: ^3.0.0 photo_view: ^0.11.1 flutter_native_splash: ^1.1.5+1 flutter_spinkit: ^5.0.0 lint: ^1.5.3 pull_to_refresh: 2.0.0 timeline_tile: 2.0.0 search_page: 2.0.0+1 location: 4.1.1 flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter flutter_icons: image_path_ios: assets/launcher/icon.png image_path_android: assets/launcher/icon.png adaptive_icon_background: assets/launcher/launcher_bg.png adaptive_icon_foreground: assets/launcher/launcher_fg.png ios: true android: true flutter_native_splash: image: assets/launcher/icon.png color: 6b8eef android_disable_fullscreen: true flutter: assets: - assets/images/ - assets/logo/ - assets/flare/ - assets/launcher/ uses-material-design: true ================================================ FILE: test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spo_balaesang/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); }