Update isi source kode NAKULA
|
|
@ -0,0 +1,8 @@
|
|||
Setup_Dataset/datasetfix/
|
||||
Setup_Dataset/dataset_split_v2/
|
||||
Setup_Dataset/hasil_klasifikasi/
|
||||
Setup_Dataset/output/
|
||||
Setup_Dataset/output_evaluasi/
|
||||
Setup_Dataset/venv_model/
|
||||
|
||||
Nakula/node_modules/
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
BUNDLE_PATH: "vendor/bundle"
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: 1
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native',
|
||||
};
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
**/.xcode.env.local
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
.kotlin/
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
# Ruby / CocoaPods
|
||||
**/Pods/
|
||||
/vendor/bundle/
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# Yarn
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
import { StatusBar } from "react-native";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import BottomTabNavigator from "./src/navigation/BottomTabNavigator";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<NavigationContainer>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#f8fafc" />
|
||||
<BottomTabNavigator />
|
||||
</NavigationContainer>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby ">= 2.6.10"
|
||||
|
||||
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem 'xcodeproj', '< 1.26.0'
|
||||
gem 'concurrent-ruby', '< 1.3.4'
|
||||
|
||||
# Ruby 3.4.0 has removed some libraries from the standard library.
|
||||
gem 'bigdecimal'
|
||||
gem 'logger'
|
||||
gem 'benchmark'
|
||||
gem 'mutex_m'
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli).
|
||||
|
||||
# Getting Started
|
||||
|
||||
> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding.
|
||||
|
||||
## Step 1: Start Metro
|
||||
|
||||
First, you will need to run **Metro**, the JavaScript build tool for React Native.
|
||||
|
||||
To start the Metro dev server, run the following command from the root of your React Native project:
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
npm start
|
||||
|
||||
# OR using Yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Step 2: Build and run your app
|
||||
|
||||
With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app:
|
||||
|
||||
### Android
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
npm run android
|
||||
|
||||
# OR using Yarn
|
||||
yarn android
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps).
|
||||
|
||||
The first time you create a new project, run the Ruby bundler to install CocoaPods itself:
|
||||
|
||||
```sh
|
||||
bundle install
|
||||
```
|
||||
|
||||
Then, and every time you update your native dependencies, run:
|
||||
|
||||
```sh
|
||||
bundle exec pod install
|
||||
```
|
||||
|
||||
For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
npm run ios
|
||||
|
||||
# OR using Yarn
|
||||
yarn ios
|
||||
```
|
||||
|
||||
If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device.
|
||||
|
||||
This is one way to run your app — you can also build it directly from Android Studio or Xcode.
|
||||
|
||||
## Step 3: Modify your app
|
||||
|
||||
Now that you have successfully run the app, let's make changes!
|
||||
|
||||
Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh).
|
||||
|
||||
When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload:
|
||||
|
||||
- **Android**: Press the <kbd>R</kbd> key twice or select **"Reload"** from the **Dev Menu**, accessed via <kbd>Ctrl</kbd> + <kbd>M</kbd> (Windows/Linux) or <kbd>Cmd ⌘</kbd> + <kbd>M</kbd> (macOS).
|
||||
- **iOS**: Press <kbd>R</kbd> in iOS Simulator.
|
||||
|
||||
## Congratulations! :tada:
|
||||
|
||||
You've successfully run and modified your React Native App. :partying_face:
|
||||
|
||||
### Now what?
|
||||
|
||||
- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps).
|
||||
- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started).
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page.
|
||||
|
||||
# Learn More
|
||||
|
||||
To learn more about React Native, take a look at the following resources:
|
||||
|
||||
- [React Native Website](https://reactnative.dev) - learn more about React Native.
|
||||
- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment.
|
||||
- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**.
|
||||
- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts.
|
||||
- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactTestRenderer from 'react-test-renderer';
|
||||
import App from '../App';
|
||||
|
||||
test('renders correctly', async () => {
|
||||
await ReactTestRenderer.act(() => {
|
||||
ReactTestRenderer.create(<App />);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
|
||||
// cliFile = file("../../node_modules/react-native/cli.js")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized".
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace "com.nakula"
|
||||
defaultConfig {
|
||||
applicationId "com.nakula"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:supportsRtl="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.nakula
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "Nakula"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.nakula
|
||||
|
||||
import android.app.Application
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactHost: ReactHost by lazy {
|
||||
getDefaultReactHost(
|
||||
context = applicationContext,
|
||||
packageList =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
loadReactNative(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
|
||||
>
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Nakula</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "36.0.0"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
ndkVersion = "27.2.12479018"
|
||||
kotlinVersion = "2.1.20"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Use this property to enable edge-to-edge display support.
|
||||
# This allows your app to draw behind system bars for an immersive UI.
|
||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||
edgeToEdgeEnabled=false
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
@REM Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
@REM
|
||||
@REM This source code is licensed under the MIT license found in the
|
||||
@REM LICENSE file in the root directory of this source tree.
|
||||
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||
plugins { id("com.facebook.react.settings") }
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||
rootProject.name = 'Nakula'
|
||||
include ':app'
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Nakula",
|
||||
"displayName": "Nakula",
|
||||
"plugins": [
|
||||
"expo-sharing"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
presets: ['module:@react-native/babel-preset'],
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
import { AppRegistry } from 'react-native';
|
||||
import App from './App';
|
||||
import { name as appName } from './app.json';
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# This `.xcode.env` file is versioned and is used to source the environment
|
||||
# used when running script phases inside Xcode.
|
||||
# To customize your local environment, you can create an `.xcode.env.local`
|
||||
# file that is not versioned.
|
||||
|
||||
# NODE_BINARY variable contains the PATH to the node executable.
|
||||
#
|
||||
# Customize the NODE_BINARY variable here.
|
||||
# For example, to use nvm with brew, add the following line
|
||||
# . "$(brew --prefix nvm)/nvm.sh" --no-use
|
||||
export NODE_BINARY=$(command -v node)
|
||||
|
|
@ -0,0 +1,475 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C80B921A6F3F58F76C31292 /* libPods-Nakula.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-Nakula.a */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
13B07F961A680F5B00A75B9A /* Nakula.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nakula.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nakula/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nakula/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = Nakula/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
3B4392A12AC88292D35C810B /* Pods-Nakula.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nakula.debug.xcconfig"; path = "Target Support Files/Pods-Nakula/Pods-Nakula.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
5709B34CF0A7D63546082F79 /* Pods-Nakula.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nakula.release.xcconfig"; path = "Target Support Files/Pods-Nakula/Pods-Nakula.release.xcconfig"; sourceTree = "<group>"; };
|
||||
5DCACB8F33CDC322A6C60F78 /* libPods-Nakula.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nakula.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Nakula/AppDelegate.swift; sourceTree = "<group>"; };
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Nakula/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C80B921A6F3F58F76C31292 /* libPods-Nakula.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
13B07FAE1A68108700A75B9A /* Nakula */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
761780EC2CA45674006654EE /* AppDelegate.swift */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
name = Nakula;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
5DCACB8F33CDC322A6C60F78 /* libPods-Nakula.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* Nakula */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
usesTabs = 0;
|
||||
};
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* Nakula.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B4392A12AC88292D35C810B /* Pods-Nakula.debug.xcconfig */,
|
||||
5709B34CF0A7D63546082F79 /* Pods-Nakula.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* Nakula */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nakula" */;
|
||||
buildPhases = (
|
||||
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */,
|
||||
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Nakula;
|
||||
productName = Nakula;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* Nakula.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1210;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Nakula" */;
|
||||
compatibilityVersion = "Xcode 12.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* Nakula */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(SRCROOT)/.xcode.env.local",
|
||||
"$(SRCROOT)/.xcode.env",
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"\\\"$WITH_ENVIRONMENT\\\" \\\"$REACT_NATIVE_XCODE\\\"\"\n";
|
||||
};
|
||||
00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Nakula/Pods-Nakula-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Nakula/Pods-Nakula-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nakula/Pods-Nakula-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Nakula-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Nakula/Pods-Nakula-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Nakula/Pods-Nakula-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nakula/Pods-Nakula-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-Nakula.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Nakula/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = Nakula;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-Nakula.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = Nakula/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_NAME = Nakula;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
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;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
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_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
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 = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(SDKROOT)/usr/lib/swift\"",
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-DFOLLY_NO_CONFIG",
|
||||
"-DFOLLY_MOBILE=1",
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
83CBBA211A601CBA00E9B192 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
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 = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||
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 = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(SDKROOT)/usr/lib/swift\"",
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-DFOLLY_NO_CONFIG",
|
||||
"-DFOLLY_MOBILE=1",
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
);
|
||||
SDKROOT = iphoneos;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nakula" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
13B07F951A680F5B00A75B9A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Nakula" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
83CBBA211A601CBA00E9B192 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1210"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Nakula.app"
|
||||
BlueprintName = "Nakula"
|
||||
ReferencedContainer = "container:Nakula.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||
BuildableName = "NakulaTests.xctest"
|
||||
BlueprintName = "NakulaTests"
|
||||
ReferencedContainer = "container:Nakula.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Nakula.app"
|
||||
BlueprintName = "Nakula"
|
||||
ReferencedContainer = "container:Nakula.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "Nakula.app"
|
||||
BlueprintName = "Nakula"
|
||||
ReferencedContainer = "container:Nakula.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import UIKit
|
||||
import React
|
||||
import React_RCTAppDelegate
|
||||
import ReactAppDependencyProvider
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
var reactNativeDelegate: ReactNativeDelegate?
|
||||
var reactNativeFactory: RCTReactNativeFactory?
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
let delegate = ReactNativeDelegate()
|
||||
let factory = RCTReactNativeFactory(delegate: delegate)
|
||||
delegate.dependencyProvider = RCTAppDependencyProvider()
|
||||
|
||||
reactNativeDelegate = delegate
|
||||
reactNativeFactory = factory
|
||||
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
|
||||
factory.startReactNative(
|
||||
withModuleName: "Nakula",
|
||||
in: window,
|
||||
launchOptions: launchOptions
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
|
||||
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
||||
self.bundleURL()
|
||||
}
|
||||
|
||||
override func bundleURL() -> URL? {
|
||||
#if DEBUG
|
||||
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
||||
#else
|
||||
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nakula</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Nakula" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="GJd-Yh-RWb">
|
||||
<rect key="frame" x="0.0" y="202" width="375" height="43"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Powered by React Native" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="MN2-I3-ftu">
|
||||
<rect key="frame" x="0.0" y="626" width="375" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="bottom" secondItem="MN2-I3-ftu" secondAttribute="bottom" constant="20" id="OZV-Vh-mqD"/>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="centerX" secondItem="GJd-Yh-RWb" secondAttribute="centerX" id="Q3B-4B-g5h"/>
|
||||
<constraint firstItem="MN2-I3-ftu" firstAttribute="centerX" secondItem="Bcu-3y-fUS" secondAttribute="centerX" id="akx-eg-2ui"/>
|
||||
<constraint firstItem="MN2-I3-ftu" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" id="i1E-0Y-4RG"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="bottom" multiplier="1/3" constant="1" id="moa-c2-u7t"/>
|
||||
<constraint firstItem="GJd-Yh-RWb" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" symbolic="YES" id="x7j-FC-K8j"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="52.173913043478265" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Resolve react_native_pods.rb with node to allow for hoisting
|
||||
require Pod::Executable.execute_command('node', ['-p',
|
||||
'require.resolve(
|
||||
"react-native/scripts/react_native_pods.rb",
|
||||
{paths: [process.argv[1]]},
|
||||
)', __dir__]).strip
|
||||
|
||||
platform :ios, min_ios_version_supported
|
||||
prepare_react_native_project!
|
||||
|
||||
linkage = ENV['USE_FRAMEWORKS']
|
||||
if linkage != nil
|
||||
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
|
||||
use_frameworks! :linkage => linkage.to_sym
|
||||
end
|
||||
|
||||
target 'Nakula' do
|
||||
config = use_native_modules!
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
|
||||
post_install do |installer|
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
# :ccache_enabled => true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
preset: 'react-native',
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
|
||||
|
||||
/**
|
||||
* Metro configuration
|
||||
* https://reactnative.dev/docs/metro
|
||||
*
|
||||
* @type {import('@react-native/metro-config').MetroConfig}
|
||||
*/
|
||||
const config = {};
|
||||
|
||||
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "Nakula",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-community/datetimepicker": "^9.1.0",
|
||||
"@react-native-community/slider": "^5.1.2",
|
||||
"@react-native/new-app-screen": "0.84.1",
|
||||
"@react-navigation/bottom-tabs": "^7.15.2",
|
||||
"@react-navigation/native": "^7.1.31",
|
||||
"@react-navigation/native-stack": "^7.14.2",
|
||||
"axios": "^1.15.1",
|
||||
"react": "19.2.3",
|
||||
"react-native": "0.84.1",
|
||||
"react-native-chart-kit": "^6.12.0",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "^2.30.0",
|
||||
"react-native-html-to-pdf": "^1.3.0",
|
||||
"react-native-print": "^0.11.0",
|
||||
"react-native-reanimated": "^4.2.2",
|
||||
"react-native-safe-area-context": "^5.7.0",
|
||||
"react-native-screens": "^4.24.0",
|
||||
"react-native-share": "^12.2.6",
|
||||
"react-native-svg": "^15.15.3",
|
||||
"react-native-worklets": "^0.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.3",
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"@react-native-community/cli": "20.1.0",
|
||||
"@react-native-community/cli-platform-android": "20.1.0",
|
||||
"@react-native-community/cli-platform-ios": "20.1.0",
|
||||
"@react-native/babel-preset": "0.84.1",
|
||||
"@react-native/eslint-config": "0.84.1",
|
||||
"@react-native/metro-config": "0.84.1",
|
||||
"@react-native/typescript-config": "0.84.1",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-test-renderer": "^19.1.0",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^29.6.3",
|
||||
"prettier": "2.8.8",
|
||||
"react-native-make": "^1.0.1",
|
||||
"react-test-renderer": "19.2.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.11.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
import React from "react";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { HistoryPoint } from "../services/api";
|
||||
|
||||
interface BarChartProps {
|
||||
history: HistoryPoint[];
|
||||
}
|
||||
|
||||
const GRADE_COLORS = {
|
||||
A: "#16a34a",
|
||||
B: "#d97706",
|
||||
C: "#dc2626",
|
||||
TL: "#6b7280",
|
||||
};
|
||||
|
||||
export default function BarChart({ history }: BarChartProps) {
|
||||
if (!history?.length) return null;
|
||||
|
||||
const show = history.slice(-5);
|
||||
const maxVal = Math.max(...show.flatMap((h) => [h.A, h.B, h.C, h.TL ?? 0]), 1);
|
||||
const rawMax = Math.max(...show.flatMap((h) => [h.A, h.B, h.C, h.TL ?? 0]), 1);
|
||||
const niceMax = Math.ceil(rawMax / 10) * 10;
|
||||
|
||||
const ySteps = [
|
||||
niceMax,
|
||||
niceMax * 0.75,
|
||||
niceMax * 0.5,
|
||||
niceMax * 0.25,
|
||||
0,
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Y Vertical */}
|
||||
<View style={styles.yAxisRow}>
|
||||
<View style={styles.yAxis}>
|
||||
{/* {[100, 75, 50, 25, 0].map((v) => ( */}
|
||||
{ySteps.map((v, i) => (
|
||||
// <Text key={v} style={styles.yLabel}>{v}</Text>
|
||||
<Text key={i} style={styles.yLabel}>{Math.round(v)}</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Chart area */}
|
||||
<View style={styles.chartArea}>
|
||||
{ySteps.map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
styles.gridLine,
|
||||
{
|
||||
bottom: `${(i / (ySteps.length - 1)) * 100}%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Bars */}
|
||||
<View style={styles.chartRow}>
|
||||
{show.map((item, i) => (
|
||||
<View key={i} style={styles.barGroup}>
|
||||
<View style={styles.barsContainer}>
|
||||
{(["A", "B", "C", "TL"] as const).map((g) => {
|
||||
const val = item[g] ?? 0;
|
||||
// const heightPct = Math.max(2, (val / maxVal) * 100);
|
||||
const heightPct = Math.max(2, (val / niceMax) * 100);
|
||||
return (
|
||||
<View key={g} style={styles.barWrapper}>
|
||||
<Text style={[styles.barValue, { color: GRADE_COLORS[g] }]}>
|
||||
{val > 0 ? val : ""}
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.bar,
|
||||
{
|
||||
height: (heightPct / 100) * 160,
|
||||
backgroundColor: GRADE_COLORS[g],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<Text style={styles.timeLabel}>{item.time}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Legend */}
|
||||
<View style={styles.legend}>
|
||||
{(["A", "B", "C", "TL"] as const).map((g) => (
|
||||
<View key={g} style={styles.legendItem}>
|
||||
<View style={[styles.legendDot, { backgroundColor: GRADE_COLORS[g] }]} />
|
||||
<Text style={styles.legendText}>
|
||||
{g === "TL" ? "Tidak Layak" : `Grade ${g}`}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 8,
|
||||
paddingTop: 8,
|
||||
},
|
||||
|
||||
/* Y-axis + chart side-by-side */
|
||||
yAxisRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
yAxis: {
|
||||
width: 28,
|
||||
height: 180, // matches chartArea height
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-end",
|
||||
paddingRight: 4,
|
||||
paddingBottom: 22, // reserve space for time labels
|
||||
},
|
||||
yLabel: {
|
||||
fontSize: 9,
|
||||
color: "#94a3b8",
|
||||
},
|
||||
|
||||
/* Chart area */
|
||||
chartArea: {
|
||||
flex: 1,
|
||||
height: 180,
|
||||
position: "relative",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
gridLine: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 1,
|
||||
backgroundColor: "#e2e8f0",
|
||||
opacity: 0.6,
|
||||
},
|
||||
|
||||
/* Bar row */
|
||||
chartRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
height: 160,
|
||||
gap: 6,
|
||||
paddingHorizontal: 2,
|
||||
},
|
||||
barGroup: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
},
|
||||
barsContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
gap: 2,
|
||||
height: 160,
|
||||
},
|
||||
barWrapper: {
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
barValue: {
|
||||
fontSize: 8,
|
||||
fontWeight: "600",
|
||||
marginBottom: 1,
|
||||
},
|
||||
bar: {
|
||||
width: 10,
|
||||
borderRadius: 3,
|
||||
},
|
||||
timeLabel: {
|
||||
fontSize: 9,
|
||||
color: "#64748b",
|
||||
marginTop: 5,
|
||||
textAlign: "center",
|
||||
},
|
||||
|
||||
/* Legend */
|
||||
legend: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 14,
|
||||
marginTop: 14,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
legendItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
},
|
||||
legendDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 3,
|
||||
},
|
||||
legendText: {
|
||||
fontSize: 12,
|
||||
color: "#475569",
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Modal,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
|
||||
// ─── Helper ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const BULAN = [
|
||||
"Januari","Februari","Maret","April","Mei","Juni",
|
||||
"Juli","Agustus","September","Oktober","November","Desember",
|
||||
];
|
||||
const HARI = ["Min","Sen","Sel","Rab","Kam","Jum","Sab"];
|
||||
|
||||
export const toISO = (d: Date) =>
|
||||
`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
|
||||
|
||||
export const fromISO = (s: string): Date => {
|
||||
const [y,m,d] = s.split("-").map(Number);
|
||||
return new Date(y, m-1, d);
|
||||
};
|
||||
|
||||
export const toDisplay = (s: string): string => {
|
||||
const d = fromISO(s);
|
||||
return `${String(d.getDate()).padStart(2,"0")}/${String(d.getMonth()+1).padStart(2,"0")}/${d.getFullYear()}`;
|
||||
};
|
||||
|
||||
function getDaysInMonth(year: number, month: number) {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
}
|
||||
|
||||
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface CalendarPickerProps {
|
||||
visible: boolean;
|
||||
value: string; // "yyyy-mm-dd"
|
||||
minDate?: string;
|
||||
maxDate?: string;
|
||||
title?: string;
|
||||
onConfirm: (date: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// ─── Komponen ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function CalendarPicker({
|
||||
visible, value, minDate, maxDate, title = "Pilih Tanggal",
|
||||
onConfirm, onClose,
|
||||
}: CalendarPickerProps) {
|
||||
|
||||
const initDate = fromISO(value);
|
||||
const [viewYear, setViewYear] = useState(initDate.getFullYear());
|
||||
const [viewMonth, setViewMonth] = useState(initDate.getMonth());
|
||||
const [selected, setSelected] = useState(value);
|
||||
|
||||
const todayISO = toISO(new Date());
|
||||
const totalDays = getDaysInMonth(viewYear, viewMonth);
|
||||
const firstDay = new Date(viewYear, viewMonth, 1).getDay(); // 0=Minggu
|
||||
|
||||
// Navigasi bulan
|
||||
const prevMonth = () => {
|
||||
if (viewMonth === 0) { setViewMonth(11); setViewYear(y => y - 1); }
|
||||
else setViewMonth(m => m - 1);
|
||||
};
|
||||
const nextMonth = () => {
|
||||
if (viewMonth === 11) { setViewMonth(0); setViewYear(y => y + 1); }
|
||||
else setViewMonth(m => m + 1);
|
||||
};
|
||||
|
||||
const isDisabled = (iso: string) => {
|
||||
if (minDate && iso < minDate) return true;
|
||||
if (maxDate && iso > maxDate) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const isToday = (iso: string) => iso === todayISO;
|
||||
const isSelected = (iso: string) => iso === selected;
|
||||
|
||||
// Buat grid hari (null = sel kosong sebelum hari pertama)
|
||||
const cells: (number | null)[] = [
|
||||
...Array(firstDay).fill(null),
|
||||
...Array.from({ length: totalDays }, (_, i) => i + 1),
|
||||
];
|
||||
// Lengkapi sampai kelipatan 7
|
||||
while (cells.length % 7 !== 0) cells.push(null);
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
||||
<TouchableOpacity style={s.overlay} activeOpacity={1} onPress={onClose}>
|
||||
<TouchableOpacity activeOpacity={1} style={s.card} onPress={() => {}}>
|
||||
|
||||
{/* Judul */}
|
||||
<Text style={s.title}>{title}</Text>
|
||||
|
||||
{/* Nav bulan */}
|
||||
<View style={s.navRow}>
|
||||
<TouchableOpacity onPress={prevMonth} style={s.navBtn}>
|
||||
<Text style={s.navArrow}>‹</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={s.navLabel}>
|
||||
{BULAN[viewMonth]} {viewYear}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={nextMonth} style={s.navBtn}>
|
||||
<Text style={s.navArrow}>›</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Header hari */}
|
||||
<View style={s.weekRow}>
|
||||
{HARI.map(h => (
|
||||
<Text key={h} style={[s.weekCell, h === "Min" && { color: "#dc2626" }]}>{h}</Text>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Grid tanggal */}
|
||||
<View style={s.grid}>
|
||||
{cells.map((day, i) => {
|
||||
if (!day) return <View key={`e${i}`} style={s.dayCell} />;
|
||||
|
||||
const iso = `${viewYear}-${String(viewMonth+1).padStart(2,"0")}-${String(day).padStart(2,"0")}`;
|
||||
const disabled = isDisabled(iso);
|
||||
const today = isToday(iso);
|
||||
const sel = isSelected(iso);
|
||||
const isSun = (firstDay + day - 1) % 7 === 0;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={iso}
|
||||
disabled={disabled}
|
||||
onPress={() => setSelected(iso)}
|
||||
style={[
|
||||
s.dayCell,
|
||||
sel && s.daySel,
|
||||
today && !sel && s.dayToday,
|
||||
]}
|
||||
>
|
||||
<Text style={[
|
||||
s.dayText,
|
||||
isSun && !sel && { color: "#dc2626" },
|
||||
today && !sel && { color: "#4f46e5", fontWeight: "700" },
|
||||
sel && { color: "#fff", fontWeight: "800" },
|
||||
disabled && { color: "#d1d5db" },
|
||||
]}>
|
||||
{day}
|
||||
</Text>
|
||||
{today && !sel && <View style={s.todayDot} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Preview tanggal dipilih */}
|
||||
<View style={s.previewRow}>
|
||||
<Text style={s.previewText}>
|
||||
Dipilih:{" "}
|
||||
<Text style={s.previewVal}>{toDisplay(selected)}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Tombol aksi */}
|
||||
<View style={s.btnRow}>
|
||||
<TouchableOpacity onPress={onClose} style={s.btnCancel}>
|
||||
<Text style={s.btnCancelText}>Batal</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => { onConfirm(selected); onClose(); }} style={s.btnOk}>
|
||||
<Text style={s.btnOkText}>Pilih</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||
const CELL_SIZE = 38;
|
||||
|
||||
const s = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1, backgroundColor: "rgba(0,0,0,0.45)",
|
||||
justifyContent: "center", alignItems: "center",
|
||||
},
|
||||
card: {
|
||||
backgroundColor: "#fff", borderRadius: 20, padding: 20,
|
||||
width: 320,
|
||||
shadowColor: "#000", shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.18, shadowRadius: 20, elevation: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16, fontWeight: "800", color: "#0f172a",
|
||||
textAlign: "center", marginBottom: 16,
|
||||
},
|
||||
|
||||
// Nav
|
||||
navRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 12 },
|
||||
navBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: "#f1f5f9", alignItems: "center", justifyContent: "center" },
|
||||
navArrow: { fontSize: 22, color: "#4f46e5", lineHeight: 26 },
|
||||
navLabel: { fontSize: 15, fontWeight: "700", color: "#0f172a" },
|
||||
|
||||
// Header hari
|
||||
weekRow: { flexDirection: "row", marginBottom: 6 },
|
||||
weekCell: { width: CELL_SIZE, textAlign: "center", fontSize: 11, fontWeight: "600", color: "#94a3b8" },
|
||||
|
||||
// Grid
|
||||
grid: { flexDirection: "row", flexWrap: "wrap" },
|
||||
dayCell: {
|
||||
width: CELL_SIZE, height: CELL_SIZE,
|
||||
alignItems: "center", justifyContent: "center",
|
||||
borderRadius: CELL_SIZE / 2, marginBottom: 2,
|
||||
},
|
||||
daySel: { backgroundColor: "#4f46e5" },
|
||||
dayToday: { borderWidth: 1.5, borderColor: "#4f46e5" },
|
||||
dayText: { fontSize: 13, color: "#334155" },
|
||||
todayDot: {
|
||||
position: "absolute", bottom: 4,
|
||||
width: 4, height: 4, borderRadius: 2, backgroundColor: "#4f46e5",
|
||||
},
|
||||
|
||||
// Preview
|
||||
previewRow: {
|
||||
backgroundColor: "#f8fafc", borderRadius: 10, padding: 10,
|
||||
marginTop: 12, alignItems: "center",
|
||||
},
|
||||
previewText: { fontSize: 12, color: "#64748b" },
|
||||
previewVal: { fontWeight: "700", color: "#0f172a" },
|
||||
|
||||
// Buttons
|
||||
btnRow: { flexDirection: "row", gap: 10, marginTop: 14 },
|
||||
btnCancel: { flex: 1, paddingVertical: 12, borderRadius: 12, backgroundColor: "#f1f5f9", alignItems: "center" },
|
||||
btnCancelText:{ fontSize: 14, color: "#64748b", fontWeight: "600" },
|
||||
btnOk: { flex: 2, paddingVertical: 12, borderRadius: 12, backgroundColor: "#4f46e5", alignItems: "center" },
|
||||
btnOkText: { fontSize: 14, color: "#fff", fontWeight: "800" },
|
||||
});
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import React, { useRef } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
PanResponder,
|
||||
GestureResponderEvent,
|
||||
} from "react-native";
|
||||
|
||||
interface CylinderSliderProps {
|
||||
value: number;
|
||||
max: number;
|
||||
onChange: (value: number) => void;
|
||||
color: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const CYLINDER_HEIGHT = 180;
|
||||
|
||||
export default function CylinderSlider({
|
||||
value,
|
||||
max,
|
||||
onChange,
|
||||
color,
|
||||
label,
|
||||
}: CylinderSliderProps) {
|
||||
const pct = value / max;
|
||||
const fillHeight = pct * CYLINDER_HEIGHT;
|
||||
|
||||
// Simpan pageY saat touch mulai dan nilai saat itu
|
||||
const startPageY = useRef(0);
|
||||
const startValue = useRef(value);
|
||||
|
||||
const clamp = (v: number) => Math.max(0, Math.min(max, Math.round(v)));
|
||||
|
||||
const panResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
|
||||
onPanResponderGrant: (e: GestureResponderEvent) => {
|
||||
// Catat posisi pageY dan nilai awal saat jari menyentuh
|
||||
startPageY.current = e.nativeEvent.pageY;
|
||||
startValue.current = value;
|
||||
},
|
||||
|
||||
onPanResponderMove: (e: GestureResponderEvent) => {
|
||||
// Hitung delta dari posisi awal (geser ke atas = naik, ke bawah = turun)
|
||||
const deltaY = e.nativeEvent.pageY - startPageY.current;
|
||||
// deltaY negatif = geser ke atas = nilai naik
|
||||
const deltaPct = -deltaY / CYLINDER_HEIGHT;
|
||||
const newVal = clamp(startValue.current + deltaPct * max);
|
||||
onChange(newVal);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
|
||||
<View
|
||||
{...panResponder.panHandlers}
|
||||
style={[
|
||||
styles.cylinder,
|
||||
{ borderColor: color + "66", shadowColor: color },
|
||||
]}
|
||||
>
|
||||
{/* Fill dari bawah */}
|
||||
<View style={[styles.fill, { height: fillHeight, backgroundColor: color }]} />
|
||||
|
||||
{/* Shine */}
|
||||
<View style={styles.shine} />
|
||||
|
||||
{/* Tick marks */}
|
||||
{[0.25, 0.5, 0.75].map((t) => (
|
||||
<View
|
||||
key={t}
|
||||
style={[styles.tick, { top: (1 - t) * CYLINDER_HEIGHT }]}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Label persentase di tengah cylinder */}
|
||||
<View style={styles.pctOverlay}>
|
||||
<Text style={[styles.pctText, { color: pct > 0.45 ? "#fff" : color }]}>
|
||||
{Math.round(pct * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.value, { color }]}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { alignItems: "center", gap: 10 },
|
||||
|
||||
label: {
|
||||
fontSize: 10,
|
||||
fontFamily: "monospace",
|
||||
letterSpacing: 0.5,
|
||||
color: "#94a3b8",
|
||||
textAlign: "center",
|
||||
},
|
||||
|
||||
cylinder: {
|
||||
width: 56,
|
||||
height: CYLINDER_HEIGHT,
|
||||
borderRadius: 28,
|
||||
backgroundColor: "#f1f5f9",
|
||||
borderWidth: 2,
|
||||
overflow: "hidden",
|
||||
justifyContent: "flex-end",
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
|
||||
fill: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderRadius: 26,
|
||||
opacity: 0.85,
|
||||
},
|
||||
|
||||
shine: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: 14,
|
||||
backgroundColor: "rgba(255,255,255,0.5)",
|
||||
},
|
||||
|
||||
tick: {
|
||||
position: "absolute",
|
||||
left: 10,
|
||||
right: 10,
|
||||
height: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.08)",
|
||||
},
|
||||
|
||||
pctOverlay: {
|
||||
position: "absolute",
|
||||
top: 0, bottom: 0, left: 0, right: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
pctText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "800",
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
|
||||
value: {
|
||||
fontSize: 18,
|
||||
fontWeight: "800",
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
import Svg, { Polyline } from "react-native-svg";
|
||||
|
||||
interface SparklineProps {
|
||||
data: number[];
|
||||
color: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export default function Sparkline({
|
||||
data,
|
||||
color,
|
||||
width = 80,
|
||||
height = 30,
|
||||
}: SparklineProps) {
|
||||
if (!data || data.length < 2) return null;
|
||||
|
||||
const max = Math.max(...data);
|
||||
const min = Math.min(...data);
|
||||
const range = max - min || 1;
|
||||
|
||||
const points = data
|
||||
.map((v, i) => {
|
||||
const x = (i / (data.length - 1)) * width;
|
||||
const y = height - ((v - min) / range) * height;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<Svg width={width} height={height}>
|
||||
<Polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import React from "react";
|
||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { View, Text, StyleSheet, Image } from "react-native";
|
||||
import DashboardScreen from "../screens/DashboardScreen";
|
||||
import LaporanScreen from "../screens/LaporanScreen";
|
||||
import ControlScreen from "../screens/ControlScreen";
|
||||
|
||||
function TabIcon({
|
||||
focused,
|
||||
icon,
|
||||
color,
|
||||
}: {
|
||||
focused: boolean;
|
||||
icon: any;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.iconContainer,
|
||||
focused && { transform: [{ scale: 1.15 }] },
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={icon}
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
tintColor: color,
|
||||
resizeMode: "contain",
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
const TAB_CONFIG = {
|
||||
Dashboard: {
|
||||
icon: require("../../assets/dashboard.png"),
|
||||
color: "#4f46e5",
|
||||
},
|
||||
Laporan: {
|
||||
icon: require("../../assets/report.png"),
|
||||
color: "#16a34a",
|
||||
},
|
||||
Kontrol: {
|
||||
icon: require("../../assets/control.png"),
|
||||
color: "#0284c7",
|
||||
},
|
||||
};
|
||||
|
||||
export default function BottomTabNavigator() {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={({ route }) => {
|
||||
const config = TAB_CONFIG[route.name as keyof typeof TAB_CONFIG];
|
||||
return {
|
||||
headerShown: false,
|
||||
tabBarStyle: styles.tabBar,
|
||||
tabBarActiveTintColor: config.color,
|
||||
tabBarInactiveTintColor: "#94a3b8",
|
||||
tabBarLabelStyle: styles.tabLabel,
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<TabIcon focused={focused} icon={config.icon} color={color} />
|
||||
),
|
||||
tabBarItemStyle: styles.tabItem,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Tab.Screen name="Dashboard" component={DashboardScreen} />
|
||||
<Tab.Screen name="Laporan" component={LaporanScreen} />
|
||||
<Tab.Screen name="Kontrol" component={ControlScreen} />
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabBar: {
|
||||
backgroundColor: "#fff",
|
||||
borderTopColor: "#e2e8f0",
|
||||
borderTopWidth: 1,
|
||||
height: 72,
|
||||
paddingBottom: 10,
|
||||
paddingTop: 6,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: -2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
},
|
||||
tabItem: {
|
||||
paddingTop: 4,
|
||||
},
|
||||
tabLabel: {
|
||||
fontSize: 10,
|
||||
fontWeight: "600",
|
||||
marginTop: 2,
|
||||
},
|
||||
iconContainer: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
Keyboard,
|
||||
} from "react-native";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import Slider from "@react-native-community/slider";
|
||||
import CylinderSlider from "../components/CylinderSlinder";
|
||||
import { setConveyorSpeed, getConveyorSpeed } from "../services/api";
|
||||
|
||||
// ─── Konstanta ────────────────────────────────────────────────────────────────
|
||||
|
||||
const SPEED_MODES = [
|
||||
{ key: "pelan", label: "Pelan", value: 50, desc: "Cocok untuk telur rapuh" },
|
||||
{ key: "sedang", label: "Sedang", value: 128, desc: "Mode normal" },
|
||||
{ key: "cepat", label: "Cepat", value: 220, desc: "Throughput tinggi" },
|
||||
];
|
||||
|
||||
function getSpeedLabel(v: number) {
|
||||
if (v < 80) return "Pelan";
|
||||
if (v < 170) return "Sedang";
|
||||
return "Cepat";
|
||||
}
|
||||
|
||||
function getSpeedColor(v: number) {
|
||||
if (v < 80) return "#16a34a";
|
||||
if (v < 170) return "#d97706";
|
||||
return "#dc2626";
|
||||
}
|
||||
|
||||
// ─── Komponen utama ───────────────────────────────────────────────────────────
|
||||
|
||||
export default function ControlScreen() {
|
||||
const [speed, setSpeed] = useState(128);
|
||||
const [speedMode, setSpeedMode] = useState("sedang");
|
||||
const [speedInput, setSpeedInput] = useState("128");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [loadingCurrent, setLoadingCurrent] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// ── Fetch speed dari server ───────────────────────────────────────────────
|
||||
const fetchSpeed = useCallback(async (isManual = false) => {
|
||||
if (isManual) setRefreshing(true);
|
||||
else setLoadingCurrent(true);
|
||||
try {
|
||||
const currentSpeed = await getConveyorSpeed();
|
||||
setSpeed(currentSpeed);
|
||||
setSpeedInput(String(currentSpeed));
|
||||
const match = SPEED_MODES.find((m) => m.value === currentSpeed);
|
||||
setSpeedMode(match ? match.key : "");
|
||||
} catch {
|
||||
if (isManual) {
|
||||
Alert.alert("Gagal", "Tidak dapat mengambil data dari server.");
|
||||
}
|
||||
} finally {
|
||||
setLoadingCurrent(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch pertama kali saat app buka
|
||||
useEffect(() => { fetchSpeed(); }, []);
|
||||
|
||||
// ✅ Fetch ulang SETIAP KALI tab Control difokuskan (balik dari tab lain)
|
||||
// Ini memastikan data selalu sinkron dengan server, bukan dari state lama
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchSpeed();
|
||||
}, [fetchSpeed])
|
||||
);
|
||||
|
||||
// ── Handler ───────────────────────────────────────────────────────────────
|
||||
|
||||
const applySpeedMode = (key: string, value: number) => {
|
||||
setSpeedMode(key);
|
||||
setSpeed(value);
|
||||
setSpeedInput(String(value));
|
||||
};
|
||||
|
||||
// Dipanggil dari cylinder atau slider — TIDAK dari input teks
|
||||
const handleSpeedChange = (v: number) => {
|
||||
const rounded = Math.round(v);
|
||||
setSpeed(rounded);
|
||||
setSpeedInput(String(rounded));
|
||||
const match = SPEED_MODES.find((m) => m.value === rounded);
|
||||
setSpeedMode(match ? match.key : "");
|
||||
};
|
||||
|
||||
/**
|
||||
* Terapkan nilai dari input teks.
|
||||
* HANYA dipanggil saat tombol "Terapkan" diklik — BUKAN saat blur/dismiss keyboard.
|
||||
*/
|
||||
const handleApplyInput = () => {
|
||||
Keyboard.dismiss();
|
||||
const val = Math.max(0, Math.min(255, parseInt(speedInput, 10) || 0));
|
||||
setSpeed(val);
|
||||
setSpeedInput(String(val));
|
||||
const match = SPEED_MODES.find((m) => m.value === val);
|
||||
setSpeedMode(match ? match.key : "");
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
await setConveyorSpeed(speed);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2500);
|
||||
} catch {
|
||||
Alert.alert("Error", "Gagal mengirim data ke perangkat. Cek koneksi server.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const activeColor = getSpeedColor(speed);
|
||||
|
||||
// ── Loading state ─────────────────────────────────────────────────────────
|
||||
|
||||
if (loadingCurrent) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator color="#0284c7" size="large" />
|
||||
<Text style={styles.loadingText}>Memuat konfigurasi...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled" // ← tap di luar input tidak dismiss & terapkan
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.title}>Kontrol Conveyor</Text>
|
||||
<Text style={styles.subtitle}>Atur kecepatan laju conveyor</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => fetchSpeed(true)}
|
||||
disabled={refreshing}
|
||||
style={[styles.refreshBtn, refreshing && { opacity: 0.5 }]}
|
||||
>
|
||||
{refreshing
|
||||
? <ActivityIndicator color="#0284c7" size="small" />
|
||||
: <Text style={styles.refreshIcon}>↺</Text>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Cylinder */}
|
||||
<View style={styles.cylinderCard}>
|
||||
<View style={styles.cylinderWrapper}>
|
||||
<CylinderSlider
|
||||
value={speed}
|
||||
max={255}
|
||||
onChange={handleSpeedChange}
|
||||
color={activeColor}
|
||||
label="CONVEYOR SPEED"
|
||||
/>
|
||||
<View style={styles.cylinderInfo}>
|
||||
<Text style={styles.cylinderPct}>
|
||||
{Math.round((speed / 255) * 100)}%
|
||||
</Text>
|
||||
<Text style={styles.cylinderHint}>Geser ke atas / bawah untuk adjust</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Control Card */}
|
||||
<View style={[styles.controlCard, { borderColor: activeColor + "44" }]}>
|
||||
|
||||
{/* Header nilai */}
|
||||
<View style={styles.controlHeaderRow}>
|
||||
<View>
|
||||
<Text style={[styles.controlLabel, { color: activeColor }]}>
|
||||
⚙️ Kecepatan Conveyor
|
||||
</Text>
|
||||
<View style={styles.valueRow}>
|
||||
<Text style={[styles.controlValueText, { color: "#1e293b" }]}>{speed}</Text>
|
||||
<Text style={styles.controlValueUnit}> / 255</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[
|
||||
styles.modeBadge,
|
||||
{ backgroundColor: activeColor + "15", borderColor: activeColor + "44" }
|
||||
]}>
|
||||
<Text style={[styles.modeBadgeText, { color: activeColor }]}>
|
||||
{getSpeedLabel(speed)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Mode preset */}
|
||||
<Text style={styles.sectionLabel}>Mode Cepat</Text>
|
||||
<View style={styles.modeRow}>
|
||||
{SPEED_MODES.map((m) => {
|
||||
const active = speedMode === m.key;
|
||||
const mColor = getSpeedColor(m.value);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={m.key}
|
||||
onPress={() => applySpeedMode(m.key, m.value)}
|
||||
style={[
|
||||
styles.modeButton,
|
||||
{
|
||||
borderColor: active ? mColor : "#e2e8f0",
|
||||
backgroundColor: active ? mColor + "15" : "#f8fafc",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modeButtonLabel, { color: active ? mColor : "#64748b" }]}>
|
||||
{m.label}
|
||||
</Text>
|
||||
<Text style={styles.modeButtonDesc}>{m.desc}</Text>
|
||||
{active && (
|
||||
<View style={[styles.activeIndicator, { backgroundColor: mColor }]} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Slider */}
|
||||
<Text style={styles.sectionLabel}>Slider Manual</Text>
|
||||
<View style={styles.sliderRow}>
|
||||
<Text style={styles.sliderMin}>0</Text>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
minimumValue={0}
|
||||
maximumValue={255}
|
||||
step={1}
|
||||
value={speed}
|
||||
onValueChange={handleSpeedChange}
|
||||
minimumTrackTintColor={activeColor}
|
||||
maximumTrackTintColor="#e2e8f0"
|
||||
thumbTintColor={activeColor}
|
||||
/>
|
||||
<Text style={styles.sliderMax}>255</Text>
|
||||
</View>
|
||||
|
||||
{/* Input angka — onBlur DIHAPUS, hanya terapkan via tombol */}
|
||||
<Text style={styles.sectionLabel}>Input Angka (0–255)</Text>
|
||||
<View style={styles.inputRow}>
|
||||
<TextInput
|
||||
value={speedInput}
|
||||
onChangeText={setSpeedInput}
|
||||
// ✅ onBlur TIDAK ADA — supaya dismiss keyboard tidak langsung terapkan
|
||||
// ✅ onSubmitEditing TIDAK ADA — supaya tekan Enter tidak langsung terapkan
|
||||
keyboardType="numeric"
|
||||
maxLength={3}
|
||||
returnKeyType="done"
|
||||
style={[styles.numberInput, { borderColor: activeColor + "66" }]}
|
||||
/>
|
||||
{/* Tombol Terapkan — SATU-SATUNYA cara nilai input diterapkan */}
|
||||
<TouchableOpacity
|
||||
onPress={handleApplyInput}
|
||||
style={[
|
||||
styles.applyButton,
|
||||
{ backgroundColor: activeColor + "15", borderColor: activeColor + "44" },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.applyButtonText, { color: activeColor }]}>Terapkan</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
|
||||
{/* Speed bar indikator */}
|
||||
<View style={styles.speedBar}>
|
||||
<View style={[
|
||||
styles.speedBarFill,
|
||||
{ width: `${(speed / 255) * 100}%` as any, backgroundColor: activeColor },
|
||||
]} />
|
||||
<Text style={styles.speedBarLabel}>
|
||||
{speed < 80
|
||||
? "⚠️ Kecepatan rendah — pastikan conveyor bergerak"
|
||||
: speed > 200
|
||||
? "⚠️ Kecepatan tinggi — awasi kondisi telur"
|
||||
: "✓ Kecepatan normal"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Tombol kirim */}
|
||||
<TouchableOpacity
|
||||
onPress={handleSave}
|
||||
disabled={saving}
|
||||
style={[
|
||||
styles.saveButton,
|
||||
{ backgroundColor: saved ? "#16a34a" : "#0284c7", opacity: saving ? 0.7 : 1 },
|
||||
]}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>
|
||||
{saving ? "Mengirim..." : saved ? "✓ Kecepatan Tersimpan!" : "Kirim ke Perangkat"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: "#f8fafc" },
|
||||
content: { padding: 16, paddingBottom: 32 },
|
||||
loadingContainer: { flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "#f8fafc", gap: 12 },
|
||||
loadingText: { fontSize: 13, color: "#94a3b8" },
|
||||
|
||||
header: { flexDirection: "row", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 20 },
|
||||
title: { fontSize: 24, fontWeight: "800", color: "#0f172a", letterSpacing: -0.5 },
|
||||
subtitle: { fontSize: 12, color: "#94a3b8", marginTop: 2 },
|
||||
refreshBtn: {
|
||||
width: 40, height: 40, borderRadius: 20,
|
||||
backgroundColor: "#fff", borderWidth: 1, borderColor: "#e2e8f0",
|
||||
alignItems: "center", justifyContent: "center",
|
||||
shadowColor: "#000", shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05, shadowRadius: 3, elevation: 2,
|
||||
},
|
||||
refreshIcon: { fontSize: 22, color: "#0284c7", fontWeight: "700", lineHeight: 26 },
|
||||
|
||||
// Cylinder card
|
||||
cylinderCard: {
|
||||
backgroundColor: "#fff", borderWidth: 1, borderColor: "#e2e8f0",
|
||||
borderRadius: 16, paddingVertical: 28, paddingHorizontal: 16,
|
||||
marginBottom: 16, alignItems: "center",
|
||||
shadowColor: "#000", shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05, shadowRadius: 4, elevation: 2,
|
||||
},
|
||||
cylinderWrapper: { alignItems: "center", gap: 16 },
|
||||
cylinderInfo: { alignItems: "center" },
|
||||
cylinderPct: { fontSize: 32, fontWeight: "800", color: "#0f172a", fontFamily: "monospace" },
|
||||
cylinderHint: { fontSize: 11, color: "#94a3b8", marginTop: 2 },
|
||||
|
||||
// Control card
|
||||
controlCard: {
|
||||
backgroundColor: "#fff", borderWidth: 1, borderRadius: 16,
|
||||
padding: 16, marginBottom: 12,
|
||||
shadowColor: "#000", shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.04, shadowRadius: 3, elevation: 1,
|
||||
},
|
||||
controlHeaderRow: {
|
||||
flexDirection: "row", justifyContent: "space-between",
|
||||
alignItems: "flex-start", marginBottom: 16,
|
||||
},
|
||||
controlLabel: { fontSize: 12, fontWeight: "700", marginBottom: 4 },
|
||||
valueRow: { flexDirection: "row", alignItems: "baseline" },
|
||||
controlValueText: { fontSize: 32, fontWeight: "800", fontFamily: "monospace" },
|
||||
controlValueUnit: { fontSize: 13, color: "#94a3b8" },
|
||||
modeBadge: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 20, borderWidth: 1 },
|
||||
modeBadgeText: { fontSize: 11, fontWeight: "600" },
|
||||
sectionLabel: { fontSize: 10, color: "#94a3b8", fontWeight: "600", marginBottom: 8, marginTop: 4 },
|
||||
|
||||
// Mode buttons
|
||||
modeRow: { flexDirection: "row", gap: 8, marginBottom: 16 },
|
||||
modeButton: { flex: 1, paddingVertical: 10, paddingHorizontal: 6, borderRadius: 12, borderWidth: 1, alignItems: "center", gap: 3, position: "relative" },
|
||||
modeButtonLabel:{ fontSize: 11, fontWeight: "700" },
|
||||
modeButtonDesc: { fontSize: 9, color: "#94a3b8", textAlign: "center" },
|
||||
activeIndicator:{ position: "absolute", top: 6, right: 6, width: 6, height: 6, borderRadius: 3 },
|
||||
|
||||
// Slider
|
||||
sliderRow: { flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 8 },
|
||||
sliderMin: { fontSize: 10, color: "#94a3b8", width: 16, textAlign: "center" },
|
||||
sliderMax: { fontSize: 10, color: "#94a3b8", width: 24, textAlign: "center" },
|
||||
slider: { flex: 1, height: 40 },
|
||||
|
||||
// Input angka
|
||||
inputRow: { flexDirection: "row", gap: 10, alignItems: "center" },
|
||||
numberInput: {
|
||||
width: 80, backgroundColor: "#f8fafc", borderWidth: 1.5,
|
||||
borderRadius: 10, paddingHorizontal: 10, paddingVertical: 9,
|
||||
color: "#0f172a", fontSize: 18, fontFamily: "monospace",
|
||||
textAlign: "center", fontWeight: "700",
|
||||
},
|
||||
applyButton: { flex: 1, paddingVertical: 12, borderRadius: 10, borderWidth: 1, alignItems: "center" },
|
||||
applyButtonText: { fontSize: 13, fontWeight: "700" },
|
||||
inputHint: { fontSize: 10, color: "#cbd5e1", marginTop: 6, fontStyle: "italic" },
|
||||
|
||||
// Speed bar
|
||||
speedBar: {
|
||||
backgroundColor: "#f1f5f9", borderRadius: 12, overflow: "hidden",
|
||||
marginBottom: 16, height: 40, justifyContent: "center",
|
||||
borderWidth: 1, borderColor: "#e2e8f0",
|
||||
},
|
||||
speedBarFill: { position: "absolute", top: 0, left: 0, bottom: 0, opacity: 0.15, borderRadius: 12 },
|
||||
speedBarLabel: { fontSize: 11, color: "#64748b", paddingHorizontal: 12 },
|
||||
|
||||
// Save button
|
||||
saveButton: { borderRadius: 14, paddingVertical: 16, alignItems: "center" },
|
||||
saveButtonText: { color: "#fff", fontSize: 15, fontWeight: "800", letterSpacing: 0.3 },
|
||||
});
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
View, Text, ScrollView, StyleSheet,
|
||||
ActivityIndicator, TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { getEggData, EggRecord, EggGrade } from "../services/api";
|
||||
import Sparkline from "../components/Sparkline";
|
||||
import BarChart from "../components/BarChart";
|
||||
import CalendarPicker, { toISO, fromISO, toDisplay } from "../components/CalendarPicker";
|
||||
import { HistoryPoint } from "../services/api";
|
||||
|
||||
const GRADE_CONFIG = {
|
||||
A: { color:"#16a34a", bg:"#f0fdf4", borderColor:"#bbf7d0", label:"Grade A", range:"> 60g", key:"gradeA" as const },
|
||||
B: { color:"#d97706", bg:"#fffbeb", borderColor:"#fde68a", label:"Grade B", range:"50 – 60g", key:"gradeB" as const },
|
||||
C: { color:"#dc2626", bg:"#fef2f2", borderColor:"#fecaca", label:"Grade C", range:"< 50g", key:"gradeC" as const },
|
||||
TL: { color:"#6b7280", bg:"#f1f5f9", borderColor:"#cbd5e1", label:"Tidak Layak", range:"Reject", key:"gradeTL" as const },
|
||||
};
|
||||
|
||||
/** dd/mm/yyyy*/
|
||||
const fromDisplay = (s: string): Date | null => {
|
||||
const p = s.split("/");
|
||||
if (p.length !== 3) return null;
|
||||
const d = new Date(+p[2], +p[1]-1, +p[0]);
|
||||
d.setHours(0,0,0,0);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
};
|
||||
|
||||
/** Hitung total & history yg sdh di filter */
|
||||
function buildSummary(data: EggRecord[]) {
|
||||
const acc: Record<EggGrade, { count: number; totalW: number }> = {
|
||||
A: { count:0, totalW:0 },
|
||||
B: { count:0, totalW:0 },
|
||||
C: { count:0, totalW:0 },
|
||||
TL: { count:0, totalW:0 },
|
||||
};
|
||||
data.forEach(d => {
|
||||
const g = d.grade in acc ? d.grade : "TL";
|
||||
acc[g].count++;
|
||||
acc[g].totalW += d.weight;
|
||||
});
|
||||
|
||||
const avg = (g: EggGrade) =>
|
||||
acc[g].count > 0 ? parseFloat((acc[g].totalW / acc[g].count).toFixed(1)) : 0;
|
||||
const slotMap: Record<string, Record<EggGrade, number>> = {};
|
||||
|
||||
data.forEach(d => {
|
||||
const [hStr, mStr] = d.timestamp.split(":");
|
||||
const h = parseInt(hStr) || 0;
|
||||
const m = parseInt(mStr) || 0;
|
||||
const slot30 = `${String(h).padStart(2,"0")}:${m < 30 ? "00" : "30"}`;
|
||||
const key = `${d.date} ${slot30}`;
|
||||
if (!slotMap[key]) slotMap[key] = { A:0, B:0, C:0, TL:0 };
|
||||
const g = d.grade in slotMap[key] ? d.grade : "TL";
|
||||
slotMap[key][g]++;
|
||||
});
|
||||
|
||||
const history: HistoryPoint[] = Object.entries(slotMap)
|
||||
.sort(([a],[b]) => a.localeCompare(b))
|
||||
.slice(-5)
|
||||
.map(([key, v]) => ({
|
||||
time: key.split(" ")[1],
|
||||
A: v.A, B: v.B, C: v.C, TL: v.TL,
|
||||
}));
|
||||
|
||||
return {
|
||||
gradeA: { count: acc.A.count, avgWeight: avg("A") },
|
||||
gradeB: { count: acc.B.count, avgWeight: avg("B") },
|
||||
gradeC: { count: acc.C.count, avgWeight: avg("C") },
|
||||
gradeTL: { count: acc.TL.count, avgWeight: avg("TL") },
|
||||
history,
|
||||
};
|
||||
}
|
||||
|
||||
// Data dummy
|
||||
const _today = new Date();
|
||||
const MOCK_DATA: EggRecord[] = Array.from({ length: 50 }, (_, i) => {
|
||||
const grades: EggGrade[] = ["A","B","C","TL"];
|
||||
const grade = grades[i % 4];
|
||||
|
||||
const weightMap = {
|
||||
A: 64,
|
||||
B: 55,
|
||||
C: 47,
|
||||
TL: 30,
|
||||
};
|
||||
|
||||
const d = new Date(_today);
|
||||
d.setDate(_today.getDate() - Math.floor(i / 10));
|
||||
d.setHours(8 + (i % 10), 0, 0, 0);
|
||||
|
||||
return {
|
||||
id: i + 1,
|
||||
grade,
|
||||
weight: weightMap[grade],
|
||||
timestamp: `${String(d.getHours()).padStart(2,"0")}:00`,
|
||||
date: `${String(d.getDate()).padStart(2,"0")}/${String(d.getMonth()+1).padStart(2,"0")}/${d.getFullYear()}`,
|
||||
};
|
||||
});
|
||||
|
||||
// ─Komponen utama
|
||||
|
||||
export default function DashboardScreen() {
|
||||
const todayISO = toISO(new Date());
|
||||
|
||||
const [allData, setAllData] = useState<EggRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// const [usingMock, setUsingMock] = useState(false);
|
||||
const [dateFrom, setDateFrom] = useState(todayISO);
|
||||
const [dateTo, setDateTo] = useState(todayISO);
|
||||
const [showCalFrom, setShowCalFrom] = useState(false);
|
||||
const [showCalTo, setShowCalTo] = useState(false);
|
||||
|
||||
// ── Fetch semua data sekali, filter di frontend ───────────────────────────
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
try {
|
||||
const result = await getEggData();
|
||||
setAllData(result);
|
||||
// setUsingMock(false);
|
||||
setError(null);
|
||||
} catch {
|
||||
// setAllData(MOCK_DATA);
|
||||
// setUsingMock(true);
|
||||
setAllData([]);
|
||||
setError("Server belum terhubung");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetch();
|
||||
const iv = setInterval(fetch, 5000);
|
||||
return () => clearInterval(iv);
|
||||
}, []);
|
||||
|
||||
// ── Filter data berdasarkan rentang tanggal ───────────────────────────────
|
||||
const filtered = allData.filter(d => {
|
||||
const itemDate = fromDisplay(d.date);
|
||||
const from = fromISO(dateFrom);
|
||||
const to = fromISO(dateTo);
|
||||
to.setHours(23,59,59,999);
|
||||
return itemDate ? itemDate >= from && itemDate <= to : true;
|
||||
});
|
||||
|
||||
// ── Hitung summary dari data yang sudah difilter ──────────────────────────
|
||||
const summary = buildSummary(filtered);
|
||||
|
||||
const totalEggs =
|
||||
summary.gradeA.count + summary.gradeB.count +
|
||||
summary.gradeC.count + summary.gradeTL.count;
|
||||
|
||||
const rangeLabel = dateFrom === dateTo
|
||||
? `Hari ini, ${toDisplay(dateFrom)}`
|
||||
: `${toDisplay(dateFrom)} – ${toDisplay(dateTo)}`;
|
||||
|
||||
// Helper ambil data per key
|
||||
const getSummaryByKey = (key: "gradeA"|"gradeB"|"gradeC"|"gradeTL") => summary[key];
|
||||
const getSparkByGrade = (grade: EggGrade) => summary.history.map(h => h[grade]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={s.container}
|
||||
contentContainerStyle={s.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={s.header}>
|
||||
<View>
|
||||
<Text style={s.title}>Dashboard</Text>
|
||||
<Text style={s.subtitle}>Live · Refresh 5s</Text>
|
||||
</View>
|
||||
|
||||
<View style={[
|
||||
s.statusBadge,
|
||||
{
|
||||
backgroundColor: error ? "#fef2f2" : "#f0fdf4",
|
||||
borderColor: error ? "#fecaca" : "#bbf7d0"
|
||||
}
|
||||
]}>
|
||||
<View style={[
|
||||
s.statusDot,
|
||||
{ backgroundColor: error ? "#dc2626" : "#16a34a" }
|
||||
]} />
|
||||
|
||||
<Text style={[
|
||||
s.statusText,
|
||||
{ color: error ? "#dc2626" : "#16a34a" }
|
||||
]}>
|
||||
{error ? "OFFLINE" : "ONLINE"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Filter Tanggal */}
|
||||
<View style={s.dateCard}>
|
||||
<View style={s.dateRangeRow}>
|
||||
{/* Dari */}
|
||||
<View style={s.dateField}>
|
||||
<Text style={s.dateFieldLabel}>Dari Tanggal</Text>
|
||||
<TouchableOpacity onPress={() => setShowCalFrom(true)} style={s.dateFieldBtn}>
|
||||
<Text style={s.dateFieldText}>{toDisplay(dateFrom)}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={s.dateDash}>—</Text>
|
||||
|
||||
{/* Sampai */}
|
||||
<View style={s.dateField}>
|
||||
<Text style={s.dateFieldLabel}>Sampai Tanggal</Text>
|
||||
<TouchableOpacity onPress={() => setShowCalTo(true)} style={s.dateFieldBtn}>
|
||||
<Text style={s.dateFieldText}>{toDisplay(dateTo)}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Label aktif + Reset */}
|
||||
<View style={s.activeRow}>
|
||||
{(dateFrom!==todayISO || dateTo!==todayISO) && (
|
||||
<TouchableOpacity onPress={() => { setDateFrom(todayISO); setDateTo(todayISO); }}>
|
||||
<Text style={s.resetText}>Reset ke Hari Ini</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Total Card */}
|
||||
<View style={s.totalCard}>
|
||||
<View>
|
||||
<Text style={s.totalLabel}>Total Telur</Text>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#4f46e5" style={{marginTop:8}} />
|
||||
) : (
|
||||
<Text style={s.totalValue}>{String(totalEggs).padStart(3,"0")}</Text>
|
||||
)}
|
||||
<Text style={s.totalSub}>{rangeLabel}</Text>
|
||||
</View>
|
||||
<View style={s.gradeBadgeRow}>
|
||||
<Text style={s.totalSubLabel}>Grade</Text>
|
||||
<View style={s.gradeBadgesContainer}>
|
||||
{(["A","B","C","TL"] as const).map((g) => (
|
||||
<View key={g} style={[s.gradeBadge,{
|
||||
backgroundColor: GRADE_CONFIG[g].bg,
|
||||
borderColor: GRADE_CONFIG[g].borderColor,
|
||||
}]}>
|
||||
<Text style={[s.gradeBadgeText,{color:GRADE_CONFIG[g].color}]}>{g}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Grade Cards — A, B, C, TL */}
|
||||
{(["A","B","C","TL"] as const).map((grade) => {
|
||||
const cfg = GRADE_CONFIG[grade];
|
||||
const d = getSummaryByKey(cfg.key);
|
||||
const sparkData = getSparkByGrade(grade);
|
||||
|
||||
return (
|
||||
<View key={grade} style={[s.gradeCard,{backgroundColor:cfg.bg,borderColor:cfg.borderColor}]}>
|
||||
<View style={s.gradeCardInner}>
|
||||
<View style={{flex:1}}>
|
||||
<View style={s.gradeTagRow}>
|
||||
<View style={[s.gradeTag,{backgroundColor:cfg.color}]}>
|
||||
<Text style={s.gradeTagText}>{cfg.label}</Text>
|
||||
</View>
|
||||
<Text style={s.gradeRange}>{cfg.range}</Text>
|
||||
</View>
|
||||
<View style={s.statsRow}>
|
||||
<View>
|
||||
<Text style={s.statLabel}>Jumlah</Text>
|
||||
<Text style={[s.statValue,{color:cfg.color}]}>
|
||||
{loading ? "—" : d.count}
|
||||
</Text>
|
||||
<Text style={s.statUnit}>butir</Text>
|
||||
</View>
|
||||
<View style={{marginLeft:24}}>
|
||||
<Text style={s.statLabel}>Rata-rata</Text>
|
||||
<Text style={[s.statValue,{color:"#1e293b"}]}>
|
||||
{loading ? "—" : d.avgWeight > 0 ? d.avgWeight.toFixed(1) : "0"}
|
||||
</Text>
|
||||
<Text style={s.statUnit}>gram</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={s.sparklineContainer}>
|
||||
{sparkData.length >= 2
|
||||
? <Sparkline data={sparkData} color={cfg.color} width={80} height={40} />
|
||||
: <View style={s.noSparkline}><Text style={s.noSparklineText}>—</Text></View>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Bar Chart */}
|
||||
<View style={s.chartCard}>
|
||||
<Text style={s.chartTitle}>Distribusi per Waktu · {rangeLabel}</Text>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#94a3b8" style={{marginTop:20}} />
|
||||
) : summary.history.length > 0 ? (
|
||||
<BarChart history={summary.history} />
|
||||
) : (
|
||||
<Text style={s.emptyChart}>Belum ada data pada rentang tanggal ini</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Calendar popup — Dari */}
|
||||
<CalendarPicker
|
||||
visible={showCalFrom}
|
||||
value={dateFrom}
|
||||
maxDate={dateTo}
|
||||
title="Pilih Tanggal Awal"
|
||||
onConfirm={(v) => { setDateFrom(v); if (v > dateTo) setDateTo(v); }}
|
||||
onClose={() => setShowCalFrom(false)}
|
||||
/>
|
||||
|
||||
{/* Calendar popup — Sampai */}
|
||||
<CalendarPicker
|
||||
visible={showCalTo}
|
||||
value={dateTo}
|
||||
minDate={dateFrom}
|
||||
maxDate={todayISO}
|
||||
title="Pilih Tanggal Akhir"
|
||||
onConfirm={(v) => setDateTo(v)}
|
||||
onClose={() => setShowCalTo(false)}
|
||||
/>
|
||||
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||
const s = StyleSheet.create({
|
||||
container: { flex:1, backgroundColor:"#f8fafc" },
|
||||
content: { padding:16, paddingBottom:24 },
|
||||
|
||||
header: { flexDirection:"row", justifyContent:"space-between", alignItems:"center", marginBottom:14 },
|
||||
title: { fontSize:24, fontWeight:"800", color:"#0f172a", letterSpacing:-0.5 },
|
||||
subtitle: { fontSize:12, color:"#94a3b8", marginTop:2 },
|
||||
statusBadge:{ flexDirection:"row", alignItems:"center", gap:6, borderWidth:1, borderRadius:20, paddingHorizontal:12, paddingVertical:6 },
|
||||
statusDot: { width:7, height:7, borderRadius:4 },
|
||||
statusText: { fontSize:10, fontWeight:"700" },
|
||||
|
||||
mockBanner: { backgroundColor:"#fffbeb", borderWidth:1, borderColor:"#fde68a", borderRadius:10, padding:9, marginBottom:12 },
|
||||
mockText: { fontSize:11, color:"#92400e" },
|
||||
|
||||
// Date card
|
||||
dateCard: { backgroundColor:"#fff", borderWidth:1, borderColor:"#e2e8f0", borderRadius:14, padding:12, marginBottom:14 },
|
||||
dateRangeRow: { flexDirection:"row", alignItems:"center", gap:8 },
|
||||
dateField: { flex:1 },
|
||||
dateFieldLabel: { fontSize:10, color:"#94a3b8", fontWeight:"600", marginBottom:4 },
|
||||
dateFieldBtn: { flexDirection:"row", alignItems:"center", gap:6, backgroundColor:"#f8fafc", borderWidth:1, borderColor:"#e2e8f0", borderRadius:10, paddingHorizontal:10, paddingVertical:9 },
|
||||
dateFieldIcon: { fontSize:14 },
|
||||
dateFieldText: { fontSize:13, color:"#0f172a", fontWeight:"600" },
|
||||
dateDash: { fontSize:18, color:"#cbd5e1", marginTop:16 },
|
||||
activeRow: { flexDirection:"row", justifyContent:"space-between", alignItems:"center", marginTop:10 },
|
||||
activeText: { fontSize:11, color:"#64748b" },
|
||||
resetText: { fontSize:11, color:"#4f46e5", fontWeight:"700" },
|
||||
|
||||
// Total Card
|
||||
totalCard: { backgroundColor:"#eef2ff", borderWidth:1, borderColor:"#c7d2fe", borderRadius:16, padding:16, marginBottom:14, flexDirection:"row", justifyContent:"space-between", alignItems:"center" },
|
||||
totalLabel: { fontSize:11, color:"#4f46e5", marginBottom:2, fontWeight:"600" },
|
||||
totalValue: { fontSize:36, fontWeight:"800", color:"#1e1b4b", fontFamily:"monospace" },
|
||||
totalSub: { fontSize:10, color:"#818cf8", marginTop:2 },
|
||||
gradeBadgeRow: { alignItems:"flex-end" },
|
||||
totalSubLabel: { fontSize:11, color:"#94a3b8", marginBottom:6 },
|
||||
gradeBadgesContainer:{ flexDirection:"row", gap:5 },
|
||||
gradeBadge: { width:28, height:28, borderRadius:7, borderWidth:1, alignItems:"center", justifyContent:"center" },
|
||||
gradeBadgeText: { fontSize:9, fontWeight:"700" },
|
||||
|
||||
// Grade Cards
|
||||
gradeCard: { borderWidth:1, borderRadius:16, padding:14, marginBottom:12 },
|
||||
gradeCardInner: { flexDirection:"row", justifyContent:"space-between", alignItems:"flex-start" },
|
||||
gradeTagRow: { flexDirection:"row", alignItems:"center", gap:8, marginBottom:10 },
|
||||
gradeTag: { paddingHorizontal:8, paddingVertical:3, borderRadius:6 },
|
||||
gradeTagText: { fontSize:11, fontWeight:"800", color:"#fff" },
|
||||
gradeRange: { fontSize:10, color:"#94a3b8" },
|
||||
statsRow: { flexDirection:"row" },
|
||||
statLabel: { fontSize:10, color:"#94a3b8", marginBottom:2 },
|
||||
statValue: { fontSize:28, fontWeight:"800", fontFamily:"monospace", lineHeight:32 },
|
||||
statUnit: { fontSize:9, color:"#94a3b8" },
|
||||
sparklineContainer: { alignItems:"flex-end", paddingTop:4 },
|
||||
noSparkline: { width:80, height:40, justifyContent:"center", alignItems:"center" },
|
||||
noSparklineText:{ color:"#cbd5e1", fontSize:18 },
|
||||
|
||||
// Chart
|
||||
chartCard: { backgroundColor:"#fff", borderWidth:1, borderColor:"#e2e8f0", borderRadius:16, padding:16 },
|
||||
chartTitle: { fontSize:12, color:"#64748b", fontWeight:"600", marginBottom:12 },
|
||||
emptyChart: { fontSize:12, color:"#94a3b8", textAlign:"center", paddingVertical:20 },
|
||||
});
|
||||
|
|
@ -0,0 +1,452 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
View, Text, StyleSheet, TouchableOpacity, TextInput,
|
||||
ActivityIndicator, Alert, FlatList, Modal,
|
||||
} from "react-native";
|
||||
import RNPrint from "react-native-print";
|
||||
import CalendarPicker, { toISO, fromISO, toDisplay } from "../components/CalendarPicker";
|
||||
import { getEggData, EggRecord, EggGrade } from "../services/api";
|
||||
|
||||
const GRADE_CONFIG = {
|
||||
A: { color: "#16a34a", bg: "#f0fdf4", borderColor: "#bbf7d0" },
|
||||
B: { color: "#d97706", bg: "#fffbeb", borderColor: "#fde68a" },
|
||||
C: { color: "#dc2626", bg: "#fef2f2", borderColor: "#fecaca" },
|
||||
TL: { color: "#6b7280", bg: "#f1f5f9", borderColor: "#cbd5e1" },
|
||||
};
|
||||
|
||||
const fromDisplay = (s: string): Date | null => {
|
||||
const p = s.split("/");
|
||||
if (p.length !== 3) return null;
|
||||
const d = new Date(+p[2], +p[1]-1, +p[0]);
|
||||
d.setHours(0,0,0,0);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
};
|
||||
|
||||
type GradeFilter = "all" | EggGrade;
|
||||
|
||||
|
||||
export default function laporanScreen() {
|
||||
const todayISO = toISO(new Date());
|
||||
|
||||
const [allData, setAllData] = useState<EggRecord[]>([]);
|
||||
const [gradeFilter, setGradeFilter] = useState<GradeFilter>("all");
|
||||
const [dateFrom, setDateFrom] = useState(todayISO);
|
||||
const [dateTo, setDateTo] = useState(todayISO);
|
||||
const [search, setSearch] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [printing, setPrinting] = useState(false);
|
||||
const [showCalFrom, setShowCalFrom] = useState(false);
|
||||
const [showCalTo, setShowCalTo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
try {
|
||||
const result = await getEggData();
|
||||
setAllData(result);
|
||||
|
||||
} catch (err) {
|
||||
setAllData([]);
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
fetch();
|
||||
const iv = setInterval(fetch, 8000);
|
||||
return () => clearInterval(iv);
|
||||
}, []);
|
||||
|
||||
// ── Filter ────────────────────────────────────────────────────────────────
|
||||
const filtered = allData.filter((d) => {
|
||||
const matchGrade = gradeFilter === "all" || d.grade === gradeFilter;
|
||||
const itemDate = fromDisplay(d.date);
|
||||
const from = fromISO(dateFrom);
|
||||
const to = fromISO(dateTo); to.setHours(23,59,59,999);
|
||||
const matchDate = itemDate ? itemDate >= from && itemDate <= to : true;
|
||||
const matchSearch = !search || (
|
||||
d.grade.toLowerCase().includes(search.toLowerCase()) ||
|
||||
d.weight.toString().includes(search) || d.date.includes(search)
|
||||
);
|
||||
return matchGrade && matchDate && matchSearch;
|
||||
});
|
||||
|
||||
const countByGrade = (g: EggGrade) => filtered.filter(d => d.grade === g).length;
|
||||
|
||||
const rangeLabel = dateFrom === dateTo
|
||||
? `Hari ini, ${toDisplay(dateFrom)}`
|
||||
: `${toDisplay(dateFrom)} – ${toDisplay(dateTo)}`;
|
||||
|
||||
// ── PDF ───────────────────────────────────────────────────────────────────
|
||||
const handlePrintPDF = async () => {
|
||||
setPrinting(true);
|
||||
try {
|
||||
const gradeLabel = gradeFilter === "all" ? "Semua Grade" : `Grade ${gradeFilter}`;
|
||||
const printDate = new Date().toLocaleString("id-ID",{
|
||||
weekday:"long", year:"numeric", month:"long", day:"numeric", hour:"2-digit", minute:"2-digit"
|
||||
});
|
||||
|
||||
const pct = (n: number) => filtered.length > 0 ? ((n/filtered.length)*100).toFixed(1) : "0.0";
|
||||
const cntA = countByGrade("A"), cntB = countByGrade("B"),
|
||||
cntC = countByGrade("C"), cntTL = countByGrade("TL");
|
||||
|
||||
const rows = filtered.length === 0
|
||||
? `<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8;font-style:italic">Tidak ada data sesuai filter</td></tr>`
|
||||
: filtered.map((d,i) => `
|
||||
<tr>
|
||||
<td class="tc muted">${i+1}</td>
|
||||
<td>${d.date}</td>
|
||||
<td class="muted">${d.timestamp}</td>
|
||||
<td class="tc"><span class="badge grade-${d.grade}">${d.grade==="TL"?"Tidak Layak":"Grade "+d.grade}</span></td>
|
||||
<td class="tr mono">${d.weight} g</td>
|
||||
</tr>`).join("");
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="id"><head><meta charset="UTF-8"><title>Laporan Sortir Telur</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:Arial,sans-serif;font-size:12px;color:#1e293b;padding:28px 32px}
|
||||
.ph{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:2px solid #e2e8f0;padding-bottom:14px;margin-bottom:20px}
|
||||
.ph h1{font-size:18px;font-weight:800;color:#0f172a}.ph p{font-size:11px;color:#64748b;margin-top:2px}
|
||||
.meta{text-align:right;font-size:11px;color:#64748b;line-height:1.8}
|
||||
.bf{display:inline-block;background:#eef2ff;color:#4f46e5;border:1px solid #c7d2fe;border-radius:20px;padding:3px 12px;font-size:11px;font-weight:600;margin-top:4px}
|
||||
.sg{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:20px}
|
||||
.sc{border-radius:10px;border:1px solid #e2e8f0;padding:12px 8px;text-align:center;position:relative;overflow:hidden}
|
||||
.sc::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
||||
.sc.tot::before{background:#1e293b}.sc.tot .num{color:#0f172a}
|
||||
.sc.grA{background:#f0fdf4;border-color:#bbf7d0}.sc.grA::before{background:#16a34a}.sc.grA .num{color:#16a34a}
|
||||
.sc.grB{background:#fffbeb;border-color:#fde68a}.sc.grB::before{background:#d97706}.sc.grB .num{color:#d97706}
|
||||
.sc.grC{background:#fef2f2;border-color:#fecaca}.sc.grC::before{background:#dc2626}.sc.grC .num{color:#dc2626}
|
||||
.sc.grT{background:#f8fafc;border-color:#cbd5e1}.sc.grT::before{background:#6b7280}.sc.grT .num{color:#6b7280}
|
||||
.sc .num{font-size:26px;font-weight:800;line-height:1}.sc .lbl{font-size:10px;color:#64748b;margin-top:3px}.sc .pct{font-size:10px;color:#94a3b8}
|
||||
.db{background:#f1f5f9;border-radius:6px;height:12px;overflow:hidden;display:flex;margin-bottom:8px}
|
||||
.ds{height:100%}.ds.A{background:#16a34a}.ds.B{background:#d97706}.ds.C{background:#dc2626}.ds.TL{background:#94a3b8}
|
||||
.dl{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:20px}
|
||||
.di{display:flex;align-items:center;gap:5px;font-size:10px;color:#64748b}
|
||||
.dot{width:8px;height:8px;border-radius:50%}.dot.A{background:#16a34a}.dot.B{background:#d97706}.dot.C{background:#dc2626}.dot.TL{background:#94a3b8}
|
||||
.ts h2{font-size:11px;color:#94a3b8;font-weight:600;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}
|
||||
table{width:100%;border-collapse:collapse}thead tr{background:#0f172a}
|
||||
th{padding:9px 10px;text-align:left;font-size:10px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.4px}
|
||||
td{padding:8px 10px;font-size:11px;border-bottom:1px solid #f1f5f9}tr:nth-child(even) td{background:#f8fafc}
|
||||
.tc{text-align:center}.tr{text-align:right}.muted{color:#94a3b8}.mono{font-family:'Courier New',monospace;font-weight:600}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-weight:700;font-size:10px}
|
||||
.grade-A{background:#dcfce7;color:#15803d;border:1px solid #bbf7d0}
|
||||
.grade-B{background:#fef9c3;color:#b45309;border:1px solid #fde68a}
|
||||
.grade-C{background:#fee2e2;color:#b91c1c;border:1px solid #fecaca}
|
||||
.grade-TL{background:#f1f5f9;color:#4b5563;border:1px solid #cbd5e1}
|
||||
.pf{margin-top:20px;padding-top:12px;border-top:1px solid #e2e8f0;display:flex;justify-content:space-between;font-size:10px;color:#94a3b8}
|
||||
@media print{body{padding:14px 18px}tr{page-break-inside:avoid}}
|
||||
</style></head><body>
|
||||
<div class="ph">
|
||||
<div><h1>🥚 Laporan Sortir Telur</h1><p>Sistem Sortir Telur Otomatis — NAKULA</p></div>
|
||||
<div class="meta">Dicetak: ${printDate}<br><span class="bf">${gradeLabel} · ${rangeLabel}</span></div>
|
||||
</div>
|
||||
<div class="sg">
|
||||
<div class="sc tot"><div class="num">${filtered.length}</div><div class="lbl">Total</div><div class="pct">100%</div></div>
|
||||
<div class="sc grA"><div class="num">${cntA}</div><div class="lbl">Grade A</div><div class="pct">${pct(cntA)}%</div></div>
|
||||
<div class="sc grB"><div class="num">${cntB}</div><div class="lbl">Grade B</div><div class="pct">${pct(cntB)}%</div></div>
|
||||
<div class="sc grC"><div class="num">${cntC}</div><div class="lbl">Grade C</div><div class="pct">${pct(cntC)}%</div></div>
|
||||
<div class="sc grT"><div class="num">${cntTL}</div><div class="lbl">Tdk Layak</div><div class="pct">${pct(cntTL)}%</div></div>
|
||||
</div>
|
||||
${filtered.length>0?`
|
||||
<div class="db">
|
||||
<div class="ds A" style="width:${pct(cntA)}%"></div>
|
||||
<div class="ds B" style="width:${pct(cntB)}%"></div>
|
||||
<div class="ds C" style="width:${pct(cntC)}%"></div>
|
||||
<div class="ds TL" style="width:${pct(cntTL)}%"></div>
|
||||
</div>
|
||||
<div class="dl">
|
||||
<div class="di"><div class="dot A"></div>Grade A — ${cntA} butir (${pct(cntA)}%)</div>
|
||||
<div class="di"><div class="dot B"></div>Grade B — ${cntB} butir (${pct(cntB)}%)</div>
|
||||
<div class="di"><div class="dot C"></div>Grade C — ${cntC} butir (${pct(cntC)}%)</div>
|
||||
<div class="di"><div class="dot TL"></div>Tidak Layak — ${cntTL} butir (${pct(cntTL)}%)</div>
|
||||
</div>`:""}
|
||||
<div class="ts">
|
||||
<h2>Detail Data (${filtered.length} butir)</h2>
|
||||
<table>
|
||||
<thead><tr><th style="width:32px">#</th><th>Tanggal</th><th>Jam</th><th class="tc">Grade</th><th class="tr">Berat</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pf">
|
||||
<span>Sistem Sortir Telur Otomatis © ${new Date().getFullYear()}</span>
|
||||
<span>${gradeLabel} · ${rangeLabel}</span>
|
||||
</div>
|
||||
</body></html>`;
|
||||
|
||||
await RNPrint.print({ html });
|
||||
} catch (e: any) {
|
||||
Alert.alert("Gagal", e?.message ?? "Tidak dapat membuka dialog cetak.");
|
||||
} finally { setPrinting(false); }
|
||||
};
|
||||
|
||||
// ── Render row ────────────────────────────────────────────────────────────
|
||||
const renderItem = ({ item, index }: { item: EggRecord; index: number }) => {
|
||||
const cfg = GRADE_CONFIG[item.grade] ?? GRADE_CONFIG.TL;
|
||||
return (
|
||||
<View style={[s.tableRow, { backgroundColor: index%2===0 ? "#fff" : "#f8fafc" }]}>
|
||||
<Text style={[s.cell, s.cellId]}>{item.id}</Text>
|
||||
<Text style={[s.cell, s.cellDate]}>{item.date}</Text>
|
||||
<Text style={[s.cell, s.cellTime]}>{item.timestamp}</Text>
|
||||
<View style={s.cellGradeCont}>
|
||||
<View style={[s.gradeBadge, { backgroundColor: cfg.bg, borderColor: cfg.borderColor }]}>
|
||||
<Text style={[s.gradeBadgeText, { color: cfg.color }]}>
|
||||
{item.grade==="TL" ? "Tdk Layak" : `Grade ${item.grade}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[s.cell, s.cellWeight]}>{item.weight}g</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
|
||||
{/* Header */}
|
||||
<View style={s.header}>
|
||||
<View>
|
||||
<Text style={s.title}>Laporan</Text>
|
||||
<Text style={s.subtitle}>Data realtime sortir telur</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* <View style={[
|
||||
s.statusBanner,
|
||||
{
|
||||
backgroundColor: error ? "#fef2f2" : "#f0fdf4",
|
||||
borderColor: error ? "#fecaca" : "#bbf7d0"
|
||||
}
|
||||
]}>
|
||||
<Text style={[
|
||||
s.statusText,
|
||||
{ color: error ? "#dc2626" : "#16a34a" }
|
||||
]}>
|
||||
{error ? "🔴 OFFLINE — Server tidak terhubung" : "🟢 ONLINE — Terhubung ke server"}
|
||||
</Text>
|
||||
</View> */}
|
||||
|
||||
{/* Filter Grade */}
|
||||
<View style={s.filterRow}>
|
||||
{(["all","A","B","C","TL"] as const).map((g) => {
|
||||
const active = gradeFilter === g;
|
||||
const color = g==="all" ? "#4f46e5" : GRADE_CONFIG[g as EggGrade].color;
|
||||
const bg = g==="all" ? "#eef2ff" : GRADE_CONFIG[g as EggGrade].bg;
|
||||
const border = g==="all" ? "#c7d2fe" : GRADE_CONFIG[g as EggGrade].borderColor;
|
||||
return (
|
||||
<TouchableOpacity key={g} onPress={() => setGradeFilter(g)}
|
||||
style={[s.filterChip,{ borderColor: active?border:"#e2e8f0", backgroundColor: active?bg:"#fff" }]}>
|
||||
<Text style={[s.filterChipText,{ color: active?color:"#94a3b8", fontWeight: active?"700":"400" }]}>
|
||||
{g==="all"?"Semua":g==="TL"?"TL":`Grade ${g}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Filter Tanggal — popup calendar */}
|
||||
<View style={s.dateRangeCard}>
|
||||
<View style={s.dateRangeRow}>
|
||||
{/* Dari */}
|
||||
<View style={s.dateField}>
|
||||
<Text style={s.dateFieldLabel}>Dari Tanggal</Text>
|
||||
<TouchableOpacity onPress={() => setShowCalFrom(true)} style={s.dateFieldBtn}>
|
||||
{/* <Text style={s.dateFieldIcon}>📅</Text> */}
|
||||
<Text style={s.dateFieldText}>{toDisplay(dateFrom)}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={s.dateDash}>—</Text>
|
||||
|
||||
{/* Sampai */}
|
||||
<View style={s.dateField}>
|
||||
<Text style={s.dateFieldLabel}>Sampai Tanggal</Text>
|
||||
<TouchableOpacity onPress={() => setShowCalTo(true)} style={s.dateFieldBtn}>
|
||||
{/* <Text style={s.dateFieldIcon}>📅</Text> */}
|
||||
<Text style={s.dateFieldText}>{toDisplay(dateTo)}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Label aktif + Reset */}
|
||||
<View style={s.activeRow}>
|
||||
<Text style={s.activeText}>
|
||||
{rangeLabel}{" · "}
|
||||
<Text style={{color:"#4f46e5",fontWeight:"700"}}>{filtered.length} data</Text>
|
||||
</Text>
|
||||
{(dateFrom!==todayISO || dateTo!==todayISO) && (
|
||||
<TouchableOpacity onPress={() => { setDateFrom(todayISO); setDateTo(todayISO); }}>
|
||||
<Text style={s.resetText}>Reset</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Search */}
|
||||
<TextInput
|
||||
value={search} onChangeText={setSearch}
|
||||
placeholder="Cari grade / berat"
|
||||
placeholderTextColor="#cbd5e1"
|
||||
style={s.searchInput}
|
||||
/>
|
||||
|
||||
{/* Stats */}
|
||||
<View style={s.statsRow}>
|
||||
{(["A","B","C","TL"] as const).map((g) => {
|
||||
const cnt = countByGrade(g); const cfg = GRADE_CONFIG[g];
|
||||
return (
|
||||
<View key={g} style={[s.statBox,{
|
||||
backgroundColor: cnt>0?cfg.bg:"#f8fafc",
|
||||
borderColor: cnt>0?cfg.borderColor:"#e2e8f0",
|
||||
}]}>
|
||||
<Text style={[s.statNum,{color:cfg.color}]}>{cnt}</Text>
|
||||
<Text style={s.statLabel}>{g==="TL"?"TL":`Grade ${g}`}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
<View style={[s.statBox,{backgroundColor:"#f1f5f9",borderColor:"#e2e8f0"}]}>
|
||||
<Text style={[s.statNum,{color:"#1e293b"}]}>{filtered.length}</Text>
|
||||
<Text style={s.statLabel}>Total</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Table Header */}
|
||||
<View style={s.tableHeader}>
|
||||
<Text style={[s.thText, s.cellId]}>#</Text>
|
||||
<Text style={[s.thText, s.cellDate]}>Tanggal</Text>
|
||||
<Text style={[s.thText, s.cellTime]}>Waktu</Text>
|
||||
<Text style={[s.thText, s.cellGrade]}>Grade</Text>
|
||||
<Text style={[s.thText, s.cellWeight]}>Berat</Text>
|
||||
</View>
|
||||
|
||||
{/* Table Body */}
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#94a3b8" style={{marginTop:40}} />
|
||||
) : filtered.length===0 ? (
|
||||
<Text style={s.emptyText}>Tidak ada data pada rentang tanggal ini</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filtered}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
renderItem={renderItem}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={s.tableBody}
|
||||
contentContainerStyle={{paddingBottom:90}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* FAB PDF */}
|
||||
<TouchableOpacity
|
||||
onPress={handlePrintPDF} disabled={printing}
|
||||
style={[s.fab, printing && {opacity:0.7}]}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
{printing
|
||||
? <ActivityIndicator color="#fff" size="small" />
|
||||
: <Text style={s.fabText}>PDF</Text>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Calendar — Dari */}
|
||||
<CalendarPicker
|
||||
visible={showCalFrom}
|
||||
value={dateFrom}
|
||||
maxDate={dateTo}
|
||||
title="Pilih Tanggal Awal"
|
||||
onConfirm={(v) => { setDateFrom(v); if (v > dateTo) setDateTo(v); }}
|
||||
onClose={() => setShowCalFrom(false)}
|
||||
/>
|
||||
|
||||
{/* Calendar — Sampai */}
|
||||
<CalendarPicker
|
||||
visible={showCalTo}
|
||||
value={dateTo}
|
||||
minDate={dateFrom}
|
||||
maxDate={todayISO}
|
||||
title="Pilih Tanggal Akhir"
|
||||
onConfirm={(v) => setDateTo(v)}
|
||||
onClose={() => setShowCalTo(false)}
|
||||
/>
|
||||
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Styles ───────────────────────────────────────────────────────────────────
|
||||
const s = StyleSheet.create({
|
||||
container: { flex:1, backgroundColor:"#f8fafc", paddingTop:16, paddingHorizontal:16 },
|
||||
|
||||
header: { flexDirection:"row", justifyContent:"space-between", alignItems:"flex-start", marginBottom:10 },
|
||||
title: { fontSize:24, fontWeight:"800", color:"#0f172a", letterSpacing:-0.5 },
|
||||
subtitle: { fontSize:12, color:"#94a3b8", marginTop:2 },
|
||||
|
||||
// mockBanner: { backgroundColor:"#fffbeb", borderWidth:1, borderColor:"#fde68a", borderRadius:10, padding:9, marginBottom:10 },
|
||||
// mockText: { fontSize:11, color:"#92400e" },
|
||||
statusBanner: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
|
||||
statusText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
|
||||
filterRow: { flexDirection:"row", gap:6, marginBottom:10 },
|
||||
filterChip: { flex:1, paddingVertical:7, borderRadius:20, borderWidth:1, alignItems:"center" },
|
||||
filterChipText:{ fontSize:11 },
|
||||
|
||||
// Date range card
|
||||
dateRangeCard: {
|
||||
backgroundColor:"#fff", borderWidth:1, borderColor:"#e2e8f0",
|
||||
borderRadius:14, padding:12, marginBottom:10,
|
||||
},
|
||||
dateRangeRow: { flexDirection:"row", alignItems:"center", gap:8 },
|
||||
dateField: { flex:1 },
|
||||
dateFieldLabel:{ fontSize:10, color:"#94a3b8", fontWeight:"600", marginBottom:4 },
|
||||
dateFieldBtn: {
|
||||
flexDirection:"row", alignItems:"center", gap:6,
|
||||
backgroundColor:"#f8fafc", borderWidth:1, borderColor:"#e2e8f0",
|
||||
borderRadius:10, paddingHorizontal:10, paddingVertical:9,
|
||||
},
|
||||
dateFieldIcon: { fontSize:14 },
|
||||
dateFieldText: { fontSize:13, color:"#0f172a", fontWeight:"600" },
|
||||
dateDash: { fontSize:18, color:"#cbd5e1", marginTop:16 },
|
||||
|
||||
activeRow: { flexDirection:"row", justifyContent:"space-between", alignItems:"center", marginTop:10 },
|
||||
activeText: { fontSize:11, color:"#64748b" },
|
||||
resetText: { fontSize:11, color:"#4f46e5", fontWeight:"700" },
|
||||
|
||||
searchInput: {
|
||||
backgroundColor:"#fff", borderWidth:1, borderColor:"#e2e8f0",
|
||||
borderRadius:10, paddingHorizontal:12, paddingVertical:9,
|
||||
color:"#0f172a", fontSize:13, marginBottom:10,
|
||||
},
|
||||
|
||||
statsRow: { flexDirection:"row", gap:6, marginBottom:10 },
|
||||
statBox: { flex:1, borderWidth:1, borderRadius:10, padding:6, alignItems:"center" },
|
||||
statNum: { fontSize:16, fontWeight:"800", fontFamily:"monospace" },
|
||||
statLabel:{ fontSize:8, color:"#94a3b8", marginTop:2 },
|
||||
|
||||
tableHeader: { flexDirection:"row", paddingVertical:10, paddingHorizontal:4, borderBottomWidth:1, borderBottomColor:"#e2e8f0" },
|
||||
thText: { fontSize:10, color:"#94a3b8", fontWeight:"600" },
|
||||
tableBody: { flex:1 },
|
||||
tableRow: { flexDirection:"row", alignItems:"center", paddingVertical:9, paddingHorizontal:4, borderBottomWidth:1, borderBottomColor:"#f1f5f9" },
|
||||
cell: { fontSize:11, color:"#334155" },
|
||||
cellId: { width:28, color:"#94a3b8", fontFamily:"monospace", fontSize:10 },
|
||||
cellDate: { width:68, fontSize:10, color:"#64748b" },
|
||||
cellTime: { width:44, fontSize:10, color:"#64748b" },
|
||||
cellGrade: { flex:1 },
|
||||
cellGradeCont:{ flex:1 },
|
||||
gradeBadge: { alignSelf:"flex-start", paddingHorizontal:6, paddingVertical:2, borderRadius:6, borderWidth:1 },
|
||||
gradeBadgeText:{ fontSize:9, fontWeight:"700" },
|
||||
cellWeight: { width:46, fontFamily:"monospace", fontWeight:"600", textAlign:"right", color:"#1e293b", fontSize:11 },
|
||||
emptyText: { textAlign:"center", color:"#94a3b8", marginTop:40, fontSize:13 },
|
||||
|
||||
fab: {
|
||||
position:"absolute", bottom:24, right:20,
|
||||
backgroundColor:"#4f46e5", borderRadius:18,
|
||||
paddingHorizontal:22, paddingVertical:14,
|
||||
shadowColor:"#4f46e5", shadowOffset:{width:0,height:6},
|
||||
shadowOpacity:0.4, shadowRadius:12, elevation:10,
|
||||
},
|
||||
fabText: { color:"#fff", fontSize:14, fontWeight:"800", letterSpacing:0.3 },
|
||||
});
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import axios from "axios";
|
||||
|
||||
|
||||
const BASE_URL = "http://10.10.1.112:5000";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type EggGrade = "A" | "B" | "C" | "TL";
|
||||
export type Period = "today" | "week" | "month" | "all";
|
||||
|
||||
export interface GradeSummary {
|
||||
count: number;
|
||||
avgWeight: number;
|
||||
}
|
||||
|
||||
export interface HistoryPoint {
|
||||
time: string;
|
||||
A: number;
|
||||
B: number;
|
||||
C: number;
|
||||
TL: number;
|
||||
}
|
||||
|
||||
export interface SummaryResponse {
|
||||
gradeA: GradeSummary;
|
||||
gradeB: GradeSummary;
|
||||
gradeC: GradeSummary;
|
||||
gradeTL: GradeSummary;
|
||||
history: HistoryPoint[];
|
||||
}
|
||||
|
||||
export interface EggRecord {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
date: string;
|
||||
grade: EggGrade;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface LatestDataResponse {
|
||||
berat: number;
|
||||
grade: EggGrade;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// ─── Axios instance dengan timeout ───────────────────────────────────────────
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 8000,
|
||||
});
|
||||
|
||||
// ─── API Calls ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
//Ambil semua data telur dari MySQL
|
||||
|
||||
export const getEggData = async (): Promise<EggRecord[]> => {
|
||||
const res = await api.get("/telur");
|
||||
return res.data;
|
||||
};
|
||||
|
||||
|
||||
//Ambil data telur terbaru.
|
||||
|
||||
export const getLatestData = async (): Promise<LatestDataResponse> => {
|
||||
const res = await api.get("/latest");
|
||||
return res.data;
|
||||
};
|
||||
|
||||
// Ringkasan per grade + history chart untuk dashboard.
|
||||
|
||||
export const getSummary = async (period: Period = "today"): Promise<SummaryResponse> => {
|
||||
const res = await api.get("/summary", { params: { period } });
|
||||
return res.data;
|
||||
};
|
||||
|
||||
//Set kecepatan conveyor (0–255).
|
||||
export const setConveyorSpeed = async (speed: number): Promise<void> => {
|
||||
await api.post("/setSpeed", { speed });
|
||||
};
|
||||
|
||||
//Ambil kecepatan conveyor saat ini.
|
||||
|
||||
export const getConveyorSpeed = async (): Promise<number> => {
|
||||
const res = await api.get("/getSpeed");
|
||||
return res.data.speed;
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@react-native/typescript-config",
|
||||
"compilerOptions": {
|
||||
"types": ["jest"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["**/node_modules", "**/Pods"]
|
||||
}
|
||||
|
|
@ -0,0 +1,654 @@
|
|||
from flask import Flask, request, jsonify
|
||||
import numpy as np
|
||||
import cv2
|
||||
import os
|
||||
from ultralytics import YOLO
|
||||
import tflite_runtime.interpreter as tflite
|
||||
import mysql.connector
|
||||
from mysql.connector import Error
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from flask_cors import CORS
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# ===== LOAD MODEL =====
|
||||
yolo = YOLO("best.pt")
|
||||
interpreter = tflite.Interpreter(model_path="model_telur_cnn.tflite")
|
||||
interpreter.allocate_tensors()
|
||||
|
||||
input_details = interpreter.get_input_details()
|
||||
output_details = interpreter.get_output_details()
|
||||
|
||||
labels = ['A', 'B', 'C', 'TL']
|
||||
|
||||
# ===== KONFIGURASI DB =====
|
||||
DB_CONFIG = {
|
||||
"host": "10.10.1.112",
|
||||
"user": "admin",
|
||||
"password": "Telur@12344321",
|
||||
"database": "klasifikasi_telur",
|
||||
"init_command": "SET time_zone = '+07:00'"
|
||||
}
|
||||
|
||||
def get_db():
|
||||
return mysql.connector.connect(**DB_CONFIG)
|
||||
|
||||
# ===== HELPER =====
|
||||
def map_grade(g: str) -> str:
|
||||
if not g:
|
||||
return "TL"
|
||||
g = g.strip().upper()
|
||||
if g == "A": return "A"
|
||||
if g == "B": return "B"
|
||||
if g == "C": return "C"
|
||||
return "TL"
|
||||
|
||||
def period_to_sql(period: str):
|
||||
if period == "today":
|
||||
return "DATE(created_at) = CURDATE()", "24 HOUR"
|
||||
elif period == "week":
|
||||
return "created_at >= NOW() - INTERVAL 7 DAY", "7 DAY"
|
||||
elif period == "month":
|
||||
return "created_at >= NOW() - INTERVAL 30 DAY", "30 DAY"
|
||||
else:
|
||||
return "1=1", "3650 DAY"
|
||||
|
||||
# ===== CNN =====
|
||||
def predict_cnn(img):
|
||||
img_r = cv2.resize(img, (224, 224))
|
||||
img_r = img_r.astype(np.float32) / 255.0
|
||||
img_r = np.expand_dims(img_r, axis=0)
|
||||
|
||||
interpreter.set_tensor(input_details[0]['index'], img_r)
|
||||
interpreter.invoke()
|
||||
|
||||
output = interpreter.get_tensor(output_details[0]['index'])[0]
|
||||
idx = int(np.argmax(output))
|
||||
conf = float(np.max(output))
|
||||
return labels[idx], conf
|
||||
|
||||
# ===== YOLO =====
|
||||
def predict_yolo(img):
|
||||
results = yolo(img)
|
||||
for r in results:
|
||||
if r.boxes is not None and len(r.boxes) > 0:
|
||||
cls = int(r.boxes.cls[0])
|
||||
conf = float(r.boxes.conf[0])
|
||||
return labels[cls], conf
|
||||
return "TL", 0.0
|
||||
|
||||
def fusion(yolo_pred, yolo_conf, cnn_pred, cnn_conf):
|
||||
# kalau dua-duanya sama, langsung pakai itu
|
||||
if yolo_pred == cnn_pred:
|
||||
return yolo_pred
|
||||
|
||||
# pilih berdasarkan confidence tertinggi
|
||||
if cnn_conf > yolo_conf:
|
||||
return cnn_pred
|
||||
else:
|
||||
return yolo_pred
|
||||
|
||||
# ===== FINAL DECISION (AI + BERAT) =====
|
||||
# FIX: hanya satu definisi, logika berat yang dipakai
|
||||
def final_decision(ai_grade, weight):
|
||||
# tentukan grade dari berat
|
||||
if weight < 40:
|
||||
weight_grade = "TL"
|
||||
elif weight <= 49:
|
||||
weight_grade = "C"
|
||||
elif weight <= 59:
|
||||
weight_grade = "B"
|
||||
else:
|
||||
weight_grade = "A"
|
||||
|
||||
# ranking (semakin kecil = semakin jelek)
|
||||
priority = {"TL": 0, "C": 1, "B": 2, "A": 3}
|
||||
|
||||
# ambil yang PALING RENDAH
|
||||
return ai_grade if priority[ai_grade] < priority[weight_grade] else weight_grade
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# POST /upload — dipanggil ESP32-CAM setelah ambil foto
|
||||
# Alur: simpan gambar → predict AI → insert telur (berat=0) → insert gambar,
|
||||
# cnn_result, yolo_result → kembalikan telur_id ke CAM
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload():
|
||||
file = request.files.get('image')
|
||||
|
||||
filename = datetime.now().strftime("%Y%m%d%H%M%S") + ".jpg"
|
||||
os.makedirs("uploads", exist_ok=True)
|
||||
path = os.path.join("uploads", filename)
|
||||
|
||||
if file:
|
||||
file.save(path)
|
||||
img = cv2.imread(path)
|
||||
else:
|
||||
data = request.get_data()
|
||||
npimg = np.frombuffer(data, np.uint8)
|
||||
img = cv2.imdecode(npimg, cv2.IMREAD_COLOR)
|
||||
if img is None:
|
||||
return jsonify({"error": "Image tidak valid"}), 400
|
||||
cv2.imwrite(path, img)
|
||||
|
||||
yolo_pred, yolo_conf = predict_yolo(img)
|
||||
cnn_pred, cnn_conf = predict_cnn(img)
|
||||
ai_grade = fusion(yolo_pred, yolo_conf, cnn_pred, cnn_conf)
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# Insert telur dulu dengan berat=0, grade sementara dari AI
|
||||
# Berat & grade final akan diupdate oleh DevKit via /updateFinal
|
||||
cursor.execute(
|
||||
"INSERT INTO telur (grade, berat) VALUES (%s, %s)",
|
||||
(ai_grade, 0)
|
||||
)
|
||||
telur_id = cursor.lastrowid
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO gambar (telur_id, path) VALUES (%s, %s)",
|
||||
(telur_id, path)
|
||||
)
|
||||
gambar_id = cursor.lastrowid
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO yolo_result (gambar_id, grade, confidence) VALUES (%s, %s, %s)",
|
||||
(gambar_id, yolo_pred, yolo_conf)
|
||||
)
|
||||
cursor.execute(
|
||||
"INSERT INTO cnn_result (gambar_id, grade, confidence) VALUES (%s, %s, %s)",
|
||||
(gambar_id, cnn_pred, cnn_conf)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# FIX: kembalikan "telur_id" agar konsisten dengan semua endpoint lain
|
||||
return jsonify({"telur_id":telur_id,"grade":ai_grade,"yolo":yolo_pred,"yolo_conf": round(yolo_conf, 3),"cnn":cnn_pred,"cnn_conf":round(cnn_conf, 3),
|
||||
})
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GET /latest — dipanggil DevKit untuk polling hasil foto terbaru
|
||||
# FIX: kembalikan key "telur_id" (bukan "id") agar DevKit bisa parse
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@app.route('/latest', methods=['GET'])
|
||||
def latest():
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT id, grade, berat, created_at
|
||||
FROM telur
|
||||
WHERE berat = 0
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
row = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if not row:
|
||||
return jsonify({"telur_id": 0, "berat": 0, "grade": "TL", "timestamp": "-"})
|
||||
|
||||
return jsonify({
|
||||
"telur_id": row["id"], # FIX: pakai key "telur_id"
|
||||
"berat": float(row["berat"]) if row["berat"] else 0,
|
||||
"grade": map_grade(row["grade"] or "TL"),
|
||||
"timestamp": row["created_at"].strftime("%H:%M") if row["created_at"] else "-",
|
||||
})
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# POST /updateFinal — dipanggil DevKit dengan berat aktual
|
||||
# Server hitung grade final (AI + berat) lalu update tabel telur
|
||||
# FIX: indentasi try/except diperbaiki, masuk ke dalam fungsi
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@app.route('/updateFinal', methods=['POST'])
|
||||
def update_final():
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "No data"}), 400
|
||||
|
||||
telur_id = data.get("id")
|
||||
berat = float(data.get("berat", 0))
|
||||
|
||||
if not telur_id:
|
||||
return jsonify({"error": "id tidak ada"}), 400
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Ambil AI grade yang sudah tersimpan saat /upload
|
||||
cursor.execute("SELECT grade FROM telur WHERE id=%s", (telur_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
ai_grade = row[0] if row else "TL"
|
||||
|
||||
# Hitung grade final: kombinasi AI + berat
|
||||
final = final_decision(ai_grade, berat)
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE telur SET berat=%s, grade=%s WHERE id=%s",
|
||||
(berat, final, telur_id)
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print(f"[updateFinal] id={telur_id}, berat={berat}, ai={ai_grade}, final={final}")
|
||||
|
||||
return jsonify({"final": final, "berat": round(berat, 2)})
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GET /telur — list semua telur untuk dashboard
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@app.route('/telur', methods=['GET'])
|
||||
def get_telur():
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT id, grade, berat, created_at
|
||||
FROM telur
|
||||
ORDER BY id DESC
|
||||
LIMIT 500
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
result = []
|
||||
for row in rows:
|
||||
result.append({
|
||||
"id": row["id"],
|
||||
"grade": map_grade(row["grade"] or "TL"),
|
||||
"weight": float(row["berat"]) if row["berat"] else 0,
|
||||
"timestamp": row["created_at"].strftime("%H:%M") if row["created_at"] else "-",
|
||||
"date": row["created_at"].strftime("%d/%m/%Y") if row["created_at"] else "-",
|
||||
})
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GET /summary?period=today|week|month|all
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@app.route('/summary', methods=['GET'])
|
||||
def summary():
|
||||
period = request.args.get("period", "today")
|
||||
if period not in ("today", "week", "month", "all"):
|
||||
period = "today"
|
||||
|
||||
where_clause, history_interval = period_to_sql(period)
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT
|
||||
grade,
|
||||
COUNT(*) AS jumlah,
|
||||
AVG(berat) AS rata_berat
|
||||
FROM telur
|
||||
WHERE {where_clause}
|
||||
GROUP BY grade
|
||||
""")
|
||||
grade_rows = cursor.fetchall()
|
||||
|
||||
result = {
|
||||
"gradeA": {"count": 0, "avgWeight": 0},
|
||||
"gradeB": {"count": 0, "avgWeight": 0},
|
||||
"gradeC": {"count": 0, "avgWeight": 0},
|
||||
"gradeTL": {"count": 0, "avgWeight": 0},
|
||||
}
|
||||
for row in grade_rows:
|
||||
g = map_grade(row["grade"] or "TL")
|
||||
key = f"grade{g}"
|
||||
if key in result:
|
||||
result[key]["count"] = int(row["jumlah"])
|
||||
result[key]["avgWeight"] = round(float(row["rata_berat"] or 0), 1)
|
||||
|
||||
if period in ("today", "week"):
|
||||
slot_expr = """DATE_FORMAT(
|
||||
DATE_SUB(created_at, INTERVAL MOD(MINUTE(created_at), 30) MINUTE),
|
||||
'%%H:%%i'
|
||||
)"""
|
||||
else:
|
||||
slot_expr = "DATE_FORMAT(created_at, '%%d %%b')"
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT
|
||||
{slot_expr} AS slot,
|
||||
grade,
|
||||
COUNT(*) AS jumlah
|
||||
FROM telur
|
||||
WHERE created_at >= NOW() - INTERVAL {history_interval}
|
||||
GROUP BY slot, grade
|
||||
ORDER BY slot ASC
|
||||
""")
|
||||
hist_rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
hist_map = defaultdict(lambda: {"A": 0, "B": 0, "C": 0, "TL": 0})
|
||||
for row in hist_rows:
|
||||
g = map_grade(row["grade"] or "TL")
|
||||
hist_map[row["slot"]][g] += int(row["jumlah"])
|
||||
|
||||
history = [
|
||||
{"time": slot, "A": v["A"], "B": v["B"], "C": v["C"], "TL": v["TL"]}
|
||||
for slot, v in sorted(hist_map.items())
|
||||
]
|
||||
|
||||
return jsonify({**result, "history": history})
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GET /getSpeed & POST /setSpeed
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@app.route('/getSpeed', methods=['GET'])
|
||||
def get_speed():
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT conveyor_speed FROM settings LIMIT 1")
|
||||
row = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return jsonify({"speed": row["conveyor_speed"] if row else 200})
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/setSpeed', methods=['POST'])
|
||||
def set_speed():
|
||||
data = request.get_json()
|
||||
speed = int(data["speed"])
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("UPDATE settings SET conveyor_speed=%s WHERE id=1", (speed,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return jsonify({"status": "ok", "speed": speed})
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# POST /login & POST /register
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@app.route('/login', methods=['POST'])
|
||||
def login():
|
||||
data = request.get_json()
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"SELECT * FROM users WHERE username = %s AND password = %s",
|
||||
(username, password)
|
||||
)
|
||||
user = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if user:
|
||||
return jsonify({"success": True, "username": user["username"], "nama": user["nama"], "role": user["role"]})
|
||||
return jsonify({"success": False, "message": "Username atau password salah"}), 401
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/register', methods=['POST'])
|
||||
def register():
|
||||
data = request.get_json()
|
||||
nama = data.get('nama')
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
role = data.get('role', 'user')
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
|
||||
existing = cursor.fetchone()
|
||||
if existing:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return jsonify({"success": False, "message": "Username sudah dipakai"}), 400
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO users (nama, username, password, role) VALUES (%s, %s, %s, %s)",
|
||||
(nama, username, password, role)
|
||||
)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({"success": True, "message": "Registrasi berhasil"})
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# POST /order — simpan transaksi (orders + order_items)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@app.route('/order', methods=['POST'])
|
||||
def create_order():
|
||||
data = request.get_json()
|
||||
cart = data.get("cart", [])
|
||||
nama = data.get("nama", "guest")
|
||||
|
||||
harga_map = {
|
||||
"A": 1500,
|
||||
"B": 1200,
|
||||
"C": 900
|
||||
}
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
total = 0
|
||||
|
||||
# 🔥 CEK STOK DULU (BIAR GA MINUS)
|
||||
for item in cart:
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM telur WHERE grade = %s",
|
||||
(item["grade"],)
|
||||
)
|
||||
stok = cursor.fetchone()[0]
|
||||
|
||||
if stok < item["qty"]:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": f"Stok grade {item['grade']} tidak cukup"
|
||||
})
|
||||
|
||||
# 🔥 HITUNG TOTAL
|
||||
for item in cart:
|
||||
total += harga_map[item["grade"]] * item["qty"]
|
||||
|
||||
# 🔥 INSERT KE ORDERS
|
||||
cursor.execute(
|
||||
"INSERT INTO orders (nama, total) VALUES (%s, %s)",
|
||||
(nama, total)
|
||||
)
|
||||
|
||||
order_id = cursor.lastrowid
|
||||
|
||||
# 🔥 INSERT ITEMS + HAPUS STOK
|
||||
for item in cart:
|
||||
grade = item["grade"]
|
||||
qty = item["qty"]
|
||||
harga = harga_map[grade]
|
||||
|
||||
# insert item
|
||||
cursor.execute(
|
||||
"INSERT INTO order_items (order_id, grade, qty, harga) VALUES (%s,%s,%s,%s)",
|
||||
(order_id, grade, qty, harga)
|
||||
)
|
||||
|
||||
# 🔥 HAPUS STOK (INI INTI NYA)
|
||||
cursor.execute("""
|
||||
DELETE FROM telur
|
||||
WHERE grade = %s
|
||||
ORDER BY id ASC
|
||||
LIMIT %s
|
||||
""", (grade, qty))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print(f"[ORDER] sukses id={order_id}, total={total}")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"order_id": order_id
|
||||
})
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 📦 GET ALL ORDERS (ADMIN / HISTORY)
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/orders', methods=['GET'])
|
||||
def get_orders():
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT o.id, o.nama, o.total, o.status, o.bukti_pembayaran, o.created_at
|
||||
FROM orders o
|
||||
ORDER BY o.id DESC
|
||||
""")
|
||||
|
||||
orders = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify(orders)
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 🔄 UPDATE STATUS ORDER (ADMIN APPROVE)
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/order/status', methods=['POST'])
|
||||
def update_status():
|
||||
data = request.json
|
||||
order_id = data.get("order_id")
|
||||
status = data.get("status")
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE orders SET status=%s WHERE id=%s",
|
||||
(status, order_id)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 📤 UPLOAD BUKTI PEMBAYARAN
|
||||
# ─────────────────────────────────────────────
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
from datetime import datetime
|
||||
from flask import send_from_directory
|
||||
|
||||
UPLOAD_FOLDER_BUKTI = 'uploads_bukti'
|
||||
os.makedirs(UPLOAD_FOLDER_BUKTI, exist_ok=True)
|
||||
|
||||
@app.route('/upload-bukti', methods=['POST'])
|
||||
def upload_bukti():
|
||||
file = request.files.get('file')
|
||||
order_id = request.form.get('order_id')
|
||||
|
||||
if not file or not order_id:
|
||||
return jsonify({"success": False, "message": "File / order_id kosong"})
|
||||
|
||||
filename = datetime.now().strftime("%Y%m%d%H%M%S_") + secure_filename(file.filename)
|
||||
filepath = os.path.join(UPLOAD_FOLDER_BUKTI, filename)
|
||||
|
||||
file.save(filepath)
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE orders SET bukti_pembayaran=%s, status='menunggu_verifikasi' WHERE id=%s",
|
||||
(filename, order_id)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print(f"[UPLOAD BUKTI] order_id={order_id}, file={filename}")
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
except Error as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# 🖼️ AKSES FILE BUKTI PEMBAYARAN
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/uploads-bukti/<filename>')
|
||||
def get_bukti(filename):
|
||||
return send_from_directory(UPLOAD_FOLDER_BUKTI, filename)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import os
|
||||
import shutil
|
||||
import random
|
||||
|
||||
base_path = "datasetfix"
|
||||
output_path = "dataset_split_v2"
|
||||
|
||||
classes = ["A", "B", "C", "TL"]
|
||||
|
||||
train_ratio = 0.8
|
||||
|
||||
for c in classes:
|
||||
src = os.path.join(base_path, c)
|
||||
|
||||
files = os.listdir(src)
|
||||
random.shuffle(files)
|
||||
|
||||
split_index = int(len(files) * train_ratio)
|
||||
|
||||
train_files = files[:split_index]
|
||||
val_files = files[split_index:]
|
||||
|
||||
# TRAIN
|
||||
train_dst = os.path.join(output_path, "train", c)
|
||||
os.makedirs(train_dst, exist_ok=True)
|
||||
|
||||
for f in train_files:
|
||||
shutil.copy(
|
||||
os.path.join(src, f),
|
||||
os.path.join(train_dst, f)
|
||||
)
|
||||
|
||||
# VALIDATION
|
||||
val_dst = os.path.join(output_path, "val", c)
|
||||
os.makedirs(val_dst, exist_ok=True)
|
||||
|
||||
for f in val_files:
|
||||
shutil.copy(
|
||||
os.path.join(src, f),
|
||||
os.path.join(val_dst, f)
|
||||
)
|
||||
|
||||
print("Split dataset selesai!")
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
import tensorflow as tf
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
import os
|
||||
|
||||
from tensorflow.keras import layers, models
|
||||
from tensorflow.keras.applications import MobileNetV2
|
||||
from tensorflow.keras.preprocessing.image import ImageDataGenerator
|
||||
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
|
||||
|
||||
from sklearn.metrics import confusion_matrix, classification_report
|
||||
|
||||
IMG_SIZE = 224
|
||||
BATCH_SIZE = 32
|
||||
|
||||
train_dir = "dataset_split_v2/train"
|
||||
val_dir = "dataset_split_v2/val"
|
||||
|
||||
# AUGMENTASI
|
||||
train_gen = ImageDataGenerator(
|
||||
rescale=1./255,
|
||||
rotation_range=20,
|
||||
zoom_range=0.2,
|
||||
shear_range=0.15,
|
||||
width_shift_range=0.1,
|
||||
height_shift_range=0.1,
|
||||
horizontal_flip=True,
|
||||
fill_mode='nearest'
|
||||
)
|
||||
|
||||
val_gen = ImageDataGenerator(rescale=1./255)
|
||||
|
||||
train_data = train_gen.flow_from_directory(
|
||||
train_dir,
|
||||
target_size=(IMG_SIZE, IMG_SIZE),
|
||||
batch_size=BATCH_SIZE,
|
||||
class_mode='categorical'
|
||||
)
|
||||
|
||||
val_data = val_gen.flow_from_directory(
|
||||
val_dir,
|
||||
target_size=(IMG_SIZE, IMG_SIZE),
|
||||
batch_size=BATCH_SIZE,
|
||||
class_mode='categorical',
|
||||
shuffle=False
|
||||
)
|
||||
|
||||
num_classes = train_data.num_classes
|
||||
class_labels = list(train_data.class_indices.keys())
|
||||
|
||||
print("Class indices:", train_data.class_indices)
|
||||
|
||||
|
||||
# MODEL
|
||||
base_model = MobileNetV2(
|
||||
input_shape=(IMG_SIZE, IMG_SIZE, 3),
|
||||
include_top=False,
|
||||
weights='imagenet'
|
||||
)
|
||||
|
||||
base_model.trainable = False
|
||||
|
||||
x = base_model.output
|
||||
x = layers.GlobalAveragePooling2D()(x)
|
||||
x = layers.Dense(128, activation='relu')(x)
|
||||
x = layers.Dropout(0.5)(x)
|
||||
output = layers.Dense(num_classes, activation='softmax')(x)
|
||||
|
||||
model = models.Model(inputs=base_model.input, outputs=output)
|
||||
|
||||
model.compile(
|
||||
optimizer='adam',
|
||||
loss='categorical_crossentropy',
|
||||
metrics=['accuracy']
|
||||
)
|
||||
|
||||
early_stop = EarlyStopping(
|
||||
monitor='val_loss',
|
||||
patience=5,
|
||||
restore_best_weights=True
|
||||
)
|
||||
|
||||
checkpoint = ModelCheckpoint(
|
||||
"best_model_cnn.h5",
|
||||
monitor='val_accuracy',
|
||||
save_best_only=True,
|
||||
mode='max'
|
||||
)
|
||||
|
||||
# TRAINING AWAL
|
||||
history = model.fit(
|
||||
train_data,
|
||||
validation_data=val_data,
|
||||
epochs=30,
|
||||
callbacks=[early_stop, checkpoint]
|
||||
)
|
||||
|
||||
|
||||
# FINE TUNING
|
||||
base_model.trainable = True
|
||||
|
||||
for layer in base_model.layers[:-30]:
|
||||
layer.trainable = False
|
||||
|
||||
model.compile(
|
||||
optimizer=tf.keras.optimizers.Adam(1e-5),
|
||||
loss='categorical_crossentropy',
|
||||
metrics=['accuracy']
|
||||
)
|
||||
|
||||
history_fine = model.fit(
|
||||
train_data,
|
||||
validation_data=val_data,
|
||||
epochs=10
|
||||
)
|
||||
|
||||
|
||||
# SAVE MODEL
|
||||
model.save("model_telur_cnn_new.h5")
|
||||
|
||||
print("Training selesai!")
|
||||
|
||||
# EVALUASI
|
||||
loss, acc = model.evaluate(val_data)
|
||||
print(f"Akurasi akhir: {acc*100:.2f}%")
|
||||
|
||||
# CONFUSION MATRIX
|
||||
pred = model.predict(val_data)
|
||||
y_pred = np.argmax(pred, axis=1)
|
||||
y_true = val_data.classes
|
||||
|
||||
cm = confusion_matrix(y_true, y_pred)
|
||||
|
||||
# simpan gambar CM
|
||||
os.makedirs("output_cm", exist_ok=True)
|
||||
|
||||
plt.figure(figsize=(6,5))
|
||||
sns.heatmap(cm, annot=True, fmt='d',
|
||||
xticklabels=class_labels,
|
||||
yticklabels=class_labels,
|
||||
cmap='Blues')
|
||||
|
||||
plt.title('Confusion Matrix')
|
||||
plt.savefig("output_cm/confusion_matrix.png")
|
||||
plt.show()
|
||||
|
||||
|
||||
# NORMALIZED CM
|
||||
cm_norm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
|
||||
|
||||
plt.figure(figsize=(6,5))
|
||||
sns.heatmap(cm_norm, annot=True, fmt='.2f',
|
||||
xticklabels=class_labels,
|
||||
yticklabels=class_labels,
|
||||
cmap='Blues')
|
||||
|
||||
plt.title("Normalized Confusion Matrix")
|
||||
plt.savefig("output_cm/confusion_matrix_normalized.png")
|
||||
plt.show()
|
||||
|
||||
print("\nClassification Report:")
|
||||
print(classification_report(y_true, y_pred, target_names=class_labels))
|
||||
|
||||
|
||||
# GRAFIK GABUNGAN
|
||||
acc_total = history.history['accuracy'] + history_fine.history['accuracy']
|
||||
val_acc_total = history.history['val_accuracy'] + history_fine.history['val_accuracy']
|
||||
|
||||
loss_total = history.history['loss'] + history_fine.history['loss']
|
||||
val_loss_total = history.history['val_loss'] + history_fine.history['val_loss']
|
||||
|
||||
plt.plot(acc_total, label='train_acc')
|
||||
plt.plot(val_acc_total, label='val_acc')
|
||||
plt.legend()
|
||||
plt.title("Accuracy Gabungan")
|
||||
plt.savefig("output_cm/accuracy.png")
|
||||
plt.show()
|
||||
|
||||
plt.plot(loss_total, label='train_loss')
|
||||
plt.plot(val_loss_total, label='val_loss')
|
||||
plt.legend()
|
||||
plt.title("Loss Gabungan")
|
||||
plt.savefig("output_cm/loss.png")
|
||||
plt.show()
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
#include "esp_camera.h"
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include "esp_http_server.h"
|
||||
|
||||
const char* ssid = "Fridho";
|
||||
const char* password = "123456789.";
|
||||
String serverURL = "http://10.10.1.112:5000/upload";
|
||||
|
||||
#define PWDN_GPIO_NUM 32
|
||||
#define RESET_GPIO_NUM -1
|
||||
#define XCLK_GPIO_NUM 0
|
||||
#define SIOD_GPIO_NUM 26
|
||||
#define SIOC_GPIO_NUM 27
|
||||
#define Y9_GPIO_NUM 35
|
||||
#define Y8_GPIO_NUM 34
|
||||
#define Y7_GPIO_NUM 39
|
||||
#define Y6_GPIO_NUM 36
|
||||
#define Y5_GPIO_NUM 21
|
||||
#define Y4_GPIO_NUM 19
|
||||
#define Y3_GPIO_NUM 18
|
||||
#define Y2_GPIO_NUM 5
|
||||
#define VSYNC_GPIO_NUM 25
|
||||
#define HREF_GPIO_NUM 23
|
||||
#define PCLK_GPIO_NUM 22
|
||||
|
||||
// ================= FLAG =================
|
||||
volatile bool doCapture = false;
|
||||
|
||||
// ================= INIT KAMERA =================
|
||||
void startCam() {
|
||||
camera_config_t config;
|
||||
config.ledc_channel = LEDC_CHANNEL_0;
|
||||
config.ledc_timer = LEDC_TIMER_0;
|
||||
config.pin_d0 = Y2_GPIO_NUM;
|
||||
config.pin_d1 = Y3_GPIO_NUM;
|
||||
config.pin_d2 = Y4_GPIO_NUM;
|
||||
config.pin_d3 = Y5_GPIO_NUM;
|
||||
config.pin_d4 = Y6_GPIO_NUM;
|
||||
config.pin_d5 = Y7_GPIO_NUM;
|
||||
config.pin_d6 = Y8_GPIO_NUM;
|
||||
config.pin_d7 = Y9_GPIO_NUM;
|
||||
config.pin_xclk = XCLK_GPIO_NUM;
|
||||
config.pin_pclk = PCLK_GPIO_NUM;
|
||||
config.pin_vsync = VSYNC_GPIO_NUM;
|
||||
config.pin_href = HREF_GPIO_NUM;
|
||||
config.pin_sscb_sda = SIOD_GPIO_NUM;
|
||||
config.pin_sscb_scl = SIOC_GPIO_NUM;
|
||||
config.pin_pwdn = PWDN_GPIO_NUM;
|
||||
config.pin_reset = RESET_GPIO_NUM;
|
||||
config.xclk_freq_hz = 20000000;
|
||||
config.pixel_format = PIXFORMAT_JPEG;
|
||||
config.frame_size = FRAMESIZE_VGA;
|
||||
config.jpeg_quality = 12;
|
||||
config.fb_count = 1;
|
||||
|
||||
esp_err_t err = esp_camera_init(&config);
|
||||
if (err != ESP_OK) {
|
||||
Serial.printf("[CAM] Init error: 0x%x\n", err);
|
||||
return;
|
||||
}
|
||||
Serial.println("[CAM] Kamera siap.");
|
||||
}
|
||||
|
||||
// ================= UPLOAD KE SERVER =================
|
||||
void kirimGambar() {
|
||||
camera_fb_t* fb = esp_camera_fb_get();
|
||||
if (!fb) {
|
||||
Serial.println("[CAM] Gagal ambil frame");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[CAM] Frame: %u bytes, uploading...\n", fb->len);
|
||||
|
||||
HTTPClient http;
|
||||
http.begin(serverURL);
|
||||
http.addHeader("Content-Type", "application/octet-stream");
|
||||
http.setTimeout(15000);
|
||||
|
||||
int code = http.POST(fb->buf, fb->len);
|
||||
Serial.print("[CAM] Upload HTTP code: "); Serial.println(code);
|
||||
if (code > 0) {
|
||||
Serial.println("[CAM] Response: " + http.getString());
|
||||
} else {
|
||||
Serial.print("[CAM] Upload error: "); Serial.println(http.errorToString(code));
|
||||
}
|
||||
http.end();
|
||||
|
||||
esp_camera_fb_return(fb);
|
||||
Serial.println("[CAM] Upload selesai.");
|
||||
}
|
||||
|
||||
// ================= HTTP SERVER HANDLER =================
|
||||
esp_err_t capture_handler(httpd_req_t* req) {
|
||||
Serial.println("[CAM] /capture dipanggil!");
|
||||
doCapture = true;
|
||||
const char* resp = "{\"status\":\"triggered\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ping_handler(httpd_req_t* req) {
|
||||
const char* resp = "{\"status\":\"ok\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void startWebServer() {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = 80;
|
||||
|
||||
httpd_handle_t server = NULL;
|
||||
if (httpd_start(&server, &config) == ESP_OK) {
|
||||
httpd_uri_t capture_uri = {
|
||||
.uri = "/capture",
|
||||
.method = HTTP_GET,
|
||||
.handler = capture_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(server, &capture_uri);
|
||||
|
||||
httpd_uri_t ping_uri = {
|
||||
.uri = "/",
|
||||
.method = HTTP_GET,
|
||||
.handler = ping_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(server, &ping_uri);
|
||||
|
||||
Serial.println("[CAM] Web server jalan di port 80");
|
||||
Serial.println("[CAM] IP: " + WiFi.localIP().toString());
|
||||
} else {
|
||||
Serial.println("[CAM] Gagal start web server");
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= SETUP =============================
|
||||
// =====================================================
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
WiFi.begin(ssid, password);
|
||||
Serial.print("[CAM] Connecting WiFi");
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
Serial.println("\n[CAM] WiFi Connected!");
|
||||
Serial.println("[CAM] IP: " + WiFi.localIP().toString());
|
||||
|
||||
startCam();
|
||||
startWebServer();
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= LOOP ==============================
|
||||
// =====================================================
|
||||
|
||||
void loop() {
|
||||
if (doCapture) {
|
||||
doCapture = false;
|
||||
Serial.println("[CAM] Mengambil gambar...");
|
||||
delay(300);
|
||||
kirimGambar();
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
|
|
@ -0,0 +1,904 @@
|
|||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <ESP32Servo.h>
|
||||
#include "HX711.h"
|
||||
|
||||
// =====================================================
|
||||
// ================= WIFI ==============================
|
||||
// =====================================================
|
||||
|
||||
const char* ssid = "Fridho";
|
||||
const char* password = "123456789.";
|
||||
|
||||
String serverURL = "http://10.10.1.112:5000";
|
||||
String camURL = "http://192.168.137.224";
|
||||
|
||||
// =====================================================
|
||||
// ================= MOTOR =============================
|
||||
// =====================================================
|
||||
|
||||
#define IN1 12
|
||||
#define IN2 13
|
||||
#define ENA 14
|
||||
|
||||
#define IR_START 5
|
||||
#define IR_STOP 4
|
||||
|
||||
#define PWM_CHANNEL 7
|
||||
|
||||
bool motorOn = false;
|
||||
bool lastStart = HIGH;
|
||||
bool lastStop = HIGH;
|
||||
|
||||
int speedMotor = 200;
|
||||
|
||||
// =====================================================
|
||||
// ================= LOADCELL ==========================
|
||||
// =====================================================
|
||||
|
||||
#define DOUT 18
|
||||
#define CLK 19
|
||||
|
||||
HX711 scale;
|
||||
|
||||
float calibration_factor = 942.0;
|
||||
float stableWeight = 0;
|
||||
|
||||
// =====================================================
|
||||
// ================= ROBOT ARM =========================
|
||||
// =====================================================
|
||||
|
||||
Servo servo1, servo2, servo3, servo4, servo5, servo6;
|
||||
|
||||
#define SERVO1_PIN 25
|
||||
#define SERVO2_PIN 26
|
||||
#define SERVO3_PIN 27
|
||||
#define SERVO4_PIN 33
|
||||
#define SERVO5_PIN 22
|
||||
#define SERVO6_PIN 21
|
||||
|
||||
int currentPos[6] = {137, 160, 90, 70, 55, 90};
|
||||
|
||||
// =====================================================
|
||||
// ================= SERVO =============================
|
||||
// =====================================================
|
||||
|
||||
void writeServo(int num, int ang) {
|
||||
|
||||
switch (num) {
|
||||
|
||||
case 0: servo1.write(ang); break;
|
||||
case 1: servo2.write(ang); break;
|
||||
case 2: servo3.write(ang); break;
|
||||
case 3: servo4.write(ang); break;
|
||||
case 4: servo5.write(ang); break;
|
||||
case 5: servo6.write(ang); break;
|
||||
}
|
||||
}
|
||||
|
||||
float easing(float t) {
|
||||
|
||||
return t * t * t * (t * (6 * t - 15) + 10);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= MOVE ALL ==========================
|
||||
// =====================================================
|
||||
|
||||
void moveAll(int target[6], int duration) {
|
||||
|
||||
int start[6];
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
start[i] = currentPos[i];
|
||||
}
|
||||
|
||||
int steps = duration / 20;
|
||||
|
||||
if (steps < 1) steps = 1;
|
||||
|
||||
for (int s = 0; s <= steps; s++) {
|
||||
|
||||
float t = easing((float)s / steps);
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
|
||||
int pos = start[i] + (target[i] - start[i]) * t;
|
||||
|
||||
writeServo(i, pos);
|
||||
|
||||
currentPos[i] = pos;
|
||||
}
|
||||
|
||||
delay(20);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= MOVE SEQUENTIAL ===================
|
||||
// =====================================================
|
||||
|
||||
void moveSequential(int target[6]) {
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
|
||||
int temp[6];
|
||||
|
||||
for (int j = 0; j < 6; j++) {
|
||||
temp[j] = currentPos[j];
|
||||
}
|
||||
|
||||
temp[i] = target[i];
|
||||
|
||||
moveAll(temp, 500);
|
||||
}
|
||||
|
||||
int temp[6];
|
||||
|
||||
for (int j = 0; j < 6; j++) {
|
||||
temp[j] = currentPos[j];
|
||||
}
|
||||
|
||||
temp[0] = target[0];
|
||||
|
||||
moveAll(temp, 800);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= POSISI ============================
|
||||
// =====================================================
|
||||
|
||||
void posisiAwal() {
|
||||
|
||||
int t[6] = {137, 160, 90, 70, 55, 90};
|
||||
|
||||
moveAll(t, 1200);
|
||||
}
|
||||
|
||||
void posisiMati() {
|
||||
|
||||
int t[6] = {137, 160, 60, 70, 55, 90};
|
||||
|
||||
moveAll(t, 1200);
|
||||
}
|
||||
|
||||
void posisiAntara() {
|
||||
|
||||
int t[6] = {137,170,75,85,55,90};
|
||||
|
||||
moveSequential(t);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= PROSES ROBOT ======================
|
||||
// =====================================================
|
||||
|
||||
void prosesRobot(char g) {
|
||||
|
||||
unsigned long tStart = millis();
|
||||
|
||||
posisiAwal();
|
||||
|
||||
delay(300);
|
||||
|
||||
// =================================================
|
||||
// POSISI AMBIL
|
||||
// =================================================
|
||||
|
||||
int ambil_open[6] = {137,165,40,95,55,90};
|
||||
|
||||
moveAll(ambil_open, 1200);
|
||||
|
||||
delay(300);
|
||||
|
||||
// =================================================
|
||||
// CAPIT TELUR
|
||||
// =================================================
|
||||
|
||||
int servo4Grip = 95;
|
||||
|
||||
// KHUSUS TL LEBIH NUNDUK
|
||||
if (g == 'T') {
|
||||
servo4Grip = 100;
|
||||
}
|
||||
|
||||
int grip[6] = {
|
||||
137,
|
||||
165,
|
||||
40,
|
||||
servo4Grip,
|
||||
35,
|
||||
110
|
||||
};
|
||||
|
||||
moveAll(grip, 700);
|
||||
|
||||
// =================================================
|
||||
// ANGKAT TELUR
|
||||
// =================================================
|
||||
|
||||
int angkat[6] = {137,170,75,85,35,110};
|
||||
|
||||
moveAll(angkat, 1200);
|
||||
|
||||
delay(300);
|
||||
|
||||
unsigned long tAmbil = millis();
|
||||
|
||||
// =================================================
|
||||
// GERAK KE GRADE
|
||||
// =================================================
|
||||
|
||||
int baseMove[6];
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
baseMove[i] = currentPos[i];
|
||||
}
|
||||
|
||||
if (g == 'A') baseMove[0] = 80;
|
||||
else if (g == 'B') baseMove[0] = 50;
|
||||
else if (g == 'C') baseMove[0] = 10;
|
||||
else if (g == 'T') baseMove[0] = 180;
|
||||
|
||||
int selisih = abs(currentPos[0] - baseMove[0]);
|
||||
|
||||
int durasiGerak = selisih * 25;
|
||||
|
||||
if (durasiGerak < 1200) {
|
||||
durasiGerak = 1200;
|
||||
}
|
||||
|
||||
moveAll(baseMove, durasiGerak);
|
||||
|
||||
delay(300);
|
||||
|
||||
// =================================================
|
||||
// TURUNKAN TELUR
|
||||
// =================================================
|
||||
|
||||
int turun[6] = {
|
||||
baseMove[0],
|
||||
130,
|
||||
60,
|
||||
50,
|
||||
35,
|
||||
110
|
||||
};
|
||||
|
||||
moveAll(turun, 1400);
|
||||
|
||||
delay(500);
|
||||
|
||||
// =================================================
|
||||
// BUKA CAPIT
|
||||
// =================================================
|
||||
|
||||
int lepas[6] = {
|
||||
baseMove[0],
|
||||
130,
|
||||
60,
|
||||
50,
|
||||
55,
|
||||
90
|
||||
};
|
||||
|
||||
moveAll(lepas, 1500);
|
||||
|
||||
delay(500);
|
||||
|
||||
unsigned long tTaruh = millis();
|
||||
|
||||
// =================================================
|
||||
// NAIK DULU
|
||||
// =================================================
|
||||
|
||||
int amanBalik[6] = {
|
||||
baseMove[0],
|
||||
170,
|
||||
75,
|
||||
85,
|
||||
55,
|
||||
90
|
||||
};
|
||||
|
||||
moveAll(amanBalik, 1400);
|
||||
|
||||
delay(300);
|
||||
|
||||
// =================================================
|
||||
// BALIK BASE
|
||||
// =================================================
|
||||
|
||||
int baseBalik[6];
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
baseBalik[i] = amanBalik[i];
|
||||
}
|
||||
|
||||
baseBalik[0] = 137;
|
||||
|
||||
int selisihBalik = abs(currentPos[0] - baseBalik[0]);
|
||||
|
||||
int durasiBalik = selisihBalik * 25;
|
||||
|
||||
if (durasiBalik < 1200) {
|
||||
durasiBalik = 1200;
|
||||
}
|
||||
|
||||
moveAll(baseBalik, durasiBalik);
|
||||
|
||||
delay(300);
|
||||
|
||||
// =================================================
|
||||
// POSISI STANDBY
|
||||
// =================================================
|
||||
|
||||
int standby[6] = {
|
||||
137,
|
||||
160,
|
||||
90,
|
||||
70,
|
||||
55,
|
||||
90
|
||||
};
|
||||
|
||||
moveAll(standby, 1500);
|
||||
|
||||
delay(300);
|
||||
|
||||
unsigned long tSelesai = millis();
|
||||
|
||||
// =================================================
|
||||
// OUTPUT TIMER
|
||||
// =================================================
|
||||
|
||||
float waktuAmbil =
|
||||
(tAmbil - tStart) / 1000.0;
|
||||
|
||||
float waktuSortir =
|
||||
(tTaruh - tAmbil) / 1000.0;
|
||||
|
||||
float waktuBalik =
|
||||
(tSelesai - tTaruh) / 1000.0;
|
||||
|
||||
float totalWaktu =
|
||||
(tSelesai - tStart) / 1000.0;
|
||||
|
||||
Serial.println("=================================");
|
||||
|
||||
Serial.print("Grade Tujuan : ");
|
||||
Serial.println(g);
|
||||
|
||||
Serial.print("Waktu ambil telur : ");
|
||||
Serial.print(waktuAmbil, 2);
|
||||
Serial.println(" detik");
|
||||
|
||||
Serial.print("Waktu sortir + jatuhkan : ");
|
||||
Serial.print(waktuSortir, 2);
|
||||
Serial.println(" detik");
|
||||
|
||||
Serial.print("Waktu balik standby : ");
|
||||
Serial.print(waktuBalik, 2);
|
||||
Serial.println(" detik");
|
||||
|
||||
Serial.print("Total waktu robot : ");
|
||||
Serial.print(totalWaktu, 2);
|
||||
Serial.println(" detik");
|
||||
|
||||
Serial.println("=================================");
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= WIFI ==============================
|
||||
// =====================================================
|
||||
|
||||
void connectWiFi() {
|
||||
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
Serial.print("Connecting WiFi");
|
||||
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
|
||||
Serial.println("");
|
||||
Serial.println("WiFi Connected!");
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= AMBIL SPEED =======================
|
||||
// =====================================================
|
||||
|
||||
void ambilSpeed() {
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) return;
|
||||
|
||||
HTTPClient http;
|
||||
|
||||
http.begin(serverURL + "/getSpeed");
|
||||
|
||||
int code = http.GET();
|
||||
|
||||
if (code == 200) {
|
||||
|
||||
String payload = http.getString();
|
||||
|
||||
int idx = payload.indexOf(":");
|
||||
int end = payload.indexOf("}");
|
||||
|
||||
if (idx != -1 && end != -1) {
|
||||
|
||||
int newSpeed =
|
||||
payload.substring(idx + 1, end).toInt();
|
||||
|
||||
if (newSpeed > 0 && newSpeed != speedMotor) {
|
||||
|
||||
speedMotor = newSpeed;
|
||||
|
||||
Serial.print("Speed update: ");
|
||||
Serial.println(speedMotor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.end();
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= TRIGGER CAMERA ====================
|
||||
// =====================================================
|
||||
|
||||
void triggerKamera() {
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) return;
|
||||
|
||||
HTTPClient http;
|
||||
|
||||
http.begin(camURL + "/capture");
|
||||
|
||||
http.setTimeout(8000);
|
||||
|
||||
int code = http.GET();
|
||||
|
||||
Serial.print("[CAM] Trigger status: ");
|
||||
Serial.println(code);
|
||||
|
||||
http.end();
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= JSON HELPER =======================
|
||||
// =====================================================
|
||||
|
||||
String cariString(String body, String key) {
|
||||
|
||||
String cari = "\"" + key + "\"";
|
||||
|
||||
int idx = body.indexOf(cari);
|
||||
|
||||
if (idx == -1) return "";
|
||||
|
||||
int start =
|
||||
body.indexOf("\"", idx + cari.length() + 1);
|
||||
|
||||
if (start == -1) return "";
|
||||
|
||||
int end = body.indexOf("\"", start + 1);
|
||||
|
||||
if (end == -1) return "";
|
||||
|
||||
return body.substring(start + 1, end);
|
||||
}
|
||||
|
||||
int cariInt(String body, String key) {
|
||||
|
||||
String cari = "\"" + key + "\"";
|
||||
|
||||
int idx = body.indexOf(cari);
|
||||
|
||||
if (idx == -1) return -1;
|
||||
|
||||
int pos = idx + cari.length();
|
||||
|
||||
while (
|
||||
pos < (int)body.length() &&
|
||||
(body[pos] == ':' || body[pos] == ' ')
|
||||
) pos++;
|
||||
|
||||
String numStr = "";
|
||||
|
||||
while (
|
||||
pos < (int)body.length() &&
|
||||
(isDigit(body[pos]) || body[pos] == '-')
|
||||
) {
|
||||
|
||||
numStr += body[pos];
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (numStr.length() == 0) return -1;
|
||||
|
||||
return numStr.toInt();
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= POLLING ===========================
|
||||
// =====================================================
|
||||
|
||||
bool pollingHasilKamera(
|
||||
int &telurId,
|
||||
String &aiGrade
|
||||
) {
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) return false;
|
||||
|
||||
unsigned long start = millis();
|
||||
|
||||
while (millis() - start < 12000) {
|
||||
|
||||
HTTPClient http;
|
||||
|
||||
http.begin(serverURL + "/latest");
|
||||
|
||||
http.setTimeout(5000);
|
||||
|
||||
int code = http.GET();
|
||||
|
||||
if (code == 200) {
|
||||
|
||||
String body = http.getString();
|
||||
|
||||
int id = cariInt(body, "telur_id");
|
||||
|
||||
String grade = cariString(body, "grade");
|
||||
|
||||
if (id > 0 && grade.length() > 0) {
|
||||
|
||||
telurId = id;
|
||||
aiGrade = grade;
|
||||
|
||||
http.end();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
http.end();
|
||||
|
||||
delay(500);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= UPDATE FINAL ======================
|
||||
// =====================================================
|
||||
|
||||
String updateFinal(
|
||||
int telurId,
|
||||
float berat
|
||||
) {
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) return "TL";
|
||||
|
||||
HTTPClient http;
|
||||
|
||||
http.begin(serverURL + "/updateFinal");
|
||||
|
||||
http.addHeader(
|
||||
"Content-Type",
|
||||
"application/json"
|
||||
);
|
||||
|
||||
String json =
|
||||
"{\"id\":" + String(telurId) +
|
||||
",\"berat\":" + String(berat, 2) + "}";
|
||||
|
||||
int code = http.POST(json);
|
||||
|
||||
String finalGrade = "C";
|
||||
|
||||
if (code == 200) {
|
||||
|
||||
String body = http.getString();
|
||||
|
||||
String fg = cariString(body, "final");
|
||||
|
||||
if (fg.length() > 0)
|
||||
finalGrade = fg;
|
||||
}
|
||||
|
||||
http.end();
|
||||
|
||||
return finalGrade;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= TIMBANG ===========================
|
||||
// =====================================================
|
||||
|
||||
void timbangTelur() {
|
||||
|
||||
float smoothed = 0;
|
||||
|
||||
bool first = true;
|
||||
|
||||
unsigned long startTime = millis();
|
||||
|
||||
while (millis() - startTime < 3000) {
|
||||
|
||||
float berat = scale.get_units(10);
|
||||
|
||||
if (berat < 0)
|
||||
berat = -berat;
|
||||
|
||||
if (first) {
|
||||
|
||||
smoothed = berat;
|
||||
first = false;
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
smoothed =
|
||||
0.8 * berat +
|
||||
0.2 * smoothed;
|
||||
}
|
||||
|
||||
delay(60);
|
||||
}
|
||||
|
||||
stableWeight = smoothed;
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= SETUP =============================
|
||||
// =====================================================
|
||||
|
||||
void setup() {
|
||||
|
||||
Serial.begin(115200);
|
||||
|
||||
connectWiFi();
|
||||
|
||||
// MOTOR
|
||||
|
||||
pinMode(IN1, OUTPUT);
|
||||
pinMode(IN2, OUTPUT);
|
||||
|
||||
pinMode(IR_START, INPUT);
|
||||
pinMode(IR_STOP, INPUT);
|
||||
|
||||
ledcSetup(PWM_CHANNEL, 1000, 8);
|
||||
|
||||
ledcAttachPin(ENA, PWM_CHANNEL);
|
||||
|
||||
digitalWrite(IN1, HIGH);
|
||||
digitalWrite(IN2, LOW);
|
||||
|
||||
ledcWrite(PWM_CHANNEL, 0);
|
||||
|
||||
// LOADCELL
|
||||
|
||||
scale.begin(DOUT, CLK);
|
||||
|
||||
while (!scale.is_ready()) {
|
||||
|
||||
Serial.println("Menunggu HX711...");
|
||||
delay(500);
|
||||
}
|
||||
|
||||
delay(2000);
|
||||
|
||||
scale.set_scale(calibration_factor);
|
||||
|
||||
scale.tare();
|
||||
|
||||
// SERVO
|
||||
|
||||
servo1.attach(SERVO1_PIN);
|
||||
servo2.attach(SERVO2_PIN);
|
||||
servo3.attach(SERVO3_PIN);
|
||||
servo4.attach(SERVO4_PIN);
|
||||
servo5.attach(SERVO5_PIN);
|
||||
servo6.attach(SERVO6_PIN);
|
||||
|
||||
delay(2000);
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
writeServo(i, currentPos[i]);
|
||||
}
|
||||
|
||||
delay(1000);
|
||||
|
||||
posisiMati();
|
||||
|
||||
delay(1000);
|
||||
|
||||
posisiAwal();
|
||||
|
||||
Serial.println("=== SISTEM SIAP ===");
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// ================= LOOP ==============================
|
||||
// =====================================================
|
||||
|
||||
void loop() {
|
||||
|
||||
ambilSpeed();
|
||||
|
||||
bool startVal = digitalRead(IR_START);
|
||||
bool stopVal = digitalRead(IR_STOP);
|
||||
|
||||
// =================================================
|
||||
// IR START
|
||||
// =================================================
|
||||
|
||||
if (
|
||||
lastStart == HIGH &&
|
||||
startVal == LOW &&
|
||||
!motorOn
|
||||
) {
|
||||
|
||||
Serial.println("IR1 → Conveyor ON");
|
||||
|
||||
motorOn = true;
|
||||
|
||||
ledcWrite(PWM_CHANNEL, speedMotor);
|
||||
|
||||
delay(200);
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// IR STOP
|
||||
// =================================================
|
||||
|
||||
if (
|
||||
lastStop == HIGH &&
|
||||
stopVal == LOW &&
|
||||
motorOn
|
||||
) {
|
||||
|
||||
Serial.println("IR2 → Conveyor OFF");
|
||||
|
||||
motorOn = false;
|
||||
|
||||
ledcWrite(PWM_CHANNEL, 0);
|
||||
|
||||
delay(800);
|
||||
|
||||
// =================================================
|
||||
// TIMBANG
|
||||
// =================================================
|
||||
|
||||
timbangTelur();
|
||||
|
||||
Serial.print("Berat: ");
|
||||
Serial.println(stableWeight);
|
||||
|
||||
// =================================================
|
||||
// TRIGGER CAMERA
|
||||
// =================================================
|
||||
|
||||
triggerKamera();
|
||||
|
||||
delay(2500);
|
||||
|
||||
// =================================================
|
||||
// POLLING HASIL AI
|
||||
// =================================================
|
||||
|
||||
int telurId = -1;
|
||||
|
||||
String aiGrade = "";
|
||||
|
||||
bool berhasil =
|
||||
pollingHasilKamera(
|
||||
telurId,
|
||||
aiGrade
|
||||
);
|
||||
|
||||
char finalChar;
|
||||
|
||||
// =================================================
|
||||
// JIKA AI BERHASIL
|
||||
// =================================================
|
||||
|
||||
if (berhasil) {
|
||||
|
||||
// =============================================
|
||||
// OUTPUT HASIL AI
|
||||
// =============================================
|
||||
|
||||
Serial.println("=================================");
|
||||
|
||||
Serial.print("ID Telur : ");
|
||||
Serial.println(telurId);
|
||||
|
||||
Serial.print("Grade AI : ");
|
||||
Serial.println(aiGrade);
|
||||
|
||||
Serial.print("Berat Telur : ");
|
||||
Serial.print(stableWeight);
|
||||
Serial.println(" gram");
|
||||
|
||||
// =============================================
|
||||
// FINAL GRADE
|
||||
// =============================================
|
||||
|
||||
String finalGrade =
|
||||
updateFinal(
|
||||
telurId,
|
||||
stableWeight
|
||||
);
|
||||
|
||||
Serial.print("Final Grade : ");
|
||||
Serial.println(finalGrade);
|
||||
|
||||
Serial.println("=================================");
|
||||
|
||||
// =============================================
|
||||
// KONVERSI FINAL GRADE
|
||||
// =============================================
|
||||
|
||||
if (finalGrade == "A")
|
||||
finalChar = 'A';
|
||||
|
||||
else if (finalGrade == "B")
|
||||
finalChar = 'B';
|
||||
|
||||
else if (finalGrade == "C")
|
||||
finalChar = 'C';
|
||||
|
||||
else
|
||||
finalChar = 'T';
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// JIKA AI GAGAL
|
||||
// =================================================
|
||||
|
||||
else {
|
||||
|
||||
Serial.println("=================================");
|
||||
Serial.println("AI Tidak Merespon");
|
||||
Serial.println("Menggunakan Berat Saja");
|
||||
|
||||
Serial.print("Berat Telur : ");
|
||||
Serial.print(stableWeight);
|
||||
Serial.println(" gram");
|
||||
|
||||
if (stableWeight >= 70)
|
||||
finalChar = 'A';
|
||||
|
||||
else if (stableWeight >= 60)
|
||||
finalChar = 'B';
|
||||
|
||||
else
|
||||
finalChar = 'C';
|
||||
|
||||
Serial.print("Final Grade : ");
|
||||
Serial.println(finalChar);
|
||||
|
||||
Serial.println("=================================");
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// ROBOT ARM
|
||||
// =================================================
|
||||
|
||||
prosesRobot(finalChar);
|
||||
|
||||
delay(1000);
|
||||
}
|
||||
|
||||
lastStart = startVal;
|
||||
lastStop = stopVal;
|
||||
|
||||
delay(10);
|
||||
}
|
||||