diff --git a/.gitignore b/.gitignore index 17a524b..2307dd6 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,47 @@ yarn-error.* app-example .qodo + +.env + +# @generated expo-cli sync-8d4afeec25ea8a192358fae2f8e2fc766bdce4ec +# The following patterns were generated by expo-cli + +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# @end expo-cli \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..8a6be07 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,16 @@ +# OSX +# +.DS_Store + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ + +# Bundle artifacts +*.jsbundle diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..09bec36 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,182 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" + +def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() + +/** + * 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 { + entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) + reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" + codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + + enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean() + // Use Expo CLI to bundle the app, this ensures the Metro config + // works correctly with Expo projects. + cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" + + /* 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") + + /* 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. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + + // + // 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 in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization). + */ +def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean() + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * 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.bip.hipmimobileapp' + defaultConfig { + applicationId 'com.bip.hipmimobileapp' + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0.0" + + buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" + } + 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 + def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false' + shrinkResources enableShrinkResources.toBoolean() + minifyEnabled enableMinifyInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true' + crunchPngs enablePngCrunchInRelease.toBoolean() + } + } + packagingOptions { + jniLibs { + def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false' + useLegacyPackaging enableLegacyPackaging.toBoolean() + } + } + androidResources { + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~' + } +} + +// Apply static values from `gradle.properties` to the `android.packagingOptions` +// Accepts values in comma delimited lists, example: +// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini +["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> + // Split option: 'foo,bar' -> ['foo', 'bar'] + def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); + // Trim all elements in place. + for (i in 0.. 0) { + println "android.packagingOptions.$prop += $options ($options.length)" + // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' + options.each { + android.packagingOptions[prop] += it + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; + def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; + def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; + + if (isGifEnabled) { + // For animated gif support + implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}") + } + + if (isWebpEnabled) { + // For webp support + implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}") + if (isWebpAnimatedEnabled) { + // Animated webp support + implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}") + } + } + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} diff --git a/android/app/debug.keystore b/android/app/debug.keystore new file mode 100644 index 0000000..364e105 Binary files /dev/null and b/android/app/debug.keystore differ diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..551eb41 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,14 @@ +# 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 + +# react-native-reanimated +-keep class com.swmansion.reanimated.** { *; } +-keep class com.facebook.react.turbomodule.** { *; } + +# Add any project specific keep options here: diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..3ec2507 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3fb12db --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/java/com/bip/hipmimobileapp/MainActivity.kt b/android/app/src/main/java/com/bip/hipmimobileapp/MainActivity.kt new file mode 100644 index 0000000..f8d846c --- /dev/null +++ b/android/app/src/main/java/com/bip/hipmimobileapp/MainActivity.kt @@ -0,0 +1,65 @@ +package com.bip.hipmimobileapp +import expo.modules.splashscreen.SplashScreenManager + +import android.os.Build +import android.os.Bundle + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +import expo.modules.ReactActivityDelegateWrapper + +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + // Set the theme to AppTheme BEFORE onCreate to support + // coloring the background, status bar, and navigation bar. + // This is required for expo-splash-screen. + // setTheme(R.style.AppTheme); + // @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af + SplashScreenManager.registerOnActivity(this) + // @generated end expo-splashscreen + super.onCreate(null) + } + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "main" + + /** + * 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 { + return ReactActivityDelegateWrapper( + this, + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, + object : DefaultReactActivityDelegate( + this, + mainComponentName, + fabricEnabled + ){}) + } + + /** + * Align the back button behavior with Android S + * where moving root activities to background instead of finishing activities. + * @see onBackPressed + */ + override fun invokeDefaultOnBackPressed() { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (!moveTaskToBack(false)) { + // For non-root activities, use the default implementation to finish them. + super.invokeDefaultOnBackPressed() + } + return + } + + // Use the default back button implementation on Android S + // because it's doing more than [Activity.moveTaskToBack] in fact. + super.invokeDefaultOnBackPressed() + } +} diff --git a/android/app/src/main/java/com/bip/hipmimobileapp/MainApplication.kt b/android/app/src/main/java/com/bip/hipmimobileapp/MainApplication.kt new file mode 100644 index 0000000..7b486a6 --- /dev/null +++ b/android/app/src/main/java/com/bip/hipmimobileapp/MainApplication.kt @@ -0,0 +1,56 @@ +package com.bip.hipmimobileapp + +import android.app.Application +import android.content.res.Configuration + +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.ReactHost +import com.facebook.react.common.ReleaseLevel +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint +import com.facebook.react.defaults.DefaultReactNativeHost + +import expo.modules.ApplicationLifecycleDispatcher +import expo.modules.ReactNativeHostWrapper + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( + this, + object : DefaultReactNativeHost(this) { + override fun getPackages(): List = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // add(MyReactNativePackage()) + } + + override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + } + ) + + override val reactHost: ReactHost + get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + DefaultNewArchitectureEntryPoint.releaseLevel = try { + ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase()) + } catch (e: IllegalArgumentException) { + ReleaseLevel.STABLE + } + loadReactNative(this) + ApplicationLifecycleDispatcher.onApplicationCreate(this) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) + } +} diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png new file mode 100644 index 0000000..764f168 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png new file mode 100644 index 0000000..36708ce Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png new file mode 100644 index 0000000..9b395b9 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..79e84e2 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png new file mode 100644 index 0000000..9c26bca Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..883b2a0 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rn_edit_text_material.xml b/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 0000000..5c25e72 --- /dev/null +++ b/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..3941bea --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..3941bea --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..af5b8a9 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..b761d3d Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..02f84fe Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..3d2909e Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..cfe629b Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..75adad8 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..27bdf8a Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..0b248da Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..d3a4744 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..cda30ce Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..59cb1de Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..838e66d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..a90cfb9 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..d67a8cb Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..6bae00c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..3c05de5 --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f387b90 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + #ffffff + #ffffff + #023c69 + #ffffff + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..b7c6ce5 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + HIPMI BADUNG + automatic + contain + false + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..45a97e6 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..0554dd1 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,24 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + 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') + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven { url 'https://www.jitpack.io' } + } +} + +apply plugin: "expo-root-project" +apply plugin: "com.facebook.react.rootproject" diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..8e39f82 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,65 @@ +# 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 + +# Enable AAPT2 PNG crunching +android.enablePngCrunchInReleaseBuilds=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -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=true + +# Enable GIF support in React Native images (~200 B increase) +expo.gif.enabled=true +# Enable webp support in React Native images (~85 KB increase) +expo.webp.enabled=true +# Enable animated webp support (~3.4 MB increase) +# Disabled by default because iOS doesn't support animated webp +expo.webp.animated=false + +# Enable network inspector +EX_DEV_CLIENT_NETWORK_INSPECTOR=true + +# Use legacy packaging to compress native libraries in the resulting APK. +expo.useLegacyPackaging=false + +# Specifies whether the app is configured to use edge-to-edge via the app config or plugin +# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge. +expo.edgeToEdgeEnabled=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..7f94d3d --- /dev/null +++ b/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 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, 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" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@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 diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..9a05bd7 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,39 @@ +pluginManagement { + def reactNativeGradlePlugin = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })") + }.standardOutput.asText.get().trim() + ).getParentFile().absolutePath + includeBuild(reactNativeGradlePlugin) + + def expoPluginsPath = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") + }.standardOutput.asText.get().trim(), + "../android/expo-gradle-plugin" + ).absolutePath + includeBuild(expoPluginsPath) +} + +plugins { + id("com.facebook.react.settings") + id("expo-autolinking-settings") +} + +extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> + if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') { + ex.autolinkLibrariesFromCommand() + } else { + ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) + } +} +expoAutolinking.useExpoModules() + +rootProject.name = 'HIPMI BADUNG' + +expoAutolinking.useExpoVersionCatalog() + +include ':app' +includeBuild(expoAutolinking.reactNativeGradlePlugin) diff --git a/app.config.js b/app.config.js new file mode 100644 index 0000000..e94d2ca --- /dev/null +++ b/app.config.js @@ -0,0 +1,75 @@ +// app.config.js +require('dotenv').config(); + +export default { + name: 'HIPMI BADUNG', + slug: 'hipmi-mobile', + version: '1.0.0', + orientation: 'portrait', + icon: './assets/images/icon.png', + scheme: 'hipmimobile', + userInterfaceStyle: 'automatic', + newArchEnabled: true, + + ios: { + supportsTablet: true, + bundleIdentifier: 'com.anonymous.hipmi-mobile', + infoPlist: { + ITSAppUsesNonExemptEncryption: false, + }, + }, + + android: { + adaptiveIcon: { + foregroundImage: './assets/images/splash-icon.png', + backgroundColor: '#ffffff', + }, + edgeToEdgeEnabled: true, + package: 'com.bip.hipmimobileapp', + // softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration + }, + + web: { + bundler: 'metro', + output: 'static', + favicon: './assets/images/favicon.png', + }, + + plugins: [ + 'expo-router', + 'expo-web-browser', + [ + 'expo-splash-screen', + { + image: './assets/images/splash-icon.png', + imageWidth: 200, + resizeMode: 'contain', + backgroundColor: '#ffffff', + }, + ], + [ + 'expo-camera', + { + cameraPermission: 'Allow $(PRODUCT_NAME) to access your camera', + microphonePermission: 'Allow $(PRODUCT_NAME) to access your microphone', + recordAudioAndroid: true, + }, + ], + 'expo-font', + ], + + experiments: { + typedRoutes: true, + }, + + extra: { + router: {}, + eas: { + projectId: '5cf15964-4889-4755-b8ed-b99c61d614d1', + }, + // Tambahkan environment variables ke sini + API_BASE_URL: process.env.API_BASE_URL, + BASE_URL: process.env.BASE_URL, + DEEP_LINK_URL: process.env.DEEP_LINK_URL, + }, +}; \ No newline at end of file diff --git a/app.json b/app.json.backup similarity index 72% rename from app.json rename to app.json.backup index a3e8560..9f11a38 100644 --- a/app.json +++ b/app.json.backup @@ -1,10 +1,10 @@ { "expo": { - "name": "hipmi-mobile", + "name": "HIPMI BADUNG", "slug": "hipmi-mobile", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/images/logo-hipmi.png", + "icon": "./assets/images/icon.png", "scheme": "hipmimobile", "userInterfaceStyle": "automatic", "newArchEnabled": true, @@ -17,7 +17,7 @@ }, "android": { "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive-icon.png", + "foregroundImage": "./assets/images/splash-icon.png", "backgroundColor": "#ffffff" }, "edgeToEdgeEnabled": true, @@ -38,7 +38,16 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" } - ] + ], + [ + "expo-camera", + { + "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera", + "microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone", + "recordAudioAndroid": true + } + ], + "expo-font" ], "experiments": { "typedRoutes": true diff --git a/app/(application)/(file)/[id].tsx b/app/(application)/(file)/[id].tsx new file mode 100644 index 0000000..b7c5784 --- /dev/null +++ b/app/(application)/(file)/[id].tsx @@ -0,0 +1,24 @@ +import { BackButton } from "@/components"; +import PdfViewer from "@/components/_ShareComponent/PdfViewer"; +import API_STRORAGE from "@/constants/base-url-api-strorage"; +import { Stack, useLocalSearchParams } from "expo-router"; +import { SafeAreaView } from "react-native-safe-area-context"; + +export default function FileScreen() { + const { id } = useLocalSearchParams(); + const url = API_STRORAGE.GET({ fileId: id as string }); + + return ( + <> + , + }} + /> + + + + + ); +} diff --git a/app/(application)/(image)/preview-image/[id]/index.tsx b/app/(application)/(image)/preview-image/[id]/index.tsx new file mode 100644 index 0000000..62a5455 --- /dev/null +++ b/app/(application)/(image)/preview-image/[id]/index.tsx @@ -0,0 +1,33 @@ +import { CenterCustom, TextCustom, ViewWrapper } from "@/components"; +import API_STRORAGE from "@/constants/base-url-api-strorage"; +import { Image } from "expo-image"; +import { useLocalSearchParams } from "expo-router"; +import React, { useState } from "react"; + +export default function PreviewImage() { + const { id } = useLocalSearchParams(); + const [isLoading, setIsLoading] = useState(true); + + return ( + + {id ? ( + { + setIsLoading(false); + }} + source={ + isLoading + ? require("@/assets/images/loading.gif") + : API_STRORAGE.GET({ fileId: id as string }) + } + contentFit="contain" + style={{ width: "100%", height: "100%" }} + /> + ) : ( + + File not found + + )} + + ); +} diff --git a/app/(application)/(image)/take-picture/[id]/index.tsx b/app/(application)/(image)/take-picture/[id]/index.tsx new file mode 100644 index 0000000..fd56052 --- /dev/null +++ b/app/(application)/(image)/take-picture/[id]/index.tsx @@ -0,0 +1,171 @@ +import { + ButtonCustom, + Spacing, + StackCustom, + ViewWrapper +} from "@/components"; +import AntDesign from "@expo/vector-icons/AntDesign"; +import FontAwesome6 from "@expo/vector-icons/FontAwesome6"; +import { CameraType, CameraView, useCameraPermissions } from "expo-camera"; +import { Image } from "expo-image"; +import * as ImagePicker from "expo-image-picker"; +import { router, useLocalSearchParams } from "expo-router"; +import { useRef, useState } from "react"; +import { Pressable, StyleSheet, Text, View } from "react-native"; + +export default function TakePicture() { + const { id } = useLocalSearchParams(); +// console.log("Take Picture ID >>", id); + + const [permission, requestPermission] = useCameraPermissions(); + const ref = useRef(null); + const [uri, setUri] = useState(null); + const [facing, setFacing] = useState("back"); + + if (!permission?.granted) { + return ( + + + We need your permission to use the camera + + + Grant permission + + + ); + } + + const takePicture = async () => { + const photo = await ref.current?.takePictureAsync(); + setUri(photo?.uri || null); + }; + + const pickImage = async () => { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 1, + }); + + if (!result.canceled) { + setUri(result.assets[0].uri); + } + }; + + const toggleFacing = () => { + setFacing((prev) => (prev === "back" ? "front" : "back")); + }; + + const renderPicture = () => { + return ( + + + + + + setUri(null)} title="Foto ulang" /> + { + console.log("Upload picture >>", id); + router.back(); + }} + title="Upload Foto" + /> + + + ); + }; + + const renderCameraUI = () => { + return ( + + + + + + + + {({ pressed }) => ( + + + + )} + + + + + + + + ); + }; + + return ( + <> + {uri ? ( + + {renderPicture()} + + ) : ( + <> + + {renderCameraUI()} + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + justifyContent: "center", + alignItems: "center", + }, + camera: { + flex: 1, + width: "100%", + }, + cameraOverlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: "flex-end", + padding: 44, + }, + shutterContainer: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + shutterBtn: { + backgroundColor: "transparent", + borderWidth: 5, + borderColor: "white", + width: 85, + height: 85, + borderRadius: 45, + alignItems: "center", + justifyContent: "center", + }, + shutterBtnInner: { + width: 70, + height: 70, + borderRadius: 50, + backgroundColor: "white", + }, +}); diff --git a/app/(application)/(user)/_layout.tsx b/app/(application)/(user)/_layout.tsx new file mode 100644 index 0000000..657c4ce --- /dev/null +++ b/app/(application)/(user)/_layout.tsx @@ -0,0 +1,633 @@ +import { BackButton } from "@/components"; +import LeftButtonCustom from "@/components/Button/BackButton"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import { HeaderStyles } from "@/styles/header-styles"; +import { Ionicons } from "@expo/vector-icons"; +import { router, Stack } from "expo-router"; + +export default function UserLayout() { + return ( + <> + + + + {/* ========== Profile Section ========= */} + + + {/* ========== Portofolio Section ========= */} + + + {/* ========== User Search Section ========= */} + , + }} + /> + + {/* ========== Notification Section ========= */} + , + }} + /> + + {/* ========== Event Section ========= */} + ( + + ), + }} + /> + , + }} + /> + + , + }} + /> + + , + }} + /> + + , + }} + /> + {/* ========== End Event Section ========= */} + + {/* ========== Collaboration Section ========= */} + , + }} + /> + , + }} + /> + , + }} + /> + {/* , + }} + /> */} + , + }} + /> + , + }} + /> + , + }} + /> + + {/* ========== End Collaboration Section ========= */} + + {/* ========== Voting Section ========= */} + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + {/* ========== End Voting Section ========= */} + + {/* ========== Crowdfunding Section ========= */} + , + }} + /> + + {/* ========== End Crowdfunding Section ========= */} + + {/* ========== Investment Section ========= */} + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + , + }} + /> + + , + }} + /> + , + }} + /> + , + }} + /> + + , + }} + /> + + , + }} + /> + + , + }} + /> + , + }} + /> + ( + + router.navigate(`/investment/(tabs)/transaction`) + } + /> + ), + }} + /> + ( + + router.navigate(`/investment/(tabs)/transaction`) + } + /> + ), + }} + /> + , + }} + /> + , + }} + /> + + , + }} + /> + {/* ========== End Investment Section ========= */} + + {/* ========== Donation Section ========= */} + , + }} + /> + + , + }} + /> + , + }} + /> + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + , + }} + /> + , + }} + /> + , + }} + /> + + , + }} + /> + , + }} + /> + ( + router.navigate(`/donation/(tabs)/my-donation`)} + /> + ), + }} + /> + ( + router.navigate(`/donation/(tabs)/my-donation`)} + /> + ), + }} + /> + , + }} + /> + , + }} + /> + + {/* ========== End Donation Section ========= */} + + {/* ========== Job Section ========= */} + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + {/* ========== End Job Section ========= */} + + {/* ========== Forum Section ========= */} + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + {/* ========== Maps Section ========= */} + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + {/* ========== Marketplace Section ========= */} + , + }} + /> + + + ); +} diff --git a/app/(application)/(user)/collaboration/(tabs)/_layout.tsx b/app/(application)/(user)/collaboration/(tabs)/_layout.tsx new file mode 100644 index 0000000..cc4925a --- /dev/null +++ b/app/(application)/(user)/collaboration/(tabs)/_layout.tsx @@ -0,0 +1,36 @@ +import { IconHome } from "@/components/_Icon"; +import { TabsStyles } from "@/styles/tabs-styles"; +import { Ionicons } from "@expo/vector-icons"; +import { Tabs } from "expo-router"; + +export default function CollaborationTabsLayout() { + return ( + + , + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} diff --git a/app/(application)/(user)/collaboration/(tabs)/group.tsx b/app/(application)/(user)/collaboration/(tabs)/group.tsx new file mode 100644 index 0000000..895a51d --- /dev/null +++ b/app/(application)/(user)/collaboration/(tabs)/group.tsx @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + Grid, + LoaderCustom, + StackCustom, + TextCustom, +} from "@/components"; +import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; +import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration"; +import { Feather } from "@expo/vector-icons"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useState, useCallback } from "react"; + +export default function CollaborationGroup() { + const { user } = useAuth(); + const [listData, setListData] = useState(); + const [loadingGetData, setLoadingGetData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id]) + ); + + const onLoadData = async () => { + try { + setLoadingGetData(true); + const response = await apiCollaborationGetAll({ + category: "group", + authorId: user?.id, + }); + + if (response.success) { + setListData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetData(false); + } + }; + + return ( + + {loadingGetData ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada data + ) : ( + + {listData?.map((item: any, index: any) => ( + + + + + {item?.ProjectCollaboration_RoomChat?.name} + + + { + item?.ProjectCollaboration_RoomChat + ?.ProjectCollaboration_AnggotaRoomChat?.length + }{" "} + Anggota + + + + + + + + ))} + + )} + + ); +} + +function generateProjectName() { + const adjectives = [ + "Blue", + "Dark", + "Bright", + "Quantum", + "Silent", + "Cyber", + "Epic", + "Golden", + "Shadow", + "Rapid", + ]; + + const nouns = [ + "Spark", + "Core", + "Orbit", + "Nest", + "Drive", + "Nova", + "Cloud", + "Blade", + "Matrix", + "Link", + ]; + + const randomAdjective = + adjectives[Math.floor(Math.random() * adjectives.length)]; + const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]; + + return randomAdjective + randomNoun; +} diff --git a/app/(application)/(user)/collaboration/(tabs)/index.tsx b/app/(application)/(user)/collaboration/(tabs)/index.tsx new file mode 100644 index 0000000..8c15106 --- /dev/null +++ b/app/(application)/(user)/collaboration/(tabs)/index.tsx @@ -0,0 +1,66 @@ +import { + FloatingButton, + LoaderCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import Collaboration_BoxPublishSection from "@/screens/Collaboration/BoxPublishSection"; +import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function CollaborationBeranda() { + const [listData, setListData] = useState(); + const [loadingGetData, setLoadingGetData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, []) + ); + + const onLoadData = async () => { + try { + setLoadingGetData(true); + const response = await apiCollaborationGetAll({ + category: "beranda", + }); + + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetData(false); + } + }; + + return ( + <> + { + router.push("/collaboration/create"); + }} + /> + } + > + {loadingGetData ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada data + ) : ( + listData?.map((item: any, index: number) => ( + + )) + )} + + + ); +} diff --git a/app/(application)/(user)/collaboration/(tabs)/participant.tsx b/app/(application)/(user)/collaboration/(tabs)/participant.tsx new file mode 100644 index 0000000..6afd1b1 --- /dev/null +++ b/app/(application)/(user)/collaboration/(tabs)/participant.tsx @@ -0,0 +1,113 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components"; +import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import Collaboration_BoxPublishSection from "@/screens/Collaboration/BoxPublishSection"; +import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import { View } from "react-native"; + +export default function CollaborationParticipans() { + const [activeCategory, setActiveCategory] = useState< + "participant" | "my-project" + >("participant"); + const { user } = useAuth(); + const [listData, setListData] = useState(); + const [loadingGetData, setLoadingGetData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [activeCategory]) + ); + + const onLoadData = async () => { + try { + setLoadingGetData(true); + const response = await apiCollaborationGetAll({ + category: + activeCategory === "participant" ? "participant" : "my-project", + authorId: user?.id, + }); + + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetData(false); + } + }; + + const handlePress = (item: any) => { + setActiveCategory(item); + // tambahkan logika lain seperti filter dsb. + }; + + const headerComponent = ( + + handlePress("participant")} + > + Partisipasi Proyek + + + handlePress("my-project")} + > + Proyek Saya + + + ); + + return ( + + {loadingGetData ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada data + ) : activeCategory === "participant" ? ( + listData?.map((item: any, index: number) => ( + + )) + ) : ( + listData?.map((item: any, index: number) => ( + + )) + )} + + ); +} diff --git a/app/(application)/(user)/collaboration/[id]/[detail]/info.tsx b/app/(application)/(user)/collaboration/[id]/[detail]/info.tsx new file mode 100644 index 0000000..316245b --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/[detail]/info.tsx @@ -0,0 +1,102 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + AvatarUsernameAndOtherComponent, + BackButton, + BaseBox, + BoxWithHeaderSection, + Grid, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { apiCollaborationGroup } from "@/service/api-client/api-collaboration"; +import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useState, useCallback } from "react"; + +export default function CollaborationRoomInfo() { + const { id, detail } = useLocalSearchParams(); + const [data, setData] = useState(); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiCollaborationGroup({ id: id as string }); + + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + return ( + <> + , + }} + /> + + + + + {listData({ data }).map((item, index) => ( + + + {item.title} + + + {item.value} + + + ))} + + + + + + {data?.ProjectCollaboration_AnggotaRoomChat?.map( + (item: any, index: number) => ( + + ) + )} + + + + + ); +} + +const listData = ({ data }: { data: any }) => [ + { + title: "Judul Proyek", + value: data?.ProjectCollaboration?.title || "-", + }, + { + title: "Industri", + value: + data?.ProjectCollaboration?.ProjectCollaborationMaster_Industri?.name || + "-", + }, + { + title: "Tujuan Proyek", + value: data?.ProjectCollaboration?.purpose || "-", + }, + { + title: "Keuntungan Proyek", + value: data?.ProjectCollaboration?.benefit || "-", + }, +]; diff --git a/app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx b/app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx new file mode 100644 index 0000000..b9d1602 --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx @@ -0,0 +1,31 @@ +import { BackButton } from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import ChatScreen from "@/screens/Collaboration/GroupChatSection"; +import { Feather } from "@expo/vector-icons"; +import { router, Stack, useLocalSearchParams } from "expo-router"; + +export default function CollaborationRoomChat() { + const { id, detail } = useLocalSearchParams(); + + return ( + <> + , + headerRight: () => ( + router.push(`/collaboration/${id}/${detail}/info`)} + /> + ), + }} + /> + + + + ); +} diff --git a/app/(application)/(user)/collaboration/[id]/create-pacticipants.tsx b/app/(application)/(user)/collaboration/[id]/create-pacticipants.tsx new file mode 100644 index 0000000..c7727dc --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/create-pacticipants.tsx @@ -0,0 +1,80 @@ +import { + AlertDefaultSystem, + ButtonCustom, + TextAreaCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { apiCollaborationCreatePartisipasi } from "@/service/api-client/api-collaboration"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function CollaborationCreatePartisipans() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + const [description, setDescription] = useState(""); + const [isLoading, setLoading] = useState(false); + + const handlerSubmitParticipans = async () => { + try { + setLoading(true); + const response = await apiCollaborationCreatePartisipasi({ + id: id as string, + data: { + authorId: user?.id, + description, + }, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Data berhasil disimpan", + }); + router.replace(`/collaboration/${id}/list-of-participants`); + } else { + Toast.show({ + type: "error", + text1: "Gagal menyimpan data", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + { + AlertDefaultSystem({ + title: "Simpan data deskripsi", + message: "Apakah anda sudah yakin ingin menyimpan data ini ?", + textLeft: "Batal", + textRight: "Simpan", + onPressRight: () => { + handlerSubmitParticipans(); + }, + }); + }} + > + Simpan + + + ); +} diff --git a/app/(application)/(user)/collaboration/[id]/detail-participant.tsx b/app/(application)/(user)/collaboration/[id]/detail-participant.tsx new file mode 100644 index 0000000..e7bd6b8 --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/detail-participant.tsx @@ -0,0 +1,110 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + DotButton, + DrawerCustom, + MenuDrawerDynamicGrid, + ViewWrapper +} from "@/components"; +import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection"; +import { apiCollaborationGetOne } from "@/service/api-client/api-collaboration"; +import { Ionicons } from "@expo/vector-icons"; +import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; + +export default function CollaborationDetailParticipant() { + const { id } = useLocalSearchParams(); + const [openDrawerParticipant, setOpenDrawerParticipant] = useState(false); + const [data, setData] = useState(); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiCollaborationGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + return ( + <> + , + headerRight: () => ( + setOpenDrawerParticipant(true)} /> + ), + }} + /> + + + + {/* + + Partisipan + + + {Array.from({ length: 5 }).map((_, index) => ( + setOpenDrawerParticipant(true)} + /> + } + /> + ))} + */} + + + setOpenDrawerParticipant(false)} + height={"auto"} + > + , + label: "Daftar Partisipan", + path: `/collaboration/${id}/list-of-participants`, + }, + ]} + onPressItem={(item) => { + router.push(item.path as any); + setOpenDrawerParticipant(false); + }} + /> + + + {/* setOpenDrawerParticipant(false)} + height={"auto"} + > + + Deskripsi Diri + + Lorem ipsum dolor sit, amet consectetur adipisicing elit. Commodi, + itaque adipisci. Voluptas, sed quod! Ad facere labore voluptates, + neque quidem aut reprehenderit ducimus mollitia quisquam temporibus! + Temporibus iusto soluta necessitatibus. + + + */} + + ); +} diff --git a/app/(application)/(user)/collaboration/[id]/detail-project-main.tsx b/app/(application)/(user)/collaboration/[id]/detail-project-main.tsx new file mode 100644 index 0000000..04d704a --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/detail-project-main.tsx @@ -0,0 +1,117 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + DotButton, + DrawerCustom, + LoaderCustom, + MenuDrawerDynamicGrid, + Spacing, + ViewWrapper +} from "@/components"; +import { IconEdit } from "@/components/_Icon"; +import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection"; +import { + apiCollaborationGetOne +} from "@/service/api-client/api-collaboration"; +import { MaterialIcons } from "@expo/vector-icons"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import { useCallback, useState } from "react"; + +export default function CollaborationDetailProjectMain() { + const { id } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [data, setData] = useState(); + const [loadingGetData, setLoadingGetData] = useState(false); + + useFocusEffect( + useCallback(() => { + handlerLoadData(); + }, [id]) + ); + + const handlerLoadData = async () => { + try { + setLoadingGetData(true); + await onLoadData(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetData(false); + } + }; + + const onLoadData = async () => { + try { + const response = await apiCollaborationGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handleSubmit = (item: any) => { + console.log("item :", item); + router.push(item.path); + setOpenDrawer(false); + }; + + return ( + <> + , + headerRight: () => setOpenDrawer(true)} />, + }} + /> + + {loadingGetData ? ( + + ) : ( + <> + + {/* */} + + + + )} + + + setOpenDrawer(false)} + height={"auto"} + > + , + }, + { + label: "Pilih Partisipan", + path: `/(application)/(user)/collaboration/${id}/select-of-participants`, + icon: , + }, + ]} + onPressItem={(item: any) => { + handleSubmit(item); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/collaboration/[id]/edit.tsx b/app/(application)/(user)/collaboration/[id]/edit.tsx new file mode 100644 index 0000000..dcbbe89 --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/edit.tsx @@ -0,0 +1,170 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCustom, + LoaderCustom, + SelectCustom, + StackCustom, + TextAreaCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { + apiCollaborationEditData, + apiCollaborationGetOne, +} from "@/service/api-client/api-collaboration"; +import { apiMasterCollaborationType } from "@/service/api-client/api-master"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function CollaborationEdit() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(); + const [listMaster, setListMaster] = useState([]); + const [loadingData, setLoadingData] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + const fetchData = async () => { + try { + setLoadingData(true); + await onLoadData(); + await onLoadMaster(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingData(false); + } + }; + fetchData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiCollaborationGetOne({ id: id as string }); + + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + async function onLoadMaster() { + try { + const response = await apiMasterCollaborationType(); + setListMaster(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + } + + const handlerSubmitUpdate = async () => { + if ( + !data?.title || + !data?.lokasi || + !data?.projectCollaborationMaster_IndustriId || + !data?.purpose || + !data?.benefit + ) { + Toast.show({ + type: "error", + text1: "Gagal", + text2: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + const response = await apiCollaborationEditData({ + id: id as string, + data: data, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: response.message, + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: response.message, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + + {loadingData ? ( + + ) : ( + + setData({ ...data, title: value })} + /> + setData({ ...data, lokasi: value })} + /> + ({ + label: item.name, + value: item.id, + }))} + value={data?.projectCollaborationMaster_IndustriId} + onChange={(value) => + setData({ ...data, projectCollaborationMaster_IndustriId: value }) + } + /> + + setData({ ...data, purpose: value })} + /> + + setData({ ...data, benefit: value })} + /> + + { + handlerSubmitUpdate(); + }} + /> + + )} + + ); +} diff --git a/app/(application)/(user)/collaboration/[id]/index.tsx b/app/(application)/(user)/collaboration/[id]/index.tsx new file mode 100644 index 0000000..5d5800d --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/index.tsx @@ -0,0 +1,168 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + ButtonCustom, + DotButton, + DrawerCustom, + InformationBox, + LoaderCustom, + MenuDrawerDynamicGrid, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection"; +import { + apiCollaborationGetOne, + apiCollaborationGetParticipants, +} from "@/service/api-client/api-collaboration"; +import { Ionicons } from "@expo/vector-icons"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import { useCallback, useState } from "react"; + +export default function CollaborationDetail() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + const [data, setData] = useState(); + const [openDrawerMenu, setOpenDrawerMenu] = useState(false); + const [isParticipant, setIsParticipant] = useState(false); + const [loadingIsParticipant, setLoadingIsParticipant] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + onLoadParticipants(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiCollaborationGetOne({ id: id as string }); + + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const onLoadParticipants = async () => { + try { + setLoadingIsParticipant(true); + const response = await apiCollaborationGetParticipants({ + category: "check-participant", + id: id as string, + authorId: user?.id, + }); + + if (response.success) { + setIsParticipant(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingIsParticipant(false); + } + }; + + return ( + <> + , + headerRight: () => ( + setOpenDrawerMenu(true)} /> + ), + }} + /> + + {!data && !isParticipant ? ( + + ) : ( + <> + {user?.id === data?.Author?.id && ( + + )} + + {user?.id !== data?.Author?.id && ( + { + router.push(`/collaboration/${id}/create-pacticipants`); + // setOpenDrawerPartisipasi(true); + }} + > + {isParticipant ? "Anda telah berpartisipasi" : "Partisipasi"} + + )} + + )} + + + {/* Drawer Partisipasi */} + {/* setOpenDrawerPartisipasi(false)} + height={300} + > + + + { + AlertDefaultSystem({ + title: "Simpan data deskripsi", + message: "Apakah anda sudah yakin ingin menyimpan data ini ?", + textLeft: "Batal", + textRight: "Simpan", + onPressRight: () => { + handlerSubmitParticipans(); + }, + }); + }} + > + Simpan + + */} + + {/* Drawer Menu */} + setOpenDrawerMenu(false)} + height={"auto"} + > + , + label: "Daftar Partisipan", + path: `/collaboration/${id}/list-of-participants`, + }, + ]} + onPressItem={(item) => { + router.push(item.path as any); + setOpenDrawerMenu(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/collaboration/[id]/list-of-participants.tsx b/app/(application)/(user)/collaboration/[id]/list-of-participants.tsx new file mode 100644 index 0000000..dddb768 --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/list-of-participants.tsx @@ -0,0 +1,96 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarUsernameAndOtherComponent, + BaseBox, + DrawerCustom, + LoaderCustom, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { apiCollaborationGetParticipants } from "@/service/api-client/api-collaboration"; +import { Feather } from "@expo/vector-icons"; +import { useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import { ScrollView } from "react-native"; + +export default function CollaborationListOfParticipants() { + const { id } = useLocalSearchParams(); + const [listData, setListData] = useState(); + const [loadingGetData, setLoadingGetData] = useState(false); + const [description, setDescription] = useState(""); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + setLoadingGetData(true); + const response = await apiCollaborationGetParticipants({ + category: "list", + id: id as string, + }); + + if (response.success) { + setListData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetData(false); + } + }; + + const [openDrawer, setOpenDrawer] = useState(false); + return ( + <> + + {loadingGetData ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada partisipan + ) : ( + listData?.map((item: any, index: number) => ( + + { + setDescription(item?.deskripsi_diri); + setOpenDrawer(true); + }} + /> + } + /> + + )) + )} + + + {/* Drawer */} + setOpenDrawer(false)} + > + + Deskripsi diri + + + {description} + + + + + + + ); +} diff --git a/app/(application)/(user)/collaboration/[id]/select-of-participants.tsx b/app/(application)/(user)/collaboration/[id]/select-of-participants.tsx new file mode 100644 index 0000000..8c2cb8a --- /dev/null +++ b/app/(application)/(user)/collaboration/[id]/select-of-participants.tsx @@ -0,0 +1,251 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AlertDefaultSystem, + AvatarComp, + BaseBox, + BoxButtonOnFooter, + ButtonCustom, + CheckboxCustom, + CheckboxGroup, + DrawerCustom, + Grid, + LoaderCustom, + Spacing, + StackCustom, + TextAreaCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import ModalCustom from "@/components/Modal/ModalCustom"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import { useAuth } from "@/hooks/use-auth"; +import { + apiCollaborationCreateGroup, + apiCollaborationGetParticipants, +} from "@/service/api-client/api-collaboration"; +import { MaterialIcons } from "@expo/vector-icons"; +import { router, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import { ScrollView, View } from "react-native"; +import Toast from "react-native-toast-message"; + +export default function CollaborationSelectOfParticipants() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + const [listData, setListData] = useState(); + const [loadingGetData, setLoadingGetData] = useState(false); + const [description, setDescription] = useState(""); + const [selected, setSelected] = useState<(string | number)[]>([]); + const [openDrawer, setOpenDrawer] = useState(false); + const [nameGroup, setNameGroup] = useState(""); + const [openModal, setOpenModal] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + setLoadingGetData(true); + const response = await apiCollaborationGetParticipants({ + category: "list", + id: id as string, + }); + // console.log("response :", JSON.stringify(response.data, null, 2)); + + if (response.success) { + setListData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetData(false); + } + }; + + const handlerCreateGroup = async () => { + if (_.isEmpty(nameGroup)) { + Toast.show({ + type: "error", + text1: "Nama grup tidak boleh kosong", + }); + return; + } + try { + setIsLoading(true); + const response = await apiCollaborationCreateGroup({ + id: id as string, + data: { + authorId: user?.id, + nameGroup: nameGroup, + listSelect: selected, + }, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Grup berhasil dibuat", + }); + router.push("/(application)/(user)/collaboration/(tabs)/group"); + } else { + Toast.show({ + type: "error", + text1: "Gagal membuat grup", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const handlerSubmit = () => { + return ( + <> + + { + setOpenModal(true); + setNameGroup(""); + }} + > + Buat Grup + + + + ); + }; + + return ( + <> + + {loadingGetData ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada partisipan + ) : ( + + + *{" "} + + Pilih user yang akan menjadi tim proyek anda + + + + { + console.log("val :", val); + setSelected(val); + }} + > + {listData?.map((item: any, index: any) => ( + + + + + + + + + + + {item?.User?.username} + + + + { + setOpenDrawer(true); + setDescription(item?.deskripsi_diri); + }} + /> + + + + ))} + + + )} + + + + + setNameGroup(val)} + /> + + + { + setOpenModal(false); + }} + > + Batal + + + + { + AlertDefaultSystem({ + title: "Buat Grup", + message: + "Apakah anda yakin ingin membuat grup untuk proyek ini ?", + textLeft: "Tidak", + textRight: "Ya", + onPressLeft: () => {}, + onPressRight: () => { + handlerCreateGroup(); + }, + }); + }} + > + Simpan + + + + + + + {/* Drawer */} + setOpenDrawer(false)} + > + + Deskripsi diri + + + {description} + + + + + + + ); +} diff --git a/app/(application)/(user)/collaboration/create.tsx b/app/(application)/(user)/collaboration/create.tsx new file mode 100644 index 0000000..06736ee --- /dev/null +++ b/app/(application)/(user)/collaboration/create.tsx @@ -0,0 +1,174 @@ +import { + ButtonCustom, + LoaderCustom, + SelectCustom, + StackCustom, + TextAreaCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { apiCollaborationCreate } from "@/service/api-client/api-collaboration"; +import { apiMasterCollaborationType } from "@/service/api-client/api-master"; +import { router } from "expo-router"; +import React, { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +interface CollaborationCreateProps { + title?: string; + lokasi?: string; + purpose?: string; + benefit?: string; + projectCollaborationMaster_IndustriId?: string; + userId?: string; +} + +export default function CollaborationCreate() { + const { user } = useAuth(); + const [listMaster, setListMaster] = useState([]); + const [loadingMaster, setLoadingMaster] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = React.useState({ + title: "", + lokasi: "", + purpose: "", + benefit: "", + projectCollaborationMaster_IndustriId: "", + userId: "", + }); + + useEffect(() => { + onLoadMaster(); + }, []); + + async function onLoadMaster() { + try { + setLoadingMaster(true); + const response = await apiMasterCollaborationType(); + setListMaster(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingMaster(false); + } + } + + const handlerSubmit = async () => { + if ( + !data?.title || + !data?.lokasi || + !data?.purpose || + !data?.benefit || + !data?.projectCollaborationMaster_IndustriId + ) { + Toast.show({ + type: "error", + text1: "Gagal", + text2: "Harap isi semua data", + }); + return; + } + + const newData: CollaborationCreateProps = { + title: data?.title, + lokasi: data?.lokasi, + purpose: data?.purpose, + benefit: data?.benefit, + projectCollaborationMaster_IndustriId: + data?.projectCollaborationMaster_IndustriId, + userId: user?.id, + }; + + try { + setIsLoading(true); + + const response = await apiCollaborationCreate({ data: newData }); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: response.message, + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: "Gagal", + text2: response.message, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + + {loadingMaster ? ( + + ) : ( + + setData({ ...data, title: value })} + /> + + setData({ ...data, lokasi: value })} + /> + + ({ + label: item.name, + value: item.id, + }))} + value={data?.projectCollaborationMaster_IndustriId} + onChange={(value: any) => { + console.log(value); + setData({ + ...data, + projectCollaborationMaster_IndustriId: value, + }); + }} + /> + + setData({ ...data, purpose: value })} + /> + + setData({ ...data, benefit: value })} + /> + + handlerSubmit()} + /> + + )} + + ); +} diff --git a/app/(application)/(user)/crowdfunding/index.tsx b/app/(application)/(user)/crowdfunding/index.tsx new file mode 100644 index 0000000..0a5a76a --- /dev/null +++ b/app/(application)/(user)/crowdfunding/index.tsx @@ -0,0 +1,68 @@ +import { + BaseBox, + Grid, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import { Feather } from "@expo/vector-icons"; +import { Image } from "expo-image"; + +export default function Crowdfunding() { + const listPage = [ + { + title: "Investasi", + desc: "Buat investasi dan jual beli saham lebih mudah dengan pengguna lain.", + path: "investment/(tabs)", + }, + { + title: "Donasi", + desc: "Berbagi info untuk berdonasi lebih luas dan lebih efisien.", + path: "donation/(tabs)", + }, + ]; + + return ( + + + + + {listPage.map((item, index) => ( + + + + + + {item.title} + + {item.desc} + + + + + + + + ))} + + + ); +} diff --git a/app/(application)/(user)/donation/(tabs)/_layout.tsx b/app/(application)/(user)/donation/(tabs)/_layout.tsx new file mode 100644 index 0000000..0c4f12f --- /dev/null +++ b/app/(application)/(user)/donation/(tabs)/_layout.tsx @@ -0,0 +1,37 @@ +import { IconHome, IconStatus } from "@/components/_Icon"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import { TabsStyles } from "@/styles/tabs-styles"; +import { + FontAwesome5 +} from "@expo/vector-icons"; +import { Tabs } from "expo-router"; + +export default function InvestmentTabsLayout() { + return ( + + , + }} + /> + , + }} + /> + ( + + ), + }} + /> + + ); +} diff --git a/app/(application)/(user)/donation/(tabs)/index.tsx b/app/(application)/(user)/donation/(tabs)/index.tsx new file mode 100644 index 0000000..5d843a1 --- /dev/null +++ b/app/(application)/(user)/donation/(tabs)/index.tsx @@ -0,0 +1,57 @@ +import { + FloatingButton, + LoaderCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import Donation_BoxPublish from "@/screens/Donation/BoxPublish"; +import { apiDonationGetAll } from "@/service/api-client/api-donation"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function DonationBeranda() { + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, []) + ); + + const onLoadData = async () => { + try { + setLoadList(true); + const response = await apiDonationGetAll({ + category: "beranda" + }); + console.log("[RES GET ALL]", JSON.stringify(response.data, null, 2)); + + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadList(false); + } + }; + + return ( + router.push("/donation/create")} /> + } + > + {loadList ? ( + + ) : _.isEmpty(list) ? ( + Belum ada donasi + ) : ( + list?.map((item: any, index: number) => ( + + )) + )} + + ); +} diff --git a/app/(application)/(user)/donation/(tabs)/my-donation.tsx b/app/(application)/(user)/donation/(tabs)/my-donation.tsx new file mode 100644 index 0000000..b0c0344 --- /dev/null +++ b/app/(application)/(user)/donation/(tabs)/my-donation.tsx @@ -0,0 +1,142 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BadgeCustom, + BaseBox, + DummyLandscapeImage, + Grid, + LoaderCustom, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { apiDonationGetAll } from "@/service/api-client/api-donation"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import { Href, router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import { View } from "react-native"; + +export default function DonationMyDonation() { + const { user } = useAuth(); + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id]) + ); + + const onLoadData = async () => { + try { + setLoadList(true); + const response = await apiDonationGetAll({ + category: "my-donation", + authorId: user?.id, + }); + console.log( + "[RES GET MY DONATION]", + JSON.stringify(response.data, null, 2) + ); + + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadList(false); + } + }; + + const handlerColor = (status: string) => { + if (status === "menunggu") { + return "orange"; + } else if (status === "proses") { + return "white"; + } else if (status === "berhasil") { + return "green"; + } else if (status === "gagal") { + return "red"; + } + }; + + const handlePress = ({ + invoiceId, + donationId, + status, + }: { + invoiceId: string; + donationId: string; + status: string; + }) => { + const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`; + if (status === "menunggu") { + router.push(`${url}/invoice`); + } else if (status === "proses") { + router.push(`${url}/process`); + } else if (status === "berhasil") { + router.push(`${url}/success`); + } else if (status === "gagal") { + router.push(`${url}/failed`); + } + }; + + return ( + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + + Belum ada transaksi + + ) : ( + list?.map((item, index) => ( + { + handlePress({ + status: _.lowerCase(item.statusInvoice), + invoiceId: item.id, + donationId: item.donasiId, + }); + }} + > + + + + + + + + + + + {item.title || "-"} + + + + Rp. {formatCurrencyDisplay(item.nominal)} + + + + {item.statusInvoice} + + + + + + )) + )} + + ); +} diff --git a/app/(application)/(user)/donation/(tabs)/status.tsx b/app/(application)/(user)/donation/(tabs)/status.tsx new file mode 100644 index 0000000..a10575d --- /dev/null +++ b/app/(application)/(user)/donation/(tabs)/status.tsx @@ -0,0 +1,80 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + LoaderCustom, + ScrollableCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; +import Donasi_BoxStatus from "@/screens/Donation/BoxStatus"; +import { apiDonationGetByStatus } from "@/service/api-client/api-donation"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function DonationStatus() { + const { user } = useAuth(); + const [activeCategory, setActiveCategory] = useState( + "publish" + ); + const [listData, setListData] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadList(); + }, [activeCategory]) + ); + + const onLoadList = async () => { + try { + setLoadList(true); + const response = await apiDonationGetByStatus({ + authorId: user?.id as string, + status: activeCategory as string, + }); + + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + setListData(null); + } finally { + setLoadList(false); + } + }; + + const handlePress = (item: any) => { + setActiveCategory(item.value); + // tambahkan logika lain seperti filter dsb. + }; + + const scrollComponent = ( + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as any} + /> + ); + return ( + + {loadList ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada data {activeCategory} + ) : ( + listData?.map((item: any, index: number) => ( + + )) + )} + + ); +} diff --git a/app/(application)/(user)/donation/[id]/(news)/[news]/edit-news.tsx b/app/(application)/(user)/donation/[id]/(news)/[news]/edit-news.tsx new file mode 100644 index 0000000..ed2ed10 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(news)/[news]/edit-news.tsx @@ -0,0 +1,164 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import API_STRORAGE from "@/constants/base-url-api-strorage"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { + apiDonationGetNewsById, + apiDonationUpdateNews, +} from "@/service/api-client/api-donation"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function DonationEditNews() { + const { news } = useLocalSearchParams(); + const [data, setData] = useState(null); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [news]) + ); + + const onLoadData = async () => { + try { + const response = await apiDonationGetNewsById({ + id: news as string, + category: "get-one", + }); + + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlerSubmitUpdate = async () => { + let newData; + if (!data.title || !data.deskripsi) { + Toast.show({ + type: "error", + text1: "Judul dan deskripsi harus diisi", + }); + return; + } + + try { + setLoading(true); + + newData = { + title: data?.title, + deskripsi: data?.deskripsi, + }; + + if (image && image?.uri) { + const uploadNewImage = await uploadFileService({ + dirId: DIRECTORY_ID.donasi_kabar, + imageUri: image?.uri, + }); + + newData = { + title: data?.title, + deskripsi: data?.deskripsi, + newImageId: uploadNewImage.data.id, + }; + } + + const response = await apiDonationUpdateNews({ + id: news as string, + data: newData, + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal mengupdate berita", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Berita berhasil diperbarui", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + + { + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); + }} + icon="upload" + > + Upload + + + setData({ ...data, title: value })} + /> + setData({ ...data, deskripsi: value })} + /> + + + { + handlerSubmitUpdate(); + }} + > + Update + + + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/(news)/[news]/index.tsx b/app/(application)/(user)/donation/[id]/(news)/[news]/index.tsx new file mode 100644 index 0000000..8fa1f70 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(news)/[news]/index.tsx @@ -0,0 +1,144 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AlertDefaultSystem, + BackButton, + BaseBox, + DotButton, + DrawerCustom, + DummyLandscapeImage, + MenuDrawerDynamicGrid, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IconEdit } from "@/components/_Icon"; +import { IconTrash } from "@/components/_Icon/IconTrash"; +import { useAuth } from "@/hooks/use-auth"; +import { + apiDonationDeleteNews, + apiDonationGetNewsById, +} from "@/service/api-client/api-donation"; +import { formatChatTime } from "@/utils/formatChatTime"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function DonationNews() { + const { user } = useAuth(); + const { news } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [data, setData] = useState(null); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [news]) + ); + + const onLoadData = async () => { + try { + const response = await apiDonationGetNewsById({ + id: news as string, + category: "get-one", + }); + + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + return ( + <> + , + headerRight: () => + user?.id === data?.authorId && ( + setOpenDrawer(true)} /> + ), + }} + /> + + + + + {formatChatTime(data?.createdAt)} + + + {data && data.imageId && ( + + )} + + + {data?.title || "-"} + + + {data?.deskripsi || "-"} + + + + + setOpenDrawer(false)} + height={"auto"} + > + , + label: "Edit Berita", + path: `/donation/[id]/(news)/${news}/edit-news` as any, + }, + { + icon: , + label: "Hapus Berita", + path: "", + color: "red", + }, + ]} + onPressItem={(item) => { + if ((item.path as any) === "") { + setOpenDrawer(false); + AlertDefaultSystem({ + title: "Hapus Berita", + message: "Apakah Anda yakin ingin menghapus berita ini?", + textLeft: "Batal", + textRight: "Hapus", + onPressRight: async () => { + const response = await apiDonationDeleteNews({ + id: news as string, + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal menghapus berita", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Berita berhasil dihapus", + }); + router.back(); + }, + }); + } else { + router.navigate(item.path as any); + setOpenDrawer(false); + } + }} + /> + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/(news)/add-news.tsx b/app/(application)/(user)/donation/[id]/(news)/add-news.tsx new file mode 100644 index 0000000..fc89f92 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(news)/add-news.tsx @@ -0,0 +1,132 @@ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiDonationCreateNews } from "@/service/api-client/api-donation"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function DonationAddNews() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + title: "", + deskripsi: "", + }); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + const handlerSubmit = async () => { + let newData: any = { ...data }; + try { + setLoading(true); + if (image) { + const responseUploadImage = await uploadFileService({ + dirId: DIRECTORY_ID.donasi_kabar, + imageUri: image?.uri, + }); + + newData = { + ...newData, + imageId: responseUploadImage.data.id, + }; + } + + const response = await apiDonationCreateNews({ + id: id as string, + data: newData, + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal menambah berita", + }); + + return + } + + Toast.show({ + type: "success", + text1: "Berita berhasil ditambahkan", + }); + + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + + { + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); + }} + icon="upload" + > + Upload + + + { + setData({ + ...data, + title: value, + }); + }} + /> + { + setData({ + ...data, + deskripsi: value, + }); + }} + /> + + + { + handlerSubmit(); + }} + > + Simpan + + + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/(news)/list-of-news.tsx b/app/(application)/(user)/donation/[id]/(news)/list-of-news.tsx new file mode 100644 index 0000000..c7471ef --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(news)/list-of-news.tsx @@ -0,0 +1,110 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + BaseBox, + DrawerCustom, + Grid, + LoaderCustom, + MenuDrawerDynamicGrid, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IconPlus } from "@/components/_Icon"; +import { apiDonationGetNewsById } from "@/service/api-client/api-donation"; +import { formatChatTime } from "@/utils/formatChatTime"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function DonationRecapOfNews() { + const { id } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadList(); + }, [id]) + ); + + const onLoadList = async () => { + try { + setLoadList(true); + const response = await apiDonationGetNewsById({ + id: id as string, + category: "get-all", + }); + + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + setList([]); + } finally { + setLoadList(false); + } + }; + + return ( + <> + , + }} + /> + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + + Tidak ada kabar + + ) : ( + list?.map((item: any, index: number) => ( + + + + + {item?.title || "-"} + + + + + {formatChatTime(item?.createdAt)} + + + + + )) + )} + + + setOpenDrawer(false)} + height={"auto"} + > + , + label: "Tambah Berita", + path: `/donation/${id}/(news)/add-news`, + }, + ]} + onPressItem={(item) => { + console.log("PATH ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/(news)/recap-of-news.tsx b/app/(application)/(user)/donation/[id]/(news)/recap-of-news.tsx new file mode 100644 index 0000000..b27f75a --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(news)/recap-of-news.tsx @@ -0,0 +1,112 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + BaseBox, + DotButton, + DrawerCustom, + Grid, + LoaderCustom, + MenuDrawerDynamicGrid, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IconPlus } from "@/components/_Icon"; +import { apiDonationGetNewsById } from "@/service/api-client/api-donation"; +import { formatChatTime } from "@/utils/formatChatTime"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function DonationRecapOfNews() { + const { id } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadList(); + }, [id]) + ); + + const onLoadList = async () => { + try { + setLoadList(true); + const response = await apiDonationGetNewsById({ + id: id as string, + category: "get-all", + }); + + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + setList([]); + } finally { + setLoadList(false); + } + }; + + return ( + <> + , + headerRight: () => setOpenDrawer(true)} />, + }} + /> + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + + Tidak ada kabar + + ) : ( + list?.map((item: any, index: number) => ( + + + + + {item?.title || "-"} + + + + + {formatChatTime(item?.createdAt)} + + + + + )) + )} + + + setOpenDrawer(false)} + height={"auto"} + > + , + label: "Tambah Berita", + path: `/donation/${id}/(news)/add-news`, + }, + ]} + onPressItem={(item) => { + console.log("PATH ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/failed.tsx b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/failed.tsx new file mode 100644 index 0000000..f2c1d50 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/failed.tsx @@ -0,0 +1,83 @@ +import { + BaseBox, + Grid, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { GStyles } from "@/styles/global-styles"; +import { FontAwesome6 } from "@expo/vector-icons"; +import dayjs from "dayjs"; + +export default function DonasiFailed() { + return ( + + + + + + Transaksi anda gagal karena bukti transfer tidak sesuai dengan + data kami. Jika ini masalah khusus silahkan hubungi pada kontak + whatsapp kami. + + + + + + + + + Detail Transaksi + + + + + + {listData.map((item, i) => ( + + + {item.label} + + + + {item.value} + + + + ))} + + + + + ); +} + +const listData = [ + { + label: "Bank", + value: " BCA", + }, + { + label: "Rekening Penerima", + value: "Himpunan Pengusaha Muda Indonesia", + }, + { + label: "No Rekening", + value: "2304235678854332", + }, + { + label: "Jumlah Donasi", + value: "Rp. 750.000", + }, + { + label: "Tanggal", + value: `${dayjs(new Date()).format("DD/MM/YYYY")}`, + }, +]; diff --git a/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/invoice.tsx b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/invoice.tsx new file mode 100644 index 0000000..597d10c --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/invoice.tsx @@ -0,0 +1,222 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + ButtonCenteredOnly, + ButtonCustom, + Grid, + InformationBox, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import CopyButton from "@/components/Button/CoyButton"; +import { MainColor } from "@/constants/color-palet"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { + apiDonationGetInvoiceById, + apiDonationUpdateInvoice, +} from "@/service/api-client/api-donation"; +import { uploadFileService } from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import { View } from "react-native"; +import Toast from "react-native-toast-message"; + +export default function DonationInvoice() { + const { invoiceId } = useLocalSearchParams(); + console.log("invoiceId", invoiceId); + const [data, setData] = useState(null); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [invoiceId]) + ); + + const onLoadData = async () => { + try { + const response = await apiDonationGetInvoiceById({ + id: invoiceId as string, + }); + console.log("[RESPONSE]", JSON.stringify(response, null, 2)); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlerUpdateInvoice = async () => { + try { + setLoading(true); + const responseUploadImage = await uploadFileService({ + dirId: DIRECTORY_ID.donasi_bukti_transfer, + imageUri: image?.uri, + }); + + console.log("[RESPONSE UPLOAD IMAGE]", responseUploadImage); + + if (!responseUploadImage?.data?.id) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah bukti transfer", + }); + return; + } + + const fileId = responseUploadImage?.data?.id; + + const response = await apiDonationUpdateInvoice({ + id: invoiceId as string, + fileId: fileId, + status: "proses", + }); + + console.log("[RESPONSE UPDATE]", JSON.stringify(response, null, 2)); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah bukti transfer", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Berhasil mengunggah bukti transfer", + }); + router.replace(`/donation/[id]/(transaction-flow)/${invoiceId}/process`); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + return ( + <> + + + + + + + BANK: {data?.DonasiMaster_Bank?.name} + + {/* {data?.DonasiMaster_Bank?.accountName} */} + + + + + + + {data?.DonasiMaster_Bank?.norek} + + + + + + + + + + + + + Jumlah Transaksi + + + + + + + + Rp. {formatCurrencyDisplay(data?.nominal) || "-"} + + + + + + + + + + + + + + Upload bukti transfer anda. + + {image ? ( + + + {image?.name} + + + ) : null} + { + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); + }} + icon="upload" + > + Upload + + + + + { + handlerUpdateInvoice(); + }} + > + Simpan + + + + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/process.tsx b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/process.tsx new file mode 100644 index 0000000..d08401b --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/process.tsx @@ -0,0 +1,41 @@ +import { BaseBox, StackCustom, TextCustom, ViewWrapper } from "@/components"; +import MoneyTransferAnimation from "@/components/_ShareComponent/MoneyTransferAnimation"; +import { View } from "react-native"; + +export default function DonationProcess() { + return ( + <> + + + + + Admin sedang memvalidasi data dan bukti transfer anda. Mohon + tunggu proses ini selesai. + + + + + + + + {/* + + + + Hubungi admin jika tidak kunjung di proses! Klik pada logo + Whatsapp ini. + + + + + + + */} + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/success.tsx b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/success.tsx new file mode 100644 index 0000000..41f7a9b --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/success.tsx @@ -0,0 +1,83 @@ +import { + BaseBox, + Grid, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { GStyles } from "@/styles/global-styles"; +import { FontAwesome6 } from "@expo/vector-icons"; +import dayjs from "dayjs"; + +export default function DonationSuccess() { + return ( + + + + + + + + Terimakasih telah percaya pada kami untuk mengelola dana anda! + Info mengenai update Penggalian Dana ini bisa di lihat di kolom + berita. + + + + + + + Detail Transaksi + + + + + + {listData.map((item, i) => ( + + + {item.label} + + + + {item.value} + + + + ))} + + + + + ); +} + +const listData = [ + { + label: "Bank", + value: " BCA", + }, + { + label: "Rekening Penerima", + value: "Himpunan Pengusaha Muda Indonesia", + }, + { + label: "No Rekening", + value: "2304235678854332", + }, + { + label: "Jumlah Donasi", + value: "Rp. 750.000", + }, + { + label: "Tanggal", + value: `${dayjs(new Date()).format("DD/MM/YYYY")}`, + }, +]; diff --git a/app/(application)/(user)/donation/[id]/(transaction-flow)/index.tsx b/app/(application)/(user)/donation/[id]/(transaction-flow)/index.tsx new file mode 100644 index 0000000..8b65b47 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(transaction-flow)/index.tsx @@ -0,0 +1,112 @@ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCustom, + Grid, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import { LOCAL_STORAGE_KEY } from "@/constants/local-storage-key"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import { Ionicons } from "@expo/vector-icons"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; + +export default function InvestmentInputDonation() { + const { id } = useLocalSearchParams(); + const [nominal, setNominal] = useState(0); + + const handlerSubmit = async () => { + try { + await AsyncStorage.setItem( + LOCAL_STORAGE_KEY.transactionDonation, + JSON.stringify({ nominal: nominal.toString() }) + ); + router.replace(`/donation/${id}/select-bank`); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const displayJumlah = formatCurrencyDisplay(nominal); + + const handleChangeCurrency = (text: string) => { + const numeric = text.replace(/\D/g, ""); + setNominal(Number(numeric)); + }; + + const bottomComponent = ( + + { + handlerSubmit(); + }} + > + Lanjutan + + + ); + return ( + <> + + {listData.map((item, i) => ( + setNominal(item.value)}> + + + + Rp. {item.label} + + + + + + + + ))} + + + handleChangeCurrency(value)} + /> + + Minimal donasi Rp. 10.000 + + + + + ); +} + +const listData = [ + { + label: "25.000", + value: 25000, + }, + { + label: "50.000", + value: 50000, + }, + { + label: "100.000", + value: 100000, + }, + { + label: "250.000", + value: 250000, + }, +]; diff --git a/app/(application)/(user)/donation/[id]/(transaction-flow)/select-bank.tsx b/app/(application)/(user)/donation/[id]/(transaction-flow)/select-bank.tsx new file mode 100644 index 0000000..558bee0 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/(transaction-flow)/select-bank.tsx @@ -0,0 +1,106 @@ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCustom, + ViewWrapper, +} from "@/components"; +import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom"; +import { LOCAL_STORAGE_KEY } from "@/constants/local-storage-key"; +import { useAuth } from "@/hooks/use-auth"; +import { apiDonationCreateInvoice } from "@/service/api-client/api-donation"; +import { apiMasterBank } from "@/service/api-client/api-master"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { router, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; + +export default function DonationSelectBank() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + const [select, setSelect] = useState(""); + const [listBank, setListBank] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + loadListBank(); + }, []); + + const loadListBank = async () => { + try { + const response = await apiMasterBank(); + + setListBank(response.data); + } catch (error) { + console.log("[ERROR]", error); + setListBank([]); + } + }; + + const handlerSubmit = async () => { + try { + setIsLoading(true); + const dataStorage = await AsyncStorage.getItem( + LOCAL_STORAGE_KEY.transactionDonation + ); + + if (dataStorage) { + const storage = JSON.parse(dataStorage); + const newData = { + ...storage, + bankId: select, + authorId: user?.id, + }; + + const response = await apiDonationCreateInvoice({ + id: id as string, + data: newData, + }); + + if (response.success) { + const invoiceId = response.data.id; + + await AsyncStorage.removeItem(LOCAL_STORAGE_KEY.transactionDonation); + + router.replace( + `/(application)/(user)/donation/[id]/(transaction-flow)/${invoiceId}/invoice` + ); + } else { + console.log("[FAILED]", response); + } + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = () => { + return ( + <> + + handlerSubmit()} + > + Pilih + + + + ); + }; + return ( + + + {_.isEmpty(listBank) + ? [] + : listBank?.map((item: any, index: number) => ( + + + + ))} + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/[status]/detail.tsx b/app/(application)/(user)/donation/[id]/[status]/detail.tsx new file mode 100644 index 0000000..ca9a775 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/[status]/detail.tsx @@ -0,0 +1,152 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + DotButton, + DrawerCustom, + MenuDrawerDynamicGrid, + Spacing, + ViewWrapper, +} from "@/components"; +import { IconEdit, IconNews } from "@/components/_Icon"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import Donation_ButtonStatusSection from "@/screens/Donation/ButtonStatusSection"; +import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData"; +import Donation_ComponentStoryFunrising from "@/screens/Donation/ComponentStoryFunrising"; +import Donation_ProgressSection from "@/screens/Donation/ProgressSection"; +import { apiDonationGetOne } from "@/service/api-client/api-donation"; +import { FontAwesome6 } from "@expo/vector-icons"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function DonasiDetailStatus() { + const { id, status } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [openDrawerPublish, setOpenDrawerPublish] = useState(false); + + const [data, setData] = useState(); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiDonationGetOne({ + id: id as string, + category: "permanent", + }); + + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlePress = (item: IMenuDrawerItem) => { + console.log("PATH ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }; + + return ( + <> + , + headerRight: () => + status === "draft" ? ( + setOpenDrawer(true)} /> + ) : status === "publish" ? ( + setOpenDrawerPublish(true)} /> + ) : null, + }} + /> + + + ) + } + /> + + + + + + + setOpenDrawer(false)} + height={"auto"} + > + , + label: "Edit Donasi", + path: `/donation/${id}/edit`, + }, + { + icon: , + label: "Edit Cerita", + path: `/donation/${id}/edit-story`, + }, + { + icon: ( + + ), + label: "Edit Rekening", + path: `/donation/${id}/edit-rekening`, + }, + ]} + columns={4} + onPressItem={handlePress as any} + /> + + + setOpenDrawerPublish(false)} + height={"auto"} + > + , + label: "Rekap Kabar", + path: `/donation/${id}/(news)/recap-of-news`, + }, + ]} + onPressItem={(item) => { + console.log("PATH ", item.path); + router.navigate(item.path as any); + setOpenDrawerPublish(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/detail-story.tsx b/app/(application)/(user)/donation/[id]/detail-story.tsx new file mode 100644 index 0000000..051a15c --- /dev/null +++ b/app/(application)/(user)/donation/[id]/detail-story.tsx @@ -0,0 +1,43 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + DummyLandscapeImage, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { apiDonationGetOne } from "@/service/api-client/api-donation"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; + +export default function DonationDetailStory() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiDonationGetOne({ + id: id as string, + category: "permanent", + }); + + setData(response.data.CeritaDonasi); + } catch (error) { + console.log("[ERROR]", error); + } + }; + return ( + + + {data?.pembukaan || "-"} + + {data?.cerita || "-"} + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/edit-rekening.tsx b/app/(application)/(user)/donation/[id]/edit-rekening.tsx new file mode 100644 index 0000000..7e71a1e --- /dev/null +++ b/app/(application)/(user)/donation/[id]/edit-rekening.tsx @@ -0,0 +1,110 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ViewWrapper, + StackCustom, + InformationBox, + TextInputCustom, + Spacing, + ButtonCustom, +} from "@/components"; +import { + apiDonationGetOne, + apiDonationUpdateData, +} from "@/service/api-client/api-donation"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function DonationEditRekening() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + namaBank: "", + rekening: "", + }); + const [isLoading, setLoading] = useState(false); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + const response = await apiDonationGetOne({ + id: id as string, + category: "permanent", + }); + + const resData = response.data; + console.log("[RESPONSE]", JSON.stringify(resData, null, 2)); + + setData({ + namaBank: resData.namaBank, + rekening: resData.rekening, + }); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlerSubmitUpdate = async () => { + try { + setLoading(true); + + const response = await apiDonationUpdateData({ + id: id as string, + data: data, + category: "edit-bank-account", + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal mengupdate data bank", + }); + } + + Toast.show({ + type: "success", + text1: "Data bank berhasil diupdate", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + setData({ ...data, namaBank: value })} + /> + setData({ ...data, rekening: value })} + /> + + + { + handlerSubmitUpdate(); + }} + > + Update + + + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/edit-story.tsx b/app/(application)/(user)/donation/[id]/edit-story.tsx new file mode 100644 index 0000000..b0aef59 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/edit-story.tsx @@ -0,0 +1,151 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + StackCustom, + TextAreaCustom, + ViewWrapper, +} from "@/components"; +import API_IMAGE from "@/constants/api-storage"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { + apiDonationGetOne, + apiDonationUpdateData, +} from "@/service/api-client/api-donation"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile from "@/utils/pickFile"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function DonationEditStory() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(); + const [imageStory, setImageStory] = useState(null); + const [isLoading, setLoading] = useState(false); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + const response = await apiDonationGetOne({ + id: id as string, + category: "permanent", + }); + + setData(response.data.CeritaDonasi); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlerSubmitUpdate = async () => { + let newData; + try { + setLoading(true); + + newData = { + ...data, + }; + + if (imageStory) { + const responseUploadImageDonasi = await uploadFileService({ + imageUri: imageStory, + dirId: DIRECTORY_ID.donasi_cerita_image, + }); + + newData = { + ...data, + newImageId: responseUploadImageDonasi.data.id, + }; + } + + const response = await apiDonationUpdateData({ + id: id as string, + data: newData, + category: "edit-story", + }); + + console.log("[RESPONSE]", JSON.stringify(response, null, 2)); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal membuat donasi", + }); + } + + Toast.show({ + type: "success", + text1: "Donasi berhasil disimpan", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + setData({ ...data, pembukaan: value })} + /> + + + { + pickFile({ + allowedType: "image", + setImageUri: ({ uri }) => { + setImageStory(uri); + }, + }); + }} + icon="upload" + > + Upload + + + setData({ ...data, cerita: value })} + /> + + + { + handlerSubmitUpdate(); + }} + > + Update + + + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/edit.tsx b/app/(application)/(user)/donation/[id]/edit.tsx new file mode 100644 index 0000000..55a5206 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/edit.tsx @@ -0,0 +1,276 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + LoaderCustom, + SelectCustom, + Spacing, + StackCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import API_IMAGE from "@/constants/api-storage"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { + apiDonationGetOne, + apiDonationUpdateData, +} from "@/service/api-client/api-donation"; +import { apiMasterDonation } from "@/service/api-client/api-master"; +import { uploadFileService } from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +interface IEditDonation { + donasiMaster_KategoriId: string; + donasiMaster_DurasiId: string; + title: string; + target: string; + imageId: string; +} + +export default function DonationEdit() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + donasiMaster_DurasiId: "", + donasiMaster_KategoriId: "", + title: "", + target: "", + imageId: "", + }); + + const [image, setImage] = useState(null); + const [listCategory, setListCategory] = useState([]); + const [listDuration, setListDuration] = useState([]); + const [loadList, setLoadList] = useState(false); + const [isLoading, setLoading] = useState(false); + + const displayTarget = formatCurrencyDisplay(data?.target); + const handleChangeCurrency = (field: keyof typeof data) => (text: string) => { + const numeric = text.replace(/\D/g, ""); + setData((prev: any) => ({ ...prev, [field]: numeric })); + }; + + useFocusEffect( + useCallback(() => { + onLoadData(); + onLoadList(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiDonationGetOne({ + id: id as string, + category: "permanent", + }); + + if (response.success) { + setData({ + donasiMaster_DurasiId: response.data.donasiMaster_DurasiId, + donasiMaster_KategoriId: response.data.donasiMaster_KategoriId, + title: response.data.title, + target: response.data.target, + imageId: response.data.imageId, + }); + } + + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const onLoadList = async () => { + try { + setLoadList(true); + const response = await apiMasterDonation({ category: "" }); + + setListCategory(response.data.category); + setListDuration(response.data.duration); + } catch (error) { + console.log(["ERROR"], error); + setListCategory([]); + setListDuration([]); + } finally { + setLoadList(false); + } + }; + + const validateData = async () => { + if ( + !data.donasiMaster_DurasiId || + !data.donasiMaster_KategoriId || + !data.title || + !data.target || + !data.imageId + ) { + Toast.show({ + type: "error", + text1: "Harap lengkapi data", + }); + + return false; + } + + return true; + }; + + const handlerSubmitUpdate = async () => { + const isValid = await validateData(); + if (!isValid) { + return; + } + + try { + let newData; + + newData = { + ...data, + }; + setLoading(true); + + if (image && image) { + const uploadNewImage = await uploadFileService({ + dirId: DIRECTORY_ID.donasi_image, + imageUri: image, + }); + + if (!uploadFileService) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + newData = { + ...data, + newImageId: uploadNewImage.data.id, + }; + } + + const response = await apiDonationUpdateData({ + id: id as string, + data: newData, + category: "edit-donation", + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: response.message, + }); + return; + } + + Toast.show({ + type: "success", + text1: "Donasi berhasil diperbarui", + }); + + router.back(); + } catch (error) { + console.log("[ERROR UPDATE DONASI]", error); + } finally { + setLoading(false); + } + }; + + return ( + + + {!data || loadList ? ( + + ) : ( + + setData({ ...data, title: value })} + /> + + + + { + pickFile({ + setImageUri: ({ uri }) => { + setImage(uri); + }, + allowedType: "image", + }); + }} + icon="upload" + > + Upload + + + + ({ + label: item.name, + value: item.id, + })) + } + label="Pilih Kategori Donasi" + placeholder="Pilih Kategori Donasi" + required + value={data?.donasiMaster_KategoriId} + onChange={(value: any) => + setData({ ...data, donasiMaster_KategoriId: value }) + } + /> + + ({ + label: item.name + " hari", + value: item.id, + })) + } + label="Pilih Durasi Donasi" + placeholder="Pilih Durasi Donasi" + required + value={data?.donasiMaster_DurasiId} + onChange={(value: any) => + setData({ ...data, donasiMaster_DurasiId: value }) + } + /> + + + { + handlerSubmitUpdate(); + }} + > + Update + + + )} + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/fund-disbursement.tsx b/app/(application)/(user)/donation/[id]/fund-disbursement.tsx new file mode 100644 index 0000000..6f28714 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/fund-disbursement.tsx @@ -0,0 +1,67 @@ +import { + BaseBox, + ButtonCenteredOnly, + Grid, + InformationBox, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import dayjs from "dayjs"; +import { router, useLocalSearchParams } from "expo-router"; + +export default function DonationFundDisbursement() { + const { id } = useLocalSearchParams(); + return ( + <> + + + + + + + Rp. 0 + + Total Pencairan Dana + + + + 0 kali + + Akumulasi Pencairan + + + + + {Array.from({ length: 10 }).map((_, index) => ( + + + + + Pencairan ke - {index + 1} + + + {dayjs().format("DD MMM YYYY")} + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + Nesciunt dolor ad sit? Eaque rem nihil natus, id, esse possimus + perferendis provident velit illo consectetur distinctio ab + accusantium quis earum omnis! + + { + router.navigate(`/(application)/(file)/${id}`); + }} + icon="file-text" + > + Bukti Transaksi + + + + ))} + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/index.tsx b/app/(application)/(user)/donation/[id]/index.tsx new file mode 100644 index 0000000..8f2e5c9 --- /dev/null +++ b/app/(application)/(user)/donation/[id]/index.tsx @@ -0,0 +1,116 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + BoxButtonOnFooter, + ButtonCustom, + DotButton, + DrawerCustom, + MenuDrawerDynamicGrid, + StackCustom, + ViewWrapper, +} from "@/components"; +import { IconNews } from "@/components/_Icon"; +import { useAuth } from "@/hooks/use-auth"; +import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData"; +import Donation_ComponentInfoFundrising from "@/screens/Donation/ComponentInfoFundrising"; +import Donation_ComponentStoryFunrising from "@/screens/Donation/ComponentStoryFunrising"; +import Donation_ProgressSection from "@/screens/Donation/ProgressSection"; +import { apiDonationGetOne } from "@/service/api-client/api-donation"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import { useCallback, useState } from "react"; + +export default function DonasiDetailBeranda() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + console.log("ID ", id); + const [openDrawer, setOpenDrawer] = useState(false); + + const [data, setData] = useState(); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiDonationGetOne({ + id: id as string, + category: "permanent", + }); + + console.log("[RES GET ONE]", JSON.stringify(response.data, null, 2)); + + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const buttonSection = ( + <> + + router.navigate(`/donation/${id}/(transaction-flow)`)} + > + Donasi + + + + ); + + return ( + <> + , + headerRight: () => + user?.id === data?.Author?.id ? ( + setOpenDrawer(true)} /> + ) : null, + }} + /> + + + } + /> + + + + + + setOpenDrawer(false)} + height={"auto"} + > + , + label: "Rekap Kabar", + path: `/donation/${id}/(news)/recap-of-news`, + }, + ]} + onPressItem={(item) => { + console.log("PATH ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/infromation-fundrising.tsx b/app/(application)/(user)/donation/[id]/infromation-fundrising.tsx new file mode 100644 index 0000000..38fe78a --- /dev/null +++ b/app/(application)/(user)/donation/[id]/infromation-fundrising.tsx @@ -0,0 +1,86 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarComp, + BaseBox, + ButtonCustom, + Grid, + LoaderCustom, + Spacing, + TextCustom, + ViewWrapper +} from "@/components"; +import Donation_BoxPublish from "@/screens/Donation/BoxPublish"; +import { apiDonationFundrising } from "@/service/api-client/api-donation"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import React, { useCallback, useState } from "react"; +import { View } from "react-native"; + +export default function DonationInformationFunrising() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(); + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + setLoadList(true); + const response = await apiDonationFundrising({ id: id as string }); + + setData(response?.data?.user); + setList(response?.data?.donasi); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadList(false); + } + }; + + return ( + <> + + + + + + + + @{data?.username} + + + + + + Kunjungi Profile + + + + + + + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + Belum ada data + ) : ( + list?.map((item: any, index: number) => ( + + )) + )} + + + ); +} diff --git a/app/(application)/(user)/donation/[id]/list-of-donatur.tsx b/app/(application)/(user)/donation/[id]/list-of-donatur.tsx new file mode 100644 index 0000000..d9f65af --- /dev/null +++ b/app/(application)/(user)/donation/[id]/list-of-donatur.tsx @@ -0,0 +1,47 @@ +import { + BaseBox, + Grid, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { FontAwesome6 } from "@expo/vector-icons"; +import dayjs from "dayjs"; + +export default function Donation_ListOfDonatur() { + return ( + <> + + {Array.from({ length: 10 }).map((_, index) => ( + + + + + + + + + Username + + Berdonas sebesar + + Rp. 100.000 + + {dayjs().format("DD MMM YYYY")} + + + + + ))} + + + ); +} diff --git a/app/(application)/(user)/donation/create-story.tsx b/app/(application)/(user)/donation/create-story.tsx new file mode 100644 index 0000000..35c3148 --- /dev/null +++ b/app/(application)/(user)/donation/create-story.tsx @@ -0,0 +1,183 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { useAuth } from "@/hooks/use-auth"; +import { + apiDonationCreate, + apiDonationGetOne, +} from "@/service/api-client/api-donation"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile from "@/utils/pickFile"; +import { router, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function DonationCreateStory() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + const [temporary, setTemporary] = useState(); + const [data, setData] = useState({ + pembukaan: "", + cerita: "", + namaBank: "", + rekening: "", + }); + const [imageStory, setImageStory] = useState(null); + const [isLoading, setLoading] = useState(false); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + const response = await apiDonationGetOne({ + id: id as string, + category: "temporary", + }); + + setTemporary(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlerSubmit = async () => { + if (_.values(data).includes("")) { + Toast.show({ + type: "error", + text1: "Harap isi semua data", + }); + return; + } + + try { + setLoading(true); + const responseUploadImageDonasi = await uploadFileService({ + imageUri: imageStory, + dirId: DIRECTORY_ID.donasi_cerita_image, + }); + + const newData = { + // Data Donasi + temporaryId: temporary?.id, + authorId: user?.id, + title: temporary?.title, + target: temporary?.target, + donasiMaster_KategoriId: temporary?.donasiMaster_KategoriId, + donasiMaster_DurasiId: temporary?.donasiMaster_DurasiId, + imageId: temporary?.imageId, + // Data Bank + namaBank: data.namaBank, + rekening: data.rekening, + // Data Cerita + imageCeritaId: responseUploadImageDonasi.data.id, + pembukaan: data.pembukaan, + cerita: data.cerita, + }; + + const response = await apiDonationCreate({ + data: newData, + category: "permanent", + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal membuat donasi", + }); + } + + Toast.show({ + type: "success", + text1: "Donasi berhasil disimpan", + }); + router.replace("/donation/status"); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + return ( + + + + setData({ ...data, pembukaan: value })} + /> + setData({ ...data, cerita: value })} + /> + + + { + pickFile({ + allowedType: "image", + setImageUri: ({ uri }) => { + setImageStory(uri); + }, + }); + }} + icon="upload" + > + Upload + + + + + setData({ ...data, namaBank: value })} + /> + setData({ ...data, rekening: value })} + /> + + + { + handlerSubmit(); + }} + > + Simpan + + + + + ); +} diff --git a/app/(application)/(user)/donation/create.tsx b/app/(application)/(user)/donation/create.tsx new file mode 100644 index 0000000..860de52 --- /dev/null +++ b/app/(application)/(user)/donation/create.tsx @@ -0,0 +1,220 @@ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + LoaderCustom, + SelectCustom, + Spacing, + StackCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiDonationCreate } from "@/service/api-client/api-donation"; +import { apiMasterDonation } from "@/service/api-client/api-master"; +import { uploadFileService } from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile from "@/utils/pickFile"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function DonationCreate() { + const [listCategory, setListCategory] = useState([]); + const [listDuration, setListDuration] = useState([]); + const [loadList, setLoadList] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [image, setImage] = useState(null); + const [data, setData] = useState({ + kategoriId: "", + title: "", + target: "", + durasiId: "", + }); + + const displayTarget = formatCurrencyDisplay(data.target); + const handleChangeCurrency = (field: keyof typeof data) => (text: string) => { + const numeric = text.replace(/\D/g, ""); + setData((prev) => ({ ...prev, [field]: numeric })); + }; + + useFocusEffect( + useCallback(() => { + onLoadList(); + }, []) + ); + + const onLoadList = async () => { + try { + setLoadList(true); + const response = await apiMasterDonation({ category: "" }); + + setListCategory(response.data.category); + setListDuration(response.data.duration); + } catch (error) { + console.log(["ERROR"], error); + setListCategory([]); + setListDuration([]); + } finally { + setLoadList(false); + } + }; + + const validateData = () => { + if (!data.title || !data.target || !data.durasiId || !data.kategoriId) { + Toast.show({ + type: "error", + text1: "Harap isi semua data", + }); + return false; + } + + return true; + }; + + const handlerSubmit = async () => { + if (!validateData()) { + return; + } + try { + setIsLoading(true); + const responseUploadImage = await uploadFileService({ + imageUri: image, + dirId: DIRECTORY_ID.donasi_image, + }); + + if (!responseUploadImage.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUploadImage.data.id; + + const newData = { + title: data.title, + target: data.target, + durasiId: data.durasiId, + kategoriId: data.kategoriId, + imageId: imageId, + }; + + const response = await apiDonationCreate({ + data: newData, + category: "temporary", + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal membuat donasi", + }); + return; + } + + router.push(`/donation/create-story?id=${response.data.id}`); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + setData({ ...data, title: value })} + /> + + + + { + pickFile({ + allowedType: "image", + setImageUri: ({ uri }) => { + setImage(uri); + }, + }); + }} + icon="upload" + > + Upload + + + + {loadList ? ( + + ) : ( + ({ + label: item.name, + value: item.id, + })) + } + label="Pilih Kategori Donasi" + placeholder="Pilih Kategori Donasi" + required + value={data.kategoriId} + onChange={(value: any) => setData({ ...data, kategoriId: value })} + /> + )} + + {loadList ? ( + + ) : ( + ({ + label: item.name + `${" hari"}`, + value: item.id, + })) + } + label="Pilih Durasi Donasi" + placeholder="Pilih Durasi Donasi" + required + value={data.durasiId} + onChange={(value: any) => setData({ ...data, durasiId: value })} + /> + )} + + + { + handlerSubmit(); + // router.push(`/donation/create-story?id=${"dasdsadsa"}`); + }} + > + Selanjutnya + + + + + + ); +} diff --git a/app/(application)/(user)/event/(tabs)/_layout.tsx b/app/(application)/(user)/event/(tabs)/_layout.tsx new file mode 100644 index 0000000..835ac21 --- /dev/null +++ b/app/(application)/(user)/event/(tabs)/_layout.tsx @@ -0,0 +1,43 @@ +import { + IconContribution, + IconHistory, + IconHome, + IconStatus, +} from "@/components/_Icon"; +import { TabsStyles } from "@/styles/tabs-styles"; +import { Tabs } from "expo-router"; + +export default function EventTabsLayout() { + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ); +} diff --git a/app/(application)/(user)/event/(tabs)/contribution.tsx b/app/(application)/(user)/event/(tabs)/contribution.tsx new file mode 100644 index 0000000..8a5266b --- /dev/null +++ b/app/(application)/(user)/event/(tabs)/contribution.tsx @@ -0,0 +1,115 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarUsernameAndOtherComponent, + BoxWithHeaderSection, + LoaderCustom, + Spacing, + StackCustom, + TextCustom, + ViewWrapper +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { + apiEventGetAll +} from "@/service/api-client/api-event"; +import { dateTimeView } from "@/utils/dateTimeView"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import React, { useCallback, useState } from "react"; + +export default function EventContribution() { + const { user } = useAuth(); + const [listData, setListData] = useState([]); + const [isLoadList, setIsLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id]) + ); + + async function onLoadData() { + try { + setIsLoadList(true); + const response = await apiEventGetAll({ + category: "contribution", + userId: user?.id, + }); + console.log("[DATA] ", JSON.stringify(response.data, null, 2)); + if (response.success) { + setListData(response.data); + + // const responseListParticipants = await apiEventListOfParticipants({ + // id: response?.data?.Event?.id, + // }); + // console.log( + // "[LIST PARTICIPANTS]", + // JSON.stringify(responseListParticipants.data, null, 2) + // ); + // if (responseListParticipants.success) { + // setListParticipants(responseListParticipants.data); + // } + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadList(false); + } + } + + return ( + + {isLoadList ? ( + + ) : _.isEmpty(listData) ? ( + Belum ada kontribusi + ) : ( + listData.map((item: any, index: number) => ( + + + + {dateTimeView({ + date: item?.Event?.tanggal, + withoutTime: true, + })} + + } + /> + + + {item?.Event?.title} + + + + {/* + {item?.Event?.Event_Peserta?.map( + (item2: any, index2: number) => ( + + + + ) + )} + */} + + + )) + )} + + ); +} diff --git a/app/(application)/(user)/event/(tabs)/history.tsx b/app/(application)/(user)/event/(tabs)/history.tsx new file mode 100644 index 0000000..661f68b --- /dev/null +++ b/app/(application)/(user)/event/(tabs)/history.tsx @@ -0,0 +1,104 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components"; +import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection"; +import { apiEventGetAll } from "@/service/api-client/api-event"; +import { dateTimeView } from "@/utils/dateTimeView"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import { View } from "react-native"; + +export default function EventHistory() { + const [activeCategory, setActiveCategory] = useState("all"); + const { user } = useAuth(); + const [listData, setListData] = useState([]); + const [isLoadList, setIsLoadList] = useState(false); + + useEffect(() => { + onLoadData({ userId: user?.id }); + }, [user?.id, activeCategory]); + + async function onLoadData({ userId }: { userId?: string }) { + try { + setIsLoadList(true); + const response = await apiEventGetAll({ + category: activeCategory === "all" ? "all-history" : "my-history", + userId: userId, + }); + if (response.success) { + setListData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadList(false); + } + } + + const handlePress = (item: any) => { + setActiveCategory(item); + // tambahkan logika lain seperti filter dsb. + }; + + const headerComponent = ( + + handlePress("all")} + > + Semua Riwayat + + + handlePress("main")} + > + Riwayat Saya + + + ); + + return ( + + {isLoadList ? ( + + ) : _.isEmpty(listData) ? ( + Belum ada riwayat + ) : ( + listData.map((item: any, index: number) => ( + + {dateTimeView({ date: item?.tanggal, withoutTime: true })} + + } + href={`/event/${item.id}/history`} + /> + )) + )} + + ); +} diff --git a/app/(application)/(user)/event/(tabs)/index.tsx b/app/(application)/(user)/event/(tabs)/index.tsx new file mode 100644 index 0000000..1e8463f --- /dev/null +++ b/app/(application)/(user)/event/(tabs)/index.tsx @@ -0,0 +1,63 @@ +import { LoaderCustom, TextCustom } from "@/components"; +import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; +import FloatingButton from "@/components/Button/FloatingButton"; +import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection"; +import { apiEventGetAll } from "@/service/api-client/api-event"; +import { dateTimeView } from "@/utils/dateTimeView"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function EventBeranda() { + const [listData, setListData] = useState([]); + const [isLoadData, setIsLoadData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, []) + ); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiEventGetAll({category: "beranda"}); + // console.log("Response", JSON.stringify(response.data, null, 2)); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + return ( + router.push("/event/create")} /> + } + > + {isLoadData ? ( + + ) : _.isEmpty(listData) ? ( + Belum ada event + ) : ( + listData.map((item: any, index) => ( + + + + {dateTimeView({ date: item?.tanggal, withoutTime: true })} + + } + /> + )) + )} + + ); +} diff --git a/app/(application)/(user)/event/(tabs)/status.tsx b/app/(application)/(user)/event/(tabs)/status.tsx new file mode 100644 index 0000000..e1c823d --- /dev/null +++ b/app/(application)/(user)/event/(tabs)/status.tsx @@ -0,0 +1,99 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BoxWithHeaderSection, + Grid, + LoaderCustom, + ScrollableCustom, + StackCustom, + TextCustom, +} from "@/components"; +import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; +import { useAuth } from "@/hooks/use-auth"; +import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; +import { apiEventGetByStatus } from "@/service/api-client/api-event"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function EventStatus() { + const { user } = useAuth(); + const id = user?.id || ""; + const [activeCategory, setActiveCategory] = useState( + "publish" + ); + const [listData, setListData] = useState([]); + const [loadingGetData, setLoadingGetData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [activeCategory, id]) + ); + + async function onLoadData() { + try { + setLoadingGetData(true); + const response = await apiEventGetByStatus({ + id: id!, + status: activeCategory!, + }); + // console.log("Response", JSON.stringify(response.data, null, 2)); + setListData(response.data); + } catch (error) { + console.log(error); + } finally { + setLoadingGetData(false); + } + } + + const handlePress = (item: any) => { + setActiveCategory(item.value); + // tambahkan logika lain seperti filter dsb. + }; + + const tabsComponent = ( + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as any} + /> + ); + + return ( + + {loadingGetData ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada data {activeCategory} + ) : ( + listData.map((item: any, i) => ( + + + + + + {item?.title} + + + + + {new Date(item?.tanggal).toLocaleDateString()} + + + + + {item?.deskripsi} + + + )) + )} + + ); +} diff --git a/app/(application)/(user)/event/[id]/[status]/detail-event.tsx b/app/(application)/(user)/event/[id]/[status]/detail-event.tsx new file mode 100644 index 0000000..f84cb03 --- /dev/null +++ b/app/(application)/(user)/event/[id]/[status]/detail-event.tsx @@ -0,0 +1,130 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + DotButton, + DrawerCustom, + Grid, + MenuDrawerDynamicGrid, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import LeftButtonCustom from "@/components/Button/BackButton"; +import Event_ButtonStatusSection from "@/screens/Event/ButtonStatusSection"; +import { menuDrawerDraftEvent } from "@/screens/Event/menuDrawerDraft"; +import { apiEventGetOne } from "@/service/api-client/api-event"; +import { dateTimeView } from "@/utils/dateTimeView"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import { useCallback, useState } from "react"; + +export default function EventDetailStatus() { + const { id, status } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + // const [openAlert, setOpenAlert] = useState(false); + + const [data, setData] = useState(); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + async function onLoadData() { + try { + const response = await apiEventGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + } + + const listData = [ + { + title: "Lokasi", + value: data?.lokasi || "-", + }, + { + title: "Tipe Acara", + value: data?.EventMaster_TipeAcara?.name || "-", + }, + { + title: "Tanggal Mulai", + value: dateTimeView({ date: data?.tanggal }) || "-", + }, + { + title: "Tanggal Berakhir", + value: dateTimeView({ date: data?.tanggalSelesai }) || "-", + }, + { + title: "Deskripsi", + value: data?.deskripsi || "-", + }, + ]; + + const handlePress = (item: IMenuDrawerItem) => { + console.log("PATH >> ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }; + + return ( + <> + , + headerRight: () => + status === "draft" ? ( + setOpenDrawer(true)} /> + ) : null, + }} + /> + + + + + {data?.title || "-"} + + {listData.map((item, index) => ( + + + {item.title} + + + {item.value} + + + ))} + + + + + + + setOpenDrawer(false)} + height={"auto"} + > + + + + ); +} diff --git a/app/(application)/(user)/event/[id]/confirmation.tsx b/app/(application)/(user)/event/[id]/confirmation.tsx new file mode 100644 index 0000000..fb73251 --- /dev/null +++ b/app/(application)/(user)/event/[id]/confirmation.tsx @@ -0,0 +1,554 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + ButtonCustom, + CenterCustom, + LoaderCustom, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import { + apiEventConfirmationAction, + apiEventGetConfirmation, + apiEventJoin, +} from "@/service/api-client/api-event"; +import { Ionicons } from "@expo/vector-icons"; +import dayjs from "dayjs"; +import isBetween from "dayjs/plugin/isBetween"; +import { + Redirect, + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import React, { useCallback, useState } from "react"; +import { View } from "react-native"; +import Toast from "react-native-toast-message"; + +// Extend Day.js dengan plugin isBetween +dayjs.extend(isBetween); + +interface DataEvent { + id: string; + title: string; + tanggal: Date; + tanggalSelesai: Date; + lokasi: string; + Author: { + id: string; + username: string; + Profile: { + id: string; + name: string; + }; + }; +} + +export default function UserEventConfirmation() { + const { token } = useAuth(); + const { id, userId: authorId } = useLocalSearchParams(); + const { user } = useAuth(); + const [data, setData] = useState(null); + const [peserta, setPeserta] = useState(null); + const [konfirmasi, setKonfirmasi] = useState(null); + + useFocusEffect( + useCallback(() => { + checkTokenAndDataParticipants() || console.log("Token is null"); + }, [token, id, user?.id]) + ); + + const checkTokenAndDataParticipants = async () => { + if (!token) { + return ; + } + + try { + const response = await apiEventGetConfirmation({ + id: id as string, + userId: user?.id as string, + }); + + if (response.success) { + setData(response.data?.dataEvent); + setPeserta(response.data?.peserta); + setKonfirmasi(response.data?.kehadiran); + } + } catch (error) { + console.log("[ERROR CONFIRMATION]", error); + } + }; + + const handlerReturn = () => { + const now = dayjs(); // asumsi: UTC, sesuai dengan API + + // --- [1] Loading & Data tidak ditemukan --- + if (data === undefined || data === null) { + if (peserta === null && konfirmasi === null) { + return ; + } + return ( + + + Data Tidak Ditemukan + + + + ); + } + + // --- [2] Ambil waktu event dari `data` --- + const eventStart = dayjs(data.tanggal); + const eventEnd = dayjs(data.tanggalSelesai); + + // --- [3] Definisikan jendela konfirmasi: 1 jam sebelum mulai → 1 jam setelah selesai --- + const confirmationStart = eventStart.subtract(1, "hour"); + const confirmationEnd = eventEnd.add(1, "hour"); + const isWithinConfirmationWindow = now.isBetween( + confirmationStart, + confirmationEnd, + null, + "[]" + ); + + // --- [4] Status waktu event (untuk pesan UI) --- + const isBeforeEvent = now.isBefore(eventStart); + const isAfterEvent = now.isAfter(eventEnd); + const isDuringEvent = !isBeforeEvent && !isAfterEvent; + + // --- [5] Handle berdasarkan waktu dan status peserta/konfirmasi --- + + // 🟢 Acara sudah selesai + if (isAfterEvent) { + if (peserta === false) { + return ( + + + + + ); + } + return ( + + + + + ); + } + + // 🔵 Acara belum mulai & belum terdaftar + if (isBeforeEvent) { + if (peserta === false) { + return ( + + ); + } + + // Peserta sudah daftar → cek apakah sudah boleh konfirmasi + if (isWithinConfirmationWindow && peserta === true) { + if (konfirmasi === false) { + return ( + + ); + } + return ( + + + + + ); + } + + return ( + + + + + ); + } + + // 🟡 Acara sedang berlangsung & belum terdaftar + if (isDuringEvent) { + if (peserta === false) { + return ( + + ); + } + + if (peserta === true) { + if (isWithinConfirmationWindow) { + if (konfirmasi === false) { + return ( + + + + ); + } + return ( + + + + + ); + } + + // Ini sangat jarang terjadi selama event berlangsung, tapi aman + return ( + + + + + ); + } + } + + // 🛑 Fallback aman + return ( + + + + + + + ); + }; + + return ( + <> + ( + + router.navigate("/(application)/(user)/event/create") + } + /> + ), + }} + /> + {handlerReturn()} + + ); +} + +const TamplateBox = ({ + data, + children, +}: { + data: DataEvent; + children: React.ReactNode; +}) => { + return ( + <> + + + + + + {data?.title} + + + + {dayjs(data?.tanggal).format("DD MMM YYYY: HH:mm")} + + + {" "} + -{" "} + + + {dayjs(data?.tanggalSelesai).format("DD MMM YYYY: HH:mm")} + + + + + {children} + + + + + ); +}; + +const TamplateText = ({ text }: { text: React.ReactNode }) => { + return ( + <> + {text} + + ); +}; + +type BackToOtherPathProps = + | { path: "home" | "beranda-event"; id?: never; isAfterEvent?: never } + | { path: "event"; id: string; isAfterEvent: boolean }; + +const BackToOtherPath = ({ path, id, isAfterEvent }: BackToOtherPathProps) => { + return ( + <> + {path === "home" ? ( + { + router.replace("/(application)/home"); + }} + > + Home + + ) : ( + + { + router.replace("/(application)/home"); + }} + > + Home + + { + if (path === "event") { + if (isAfterEvent) { + router.push(`/(application)/(user)/event/${id}/history`); + } else { + router.push(`/(application)/(user)/event/${id}/publish`); + } + } else if (path === "beranda-event") { + router.push(`/(application)/(user)/event`); + } else { + console.log("[PATH]", path); + } + }} + > + Lihat {path === "event" ? "Event" : "Beranda Event"} + + + )} + + ); +}; + +// 🔵 Acara belum mulai & belum terdaftar +const NotStarted_And_UserNotParticipan = ({ + id, + userId, + data, +}: { + id: string; + userId: string; + data: DataEvent; +}) => { + const [isLoading, setIsLoading] = useState(false); + + const handlerJoinEvent = async () => { + try { + setIsLoading(true); + + const response = await apiEventJoin({ + id: id as string, + userId: userId as string, + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Anda gagal join", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Anda berhasil join", + }); + router.navigate(`/(application)/(user)/event/${id}/publish`); + } catch (error) { + console.log("[ERROR JOIN EVENT]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + + + + Join + + + + ); +}; + +// 🟡 ZONA ACARA BERLANGSUNG +// Acara sedang berlangsung & belum terdaftar & user harus join dan konfirmasi +const UserNotParticipan_And_DuringEvent = ({ + id, + userId, + data, +}: { + id: string; + userId: string; + data: DataEvent; +}) => { + const [isLoading, setIsLoading] = useState(false); + + const handlerSubmit = async () => { + try { + setIsLoading(true); + + const response = await apiEventConfirmationAction({ + id: id as string, + userId: userId as string, + category: "join_and_confirm", + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Anda gagal join & konfirmasi", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Anda berhasil join & konfirmasi", + }); + router.navigate(`/(application)/(user)/event/${id}/publish`); + } catch (error) { + console.log("[ERROR JOIN & CONFIRMATION EVENT]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + + + + handlerSubmit()} isLoading={isLoading}> + Join & Konfirmasi + + + + ); +}; + + +// 🟡 ZONA ACARA BERLANGSUN +// User sudah terdaftar & Event sedang berlangsung & user harus konfirmasi +const UserParticipan_And_DuringEvent = ({ + id, + userId, + data, +}: { + id: string; + userId: string; + data: DataEvent; +}) => { + const [isLoading, setIsLoading] = useState(false); + + const handlerSubmit = async () => { + try { + setIsLoading(true); + + const response = await apiEventConfirmationAction({ + id: id as string, + userId: userId as string, + category: "confirmation", + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Anda gagal konfirmasi", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Anda berhasil konfirmasi", + }); + router.navigate(`/(application)/(user)/event/${id}/publish`); + } catch (error) { + console.log("[ERROR JOIN & CONFIRMATION EVENT]", error); + } finally { + setIsLoading(false); + } + }; + return ( + <> + + + + handlerSubmit()} isLoading={isLoading}> + Konfirmasi + + + + ); +}; diff --git a/app/(application)/(user)/event/[id]/contribution.tsx b/app/(application)/(user)/event/[id]/contribution.tsx new file mode 100644 index 0000000..6b8099d --- /dev/null +++ b/app/(application)/(user)/event/[id]/contribution.tsx @@ -0,0 +1,78 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + DotButton, + DrawerCustom, + LoaderCustom, + MenuDrawerDynamicGrid, + Spacing, + ViewWrapper, +} from "@/components"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import LeftButtonCustom from "@/components/Button/BackButton"; +import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection"; +import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish"; +import { apiEventGetOne } from "@/service/api-client/api-event"; +import { router, Stack, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; + +export default function EventDetailContribution() { + const { id } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [data, setData] = useState(); + const [isLoadData, setIsLoadData] = useState(false); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiEventGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + const handlePress = (item: IMenuDrawerItem) => { + console.log("PATH ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }; + + return ( + <> + , + headerRight: () => setOpenDrawer(true)} />, + }} + /> + + {isLoadData ? ( + + ) : ( + + )} + + + setOpenDrawer(false)} + height={"auto"} + > + + + + ); +} diff --git a/app/(application)/(user)/event/[id]/edit.tsx b/app/(application)/(user)/event/[id]/edit.tsx new file mode 100644 index 0000000..9ca46ed --- /dev/null +++ b/app/(application)/(user)/event/[id]/edit.tsx @@ -0,0 +1,272 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCustom, + LoaderCustom, + SelectCustom, + Spacing, + StackCustom, + TextAreaCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; +import { + apiEventGetOne, + apiEventUpdateData, +} from "@/service/api-client/api-event"; +import { apiMasterEventType } from "@/service/api-client/api-master"; +import { DateTimePickerEvent } from "@react-native-community/datetimepicker"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import React, { useCallback, useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function EventEdit() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(); + // { + // title: "", + // lokasi: "", + // deskripsi: "", + // eventMaster_TipeAcaraId: "", + // tanggal: "", + // tanggalSelesai: "", + // authorId: "", + // } + const [listTypeEvent, setListTypeEvent] = useState([]); + const [selectedDate, setSelectedDate] = useState< + Date | DateTimePickerEvent | null + >(); + + const [selectedEndDate, setSelectedEndDate] = useState< + Date | DateTimePickerEvent | null + >(); + + const [isLoading, setIsLoading] = useState(false); + const [isLoadData, setIsLoadData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + async function onLoadData() { + try { + setIsLoadData(true); + const response = await apiEventGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + setSelectedDate(new Date(response.data.tanggal)); + setSelectedEndDate(new Date(response.data.tanggalSelesai)); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + } + + useEffect(() => { + onLoadMasterEventType(); + }, []); + + const onLoadMasterEventType = async () => { + try { + const response = await apiMasterEventType(); + setListTypeEvent(response.data); + } catch (error) { + console.log("Error onLoadMasterEventType", error); + } + }; + + const validateDate = async () => { + if ( + data?.title === "" || + data?.lokasi === "" || + data?.deskripsi === "" || + data?.eventMaster_TipeAcaraId === "" + ) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Lengkapi semua data", + }); + + return false; + } + + const startDate = new Date(selectedDate as any); + const endDate = new Date(selectedEndDate as any); + + if (startDate >= endDate) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Ubah tanggal berakhirnya event", + }); + + return false; + } + + return true; + }; + + const handlerSubmit = async () => { + const isValid = await validateDate(); + if (!isValid) return; + + try { + setIsLoading(true); + const newData = { + ...data, + tanggal: new Date(selectedDate as any).toISOString(), + tanggalSelesai: new Date(selectedEndDate as any).toISOString(), + }; + + const response = await apiEventUpdateData({ + id: id as string, + data: newData, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: response.message, + }); + return router.back(); + } + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); + } + }; + + const validateDateRange = ( + selectedDate: string | Date, + selectedEndDate: string | Date + ): { isValid: boolean; error?: string } => { + const startDate = new Date(selectedDate); + const endDate = new Date(selectedEndDate); + + // Cek apakah tanggal valid + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + return { + isValid: false, + error: "Invalid date provided", + }; + } + + if (startDate >= endDate) { + return { + isValid: false, + error: "Ubah tanggal berakhirnya event", + }; + } + + return { + isValid: true, + error: undefined, + }; + }; + + return ( + <> + + {isLoadData ? ( + + ) : ( + + setData({ ...data, title: value })} + /> + ({ + label: item.name, + value: item.id, + }))} + value={data?.eventMaster_TipeAcaraId || ""} + onChange={(value) => { + console.log(value); + setData({ ...data, eventMaster_TipeAcaraId: value }); + }} + /> + setData({ ...data, lokasi: value })} + /> + { + setSelectedDate(date as any); + }} + /> + + { + setSelectedEndDate(date as any); + }} + /> + + {/* Muncul */} + {validateDateRange(selectedDate as any, selectedEndDate as any) + .isValid ? ( + + { + validateDateRange( + selectedDate as any, + selectedEndDate as any + ).error + } + + ) : ( + + { + validateDateRange( + selectedDate as any, + selectedEndDate as any + ).error + } + + )} + + + + setData({ ...data, deskripsi: value })} + /> + + + + )} + + + ); +} diff --git a/app/(application)/(user)/event/[id]/history.tsx b/app/(application)/(user)/event/[id]/history.tsx new file mode 100644 index 0000000..a96a189 --- /dev/null +++ b/app/(application)/(user)/event/[id]/history.tsx @@ -0,0 +1,69 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + DotButton, + DrawerCustom, + MenuDrawerDynamicGrid, + ViewWrapper, + Spacing, +} from "@/components"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import LeftButtonCustom from "@/components/Button/BackButton"; +import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection"; +import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish"; +import { apiEventGetOne } from "@/service/api-client/api-event"; +import { router, Stack, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; + +export default function EventDetailHistory() { + const { id } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [data, setData] = useState(); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + const response = await apiEventGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlePress = (item: IMenuDrawerItem) => { + console.log("PATH ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }; + + return ( + <> + , + headerRight: () => setOpenDrawer(true)} />, + }} + /> + + + + + setOpenDrawer(false)} + height={250} + > + + + + ); +} diff --git a/app/(application)/(user)/event/[id]/list-of-participants.tsx b/app/(application)/(user)/event/[id]/list-of-participants.tsx new file mode 100644 index 0000000..3987f4a --- /dev/null +++ b/app/(application)/(user)/event/[id]/list-of-participants.tsx @@ -0,0 +1,110 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarUsernameAndOtherComponent, + BadgeCustom, + BaseBox, + LoaderCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { + apiEventGetOne, + apiEventListOfParticipants, +} from "@/service/api-client/api-event"; +import dayjs, { Dayjs } from "dayjs"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import { View } from "react-native"; + +export default function EventListOfParticipants() { + const { id } = useLocalSearchParams(); + const [startDate, setStartDate] = useState(); + const [listData, setListData] = useState(null); + const [loadtData, setLoadData] = useState(false); + + useFocusEffect( + useCallback(() => { + handlerLoadData(); + }, [id]) + ); + + const handlerLoadData = () => { + try { + onLoadData(); + onLoadList(); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const onLoadData = async () => { + try { + const response = await apiEventGetOne({ id: id as string }); + if (response.success) { + const date = dayjs(response.data.tanggal); + setStartDate(date); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const onLoadList = async () => { + try { + setLoadData(true); + const response = await apiEventListOfParticipants({ id: id as string }); + + if (response.success) { + setListData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadData(false); + } + }; + + return ( + + {loadtData && !listData ? ( + + ) : _.isEmpty(listData) ? ( + + Belum ada peserta + + ) : ( + listData?.map((item: any, index: number) => ( + + + + {item?.isPresent ? "Hadir" : "Tidak Hadir"} + + + ) : ( + + - + + ) + } + /> + + )) + )} + + ); +} diff --git a/app/(application)/(user)/event/[id]/publish.tsx b/app/(application)/(user)/event/[id]/publish.tsx new file mode 100644 index 0000000..88efb59 --- /dev/null +++ b/app/(application)/(user)/event/[id]/publish.tsx @@ -0,0 +1,171 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AlertDefaultSystem, + ButtonCustom, + DotButton, + DrawerCustom, + LoaderCustom, + MenuDrawerDynamicGrid, + ViewWrapper, +} from "@/components"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import LeftButtonCustom from "@/components/Button/BackButton"; +import { useAuth } from "@/hooks/use-auth"; +import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection"; +import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish"; +import { + apiEventCheckParticipants, + apiEventGetOne, + apiEventJoin, +} from "@/service/api-client/api-event"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function EventDetailPublish() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [openDrawer, setOpenDrawer] = useState(false); + const [isLoadingData, setIsLoadingData] = useState(false); + const [isLoadingJoin, setIsLoadingJoin] = useState(false); + + const [data, setData] = useState(); + const [isParticipant, setIsParticipant] = useState(null); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, []) + ); + + async function onLoadData() { + try { + setIsLoadingData(true); + const response = await apiEventGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + + const responseCheckParticipants = await apiEventCheckParticipants({ + id: id as string, + userId: user?.id as string, + }); + + console.log("[RES CHECK PARTICIPANTS]", responseCheckParticipants); + + if ( + responseCheckParticipants.success && + responseCheckParticipants.data + ) { + setIsParticipant(true); + } + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadingData(false); + } + } + + console.log("[participans]", isParticipant); + + const handlePress = (item: IMenuDrawerItem) => { + console.log("PATH ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }; + + const handlerJoin = async () => { + const userId = user?.id; + if (!userId) { + return Toast.show({ + type: "error", + text2: "Anda belum login", + }); + } + + try { + setIsLoadingJoin(true); + const response = await apiEventJoin({ + id: id as string, + userId: userId as string, + }); + if (response.success) { + router.navigate( + `/(application)/(user)/event/${id}/list-of-participants` + ); + Toast.show({ + type: "success", + text1: "Anda berhasil join", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadingJoin(false); + } + }; + + const footerButton = () => { + return ( + <> + + AlertDefaultSystem({ + title: "Join event", + message: "Anda yakin ingin join sebagai peserta event ?", + textLeft: "Tidak", + textRight: "Ya", + onPressLeft: () => {}, + onPressRight: () => handlerJoin(), + }) + } + > + {isParticipant ? "Anda sudah tergabung" : "Join"} + + + ); + }; + + return ( + <> + , + headerRight: () => setOpenDrawer(true)} />, + }} + /> + + {isLoadingData ? ( + + ) : ( + + )} + + + setOpenDrawer(false)} + height={"auto"} + > + + + + ); +} diff --git a/app/(application)/(user)/event/create.tsx b/app/(application)/(user)/event/create.tsx new file mode 100644 index 0000000..f80831a --- /dev/null +++ b/app/(application)/(user)/event/create.tsx @@ -0,0 +1,205 @@ +import { + ButtonCustom, + SelectCustom, + Spacing, + StackCustom, + TextAreaCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; +import { useAuth } from "@/hooks/use-auth"; +import { apiEventCreate } from "@/service/api-client/api-event"; +import { apiMasterEventType } from "@/service/api-client/api-master"; +import { DateTimePickerEvent } from "@react-native-community/datetimepicker"; +import { router } from "expo-router"; +import React, { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +interface EventCreateProps { + title?: string; + lokasi?: string; + deskripsi?: string; + eventMaster_TipeAcaraId?: string; + tanggal?: string; + tanggalSelesai?: string; + authorId?: string; +} + +export default function EventCreate() { + const [data, setData] = useState(); + const [listTypeEvent, setListTypeEvent] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const { user } = useAuth(); + + useEffect(() => { + onLoadMasterEventType(); + }, []); + + const onLoadMasterEventType = async () => { + try { + const response = await apiMasterEventType(); + setListTypeEvent(response.data); + } catch (error) { + console.log("Error onLoadMasterEventType", error); + } + }; + + const [selectedDate, setSelectedDate] = useState< + Date | DateTimePickerEvent | null + >(null); + + const [selectedEndDate, setSelectedEndDate] = useState< + Date | DateTimePickerEvent | null + >(null); + + const handlerSubmit = async () => { + if ( + !data?.title || + !data?.lokasi || + !data?.deskripsi || + !data?.eventMaster_TipeAcaraId + ) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Lengkapi semua data", + }); + return; + } + + if (!selectedDate || !selectedEndDate) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Pilih tanggal mulai dan berakhir", + }); + return; + } + + // if (selectedDate) { + // console.log("Tanggal yang dipilih:", selectedDate); + // console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString()); + + // // Kirim ke API atau proses lanjutan + // } else { + // console.log("Tanggal belum dipilih"); + // } + + // if (selectedEndDate) { + // console.log("Tanggal yang dipilih:", selectedEndDate); + // console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString()); + // // Kirim ke API atau proses lanjutan + // } else { + // console.log("Tanggal berakhir belum dipilih"); + // } + + try { + setIsLoading(true); + + const newData = { + ...data, + tanggal: new Date(selectedDate as any).toISOString(), + tanggalSelesai: new Date(selectedEndDate as any).toISOString(), + authorId: user?.id, + }; + + console.log("Data berhasil disimpan", JSON.stringify(newData, null, 2)); + + const response = await apiEventCreate(newData); + console.log("Response", JSON.stringify(response, null, 2)); + + router.navigate("/event/status"); + } catch (error) { + console.log(error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = ( + + ); + + return ( + <> + + + setData({ ...data, title: value })} + /> + + ({ + label: item.name, + value: item.id, + }))} + value={data?.eventMaster_TipeAcaraId || ""} + onChange={(value: any) => + setData({ ...data, eventMaster_TipeAcaraId: value }) + } + /> + + setData({ ...data, lokasi: value })} + /> + + { + setSelectedDate(date as any); + }} + value={selectedDate as any} + minimumDate={new Date(Date.now())} + /> + + + { + setSelectedEndDate(date as any); + }} + value={selectedEndDate as any} + minimumDate={new Date(selectedDate as any)} + /> + {!selectedDate && ( + + Note: Pilih tanggal mulai terlebih dahulu + + )} + + + + + setData({ ...data, deskripsi: value }) + } + /> + + {buttonSubmit} + + + + ); +} diff --git a/app/(application)/(user)/event/detail/[id].tsx b/app/(application)/(user)/event/detail/[id].tsx new file mode 100644 index 0000000..372530f --- /dev/null +++ b/app/(application)/(user)/event/detail/[id].tsx @@ -0,0 +1,14 @@ +import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; +import { GStyles } from "@/styles/global-styles"; +import { useLocalSearchParams } from "expo-router"; +import { Text } from "react-native"; + +export default function DetailEvent() { + const { id } = useLocalSearchParams(); + console.log("id event >", id); + return ( + + Detail Event {id} + + ); +} diff --git a/app/(application)/(user)/forum/[id]/edit.tsx b/app/(application)/(user)/forum/[id]/edit.tsx new file mode 100644 index 0000000..096ab5c --- /dev/null +++ b/app/(application)/(user)/forum/[id]/edit.tsx @@ -0,0 +1,98 @@ +import { + BoxButtonOnFooter, + ButtonCustom, + LoaderCustom, + TextAreaCustom, + ViewWrapper, +} from "@/components"; +import { apiForumGetOne, apiForumUpdate } from "@/service/api-client/api-forum"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function ForumEdit() { + const { id } = useLocalSearchParams(); + const [text, setText] = useState(""); + const [loadingGetData, setLoadingGetData] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(id as string); + }, [id]) + ); + + const onLoadData = async (id: string) => { + try { + setLoadingGetData(true); + const response = await apiForumGetOne({ id }); + + setText(response.data.diskusi); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetData(false); + } + }; + + const handlerUpdateData = async () => { + if (!text) { + Toast.show({ + type: "error", + text1: "Harap masukkan diskusi", + }); + return; + } + try { + setIsLoading(true); + const response = await apiForumUpdate({ + id: id as string, + data: text, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil diupdate", + }); + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonFooter = () => { + return ( + <> + {!loadingGetData && ( + + + Update + + + )} + + ); + }; + + return ( + + {!loadingGetData ? ( + { + setText(value); + }} + /> + ) : ( + + )} + + ); +} diff --git a/app/(application)/(user)/forum/[id]/forumku.tsx b/app/(application)/(user)/forum/[id]/forumku.tsx new file mode 100644 index 0000000..c51d8ed --- /dev/null +++ b/app/(application)/(user)/forum/[id]/forumku.tsx @@ -0,0 +1,142 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarComp, + ButtonCustom, + CenterCustom, + DrawerCustom, + FloatingButton, + Grid, + LoaderCustom, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; +import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; +import { apiForumGetAll } from "@/service/api-client/api-forum"; +import { apiUser } from "@/service/api-client/api-user"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function Forumku() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [openDrawer, setOpenDrawer] = useState(false); + const [status, setStatus] = useState(""); + const [listData, setListData] = useState(null); + const [dataUser, setDataUser] = useState(null); + const [loadingGetList, setLoadingGetList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + onLoadDataProfile(id as string); + }, [id]) + ); + + const onLoadDataProfile = async (id: string) => { + try { + const response = await apiUser(id); + + setDataUser(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + } + }; + + const onLoadData = async () => { + try { + setLoadingGetList(true); + const response = await apiForumGetAll({ + search: "", + authorId: id as string, + }); + + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetList(false); + } + }; + + return ( + <> + + router.navigate("/(application)/(user)/forum/create") + } + /> + ) + } + > + + + + + + + + + @{dataUser?.username || "-"} + + {listData?.length || "0"} postingan + + + + Kunjungi Profile + + + + {loadingGetList ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada diskusi + ) : ( + <> + {listData?.map((item: any, index: number) => ( + { + setOpenDrawer(value.setOpenDrawer); + setStatus(value.setStatus); + }} + /> + ))} + + )} + + + + {/* Drawer Komponen Eksternal */} + setOpenDrawer(false)} + > + { + setOpenDrawer(false); + }} + authorId={id as string} + /> + + + ); +} diff --git a/app/(application)/(user)/forum/[id]/index.tsx b/app/(application)/(user)/forum/[id]/index.tsx new file mode 100644 index 0000000..5882cfa --- /dev/null +++ b/app/(application)/(user)/forum/[id]/index.tsx @@ -0,0 +1,263 @@ +import { + ButtonCustom, + DrawerCustom, + LoaderCustom, + Spacing, + TextAreaCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection"; +import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; +import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; +import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar"; +import { + apiForumCreateComment, + apiForumGetComment, + apiForumGetOne, + apiForumUpdateStatus, +} from "@/service/api-client/api-forum"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useEffect, useState } from "react"; + +interface CommentProps { + id: string; + isActive: boolean; + komentar: string; + createdAt: Date; + authorId: string; + Author: { + id: string; + username: string; + Profile: { + id: string; + imageId: string; + }; + }; +} + +export default function ForumDetail() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [openDrawer, setOpenDrawer] = useState(false); + const [data, setData] = useState(null); + const [listComment, setListComment] = useState(null); + const [isLoadingComment, setLoadingComment] = useState(false); + + // Status + const [status, setStatus] = useState(""); + const [text, setText] = useState(""); + const [authorId, setAuthorId] = useState(""); + const [dataId, setDataId] = useState(""); + + // Comentar + const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false); + const [commentId, setCommentId] = useState(""); + const [commentAuthorId, setCommentAuthorId] = useState(""); + + useFocusEffect( + useCallback(() => { + onLoadData(id as string); + }, [id]) + ); + + const onLoadData = async (id: string) => { + try { + const response = await apiForumGetOne({ id }); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + useEffect(() => { + onLoadListComment(id as string); + }, [id]); + + const onLoadListComment = async (id: string) => { + try { + const response = await apiForumGetComment({ + id: id as string, + }); + setListComment(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + // Update Status + const handlerUpdateStatus = async (value: any) => { + try { + const response = await apiForumUpdateStatus({ + id: id as string, + data: value, + }); + if (response.success) { + setStatus(response.data); + setData({ + ...data, + ForumMaster_StatusPosting: { + status: response.data, + }, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + // Create Commentar + const handlerCreateCommentar = async () => { + const newData = { + comment: text, + authorId: user?.id, + }; + + try { + setLoadingComment(true); + const response = await apiForumCreateComment({ + id: id as string, + data: newData, + }); + + if (response.success) { + setText(""); + const newComment = { + id: response.data.id, + isActive: response.data.isActive, + komentar: response.data.komentar, + createdAt: response.data.createdAt, + authorId: response.data.authorId, + Author: response.data.Author, + }; + setListComment((prev) => [newComment, ...(prev || [])]); + setData({ + ...data, + count: data.count + 1, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingComment(false); + } + }; + + return ( + <> + + {!data && !listComment ? ( + + ) : ( + <> + {/* Box Posting */} + { + setOpenDrawer(true); + setStatus(data.ForumMaster_StatusPosting?.status); + setAuthorId(data.Author?.id); + setDataId(data.id); + }} + /> + + {/* Area Commentar */} + {data?.ForumMaster_StatusPosting?.status === "Open" && ( + <> + + { + handlerCreateCommentar(); + }} + > + Balas + + + )} + + + {/* List Commentar */} + {_.isEmpty(listComment) ? ( + + Tidak ada komentar + + ) : ( + Komentar : + )} + + {listComment?.map((item: any, index: number) => ( + { + setCommentId(value.setCommentId); + setOpenDrawerCommentar(value.setOpenDrawer); + setCommentAuthorId(value.setCommentAuthorId); + }} + /> + ))} + + )} + + + {/* Posting Drawer */} + setOpenDrawer(false)} + > + { + setOpenDrawer(false); + }} + authorId={authorId} + handlerUpdateStatus={(value: any) => { + handlerUpdateStatus(value); + }} + /> + + + {/* Commentar Drawer */} + setOpenDrawerCommentar(false)} + > + { + setOpenDrawerCommentar(false); + }} + listComment={listComment} + setListComment={setListComment} + countComment={data?.count} + setCountComment={(val: any) => { + setData((prev: any) => ({ + ...prev, + count: val, + })); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/forum/[id]/other-report-commentar.tsx b/app/(application)/(user)/forum/[id]/other-report-commentar.tsx new file mode 100644 index 0000000..d2be6d7 --- /dev/null +++ b/app/(application)/(user)/forum/[id]/other-report-commentar.tsx @@ -0,0 +1,74 @@ +import { + BoxButtonOnFooter, + ButtonCustom, + TextAreaCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import { apiForumCreateReportCommentar } from "@/service/api-client/api-master"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function ForumOtherReportCommentar() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [value, setValue] = useState(""); + + const handlerSubmitReport = async () => { + const newData = { + authorId: user?.id, + description: value, + }; + + try { + const response = await apiForumCreateReportCommentar({ + id: id as string, + data: newData, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Laporan berhasil dikirim", + }); + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + Toast.show({ + type: "error", + text1: "Gagal", + text2: "Laporan gagal dikirim", + }); + } + }; + + const handleSubmit = ( + + { + handlerSubmitReport(); + }} + > + Report + + + ); + + return ( + <> + + + + + ); +} diff --git a/app/(application)/(user)/forum/[id]/other-report-posting.tsx b/app/(application)/(user)/forum/[id]/other-report-posting.tsx new file mode 100644 index 0000000..0fd61d5 --- /dev/null +++ b/app/(application)/(user)/forum/[id]/other-report-posting.tsx @@ -0,0 +1,73 @@ +import { + BoxButtonOnFooter, + ButtonCustom, + TextAreaCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import { apiForumCreateReportPosting } from "@/service/api-client/api-master"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function ForumOtherReportPosting() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [value, setValue] = useState(""); + + const handlerSubmitReport = async () => { + const newData = { + authorId: user?.id, + description: value, + }; + + try { + const response = await apiForumCreateReportPosting({ + id: id as string, + data: newData, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Laporan berhasil dikirim", + }); + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + Toast.show({ + type: "error", + text1: "Gagal", + text2: "Laporan gagal dikirim", + }); + } + }; + + const handleSubmit = ( + + { + handlerSubmitReport(); + }} + > + Report + + + ); + return ( + <> + + + + + ); +} diff --git a/app/(application)/(user)/forum/[id]/report-commentar.tsx b/app/(application)/(user)/forum/[id]/report-commentar.tsx new file mode 100644 index 0000000..b3fe93e --- /dev/null +++ b/app/(application)/(user)/forum/[id]/report-commentar.tsx @@ -0,0 +1,106 @@ +import { + ButtonCustom, + LoaderCustom, + Spacing, + StackCustom, + ViewWrapper, +} from "@/components"; +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import Forum_ReportListSection from "@/screens/Forum/ReportListSection"; +import { apiForumCreateReportCommentar, apiMasterForumReportList } from "@/service/api-client/api-master"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState, useEffect } from "react"; +import Toast from "react-native-toast-message"; + +export default function ForumReportCommentar() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [selectReport, setSelectReport] = useState(""); + const [listMaster, setListMaster] = useState(null); + const [isLoadingList, setIsLoadingList] = useState(false); + + useEffect(() => { + onLoadListMaster(); + }, []); + + const onLoadListMaster = async () => { + try { + setIsLoadingList(true); + const response = await apiMasterForumReportList(); + + setListMaster(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadingList(false); + } + }; + + const handlerReport = async () => { + const newData = { + authorId: user?.id, + categoryId: selectReport, + }; + + try { + const response = await apiForumCreateReportCommentar({ + id: id as string, + data: newData, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Laporan berhasil dikirim", + }); + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + Toast.show({ + type: "error", + text1: "Gagal", + text2: "Laporan gagal dikirim", + }); + } + }; + + return ( + <> + + {isLoadingList ? ( + + ) : ( + + + { + handlerReport(); + }} + > + Report + + { + router.replace(`/forum/${id}/other-report-commentar`); + }} + > + Lainnya + + + + )} + + + ); +} diff --git a/app/(application)/(user)/forum/[id]/report-posting.tsx b/app/(application)/(user)/forum/[id]/report-posting.tsx new file mode 100644 index 0000000..5947fb6 --- /dev/null +++ b/app/(application)/(user)/forum/[id]/report-posting.tsx @@ -0,0 +1,120 @@ +import { + AlertDefaultSystem, + ButtonCustom, + LoaderCustom, + Spacing, + StackCustom, + ViewWrapper, +} from "@/components"; +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import Forum_ReportListSection from "@/screens/Forum/ReportListSection"; +import { + apiForumCreateReportPosting, + apiMasterForumReportList, +} from "@/service/api-client/api-master"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function ForumReportPosting() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [selectReport, setSelectReport] = useState(""); + const [listMaster, setListMaster] = useState(null); + const [isLoadingList, setIsLoadingList] = useState(false); + + useEffect(() => { + onLoadListMaster(); + }, []); + + const onLoadListMaster = async () => { + try { + setIsLoadingList(true); + const response = await apiMasterForumReportList(); + + setListMaster(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadingList(false); + } + }; + + const handlerReport = async () => { + const newData = { + authorId: user?.id, + categoryId: selectReport, + }; + + try { + const response = await apiForumCreateReportPosting({ + id: id as string, + data: newData, + }); + + + if (response.success) { + Toast.show({ + type: "success", + text1: "Laporan berhasil dikirim", + }); + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + Toast.show({ + type: "error", + text1: "Gagal", + text2: "Laporan gagal dikirim", + }); + } + }; + + return ( + <> + + {isLoadingList ? ( + + ) : ( + + + + { + AlertDefaultSystem({ + title: "Laporan Posting", + message: "Apakah anda yakin ingin melaporkan postingan ini?", + textLeft: "Batal", + textRight: "Laporkan", + onPressRight: () => { + handlerReport(); + }, + }); + }} + > + Report + + { + router.replace(`/forum/${id}/other-report-posting`); + }} + > + Lainnya + + + + )} + + + ); +} diff --git a/app/(application)/(user)/forum/create.tsx b/app/(application)/(user)/forum/create.tsx new file mode 100644 index 0000000..07396e1 --- /dev/null +++ b/app/(application)/(user)/forum/create.tsx @@ -0,0 +1,66 @@ +import { + BoxButtonOnFooter, + ButtonCustom, + TextAreaCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { apiForumCreate } from "@/service/api-client/api-forum"; +import { router } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function ForumCreate() { + const { user } = useAuth(); + const [text, setText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handlerSubmit = async () => { + const newData = { + diskusi: text, + authorId: user?.id, + }; + + try { + setIsLoading(true); + const response = await apiForumCreate({ data: newData }); + if (response.success) { + Toast.show({ + type: "success", + text1: "Posting berhasil", + }); + setText(""); + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonFooter = ( + + { + handlerSubmit(); + }} + > + Posting + + + ); + + return ( + + + + ); +} diff --git a/app/(application)/(user)/forum/index.tsx b/app/(application)/(user)/forum/index.tsx new file mode 100644 index 0000000..e006a53 --- /dev/null +++ b/app/(application)/(user)/forum/index.tsx @@ -0,0 +1,129 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AvatarComp, + BackButton, + DrawerCustom, + LoaderCustom, + SearchInput, + TextCustom, + ViewWrapper, +} from "@/components"; +import FloatingButton from "@/components/Button/FloatingButton"; +import { useAuth } from "@/hooks/use-auth"; +import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; +import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; +import { apiForumGetAll } from "@/service/api-client/api-forum"; +import { apiUser } from "@/service/api-client/api-user"; +import { router, Stack, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function Forum() { + const [openDrawer, setOpenDrawer] = useState(false); + const [status, setStatus] = useState(""); + const { user } = useAuth(); + const [dataUser, setDataUser] = useState(); + const [listData, setListData] = useState(); + const [loadingGetList, setLoadingGetList] = useState(false); + const [search, setSearch] = useState(""); + const [dataId, setDataId] = useState(""); + const [authorId, setAuthorId] = useState(""); + + useFocusEffect( + useCallback(() => { + onLoadData(); + onLoadDataProfile(user?.id as string); + }, [user?.id, search]) + ); + + const onLoadDataProfile = async (id: string) => { + const response = await apiUser(id); + setDataUser(response.data); + }; + + const onLoadData = async () => { + try { + setLoadingGetList(true); + const response = await apiForumGetAll({ search: search }); + + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGetList(false); + } + }; + + return ( + <> + , + headerRight: () => ( + + ), + }} + /> + + setSearch(e)} + /> + } + floatingButton={ + + router.navigate("/(application)/(user)/forum/create") + } + /> + } + > + {loadingGetList ? ( + + ) : _.isEmpty(listData) ? ( + + Tidak ada diskusi + + ) : ( + listData?.map((e: any, i: number) => ( + { + setDataId(e.id); + setOpenDrawer(true); + setStatus(e.ForumMaster_StatusPosting?.status); + setAuthorId(e.Author?.id); + }} + isTruncate={true} + href={`/forum/${e.id}`} + isRightComponent={false} + /> + )) + )} + + + setOpenDrawer(false)} + > + { + setOpenDrawer(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/home.tsx b/app/(application)/(user)/home.tsx new file mode 100644 index 0000000..75ef4fb --- /dev/null +++ b/app/(application)/(user)/home.tsx @@ -0,0 +1,91 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable react-hooks/exhaustive-deps */ +import { StackCustom, ViewWrapper } from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection"; +import Home_ImageSection from "@/screens/Home/imageSection"; +import TabSection from "@/screens/Home/tabSection"; +import { tabsHome } from "@/screens/Home/tabsList"; +import Home_FeatureSection from "@/screens/Home/topFeatureSection"; +import { apiUser } from "@/service/api-client/api-user"; +import { apiVersion } from "@/service/api-config"; +import { Ionicons } from "@expo/vector-icons"; +import { Redirect, router, Stack } from "expo-router"; +import { useEffect, useState } from "react"; + +export default function Application() { + const { token, user } = useAuth(); + + const [data, setData] = useState(); + + useEffect(() => { + onLoadData(); + checkVersion(); + }, []); + + async function onLoadData() { + const response = await apiUser(user?.id as string); + console.log("Response profile >>", JSON.stringify(response?.data?.Profile, null, 2)); + + setData(response.data); + } + + const checkVersion = async () => { + const response = await apiVersion(); + console.log("Version >>", JSON.stringify(response.data, null, 2)); + }; + + if (data && data?.active === false) { + console.log("User is not active"); + return ; + } + + if (data && data?.Profile === null) { + console.log("Profile is null"); + return ; + } + + return ( + <> + ( + { + router.push("/user-search"); + }} + /> + ), + headerRight: () => ( + { + router.push("/notifications"); + }} + /> + ), + }} + /> + + } + > + + + + + + + + + + ); +} diff --git a/app/(application)/(user)/investment/(tabs)/_layout.tsx b/app/(application)/(user)/investment/(tabs)/_layout.tsx new file mode 100644 index 0000000..0e49df0 --- /dev/null +++ b/app/(application)/(user)/investment/(tabs)/_layout.tsx @@ -0,0 +1,59 @@ +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import { TabsStyles } from "@/styles/tabs-styles"; +import { Feather, FontAwesome6, Ionicons } from "@expo/vector-icons"; +import { Tabs } from "expo-router"; + +export default function InvestmentTabsLayout() { + return ( + + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} diff --git a/app/(application)/(user)/investment/(tabs)/index.tsx b/app/(application)/(user)/investment/(tabs)/index.tsx new file mode 100644 index 0000000..e293ce9 --- /dev/null +++ b/app/(application)/(user)/investment/(tabs)/index.tsx @@ -0,0 +1,146 @@ +import { + BaseBox, + FloatingButton, + Grid, + LoaderCustom, + ProgressCustom, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import API_STRORAGE from "@/constants/base-url-api-strorage"; +import DUMMY_IMAGE from "@/constants/dummy-image-value"; +import { apiInvestmentGetAll } from "@/service/api-client/api-investment"; +import { Ionicons } from "@expo/vector-icons"; +import dayjs from "dayjs"; +import { Image } from "expo-image"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import { View } from "react-native"; + +export default function InvestmentBursa() { + const [list, setList] = useState(null); + const [loadingList, setLoadingList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadList(); + }, []) + ); + + const onLoadList = async () => { + try { + setLoadingList(true); + const response = await apiInvestmentGetAll(); + console.log("[DATA LIST]", JSON.stringify(response.data, null, 2)); + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingList(false); + } + }; + + return ( + router.push("/investment/create")} /> + } + > + {loadingList ? ( + + ) : _.isEmpty(list) ? ( + Belum ada data + ) : ( + list?.map((item: any, index: number) => ( + + + + + + + + + + + {item.title} + + {Number(item?.pencarianInvestor) - + dayjs().diff(dayjs(item.countDown), "days") <= + 0 ? ( + + + + Periode Investasi Selesai + + + ) : ( + + Sisa waktu:{" "} + {Number(item?.pencarianInvestor) - + dayjs().diff(dayjs(item.countDown), "days")}{" "} + hari + + )} + + + + + )) + )} + + ); +} + +// +// Progress 70% +// + +// Success Progress +// + +// Warning Progress (small) +// + +// Error Indeterminate +// + +// Custom Radius +// + +// + +// + +// + +// +// ; diff --git a/app/(application)/(user)/investment/(tabs)/my-holding.tsx b/app/(application)/(user)/investment/(tabs)/my-holding.tsx new file mode 100644 index 0000000..ccdb619 --- /dev/null +++ b/app/(application)/(user)/investment/(tabs)/my-holding.tsx @@ -0,0 +1,50 @@ +import { + BaseBox, + Grid, + ProgressCustom, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { router } from "expo-router"; +import { View } from "react-native"; + +export default function InvestmentMyHolding() { + return ( + + {Array.from({ length: 10 }).map((_, index) => ( + router.push(`/investment/${index}/(my-holding)/holding-${index}`)}> + + + + + Title here : Lorem ipsum dolor sit amet consectetur + adipisicing elit. Omnis, exercitationem, sequi enim quod + distinctio maiores laudantium amet, quidem atque repellat sit + vitae qui aliquam est veritatis laborum eum voluptatum totam! + + + + Rp. 7.500.000 + 300 Lembar + + + + + + + + + + + ))} + + ); +} diff --git a/app/(application)/(user)/investment/(tabs)/portofolio.tsx b/app/(application)/(user)/investment/(tabs)/portofolio.tsx new file mode 100644 index 0000000..d801469 --- /dev/null +++ b/app/(application)/(user)/investment/(tabs)/portofolio.tsx @@ -0,0 +1,80 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + LoaderCustom, + ScrollableCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; +import Investment_StatusBox from "@/screens/Invesment/StatusBox"; +import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function InvestmentPortofolio() { + const { user } = useAuth(); + const [activeCategory, setActiveCategory] = useState( + "publish" + ); + + const [listData, setListData] = useState([]); + const [loadingList, setLoadingList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id, activeCategory]) + ); + + const onLoadData = async () => { + try { + setLoadingList(true); + const response = await apiInvestmentGetByStatus({ + authorId: user?.id as string, + status: activeCategory as string, + }); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingList(false); + } + }; + + const handlePress = (item: any) => { + setActiveCategory(item.value); + // tambahkan logika lain seperti filter dsb. + }; + + const scrollComponent = ( + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as any} + /> + ); + return ( + + {loadingList ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada data {activeCategory} + ) : ( + listData.map((item: any, index: number) => ( + + )) + )} + + ); +} diff --git a/app/(application)/(user)/investment/(tabs)/transaction.tsx b/app/(application)/(user)/investment/(tabs)/transaction.tsx new file mode 100644 index 0000000..ff3bca8 --- /dev/null +++ b/app/(application)/(user)/investment/(tabs)/transaction.tsx @@ -0,0 +1,123 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BadgeCustom, + BaseBox, + Grid, + LoaderCustom, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { apiInvestmentGetInvoice } from "@/service/api-client/api-investment"; +import { GStyles } from "@/styles/global-styles"; +import { formatChatTime } from "@/utils/formatChatTime"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import { View } from "react-native"; + +export default function InvestmentTransaction() { + const { user } = useAuth(); + const [list, setList] = useState([]); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadList(); + }, [user?.id]) + ); + + const onLoadList = async () => { + try { + setLoadList(true); + const response = await apiInvestmentGetInvoice({ + authorId: user?.id as string, + category: "transaction", + }); + console.log("[RESPONSE LIST]", JSON.stringify(response.data, null, 2)); + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadList(false); + } + }; + + const handlerColor = (status: string) => { + if (status === "menunggu") { + return "orange"; + } else if (status === "proses") { + return "white"; + } else if (status === "berhasil") { + return "green"; + } else if (status === "gagal") { + return "red"; + } + }; + + const handlePress = ({ id, status }: { id: string; status: string }) => { + if (status === "menunggu") { + router.push(`/investment/${id}/(transaction-flow)/invoice`); + } else if (status === "proses") { + router.push(`/investment/${id}/(transaction-flow)/process`); + } else if (status === "berhasil") { + router.push(`/investment/${id}/(transaction-flow)/success`); + } else if (status === "gagal") { + router.push(`/investment/${id}/(transaction-flow)/failed`); + } + }; + + return ( + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + Tidak ada data + ) : ( + list.map((item: any, i: number) => ( + { + handlePress({ + id: item.id, + status: _.lowerCase(item.statusInvoice), + }); + }} + > + + + + {item?.title || "-"} + + {formatChatTime(item?.createdAt)} + + + + + + + + + + Rp. {formatCurrencyDisplay(item?.nominal) || "-"} + + + {item?.statusInvoice || "-"} + + + + + + )) + )} + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(document)/add-document.tsx b/app/(application)/(user)/investment/[id]/(document)/add-document.tsx new file mode 100644 index 0000000..e5ae294 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(document)/add-document.tsx @@ -0,0 +1,130 @@ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + CenterCustom, + InformationBox, + Spacing, + StackCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiInvestmentUpsertDocument } from "@/service/api-client/api-investment"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile from "@/utils/pickFile"; +import { FontAwesome5 } from "@expo/vector-icons"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function InvestmentAddDocument() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + title: "", + }); + const [pdf, setPdf] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handlerSubmit = async () => { + try { + setIsLoading(true); + const responseUploadFile = await uploadFileService({ + dirId: DIRECTORY_ID.investasi_dokumen, + imageUri: pdf.uri, + }); + + if (!responseUploadFile.success) { + throw new Error(responseUploadFile.message); + } + + const newData = { + title: data.title, + fileId: responseUploadFile.data.id, + }; + + const response = await apiInvestmentUpsertDocument({ + id: id as string, + data: newData, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Data berhasil disimpan", + }); + + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + Toast.show({ + type: "error", + text1: "Gagal menyimpan data", + }); + } finally { + setIsLoading(false); + } + }; + + const buttonFooter = ( + + + Simpan + + + ); + + return ( + <> + + + + + setData({ ...data, title: value })} + /> + + + + {pdf ? ( + {pdf.name} + ) : ( + + )} + + + + + pickFile({ + allowedType: "pdf", + setPdfUri(file) { + setPdf(file); + }, + }) + } + > + Upload + + + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(document)/edit-document.tsx b/app/(application)/(user)/investment/[id]/(document)/edit-document.tsx new file mode 100644 index 0000000..42ee78e --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(document)/edit-document.tsx @@ -0,0 +1,148 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + CenterCustom, + InformationBox, + Spacing, + StackCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { + apiInvestmentGetDocument, + apiInvestmentUpsertDocument, +} from "@/service/api-client/api-investment"; +import { deleteFileService, uploadFileService } from "@/service/upload-service"; +import pickFile from "@/utils/pickFile"; +import { router, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function InvestmentEditDocument() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(null); + const [pdf, setPdf] = useState(null); + const [titleFile, setTitleFile] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetDocument({ + id: id as string, + category: "one-document", + }); + setData(response.data); + setTitleFile(response.data.title); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlerUpdate = async () => { + const prevFileId = data.fileId; + try { + setIsLoading(true); + const responseUploadFile = await uploadFileService({ + dirId: DIRECTORY_ID.investasi_dokumen, + imageUri: pdf.uri, + }); + + if (!responseUploadFile.success) { + throw new Error(responseUploadFile.message); + } + + const newData = { + title: data.title, + fileId: responseUploadFile.data.id, + }; + + const response = await apiInvestmentUpsertDocument({ + id: id as string, + data: newData, + }); + + if (response.success) { + const delPrevFile = await deleteFileService({ + id: prevFileId, + }); + + console.log("[DEL PREV FILE]", delPrevFile); + + Toast.show({ + type: "success", + text1: "Data berhasil diupdate", + }); + + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + Toast.show({ + type: "error", + text1: "Gagal mengupdate data", + }); + } finally { + setIsLoading(false); + } + }; + + const buttonFooter = ( + + + Update + + + ); + + return ( + <> + + + + + setData({ ...data, title: value })} + /> + + + + {pdf ? ( + {pdf.name} + ) : ( + {_.snakeCase(titleFile || "")}.pdf + )} + + + + + pickFile({ + allowedType: "pdf", + setPdfUri(file) { + setPdf(file); + }, + }) + } + > + Upload + + + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(document)/list-of-document.tsx b/app/(application)/(user)/investment/[id]/(document)/list-of-document.tsx new file mode 100644 index 0000000..ed76962 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(document)/list-of-document.tsx @@ -0,0 +1,58 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { LoaderCustom, TextCustom, ViewWrapper } from "@/components"; +import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail"; +import { apiInvestmentGetDocument } from "@/service/api-client/api-investment"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function InvestmentListOfDocument() { + const { id } = useLocalSearchParams(); + console.log("ID >> ", id); + + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadListDocument(); + }, [id]) + ); + + const onLoadListDocument = async () => { + try { + setLoadList(true); + const response = await apiInvestmentGetDocument({ + id: id as string, + category: "all-document", + }); + + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + setList([]); + } finally { + setLoadList(false); + } + }; + + return ( + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + + Tidak ada data + + ) : ( + list?.map((item: any, index: number) => ( + + )) + )} + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(document)/recap-of-document.tsx b/app/(application)/(user)/investment/[id]/(document)/recap-of-document.tsx new file mode 100644 index 0000000..840aee7 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(document)/recap-of-document.tsx @@ -0,0 +1,213 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AlertDefaultSystem, + BackButton, + DotButton, + DrawerCustom, + LoaderCustom, + MenuDrawerDynamicGrid, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IconEdit } from "@/components/_Icon"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail"; +import { + apiInvestmentDeleteDocument, + apiInvestmentGetDocument, +} from "@/service/api-client/api-investment"; +import { AntDesign, Ionicons } from "@expo/vector-icons"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function InvestmentRecapOfDocument() { + const { id } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [openDrawerBox, setOpenDrawerBox] = useState(false); + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + const [selectId, setSelectId] = useState(null); + + useFocusEffect( + useCallback(() => { + onLoadListDocument(); + }, [id]) + ); + + const onLoadListDocument = async () => { + try { + setLoadList(true); + const response = await apiInvestmentGetDocument({ + id: id as string, + category: "all-document", + }); + + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + setList([]); + } finally { + setLoadList(false); + } + }; + + const handlerDeleteDocument = async () => { + try { + const response = await apiInvestmentDeleteDocument({ + id: selectId as string, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Data berhasil dihapus", + }); + setList((prev: any[] | null) => { + if (!prev) return null; + return prev.filter((item: any) => item.id !== selectId); + }); + setOpenDrawerBox(false); + setSelectId(null); + } + } catch (error) { + console.log("[ERROR]", error); + Toast.show({ + type: "error", + text1: "Gagal menghapus data", + }); + } + }; + + return ( + <> + , + headerRight: () => ( + { + setOpenDrawer(true); + setOpenDrawerBox(false); + }} + /> + ), + }} + /> + + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + + Tidak ada data + + ) : ( + list?.map((item: any, index: number) => ( + { + setSelectId(item.id); + setOpenDrawerBox(true); + }} + /> + } + href={`/(file)/${item.fileId}`} + /> + )) + )} + + + {/* Drawer On Header */} + setOpenDrawer(false)} + height={"auto"} + > + + ), + label: "Tambah Dokumen", + path: `/investment/${id}/(document)/add-document`, + }, + ]} + onPressItem={(item) => { + router.push(item.path as any); + setOpenDrawer(false); + }} + /> + + + {/* Drawer On Box */} + setOpenDrawerBox(false)} + height={"auto"} + > + , + label: "Edit Dokumen", + path: `/investment/${selectId}/(document)/edit-document`, + }, + { + icon: ( + + ), + label: "Hapus Dokumen", + path: "" as any, + color: MainColor.red, + }, + ]} + onPressItem={(item) => { + if (item.path === ("" as any)) { + AlertDefaultSystem({ + title: "Hapus Dokumen", + message: "Apakah anda yakin ingin menghapus dokumen ini?", + textLeft: "Batal", + textRight: "Hapus", + onPressRight: () => { + handlerDeleteDocument(); + }, + }); + } else { + router.push(item.path as any); + } + + setOpenDrawerBox(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(my-holding)/[id].tsx b/app/(application)/(user)/investment/[id]/(my-holding)/[id].tsx new file mode 100644 index 0000000..ae0dc73 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(my-holding)/[id].tsx @@ -0,0 +1,154 @@ +import { + BackButton, + BaseBox, + DotButton, + DrawerCustom, + Grid, + MenuDrawerDynamicGrid, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IconDocument, IconEdit, IconNews } from "@/components/_Icon"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_MEDIUM } from "@/constants/constans-value"; +import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail"; +import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection"; +import { AntDesign, MaterialIcons } from "@expo/vector-icons"; +import { router, Stack, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useState } from "react"; + +export default function InvestmentDetailHolding() { + const { id, status } = useLocalSearchParams(); + const [openDrawerDraft, setOpenDrawerDraft] = useState(false); + const [openDrawerPublish, setOpenDrawerPublish] = useState(false); + + const handlePressDraft = (item: IMenuDrawerItem) => { + console.log("PATH >> ", item.path); + router.navigate(item.path as any); + setOpenDrawerDraft(false); + }; + + const handlePressPublish = (item: IMenuDrawerItem) => { + console.log("PATH >> ", item.path); + router.navigate(item.path as any); + setOpenDrawerPublish(false); + }; + + const bottomSection = ( + + ); + + return ( + <> + , + headerRight: () => + status === "draft" ? ( + setOpenDrawerDraft(true)} /> + ) : status === "publish" ? ( + setOpenDrawerPublish(true)} /> + ) : null, + }} + /> + + + + + + + Nila Transaksi + + + Rp. 7.500.000 + + + + + Saham Terbeli + + + 300 Lembar + + + + + + + + {/* ========= Draft Drawer ========= */} + setOpenDrawerDraft(false)} + height={"auto"} + > + , + label: "Edit Data", + path: `/investment/${id}/edit`, + }, + { + icon: ( + + ), + label: "Edit Prospektus", + path: `/investment/${id}/edit-prospectus`, + }, + { + icon: ( + + ), + label: "Update Dokumen", + path: `/investment/${id}/recap-of-document`, + }, + ]} + columns={4} + onPressItem={handlePressDraft as any} + /> + + + {/* ========= Publish Drawer ========= */} + setOpenDrawerPublish(false)} + height={"auto"} + > + , + label: "Update Dokumen", + path: `/investment/${id}/recap-of-document`, + }, + { + icon: , + label: "Update Berita", + path: `/investment/${id}/(news)/recap-of-news`, + }, + ]} + onPressItem={handlePressPublish as any} + /> + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(news)/[news]/index.tsx b/app/(application)/(user)/investment/[id]/(news)/[news]/index.tsx new file mode 100644 index 0000000..50d6920 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(news)/[news]/index.tsx @@ -0,0 +1,128 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AlertDefaultSystem, + BackButton, + BaseBox, + DotButton, + DrawerCustom, + DummyLandscapeImage, + MenuDrawerDynamicGrid, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IconTrash } from "@/components/_Icon/IconTrash"; +import { useAuth } from "@/hooks/use-auth"; +import { + apiInvestmentDeleteNews, + apiInvestmentGetNews, +} from "@/service/api-client/api-investment"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function InvestmentNews() { + const { user } = useAuth(); + const { news } = useLocalSearchParams(); + const id = news as string; + const [openDrawer, setOpenDrawer] = useState(false); + const [data, setData] = useState(null); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetNews({ + id: id, + category: "one-news", + }); + + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + return ( + <> + , + headerRight: () => + user?.id === data?.authorId && ( + setOpenDrawer(true)} /> + ), + }} + /> + + + + {data && data?.imageId && ( + + )} + + {(data && data?.title) || "-"} + + {(data && data?.deskripsi) || "-"} + + + + + setOpenDrawer(false)} + height={"auto"} + > + , + color: "red", + }, + ]} + onPressItem={(item) => { + AlertDefaultSystem({ + title: "Hapus Berita", + message: "Apakah Anda yakin ingin menghapus berita ini?", + textLeft: "Batal", + textRight: "Hapus", + onPressRight: async () => { + try { + const response = await apiInvestmentDeleteNews({ id }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Berita berhasil dihapus", + }); + router.back(); + setOpenDrawer(false); + } else { + Toast.show({ + type: "error", + text1: "Gagal menghapus berita", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } + }, + }); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(news)/add-news.tsx b/app/(application)/(user)/investment/[id]/(news)/add-news.tsx new file mode 100644 index 0000000..a8ed1d9 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(news)/add-news.tsx @@ -0,0 +1,125 @@ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiInvestmentCreateNews } from "@/service/api-client/api-investment"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function InvestmentAddNews() { + const { id } = useLocalSearchParams(); + const [image, setImage] = useState(null); + const [data, setData] = useState({ + title: "", + deskripsi: "", + }); + const [isLoading, setIsLoading] = useState(false); + + const handlerSubmit = async () => { + let imageId = ""; + + if (!data.title || !data.deskripsi) { + Toast.show({ + type: "error", + text1: "Judul dan deskripsi harus diisi", + }); + return; + } + + try { + setIsLoading(true); + if (image) { + const uploadImage = await uploadFileService({ + dirId: DIRECTORY_ID.investasi_berita, + imageUri: image.uri, + }); + + imageId = uploadImage.data.id; + } + + const newData = { + id: id as string, + title: data.title, + deskripsi: data.deskripsi, + imageId: imageId, + }; + + const response = await apiInvestmentCreateNews({ + id: id as string, + data: newData, + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Berita berhasil disimpan", + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: "Gagal menyimpan berita", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + { + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); + }} + icon="upload" + > + Upload + + + setData({ ...data, title: value })} + /> + setData({ ...data, deskripsi: value })} + /> + + + Simpan + + + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(news)/list-of-news.tsx b/app/(application)/(user)/investment/[id]/(news)/list-of-news.tsx new file mode 100644 index 0000000..8dd03cc --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(news)/list-of-news.tsx @@ -0,0 +1,100 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + BaseBox, + DrawerCustom, + LoaderCustom, + MenuDrawerDynamicGrid, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IconPlus } from "@/components/_Icon"; +import { apiInvestmentGetNews } from "@/service/api-client/api-investment"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function InvestmentListOfNews() { + const { id } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadList(); + }, [id]) + ); + + const onLoadList = async () => { + try { + setLoadList(true); + const response = await apiInvestmentGetNews({ + id: id as string, + category: "all-news", + }); + + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadList(false); + } + }; + return ( + <> + , + // headerRight: () => setOpenDrawer(true)} />, + }} + /> + + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + + Tidak ada data + + ) : ( + list?.map((item: any, index: number) => ( + + {item.title} + + )) + )} + + + setOpenDrawer(false)} + height={"auto"} + > + , + }, + ]} + onPressItem={(item) => { + router.push(item.path as any); + setOpenDrawer(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(news)/recap-of-news.tsx b/app/(application)/(user)/investment/[id]/(news)/recap-of-news.tsx new file mode 100644 index 0000000..f4b36ab --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(news)/recap-of-news.tsx @@ -0,0 +1,101 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + BaseBox, + DotButton, + DrawerCustom, + LoaderCustom, + MenuDrawerDynamicGrid, + TextCustom, + ViewWrapper, +} from "@/components"; +import { IconPlus } from "@/components/_Icon"; +import { apiInvestmentGetNews } from "@/service/api-client/api-investment"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function InvestmentRecapOfNews() { + const { id } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadList(); + }, [id]) + ); + + const onLoadList = async () => { + try { + setLoadList(true); + const response = await apiInvestmentGetNews({ + id: id as string, + category: "all-news", + }); + + setList(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadList(false); + } + }; + + return ( + <> + , + headerRight: () => setOpenDrawer(true)} />, + }} + /> + + {loadList ? ( + + ) : _.isEmpty(list) ? ( + + Tidak ada data + + ) : ( + list?.map((item: any, index: number) => ( + + {item.title} + + )) + )} + + + setOpenDrawer(false)} + height={"auto"} + > + , + }, + ]} + onPressItem={(item) => { + router.push(item.path as any); + setOpenDrawer(false); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(transaction-flow)/failed.tsx b/app/(application)/(user)/investment/[id]/(transaction-flow)/failed.tsx new file mode 100644 index 0000000..9b10578 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(transaction-flow)/failed.tsx @@ -0,0 +1,79 @@ +import { BaseBox, Grid, Spacing, StackCustom, TextCustom, ViewWrapper } from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { GStyles } from "@/styles/global-styles"; +import { FontAwesome6 } from "@expo/vector-icons"; + +export default function InvestmentFailed() { + return ( + + + + + + Transaksi anda gagal karena bukti transfer tidak sesuai dengan + data kami. Jika ini masalah khusus silahkan hubungi pada kontak + whatsapp kami. + + + + + + + + + Detail Transaksi + + + + + + {listData.map((item, i) => ( + + + {item.label} + + + + {item.value} + + + + ))} + + + + + ); +} + +const listData = [ + { + label: "Bank", + value: " BCA", + }, + { + label: "Rekening Penerima", + value: "Himpunan Pengusaha Muda Indonesia", + }, + { + label: "No Rekening", + value: "2304235678854332", + }, + { + label: "Jumlah", + value: "Rp. 1.000.000", + }, + { + label: "Tanggal", + value: "2022-01-01", + }, + { + label: "Lembar Terbeli", + value: "100", + }, +]; diff --git a/app/(application)/(user)/investment/[id]/(transaction-flow)/index.tsx b/app/(application)/(user)/investment/[id]/(transaction-flow)/index.tsx new file mode 100644 index 0000000..a3f2494 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(transaction-flow)/index.tsx @@ -0,0 +1,167 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCustom, + Divider, + Grid, + StackCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { LOCAL_STORAGE_KEY } from "@/constants/local-storage-key"; +import { apiInvestmentGetOne } from "@/service/api-client/api-investment"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; + +export default function InvestmentInvest() { + const { id } = useLocalSearchParams(); + // console.log("[ID]", id); + const [data, setData] = useState(null); + const [jumlah, setJumlah] = useState(0); + const [total, setTotal] = useState(0); + const [sisaLembar, setSisaLembar] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetOne({ + id: id as string, + }); + + setData(response.data); + setSisaLembar(response.data?.sisaLembar); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handleTextChange = (text: string) => { + // Izinkan input kosong → anggap sebagai 0 (atau abaikan, tergantung UX) + if (text === "") { + setJumlah(0); + setTotal(0); + return; + } + + // Regex: hanya digit (angka bulat positif) + const integerRegex = /^\d+$/; + + if (integerRegex.test(text)) { + const numValue = Number(text); + // Karena regex sudah pastikan hanya angka, isNaN biasanya false + // Tapi tetap aman untuk cek + if (!isNaN(numValue)) { + setJumlah(numValue); + setTotal(numValue * Number(data?.hargaLembar)); + console.log("[VALUE]", numValue); + } + } + // Jika input tidak valid (misal: "12a", "12."), abaikan → state tidak berubah + }; + + const buttonSubmit = () => { + return ( + <> + + sisaLembar} + onPress={async () => { + try { + setIsLoading(true); + await AsyncStorage.setItem( + LOCAL_STORAGE_KEY.transactionInvestment, + JSON.stringify({ jumlah, total }) + ); + router.push(`/investment/${id}/select-bank`); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }} + > + Beli + + + + ); + }; + + return ( + <> + + + + + + Sisa Lembar Saham + + + {data?.sisaLembar || "-"} + + + + + Harga Per Lembar + + + {data?.hargaLembar || "-"} + + + + + Jumlah Pembelian + + Minimum 10 lembar + + + + { + handleTextChange(value); + }} + /> + + + + + + Total Harga + + + Rp. {formatCurrencyDisplay(total)} + + + + + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(transaction-flow)/invoice.tsx b/app/(application)/(user)/investment/[id]/(transaction-flow)/invoice.tsx new file mode 100644 index 0000000..e76a5a1 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(transaction-flow)/invoice.tsx @@ -0,0 +1,239 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + ButtonCenteredOnly, + ButtonCustom, + Grid, + InformationBox, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import CopyButton from "@/components/Button/CoyButton"; +import { MainColor } from "@/constants/color-palet"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { + apiInvestmentGetInvoice, + apiInvestmentUpdateInvoice, +} from "@/service/api-client/api-investment"; +import { uploadFileService } from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import { View } from "react-native"; +import Toast from "react-native-toast-message"; + +export default function InvestmentInvoice() { + const { id } = useLocalSearchParams(); + console.log("[ID]", id); + const [data, setData] = useState({}); + const [image, setImage] = useState({ + name: "", + uri: "", + size: 0, + }); + const [isLoading, setIsLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetInvoice({ + id: id as string, + category: "invoice", + }); + + console.log("[RES INVOICE]", JSON.stringify(response.data, null, 2)); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlerSubmitUpdate = async () => { + try { + setIsLoading(true); + const responseUploadImage = await uploadFileService({ + dirId: DIRECTORY_ID.investasi_bukti_transfer, + imageUri: image?.uri, + }); + + console.log("[RESPONSE UPLOAD IMAGE]", responseUploadImage); + + if (!responseUploadImage?.data?.id) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah bukti transfer", + }); + return; + } + + const response = await apiInvestmentUpdateInvoice({ + id: id as string, + data: { + imageId: responseUploadImage?.data?.id, + }, + status: "proses", + }); + + if (response.success) { + console.log( + "[RESPONSE UPDATE]", + JSON.stringify(response.data, null, 2) + ); + Toast.show({ + type: "success", + text1: "Berhasil mengunggah bukti transfer", + }); + router.push(`/investment/${id}/(transaction-flow)/process`); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + + + + + + + + Bank + + + {data?.MasterBank?.namaBank} + + + + + + Nama Akun + + + {data?.MasterBank?.namaAkun} + + + + + + + + {data?.MasterBank?.norek} + + + + + + + + + + + + + Jumlah Transaksi + + + + + + + + Rp. {formatCurrencyDisplay(data?.nominal)} + + + + + + + + + + + + + + Upload bukti transfer anda. + + {image ? ( + + + {image?.name} + + + ) : ( + + Tidak ada gambar yang diunggah + + )} + { + pickFile({ + allowedType: "image", + setImageUri(file: any) { + console.log("[IMAGE]", file); + setImage(file); + }, + }); + }} + icon="upload" + > + Upload + + + + + { + handlerSubmitUpdate(); + }} + > + Saya Sudah Transfer + + + + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(transaction-flow)/process.tsx b/app/(application)/(user)/investment/[id]/(transaction-flow)/process.tsx new file mode 100644 index 0000000..8ebdd23 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(transaction-flow)/process.tsx @@ -0,0 +1,41 @@ +import { BaseBox, StackCustom, TextCustom, ViewWrapper } from "@/components"; +import MoneyTransferAnimation from "@/components/_ShareComponent/MoneyTransferAnimation"; +import { View } from "react-native"; + +export default function InvestmentProcess() { + return ( + <> + + + + + Admin sedang memvalidasi data dan bukti transfer anda. Mohon + tunggu proses ini selesai. + + + + + + + + {/* + + + + Hubungi admin jika tidak kunjung di proses! Klik pada logo + Whatsapp ini. + + + + + + + */} + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(transaction-flow)/select-bank.tsx b/app/(application)/(user)/investment/[id]/(transaction-flow)/select-bank.tsx new file mode 100644 index 0000000..9deb392 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(transaction-flow)/select-bank.tsx @@ -0,0 +1,102 @@ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCustom, + ViewWrapper, +} from "@/components"; +import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom"; +import { LOCAL_STORAGE_KEY } from "@/constants/local-storage-key"; +import { useAuth } from "@/hooks/use-auth"; +import { apiInvestmentCreateInvoice } from "@/service/api-client/api-investment"; +import { apiMasterBank } from "@/service/api-client/api-master"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { router, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; + +export default function InvestmentSelectBank() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + const [select, setSelect] = useState(""); + const [listBank, setListBank] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + loadListBank(); + }, []); + + const loadListBank = async () => { + try { + const response = await apiMasterBank(); + + setListBank(response.data); + } catch (error) { + console.log("[ERROR]", error); + setListBank([]); + } + }; + + const handlerSubmit = async () => { + try { + setIsLoading(true); + const dataCheckout = await AsyncStorage.getItem( + LOCAL_STORAGE_KEY.transactionInvestment + ); + if (dataCheckout) { + const storage = JSON.parse(dataCheckout); + const newData = { + ...storage, + bankId: select, + authorId: user?.id, + }; + + const response = await apiInvestmentCreateInvoice({ + id: id as string, + data: newData, + }); + + if (response.success) { + console.log("[RESPONSE >>]", response); + const invoiceId = response.data.id; + + const delStorage = await AsyncStorage.removeItem( + LOCAL_STORAGE_KEY.transactionInvestment + ); + + console.log("[DEL STORAGE]", delStorage); + router.replace(`/investment/${invoiceId}/invoice`); + } else { + console.log("[FAILED]", response); + } + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = () => { + return ( + <> + + handlerSubmit()}>Pilih + + + ); + }; + + return ( + + + {_.isEmpty(listBank) + ? [] + : listBank?.map((item: any) => ( + + + + ))} + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/(transaction-flow)/success.tsx b/app/(application)/(user)/investment/[id]/(transaction-flow)/success.tsx new file mode 100644 index 0000000..a087ecb --- /dev/null +++ b/app/(application)/(user)/investment/[id]/(transaction-flow)/success.tsx @@ -0,0 +1,84 @@ +import { + BaseBox, + Grid, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { GStyles } from "@/styles/global-styles"; +import { FontAwesome6 } from "@expo/vector-icons"; + +export default function InvestmentSuccess() { + return ( + + + + + + + + Terimakasih telah percaya pada kami untuk mengelola dana anda! + Info mengenai update Investasi ini bisa di lihat di kolom berita. + + + + + + + Detail Transaksi + + + + + + + {listData.map((item, i) => ( + + + {item.label} + + + {item.value} + + + ))} + + + + + ); +} + +const listData = [ + { + label: "Bank", + value: " BCA", + }, + { + label: "Rekening Penerima", + value: "Himpunan Pengusaha Muda Indonesia", + }, + { + label: "No Rekening", + value: "2304235678854332", + }, + { + label: "Jumlah", + value: "Rp. 1.000.000", + }, + { + label: "Tanggal", + value: "2022-01-01", + }, + { + label: "Lembar Terbeli", + value: "100", + }, +]; diff --git a/app/(application)/(user)/investment/[id]/[status]/detail.tsx b/app/(application)/(user)/investment/[id]/[status]/detail.tsx new file mode 100644 index 0000000..7c7254d --- /dev/null +++ b/app/(application)/(user)/investment/[id]/[status]/detail.tsx @@ -0,0 +1,167 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + DotButton, + DrawerCustom, + MenuDrawerDynamicGrid, + ViewWrapper, +} from "@/components"; +import { IconDocument, IconEdit, IconNews } from "@/components/_Icon"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_MEDIUM } from "@/constants/constans-value"; +import { useAuth } from "@/hooks/use-auth"; +import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvestasiSection"; +import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail"; +import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection"; +import { apiInvestmentGetOne } from "@/service/api-client/api-investment"; +import { AntDesign, MaterialIcons } from "@expo/vector-icons"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function InvestmentDetailStatus() { + const { user } = useAuth(); + const { id, status } = useLocalSearchParams(); + const [openDrawerDraft, setOpenDrawerDraft] = useState(false); + const [openDrawerPublish, setOpenDrawerPublish] = useState(false); + + const [data, setData] = useState(null); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id, status]) + ); + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetOne({ + id: id as string, + }); + + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlePressDraft = (item: IMenuDrawerItem) => { + console.log("PATH >> ", item.path); + router.navigate(item.path as any); + setOpenDrawerDraft(false); + }; + + const handlePressPublish = (item: IMenuDrawerItem) => { + console.log("PATH >> ", item.path); + router.navigate(item.path as any); + setOpenDrawerPublish(false); + }; + + const bottomSection = ( + + ); + + const buttonSection = ( + + ); + + return ( + <> + , + headerRight: () => + status === "draft" ? ( + setOpenDrawerDraft(true)} /> + ) : status === "publish" ? ( + setOpenDrawerPublish(true)} /> + ) : null, + }} + /> + + + + + + {/* ========= Draft Drawer ========= */} + setOpenDrawerDraft(false)} + height={"auto"} + > + , + label: "Edit Data", + path: `/investment/${id}/edit`, + }, + { + icon: ( + + ), + label: "Edit Prospektus", + path: `/investment/${id}/edit-prospectus`, + }, + { + icon: ( + + ), + label: "Update Dokumen", + path: `/investment/${id}/(document)/recap-of-document`, + }, + ]} + columns={4} + onPressItem={handlePressDraft as any} + /> + + + {/* ========= Publish Drawer ========= */} + setOpenDrawerPublish(false)} + height={"auto"} + > + , + label: "Update Dokumen", + path: `/investment/${id}/(document)/recap-of-document`, + }, + { + icon: , + label: "Update Berita", + path: `/investment/${id}/(news)/recap-of-news`, + }, + ]} + onPressItem={handlePressPublish as any} + /> + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/edit-prospectus.tsx b/app/(application)/(user)/investment/[id]/edit-prospectus.tsx new file mode 100644 index 0000000..9c33cf1 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/edit-prospectus.tsx @@ -0,0 +1,158 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + CenterCustom, + InformationBox, + LoaderCustom, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { + apiInvestmentGetOne, + apiInvestmentUpdateData, +} from "@/service/api-client/api-investment"; +import { deleteFileService, uploadFileService } from "@/service/upload-service"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function InvestmentEditProspectus() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(null); + const [loadingGet, setLoadingGet] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [pdf, setPdf] = useState(null); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + setLoadingGet(true); + const response = await apiInvestmentGetOne({ + id: id as string, + }); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingGet(false); + } + }; + + const handleSubmitUpdate = async () => { + const prevProspectusFileId = data?.prospektusFileId; + try { + setIsLoading(true); + + const responseUploadImage = await uploadFileService({ + imageUri: pdf?.uri as any, + dirId: DIRECTORY_ID.investasi_prospektus, + }); + + if (!responseUploadImage.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + const prospektusFileId = responseUploadImage.data.id; + + const responseUpdate = await apiInvestmentUpdateData({ + id: id as string, + data: prospektusFileId, + category: "prospectus", + }); + + if (responseUpdate.success) { + const deletePrevImage = await deleteFileService({ + id: prevProspectusFileId as any, + }); + + if (!deletePrevImage.success) { + console.log("[ERROR DELETE PREV IMAGE]", deletePrevImage.message); + return; + } + + Toast.show({ + type: "success", + text1: "Data berhasil diupdate", + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: responseUpdate.message, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonFooter = ( + + + Update + + + ); + return ( + + + + + + + + {loadingGet ? ( + + ) : pdf ? ( + {pdf.name} + ) : ( + + {_.snakeCase(data?.title || "").replace(/_/g, "-")}.pdf + + )} + + + { + pickFile({ + allowedType: "pdf", + setPdfUri(file: any) { + setPdf({ + uri: file.uri, + name: file.name, + size: file.size, + }); + }, + }); + }} + > + Upload + + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/edit.tsx b/app/(application)/(user)/investment/[id]/edit.tsx new file mode 100644 index 0000000..3475d27 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/edit.tsx @@ -0,0 +1,351 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + LoaderCustom, + SelectCustom, + Spacing, + StackCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import API_STRORAGE from "@/constants/base-url-api-strorage"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { + apiInvestmentGetOne, + apiInvestmentUpdateData, +} from "@/service/api-client/api-investment"; +import { apiMasterInvestment } from "@/service/api-client/api-master"; +import { + deleteFileService, + uploadFileService, +} from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +interface IInvestment { + title?: string; + targetDana?: string; + hargaLembar?: string; + totalLembar?: string; + roi?: string; + masterPencarianInvestorId?: string; + masterPeriodeDevidenId?: string; + masterPembagianDevidenId?: string; + authorId?: string; + imageId?: string; + prospektusFileId?: string; +} + +export default function InvestmentEdit() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + title: "", + targetDana: "", + hargaLembar: "", + totalLembar: "", + roi: "", + masterPencarianInvestorId: "", + masterPeriodeDevidenId: "", + masterPembagianDevidenId: "", + authorId: "", + imageId: "", + prospektusFileId: "", + }); + + const [image, setImage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [loadingMaster, setLoadingMaster] = useState(false); + const [listPencarianInvestor, setListPencarianInvestor] = useState([]); + const [listPeriodeDeviden, setListPeriodeDeviden] = useState([]); + const [listPembagianDeviden, setListPembagianDeviden] = useState([]); + + useFocusEffect( + useCallback(() => { + onLoadMaster(); + onLoadData(); + }, [id]) + ); + + const onLoadMaster = async () => { + try { + setLoadingMaster(true); + const response = await apiMasterInvestment({ category: "" }); + + setListPencarianInvestor(response.data.pencarianInvestor); + setListPeriodeDeviden(response.data.periodeDeviden); + setListPembagianDeviden(response.data.pembagianDeviden); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingMaster(false); + } + }; + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetOne({ + id: id as string, + }); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const displayTargetDana = formatCurrencyDisplay(data?.targetDana); + const displayHargaPerLembar = formatCurrencyDisplay(data?.hargaLembar); + const realTotalLembar = Number(data?.targetDana) / Number(data?.hargaLembar); + const displayTotalLembar = formatCurrencyDisplay(realTotalLembar); + + const handleChangeCurrency = (field: keyof typeof data) => (text: string) => { + const numeric = text.replace(/\D/g, ""); + setData((prev) => ({ ...prev, [field]: numeric })); + }; + + const validateData = () => { + if ( + !data.title || + !data.targetDana || + !data.hargaLembar || + !data.totalLembar || + !data.roi || + !data.masterPencarianInvestorId || + !data.masterPeriodeDevidenId || + !data.masterPembagianDevidenId + ) { + Toast.show({ + type: "info", + text1: "Harap isi semua data", + }); + return false; + } + + return true; + }; + + const handleSubmitUpdate = async () => { + let newData = { + ...data, + totalLembar: realTotalLembar.toString(), + }; + + if (!validateData()) { + return; + } + + try { + setIsLoading(true); + + if (image) { + const responseUploadImage = await uploadFileService({ + imageUri: image, + dirId: DIRECTORY_ID.investasi_image, + }); + + if (!responseUploadImage.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const deletePrevImage = await deleteFileService({ + id: data?.imageId as any, + }); + + if (!deletePrevImage.success) { + Toast.show({ + type: "error", + text1: "Gagal menghapus gambar", + }); + return; + } + + newData = { + ...newData, + imageId: responseUploadImage.data.id, + }; + } + + const responseUpdate = await apiInvestmentUpdateData({ + id: id as string, + data: newData, + category: "data" + }); + + if (responseUpdate.success) { + Toast.show({ + type: "success", + text1: "Data berhasil diupdate", + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: responseUpdate.message, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + { + pickFile({ + setImageUri: ({ uri }) => { + setImage(uri); + }, + allowedType: "image", + }); + }} + > + Upload + + + + + + + setData({ ...data, title: value })} + /> + + + + + + setData({ ...data, totalLembar: value })} + value={displayTotalLembar} + /> + + setData({ ...data, roi: value })} + value={data?.roi === "" ? "" : data?.roi} + /> + + {loadingMaster ? ( + + ) : ( + ({ + label: item.name + `${" "}hari`, + value: item.id, + })) + } + onChange={(value) => + setData({ ...data, masterPencarianInvestorId: value as any }) + } + value={data.masterPencarianInvestorId} + /> + )} + + {loadingMaster ? ( + + ) : ( + ({ + label: item.name, + value: item.id, + })) + } + onChange={(value) => + setData({ ...data, masterPeriodeDevidenId: value as any }) + } + value={data.masterPeriodeDevidenId} + /> + )} + + {loadingMaster ? ( + + ) : ( + ({ + label: item.name + `${" "}bulan`, + value: item.id, + })) + } + onChange={(value) => + setData({ ...data, masterPembagianDevidenId: value as any }) + } + value={data.masterPembagianDevidenId} + /> + )} + + + + Simpan + + + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/index.tsx b/app/(application)/(user)/investment/[id]/index.tsx new file mode 100644 index 0000000..7be9c64 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/index.tsx @@ -0,0 +1,166 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + DotButton, + DrawerCustom, + MenuDrawerDynamicGrid, + ViewWrapper, +} from "@/components"; +import { IconDocument, IconEdit, IconNews } from "@/components/_Icon"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_MEDIUM } from "@/constants/constans-value"; +import { useAuth } from "@/hooks/use-auth"; +import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvestasiSection"; +import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail"; +import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection"; +import { apiInvestmentGetOne } from "@/service/api-client/api-investment"; +import { AntDesign, MaterialIcons } from "@expo/vector-icons"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function InvestmentDetail() { + const { user } = useAuth(); + const { id, status } = useLocalSearchParams(); + const [openDrawerDraft, setOpenDrawerDraft] = useState(false); + const [openDrawerPublish, setOpenDrawerPublish] = useState(false); + const [data, setData] = useState(null); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id, status]) + ); + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetOne({ + id: id as string, + }); + + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlePressDraft = (item: IMenuDrawerItem) => { + console.log("PATH >> ", item.path); + router.navigate(item.path as any); + setOpenDrawerDraft(false); + }; + + const handlePressPublish = (item: IMenuDrawerItem) => { + console.log("PATH >> ", item.path); + router.navigate(item.path as any); + setOpenDrawerPublish(false); + }; + + const bottomSection = ( + + ); + + const buttonSection = ( + + ); + + return ( + <> + , + headerRight: () => + status === "draft" ? ( + setOpenDrawerDraft(true)} /> + ) : status === "publish" ? ( + setOpenDrawerPublish(true)} /> + ) : null, + }} + /> + + + + + + {/* ========= Draft Drawer ========= */} + setOpenDrawerDraft(false)} + height={"auto"} + > + , + label: "Edit Data", + path: `/investment/${id}/edit`, + }, + { + icon: ( + + ), + label: "Edit Prospektus", + path: `/investment/${id}/edit-prospectus`, + }, + { + icon: ( + + ), + label: "Update Dokumen", + path: `/investment/${id}/recap-of-document`, + }, + ]} + columns={4} + onPressItem={handlePressDraft as any} + /> + + + {/* ========= Publish Drawer ========= */} + setOpenDrawerPublish(false)} + height={"auto"} + > + , + label: "Update Dokumen", + path: `/investment/${id}/recap-of-document`, + }, + { + icon: , + label: "Update Berita", + path: `/investment/${id}/(news)/recap-of-news`, + }, + ]} + onPressItem={handlePressPublish as any} + /> + + + ); +} diff --git a/app/(application)/(user)/investment/[id]/investor.tsx b/app/(application)/(user)/investment/[id]/investor.tsx new file mode 100644 index 0000000..703d954 --- /dev/null +++ b/app/(application)/(user)/investment/[id]/investor.tsx @@ -0,0 +1,21 @@ +import { + AvatarUsernameAndOtherComponent, + BoxWithHeaderSection, + TextCustom, + ViewWrapper, +} from "@/components"; + +export default function InvestmentInvestor() { + return ( + <> + + {Array.from({ length: 10 }).map((_, index) => ( + + + Rp. 7.000.000 + + ))} + + + ); +} diff --git a/app/(application)/(user)/investment/create.tsx b/app/(application)/(user)/investment/create.tsx new file mode 100644 index 0000000..ca64b6b --- /dev/null +++ b/app/(application)/(user)/investment/create.tsx @@ -0,0 +1,367 @@ +import { + BaseBox, + ButtonCenteredOnly, + ButtonCustom, + CenterCustom, + InformationBox, + LandscapeFrameUploaded, + LoaderCustom, + SelectCustom, + Spacing, + StackCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { useAuth } from "@/hooks/use-auth"; +import { apiInvestmentCreate } from "@/service/api-client/api-investment"; +import { apiMasterInvestment } from "@/service/api-client/api-master"; +import { uploadFileService } from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { FontAwesome5 } from "@expo/vector-icons"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function InvestmentCreate() { + const { user } = useAuth(); + const [data, setData] = useState({ + title: "", + targetDana: "", + hargaPerLembar: "", + totalLembar: "", + rasioKeuntungan: "", + pencarianInvestor: "", + periodeDeviden: "", + pembagianDeviden: "", + authorId: "", + imageId: "", + prospektusFileId: "", + }); + const [image, setImage] = useState(null); + const [pdf, setPdf] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const [loadingMaster, setLoadingMaster] = useState(false); + const [listPencarianInvestor, setListPencarianInvestor] = useState([]); + const [listPeriodeDeviden, setListPeriodeDeviden] = useState([]); + const [listPembagianDeviden, setListPembagianDeviden] = useState([]); + + useFocusEffect( + useCallback(() => { + onLoadMaster(); + }, []) + ); + + const onLoadMaster = async () => { + try { + setLoadingMaster(true); + const response = await apiMasterInvestment({ category: "" }); + + setListPencarianInvestor(response.data.pencarianInvestor); + setListPeriodeDeviden(response.data.periodeDeviden); + setListPembagianDeviden(response.data.pembagianDeviden); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingMaster(false); + } + }; + + const displayTargetDana = formatCurrencyDisplay(data.targetDana); + const displayHargaPerLembar = formatCurrencyDisplay(data.hargaPerLembar); + const realTotalLembar = Number(data.targetDana) / Number(data.hargaPerLembar); + const displayTotalLembar = formatCurrencyDisplay(realTotalLembar); + + const handleChangeCurrency = (field: keyof typeof data) => (text: string) => { + const numeric = text.replace(/\D/g, ""); + setData((prev) => ({ ...prev, [field]: numeric })); + }; + + const validateData = () => { + if ( + !data.title || + !data.targetDana || + !data.hargaPerLembar || + !data.rasioKeuntungan || + !data.pencarianInvestor || + !data.periodeDeviden || + !data.pembagianDeviden + ) { + Toast.show({ + type: "error", + text1: "Harap isi semua data", + }); + return false; + } + + return true; + }; + + const handleSubmit = async () => { + if (!validateData()) { + return; + } + + if (!image || !pdf) { + Toast.show({ + type: "error", + text1: "Harap upload gambar dan file PDF", + }); + return; + } + + try { + setIsLoading(true); + const responseUploadImage = await uploadFileService({ + imageUri: image, + dirId: DIRECTORY_ID.investasi_image, + }); + + if (!responseUploadImage.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUploadImage.data.id; + const responseUploadPdf = await uploadFileService({ + imageUri: pdf.uri, + dirId: DIRECTORY_ID.investasi_prospektus, + }); + + if (!responseUploadPdf.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah file PDF", + }); + return; + } + + const pdfId = responseUploadPdf.data.id; + const newData = { + title: data.title, + targetDana: data.targetDana, + hargaLembar: data.hargaPerLembar, + totalLembar: realTotalLembar.toString(), + roi: data.rasioKeuntungan, + masterPencarianInvestorId: data.pencarianInvestor, + masterPembagianDevidenId: data.pembagianDeviden, + masterPeriodeDevidenId: data.periodeDeviden, + authorId: user?.id, + imageId: imageId, + prospektusFileId: pdfId, + }; + + const response = await apiInvestmentCreate({ data: newData }); + + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: response.message, + }); + router.replace("/investment/portofolio"); + } else { + Toast.show({ + type: "error", + text1: "Info", + text2: response.message, + }); + } + } catch (error) { + console.log("error", error); + } finally { + setIsLoading(false); + } + }; + + // const [coba, setCoba] = useState(""); + return ( + + + + + { + pickFile({ + setImageUri: ({ uri }) => { + setImage(uri); + }, + allowedType: "image", + }); + }} + > + Upload + + + + + + + + + {pdf ? ( + {pdf.name} + ) : ( + + )} + + + { + pickFile({ + setPdfUri: ({ uri, name, size }) => { + + setPdf({ uri, name, size }); + }, + allowedType: "pdf", + }); + }} + > + Upload File + + + + setData({ ...data, title: value })} + /> + + + + + + + + + *Total lembar dihitung dari, Target Dana / Harga Perlembar + + + + + setData({ ...data, rasioKeuntungan: value })} + value={ + data.rasioKeuntungan === "" ? "" : data.rasioKeuntungan.toString() + } + /> + + {loadingMaster ? ( + + ) : ( + ({ + label: item.name + `${" "}hari`, + value: item.id, + })) + } + onChange={(value) => + setData({ ...data, pencarianInvestor: value as any }) + } + value={data.pencarianInvestor} + /> + )} + + {loadingMaster ? ( + + ) : ( + ({ + label: item.name, + value: item.id, + })) + } + onChange={(value) => + setData({ ...data, periodeDeviden: value as any }) + } + value={data.periodeDeviden} + /> + )} + + {loadingMaster ? ( + + ) : ( + ({ + label: item.name + `${" "}bulan`, + value: item.id, + })) + } + onChange={(value) => + setData({ ...data, pembagianDeviden: value as any }) + } + value={data.pembagianDeviden} + /> + )} + + + handleSubmit()}> + Simpan + + + + + ); +} diff --git a/app/(application)/(user)/job/(tabs)/_layout.tsx b/app/(application)/(user)/job/(tabs)/_layout.tsx new file mode 100644 index 0000000..f8c8f8e --- /dev/null +++ b/app/(application)/(user)/job/(tabs)/_layout.tsx @@ -0,0 +1,34 @@ +import { IconHome, IconStatus } from "@/components/_Icon"; +import { TabsStyles } from "@/styles/tabs-styles"; +import { Ionicons } from "@expo/vector-icons"; +import { Tabs } from "expo-router"; + +export default function JobTabsLayout() { + return ( + + , + }} + /> + , + }} + /> + ( + + ), + }} + /> + + ); +} diff --git a/app/(application)/(user)/job/(tabs)/archive.tsx b/app/(application)/(user)/job/(tabs)/archive.tsx new file mode 100644 index 0000000..c885bcd --- /dev/null +++ b/app/(application)/(user)/job/(tabs)/archive.tsx @@ -0,0 +1,57 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { apiJobGetAll } from "@/service/api-client/api-job"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function JobArchive() { + const { user } = useAuth(); + const [listData, setListData] = useState([]); + const [isLoadData, setIsLoadData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id]) + ); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiJobGetAll({ + category: "archive", + authorId: user?.id, + }); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + return ( + + {isLoadData ? ( + + ) : _.isEmpty(listData) ? ( + Anda tidak memiliki arsip + ) : ( + listData.map((item, index) => ( + + + {item?.title || "-"} + + + )) + )} + + ); +} diff --git a/app/(application)/(user)/job/(tabs)/index.tsx b/app/(application)/(user)/job/(tabs)/index.tsx new file mode 100644 index 0000000..6910470 --- /dev/null +++ b/app/(application)/(user)/job/(tabs)/index.tsx @@ -0,0 +1,83 @@ +import { + AvatarUsernameAndOtherComponent, + BoxWithHeaderSection, + FloatingButton, + LoaderCustom, + SearchInput, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { apiJobGetAll } from "@/service/api-client/api-job"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function JobBeranda() { + const [listData, setListData] = useState([]); + const [isLoadData, setIsLoadData] = useState(false); + const [search, setSearch] = useState(""); + + useFocusEffect( + useCallback(() => { + onLoadData(search); + }, [search]) + ); + + const onLoadData = async (search: string) => { + try { + setIsLoadData(true); + const response = await apiJobGetAll({ search, category: "beranda" }); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + const handleSearch = (search: string) => { + setSearch(search); + onLoadData(search); + }; + + return ( + router.push("/job/create")} /> + } + headerComponent={ + + } + > + {isLoadData ? ( + + ) : _.isEmpty(listData) ? ( + Belum ada lowongan + ) : ( + listData.map((item, index) => ( + router.push(`/job/${item.id}`)} + > + + + + + {item?.title || "-"} + + + + + )) + )} + + + ); +} diff --git a/app/(application)/(user)/job/(tabs)/status.tsx b/app/(application)/(user)/job/(tabs)/status.tsx new file mode 100644 index 0000000..4c1f004 --- /dev/null +++ b/app/(application)/(user)/job/(tabs)/status.tsx @@ -0,0 +1,84 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + LoaderCustom, + ScrollableCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; +import { apiJobGetByStatus } from "@/service/api-client/api-job"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function JobStatus() { + const { user } = useAuth(); + const [activeCategory, setActiveCategory] = useState( + "publish" + ); + const [listData, setListData] = useState([]); + const [isLoadList, setIsLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id, activeCategory]) + ); + + const onLoadData = async () => { + try { + setIsLoadList(true); + const response = await apiJobGetByStatus({ + authorId: user?.id as string, + status: activeCategory as string, + }); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadList(false); + } + }; + + const handlePress = (item: any) => { + setActiveCategory(item.value); + // tambahkan logika lain seperti filter dsb. + }; + + const scrollComponent = ( + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as any} + /> + ); + + return ( + + {isLoadList ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada data {activeCategory} + ) : ( + listData.map((e, i) => ( + + + {e?.title} + + + )) + )} + + ); +} diff --git a/app/(application)/(user)/job/[id]/[status]/detail.tsx b/app/(application)/(user)/job/[id]/[status]/detail.tsx new file mode 100644 index 0000000..c6b9529 --- /dev/null +++ b/app/(application)/(user)/job/[id]/[status]/detail.tsx @@ -0,0 +1,114 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BackButton, + DotButton, + DrawerCustom, + LoaderCustom, + MenuDrawerDynamicGrid, + Spacing, + StackCustom, + ViewWrapper, +} from "@/components"; +import { IconEdit } from "@/components/_Icon"; +import { IMenuDrawerItem } from "@/components/_Interface/types"; +import ReportBox from "@/components/Box/ReportBox"; +import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection"; +import Job_ButtonStatusSection from "@/screens/Job/ButtonStatusSection"; +import { apiJobGetOne } from "@/service/api-client/api-job"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; +import { useCallback, useState } from "react"; + +export default function JobDetailStatus() { + const { id, status } = useLocalSearchParams(); + const [openDrawer, setOpenDrawer] = useState(false); + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isLoadData, setIsLoadData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiJobGetOne({ id: id as string }); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + const handlePress = (item: IMenuDrawerItem) => { + console.log("PATH >> ", item.path); + router.navigate(item.path as any); + setOpenDrawer(false); + }; + + return ( + <> + , + headerRight: () => + status === "draft" ? ( + setOpenDrawer(true)} /> + ) : null, + }} + /> + + {isLoadData ? ( + + ) : ( + <> + + {data && + data?.catatan && + (status === "draft" || status === "rejected") && ( + + )} + + + + + + + )} + + + setOpenDrawer(false)} + height={"auto"} + > + , + label: "Edit", + path: `/job/${id}/edit`, + }, + ]} + columns={4} + onPressItem={handlePress as any} + /> + + + ); +} diff --git a/app/(application)/(user)/job/[id]/archive.tsx b/app/(application)/(user)/job/[id]/archive.tsx new file mode 100644 index 0000000..9a43f1d --- /dev/null +++ b/app/(application)/(user)/job/[id]/archive.tsx @@ -0,0 +1,100 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCustom, + LoaderCustom, + Spacing, + StackCustom, + ViewWrapper, +} from "@/components"; +import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection"; +import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function JobDetailArchive() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isLoadData, setIsLoadData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiJobGetOne({ id: id as string }); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + const handleArchive = async () => { + try { + setIsLoading(true); + const response = await apiJobUpdateData({ + id: id as string, + data: false, + category: "archive", + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: response.message, + }); + router.back(); + } else { + Toast.show({ + type: "info", + text1: "Info", + text2: response.message, + }); + router.back(); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + {isLoadData ? ( + + ) : ( + + <> + + + { + handleArchive(); + }} + > + Publish kembali + + {/* */} + + + + + )} + + ); +} diff --git a/app/(application)/(user)/job/[id]/edit.tsx b/app/(application)/(user)/job/[id]/edit.tsx new file mode 100644 index 0000000..8941020 --- /dev/null +++ b/app/(application)/(user)/job/[id]/edit.tsx @@ -0,0 +1,198 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + ButtonCenteredOnly, + ButtonCustom, + DummyLandscapeImage, + InformationBox, + LandscapeFrameUploaded, + LoaderCustom, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job"; +import { + deleteFileService, + uploadFileService, +} from "@/service/upload-service"; +import pickImage from "@/utils/pickImage"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function JobEdit() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + title: "", + content: "", + deskripsi: "", + }); + const [isLoadData, setIsLoadData] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const [imageUri, setImageUri] = useState(null); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiJobGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + const handlerOnUpdate = async () => { + if (!data.title || !data.content || !data.deskripsi) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + let newImageId = ""; + + if (imageUri) { + const responseUploadImage = await uploadFileService({ + imageUri: imageUri, + dirId: DIRECTORY_ID.job_image, + }); + + if (responseUploadImage.success) { + newImageId = responseUploadImage.data.id; + } + } + + if (data?.imageId) { + const responseDeleteImage = await deleteFileService({ + id: data.imageId, + }); + + if (!responseDeleteImage.success) { + console.log("[ERROR DELETE IMAGE]", responseDeleteImage.message); + } + } + + const newData = { + title: data.title, + content: data.content, + deskripsi: data.deskripsi, + imageId: newImageId, + }; + + const response = await apiJobUpdateData({ + id: id as string, + data: newData, + category: "edit", + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: response.message, + }); + router.back(); + } else { + Toast.show({ + type: "info", + text1: "Info", + text2: response.message, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = () => { + return ( + <> + handlerOnUpdate()}> + Update + + + + ); + }; + + return ( + + {isLoadData ? ( + + ) : ( + + + + {imageUri ? ( + + ) : ( + + + + )} + + { + pickImage({ + setImageUri, + }); + }} + icon="upload" + > + Upload + + + + + setData({ ...data, title: value })} + /> + + setData({ ...data, content: value })} + /> + + setData({ ...data, deskripsi: value })} + /> + + {buttonSubmit()} + + )} + + ); +} diff --git a/app/(application)/(user)/job/[id]/index.tsx b/app/(application)/(user)/job/[id]/index.tsx new file mode 100644 index 0000000..43356d7 --- /dev/null +++ b/app/(application)/(user)/job/[id]/index.tsx @@ -0,0 +1,104 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { ButtonCustom, LoaderCustom, Spacing, StackCustom, ViewWrapper } from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection"; +import { apiJobGetOne } from "@/service/api-client/api-job"; +import { BASE_URL } from "@/service/api-config"; +import { Ionicons } from "@expo/vector-icons"; +import * as Clipboard from "expo-clipboard"; +import { useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import { Alert, Linking } from "react-native"; + +export default function JobDetail() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + setIsLoading(true); + const response = await apiJobGetOne({ id: id as string }); + + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const baseUrl = BASE_URL; + const linkUrl = `${baseUrl}/job-vacancy/`; + + const OpenLinkButton = ({ id }: { id: string }) => { + const jobUrl = `${linkUrl}${id}`; + + const openInBrowser = async () => { + const supported = await Linking.canOpenURL(jobUrl); + if (supported) { + await Linking.openURL(jobUrl); + } else { + Alert.alert("Gagal membuka link", "Browser tidak tersedia."); + } + }; + + return ( + + } + onPress={openInBrowser} + backgroundColor="green" + textColor="white" + > + Buka Lowongan di Browser + + ); + }; + + const CopyLinkButton = ({ id }: { id: string }) => { + const jobUrl = `${linkUrl}${id}`; + + const copyToClipboard = async () => { + await Clipboard.setStringAsync(jobUrl); + Alert.alert( + "Link disalin", + "Tautan lowongan telah disalin ke clipboard." + ); + }; + + return ( + } + onPress={copyToClipboard} + backgroundColor={MainColor.orange} + textColor="white" + > + Salin Link + + ); + }; + + return ( + + {isLoading ? ( + + ) : ( + <> + + + + + + + + )} + + ); +} diff --git a/app/(application)/(user)/job/create.tsx b/app/(application)/(user)/job/create.tsx new file mode 100644 index 0000000..4009c10 --- /dev/null +++ b/app/(application)/(user)/job/create.tsx @@ -0,0 +1,168 @@ +import { + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, + ViewWrapper +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { useAuth } from "@/hooks/use-auth"; +import { apiJobCreate } from "@/service/api-client/api-job"; +import { uploadFileService } from "@/service/upload-service"; +import pickImage from "@/utils/pickImage"; +import { router } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function JobCreate() { + const nextUrl = "/(application)/(user)/job/(tabs)/status"; + const { user } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [image, setImage] = useState(null); + const [data, setData] = useState({ + title: "", + content: "", + deskripsi: "", + authorId: "", + }); + + const handlerOnSubmit = async () => { + let imageId = ""; + const newData = { + title: data.title, + content: data.content, + deskripsi: data.deskripsi, + authorId: user?.id, + imageId: "", + }; + + if (!data.title || !data.content || !data.deskripsi || !user?.id) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + + if (image === null || !image) { + const response = await apiJobCreate(newData); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: "Lowongan berhasil dibuat", + }); + router.replace(nextUrl); + } + + return; + } + + const responseUploadImage = await uploadFileService({ + imageUri: image, + dirId: DIRECTORY_ID.job_image, + }); + + if (responseUploadImage.success) { + imageId = responseUploadImage.data.id; + } + + const fixData = { + ...newData, + imageId: imageId, + }; + + const response = await apiJobCreate(fixData); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: "Lowongan berhasil dibuat", + }); + router.replace(nextUrl); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = () => { + return ( + <> + handlerOnSubmit()}> + Simpan + + + + ); + }; + + return ( + + + + + {/* + + */} + + { + // router.push("/(application)/(image)/take-picture/123"); + pickImage({ + setImageUri: setImage, + }); + }} + icon="upload" + > + Upload + + + + + setData({ ...data, title: value })} + /> + + setData({ ...data, content: value })} + /> + + setData({ ...data, deskripsi: value })} + /> + + {buttonSubmit()} + + + ); +} diff --git a/app/(application)/(user)/maps/[id]/custom-pin.tsx b/app/(application)/(user)/maps/[id]/custom-pin.tsx new file mode 100644 index 0000000..8ea9893 --- /dev/null +++ b/app/(application)/(user)/maps/[id]/custom-pin.tsx @@ -0,0 +1,55 @@ +import { + AvatarCustom, + BaseBox, + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + MapCustom, + Spacing, + StackCustom, + ViewWrapper, +} from "@/components"; +import CenterCustom from "@/components/Center/CenterCustom"; +import { router, useLocalSearchParams } from "expo-router"; + +export default function MapsCustomPin() { + const { id } = useLocalSearchParams(); + + const buttonFooter = ( + + { + console.log(`Simpan maps ${id}`); + router.back(); + }} + > + Simpan + + + ); + return ( + <> + + + + + + + console.log("Upload")} + icon="upload" + > + Upload + + + + + + + + + + + ); +} diff --git a/app/(application)/(user)/maps/[id]/edit.tsx b/app/(application)/(user)/maps/[id]/edit.tsx new file mode 100644 index 0000000..75f8894 --- /dev/null +++ b/app/(application)/(user)/maps/[id]/edit.tsx @@ -0,0 +1,244 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import API_IMAGE from "@/constants/api-storage"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiMapsGetOne, apiMapsUpdate } from "@/service/api-client/api-maps"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import { StyleSheet, View } from "react-native"; +import MapView, { LatLng, Marker } from "react-native-maps"; +import Toast from "react-native-toast-message"; + +const defaultRegion = { + latitude: -8.737109, + longitude: 115.1756897, + latitudeDelta: 0.1, + longitudeDelta: 0.1, +}; +export default function MapsEdit() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + id: "", + namePin: "", + latitude: "", + longitude: "", + imageId: "", + }); + const [selectedLocation, setSelectedLocation] = useState(null); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiMapsGetOne({ id: id as string }); + + if (response.success) { + setData({ + id: response.data.id, + namePin: response.data.namePin, + latitude: response.data.latitude, + longitude: response.data.longitude, + imageId: response.data.imageId, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handleMapPress = (event: any) => { + const { latitude, longitude } = event.nativeEvent.coordinate; + const location = { latitude, longitude }; + setSelectedLocation(location); + }; + + const handleSubmit = async () => { + let newData: any; + if (!data.namePin) { + Toast.show({ + type: "error", + text1: "Nama pin harus diisi", + }); + return; + } + + newData = { + namePin: data?.namePin, + latitude: selectedLocation?.latitude || data?.latitude, + longitude: selectedLocation?.longitude || data?.longitude, + }; + + try { + setLoading(true); + if (image) { + const responseUpload = await uploadFileService({ + dirId: DIRECTORY_ID.map_image, + imageUri: image?.uri, + }); + + if (!responseUpload?.data?.id) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUpload?.data?.id; + + newData = { + namePin: data?.namePin, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + newImageId: imageId, + }; + } + + const responseUpdate = await apiMapsUpdate({ + id: data?.id, + data: newData, + }); + + if (!responseUpdate.success) { + Toast.show({ + type: "error", + text1: "Gagal mengupdate map", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Map berhasil diupdate", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + const buttonFooter = ( + + + Update + + + ); + + return ( + + + + + + {selectedLocation ? ( + + ) : ( + + )} + + + + setData({ ...data, namePin: value })} + /> + + + + + + { + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); + }} + > + Upload + + + + ); +} + +const styles = StyleSheet.create({ + container: { + width: "100%", + backgroundColor: "#f5f5f5", + overflow: "hidden", + borderRadius: 8, + marginBottom: 20, + }, + map: { + flex: 1, + }, +}); diff --git a/app/(application)/(user)/maps/create.tsx b/app/(application)/(user)/maps/create.tsx new file mode 100644 index 0000000..2c8416b --- /dev/null +++ b/app/(application)/(user)/maps/create.tsx @@ -0,0 +1,143 @@ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import MapSelected from "@/components/Map/MapSelected"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { useAuth } from "@/hooks/use-auth"; +import { apiMapsCreate } from "@/service/api-client/api-maps"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import { LatLng } from "react-native-maps"; +import Toast from "react-native-toast-message"; + +export default function MapsCreate() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + const [selectedLocation, setSelectedLocation] = useState(null); + const [name, setName] = useState(""); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + const handleSubmit = async () => { + try { + setLoading(true); + let newData: any; + newData = { + authorId: user?.id, + portofolioId: id, + namePin: name, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + }; + + if (image) { + const responseUpload = await uploadFileService({ + dirId: DIRECTORY_ID.map_image, + imageUri: image?.uri, + }); + + if (!responseUpload?.data?.id) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUpload?.data?.id; + + newData = { + authorId: user?.id, + portofolioId: id, + namePin: name, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + imageId: imageId, + }; + } + + const response = await apiMapsCreate({ + data: newData, + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal menambahkan map", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Map berhasil ditambahkan", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + const buttonFooter = ( + + + Simpan + + + ); + return ( + + + + + + + + + + + + + + { + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); + }} + > + Upload + + + + ); +} diff --git a/app/(application)/(user)/maps/index.tsx b/app/(application)/(user)/maps/index.tsx new file mode 100644 index 0000000..90ba6eb --- /dev/null +++ b/app/(application)/(user)/maps/index.tsx @@ -0,0 +1,234 @@ +import { + ButtonCustom, + DrawerCustom, + DummyLandscapeImage, + Grid, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import GridTwoView from "@/components/_ShareComponent/GridTwoView"; +import API_IMAGE from "@/constants/api-storage"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import { apiMapsGetAll } from "@/service/api-client/api-maps"; +import { openInDeviceMaps } from "@/utils/openInDeviceMaps"; +import { FontAwesome, Ionicons } from "@expo/vector-icons"; +import { Image } from "expo-image"; +import { router, useFocusEffect } from "expo-router"; +import { useCallback, useState } from "react"; +import { View } from "react-native"; +import MapView, { Marker } from "react-native-maps"; + +const defaultRegion = { + latitude: -8.737109, + longitude: 115.1756897, + latitudeDelta: 0.1, + longitudeDelta: 0.1, + height: 300, +}; + +export interface LocationItem { + id: string | number; + latitude: number; + longitude: number; + name: string; + imageId?: string; +} + +export default function Maps() { + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + const [openDrawer, setOpenDrawer] = useState(false); + const [selected, setSelected] = useState({ + id: "", + bidangBisnis: "", + nomorTelepon: "", + alamatBisnis: "", + namePin: "", + imageId: "", + portofolioId: "", + latitude: 0, + longitude: 0, + }); + + useFocusEffect( + useCallback(() => { + handlerLoadList(); + }, []) + ); + + const handlerLoadList = async () => { + try { + setLoadList(true); + const response = await apiMapsGetAll(); + + if (response.success) { + setList(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadList(false); + } + }; + + return ( + <> + + {/* */} + + {loadList ? ( + + ) : ( + + {list?.map((item: any, index: number) => { + return ( + { + setOpenDrawer(true); + setSelected({ + id: item?.id, + bidangBisnis: + item?.Portofolio?.MasterBidangBisnis?.name, + nomorTelepon: item?.Portofolio?.tlpn, + alamatBisnis: item?.Portofolio?.alamatKantor, + namePin: item?.namePin, + imageId: item?.imageId, + portofolioId: item?.Portofolio?.id, + latitude: item?.latitude, + longitude: item?.longitude, + }); + }} + // Gunakan gambar kustom jika tersedia + > + + + + + ); + })} + + )} + + + + setOpenDrawer(false)} + height={"auto"} + > + + + + + } + rightIcon={{selected.namePin}} + /> + + + } + rightIcon={{selected.bidangBisnis}} + /> + + + } + rightIcon={{selected.nomorTelepon}} + /> + + } + rightIcon={{selected.alamatBisnis}} + /> + + + + { + setOpenDrawer(false); + router.push(`/portofolio/${selected.portofolioId}`); + }} + > + Detail + + + + { + openInDeviceMaps({ + latitude: selected.latitude, + longitude: selected.longitude, + title: selected.namePin, + }); + }} + > + Buka Maps + + + + + + + ); +} diff --git a/app/(application)/(user)/marketplace/index.tsx b/app/(application)/(user)/marketplace/index.tsx new file mode 100644 index 0000000..66431b6 --- /dev/null +++ b/app/(application)/(user)/marketplace/index.tsx @@ -0,0 +1,9 @@ +import { TextCustom, ViewWrapper } from "@/components"; + +export default function Marketplace() { + return ( + + Marketplace + + ); +} \ No newline at end of file diff --git a/app/(application)/(user)/notifications/index.tsx b/app/(application)/(user)/notifications/index.tsx new file mode 100644 index 0000000..e136aad --- /dev/null +++ b/app/(application)/(user)/notifications/index.tsx @@ -0,0 +1,111 @@ +import { + BaseBox, + Grid, + ScrollableCustom, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { useState } from "react"; +import { View } from "react-native"; + +const categories = [ + { value: "all", label: "Semua" }, + { value: "event", label: "Event" }, + { value: "job", label: "Job" }, + { value: "voting", label: "Voting" }, + { value: "donasi", label: "Donasi" }, + { value: "investasi", label: "Investasi" }, + { value: "forum", label: "Forum" }, + { value: "collaboration", label: "Collaboration" }, +]; + +const selectedCategory = (value: string) => { + const category = categories.find((c) => c.value === value); + return category?.label; +}; + +const BoxNotification = ({ + index, + activeCategory, +}: { + index: number; + activeCategory: string | null; +}) => { + return ( + <> + + console.log( + "Notification >", + selectedCategory(activeCategory as string) + ) + } + > + + + # {selectedCategory(activeCategory as string)} + + + + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint odio + unde quidem voluptate quam culpa sequi molestias ipsa corrupti id, + soluta, nostrum adipisci similique, et illo asperiores deleniti eum + labore. + + + + + + {index + 1} Agustus 2025 + + + + + Belum lihat + + + + + + + ); +}; + +export default function Notifications() { + const [activeCategory, setActiveCategory] = useState("all"); + + const handlePress = (item: any) => { + setActiveCategory(item.value); + // tambahkan logika lain seperti filter dsb. + }; + return ( + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as string} + /> + } + > + {Array.from({ length: 20 }).map((e, i) => ( + + + + ))} + + ); +} diff --git a/app/(application)/(user)/portofolio/[id]/create.tsx b/app/(application)/(user)/portofolio/[id]/create.tsx new file mode 100644 index 0000000..550484c --- /dev/null +++ b/app/(application)/(user)/portofolio/[id]/create.tsx @@ -0,0 +1,362 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + ActionIcon, + AvatarComp, + BaseBox, + ButtonCenteredOnly, + CenterCustom, + Grid, + InformationBox, + SelectCustom, + Spacing, + StackCustom, + TextAreaCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { IconPlus } from "@/components/_Icon"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_XLARGE } from "@/constants/constans-value"; +import DUMMY_IMAGE from "@/constants/dummy-image-value"; +import Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio"; +import { + apiMasterBidangBisnis, + apiMasterSubBidangBisnis, +} from "@/service/api-client/api-master"; +import { + IMasterBidangBisnis, + IMasterSubBidangBisnis, +} from "@/types/Type-Master"; +import pickImage from "@/utils/pickImage"; +import { Ionicons } from "@expo/vector-icons"; +import { Image } from "expo-image"; +import { useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import { Text, TouchableOpacity, View } from "react-native"; +import PhoneInput, { ICountry } from "react-native-international-phone-number"; +import { Avatar } from "react-native-paper"; + +export default function PortofolioCreate() { + const { id } = useLocalSearchParams(); + const [selectedCountry, setSelectedCountry] = useState(null); + const [inputValue, setInputValue] = useState(""); + const [data, setData] = useState({ + namaBisnis: "", + masterBidangBisnisId: "", + alamatKantor: "", + tlpn: "", + deskripsi: "", + }); + const [imageUri, setImageUri] = useState(null); + + const [bidangBisnis, setBidangBisnis] = useState([]); + const [subBidangBisnis, setSubBidangBisnis] = useState< + IMasterSubBidangBisnis[] + >([]); + + const [selectedSubBidang, setSelectedSubBidang] = useState([]); + const [listSubBidangSelected, setListSubBidangSelected] = useState([ + { + id: "", + }, + ]); + + const [dataMedsos, setDataMedsos] = useState({ + facebook: "", + twitter: "", + instagram: "", + youtube: "", + tiktok: "", + }); + + const [isLoadingCreate, setIsLoadingCreate] = useState(false); + + function handleInputValue(phoneNumber: string) { + setInputValue(phoneNumber); + const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || ""; + const fixNumber = inputValue.replace(/\s+/g, ""); + const realNumber = callingCode + fixNumber; + setData({ ...data, tlpn: realNumber }); + } + + function handleSelectedCountry(country: ICountry) { + setSelectedCountry(country); + } + + useEffect(() => { + onLoadMaster(); + onLoadMasterSubBidangBisnis(); + }, []); + + const onLoadMaster = async () => { + try { + const response = await apiMasterBidangBisnis(); + setBidangBisnis(response.data); + } catch (error) { + setBidangBisnis([]); + console.log("Error onLoadMasterBidangBisnis", error); + } + }; + + const onLoadMasterSubBidangBisnis = async () => { + try { + const response = await apiMasterSubBidangBisnis({}); + setSubBidangBisnis(response.data); + } catch (error) { + setSubBidangBisnis([]); + console.log("Error onLoadMasterBidangBisnis", error); + } + }; + + const handlerSelectedSubBidang = ({ id }: { id: string }) => { + const selectedList = subBidangBisnis?.filter( + (item) => (item?.masterBidangBisnisId as any) === id + ); + setSelectedSubBidang(selectedList as any[]); + }; + + return ( + + } + > + {/* Portofolio Create {id} */} + + + setData({ ...data, namaBisnis: value })} + /> + + ({ + label: item.name, + value: item.id, + }))} + value={data.masterBidangBisnisId} + onChange={(value) => { + const isSameBidang = data.masterBidangBisnisId === value; + + if (!isSameBidang) { + setListSubBidangSelected([{ id: "" }]); + } + + setData({ ...(data as any), masterBidangBisnisId: value }); + handlerSelectedSubBidang({ id: value as string }); + }} + /> + + {listSubBidangSelected.map((item, index) => ( + { + const selectedValues = listSubBidangSelected.map((s) => s.id); + return ( + option.id === item.id || // biarkan tetap muncul kalau ini valuenya sendiri + !selectedValues.includes(option.id) + ); + }) + .map((e: any) => ({ + value: e.id, + label: e.name, + }))} + value={item.id || null} + onChange={(value) => { + const list = _.clone(listSubBidangSelected); + list[index].id = value as any; + setListSubBidangSelected(list); + }} + /> + ))} + + + + { + setListSubBidangSelected([ + ...listSubBidangSelected, + { id: "" }, + ]); + }} + icon={ + + } + size="xl" + /> + { + const list = _.clone(listSubBidangSelected); + list.pop(); + setListSubBidangSelected(list); + }} + icon={ + + } + size="xl" + /> + + + + + {/* ({ + label: item.name, + value: item.id, + }))} + value={null} + onChange={(value) => { + setData({ ...(data as any), masterBidangBisnisId: value }); + handlerSelectedSubBidang({ id: value as string }); + }} + /> */} + + {/* { + setListSubBidangSelected([...listSubBidangSelected, { id: "" }]); + }} + > + Tambah Pilihan + + */} + + {/* {JSON.stringify(bidangBisnis, null, 2)} */} + + + + + Nomor Telepon + + * + + + + + + + + setData({ ...data, alamatKantor: value }) + } + /> + + setData({ ...data, deskripsi: value })} + autosize + minRows={2} + maxRows={5} + required + showCount + maxLength={1000} + /> + + + {/* Logo */} + + + + + + + { + pickImage({ + setImageUri, + }); + }} + > + Upload + + + + {/* Social Media */} + + + setDataMedsos({ ...dataMedsos, tiktok: value }) + } + /> + + setDataMedsos({ ...dataMedsos, facebook: value }) + } + /> + + setDataMedsos({ ...dataMedsos, instagram: value }) + } + /> + + setDataMedsos({ ...dataMedsos, twitter: value }) + } + /> + + setDataMedsos({ ...dataMedsos, youtube: value }) + } + /> + + + + ); +} diff --git a/app/(application)/(user)/portofolio/[id]/edit-logo.tsx b/app/(application)/(user)/portofolio/[id]/edit-logo.tsx new file mode 100644 index 0000000..9845fb4 --- /dev/null +++ b/app/(application)/(user)/portofolio/[id]/edit-logo.tsx @@ -0,0 +1,152 @@ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + ViewWrapper +} from "@/components"; +import API_STRORAGE from "@/constants/base-url-api-strorage"; +import DIRECTORY_ID from "@/constants/directory-id"; +import DUMMY_IMAGE from "@/constants/dummy-image-value"; +import { useAuth } from "@/hooks/use-auth"; +import { apiFileDelete } from "@/service/api-client/api-file"; +import { + apiGetOnePortofolio, + apiUpdatePortofolio, +} from "@/service/api-client/api-portofolio"; +import { uploadFileService } from "@/service/upload-service"; +import pickImage from "@/utils/pickImage"; +import { Image } from "expo-image"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function PortofolioEditLogo() { + const { id } = useLocalSearchParams(); + const [logoId, setLogoId] = useState(); + const [imageUri, setImageUri] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const { token } = useAuth(); + + useEffect(() => { + onLoadData(id as string); + }, [id]); + + const onLoadData = async (id: string) => { + const response = await apiGetOnePortofolio({ id: id }); + console.log( + "Response portofolio >>", + JSON.stringify(response.data.logoId, null, 2) + ); + setLogoId(response.data.logoId); + }; + + async function onUpload() { + try { + setIsLoading(true); + + const response = await uploadFileService({ + imageUri, + dirId: DIRECTORY_ID.portofolio_logo, + }); + + if (response.success) { + const fileId = response.data.id; + const responseUpdate = await apiUpdatePortofolio({ + id: id as string, + data: { fileId }, + category: "logo", + }); + + if (!responseUpdate.success) { + Toast.show({ + type: "error", + text1: "Info", + text2: responseUpdate.message, + }); + return; + } + + if (logoId) { + const deletePrevFile = await apiFileDelete({ + token: token as string, + id: logoId as string, + }); + + if (!deletePrevFile.success) { + console.log("error delete prev file >>", deletePrevFile.message); + } + } + + Toast.show({ + type: "success", + text1: "Sukses", + text2: "Logo berhasil diupdate", + }); + + router.back(); + } + } catch (error) { + Toast.show({ + type: "error", + text1: "Gagal", + text2: error as string, + }); + } finally { + setIsLoading(false); + } + } + + const image = imageUri ? ( + + ) : ( + + ); + + const buttonFooter = ( + + { + onUpload(); + }} + > + Update + + + ); + + return ( + <> + + + {image} + + { + pickImage({ + setImageUri, + }); + }} + > + Upload + + + + ); +} diff --git a/app/(application)/(user)/portofolio/[id]/edit-social-media.tsx b/app/(application)/(user)/portofolio/[id]/edit-social-media.tsx new file mode 100644 index 0000000..1770c3f --- /dev/null +++ b/app/(application)/(user)/portofolio/[id]/edit-social-media.tsx @@ -0,0 +1,128 @@ +import { + BoxButtonOnFooter, + ButtonCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { + apiGetOnePortofolio, + apiUpdatePortofolio, +} from "@/service/api-client/api-portofolio"; +import { useLocalSearchParams, router } from "expo-router"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function PortofolioEditSocialMedia() { + const { id } = useLocalSearchParams(); + console.log("ID >>", id); + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState({ + facebook: "", + twitter: "", + instagram: "", + tiktok: "", + youtube: "", + }); + + useEffect(() => { + onLoadData(id as string); + }, [id]); + + const onLoadData = async (id: string) => { + const response = await apiGetOnePortofolio({ id: id }); + console.log( + "Response portofolio >>", + JSON.stringify(response.data.Portofolio_MediaSosial, null, 2) + ); + const data = response.data.Portofolio_MediaSosial; + setData({ + facebook: data.facebook, + twitter: data.twitter, + instagram: data.instagram, + tiktok: data.tiktok, + youtube: data.youtube, + }); + }; + + const onSubmitUpdate = async () => { + try { + setIsLoading(true); + const response = await apiUpdatePortofolio({ + id: id as string, + data: data, + category: "medsos", + }); + + if (!response.success) { + Toast.show({ + type: "info", + text1: "Info", + text2: response.message, + }); + + return; + } + + Toast.show({ + type: "success", + text1: "Sukses", + text2: "Data media terupdate", + }); + + router.back(); + } catch (error) { + console.log("Error onSubmitUpdate", error); + } finally { + setIsLoading(false); + } + }; + + const buttonFooter = ( + + + Update + + + ); + + return ( + <> + + setData({ ...data, tiktok: value })} + label="Tiktok" + placeholder="Masukkan tiktok" + /> + setData({ ...data, instagram: value })} + label="Instagram" + placeholder="Masukkan instagram" + /> + setData({ ...data, facebook: value })} + label="Facebook" + placeholder="Masukkan facebook" + /> + setData({ ...data, twitter: value })} + label="Twitter" + placeholder="Masukkan twitter" + /> + setData({ ...data, youtube: value })} + label="Youtube" + placeholder="Masukkan youtube" + /> + + + ); +} diff --git a/app/(application)/(user)/portofolio/[id]/edit.back.txt b/app/(application)/(user)/portofolio/[id]/edit.back.txt new file mode 100644 index 0000000..ac79d06 --- /dev/null +++ b/app/(application)/(user)/portofolio/[id]/edit.back.txt @@ -0,0 +1,357 @@ +import { + ActionIcon, + BoxButtonOnFooter, + ButtonCustom, + CenterCustom, + SelectCustom, + Spacing, + StackCustom, + TextAreaCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_XLARGE } from "@/constants/constans-value"; +import { + apiMasterBidangBisnis, + apiMasterSubBidangBisnis, +} from "@/service/api-client/api-master"; +import { + apiGetOnePortofolio, + apiUpdatePortofolio, +} from "@/service/api-client/api-portofolio"; +import { + IMasterBidangBisnis, + IMasterSubBidangBisnis, +} from "@/types/Type-Master"; +import { Ionicons } from "@expo/vector-icons"; +import { router, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import { Text, View } from "react-native"; +import PhoneInput, { ICountry } from "react-native-international-phone-number"; +import Toast from "react-native-toast-message"; + +interface IFormData { + id_Portofolio: string; + namaBisnis: string; + alamatKantor: string; + tlpn: string; + deskripsi: string; + masterBidangBisnisId: string; + subBidang: any[]; +} + +interface IListSubBidangSelected { + id: string; + MasterSubBidangBisnis: { + id: string; + name: string; + masterBidangBisnisId: string; + }; +} + +export default function PortofolioEdit() { + const { id } = useLocalSearchParams(); + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState({}); + + const [selectedCountry, setSelectedCountry] = useState(null); + const [bidangBisnis, setBidangBisnis] = useState([]); + const [subBidangBisnis, setSubBidangBisnis] = useState< + IMasterSubBidangBisnis[] + >([]); + const [selectedSubBidang, setSelectedSubBidang] = useState([]); + const [listSubBidangSelected, setListSubBidangSelected] = useState< + IListSubBidangSelected[] + >([ + { + id: "", + MasterSubBidangBisnis: { + id: "", + name: "", + masterBidangBisnisId: "", + }, + }, + ]); + + function handleInputValue(phoneNumber: string) { + setData({ ...data, tlpn: phoneNumber }); + } + + function handleSelectedCountry(country: ICountry) { + setSelectedCountry(country); + } + + useEffect(() => { + onLoadData(id as string); + onLoadMasterBidang(); + onLoadMasterSubBidangBisnis(); + }, [id]); + + const onLoadData = async (id: string) => { + const response = await apiGetOnePortofolio({ id: id }); + + if (response.data.tlpn && response.data.tlpn.includes("62")) { + const fixNumber = response.data.tlpn.replace("62", ""); + + setData({ ...response.data, tlpn: fixNumber }); + } + }; + + const onLoadMasterBidang = async () => { + try { + const response = await apiMasterBidangBisnis(); + setBidangBisnis(response.data); + } catch (error) { + setBidangBisnis([]); + console.log("Error onLoadMasterBidangBisnis", error); + } + }; + + async function onLoadMasterSubBidangBisnis() { + try { + const response = await apiMasterSubBidangBisnis({}); + + if (response.success) { + setSubBidangBisnis(response.data); + } + } catch (error) { + console.error("Error on load master sub bidang bisnis", error); + } + } + + const handleSubmitUpdate = async () => { + try { + setIsLoading(true); + const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || ""; + const fixNumber = data.tlpn.replace(/\s+/g, ""); + const realNumber = callingCode + fixNumber; + + const newData: IFormData = { + id_Portofolio: data.id_Portofolio, + namaBisnis: data.namaBisnis, + alamatKantor: data.alamatKantor, + tlpn: realNumber, + deskripsi: data.deskripsi, + masterBidangBisnisId: data.masterBidangBisnisId, + subBidang: listSubBidangSelected, + }; + + const response = await apiUpdatePortofolio({ + id: id as string, + data: newData, + category: "detail", + }); + + if (!response.success) { + Toast.show({ + type: "info", + text1: "Info", + text2: response.message, + }); + + return; + } + + Toast.show({ + type: "success", + text1: "Sukses", + text2: "Data terupdate", + }); + + router.back(); + } catch (error) { + console.log("Error handleSubmitUpdate", error); + } finally { + setIsLoading(false); + } + }; + + const buttonUpdate = ( + + + Update + + + ); + + return ( + <> + + + + setData({ ...data, namaBisnis: value }) + } + /> + + ({ + label: item.name, + value: item.id, + }))} + value={data.masterBidangBisnisId} + onChange={(value) => { + setData({ ...(data as any), masterBidangBisnisId: value }); + }} + /> + + {listSubBidangSelected.map((item, index) => ( + ({ + label: item.name, + value: item.id, + }))} + value={item.id || null} + onChange={(value) => { + console.log("Value >>", value); + }} + /> + ))} + + + + { + setListSubBidangSelected([ + ...listSubBidangSelected, + { + id: "", + MasterSubBidangBisnis: { + id: "", + name: "", + masterBidangBisnisId: "", + }, + }, + ]); + }} + icon={ + + } + size="xl" + /> + { + const list = _.clone(listSubBidangSelected); + list.pop(); + setListSubBidangSelected(list); + }} + icon={ + + } + size="xl" + /> + + + + + {/* + + ({ + label: item.name, + value: item.id, + }))} + value={data.masterSubBidangBisnisId} + onChange={(value) => { + setData({ ...(data as any), masterSubBidangBisnisId: value }); + }} + /> + + + console.log("delete")}> + + + + */} + {/* console.log("add")}> + Tambah Pilihan + + */} + + + + + Nomor Telepon + + * + + + + + + + + setData({ ...data, alamatKantor: value }) + } + /> + + + setData({ ...data, deskripsi: value }) + } + autosize + minRows={2} + maxRows={5} + required + showCount + maxLength={1000} + /> + + + {JSON.stringify(subBidangBisnis, null, 2)} + + + + ); +} diff --git a/app/(application)/(user)/portofolio/[id]/edit.tsx b/app/(application)/(user)/portofolio/[id]/edit.tsx new file mode 100644 index 0000000..8be839b --- /dev/null +++ b/app/(application)/(user)/portofolio/[id]/edit.tsx @@ -0,0 +1,477 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ActionIcon, + BoxButtonOnFooter, + ButtonCustom, + CenterCustom, + SelectCustom, + Spacing, + StackCustom, + TextAreaCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_XLARGE } from "@/constants/constans-value"; +import { + apiMasterBidangBisnis, + apiMasterSubBidangBisnis, +} from "@/service/api-client/api-master"; +import { + apiGetOnePortofolio, + apiUpdatePortofolio, +} from "@/service/api-client/api-portofolio"; +import { + IMasterBidangBisnis, + IMasterSubBidangBisnis, +} from "@/types/Type-Master"; +import { Ionicons } from "@expo/vector-icons"; +import { router, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import { Text, View } from "react-native"; +import PhoneInput, { ICountry } from "react-native-international-phone-number"; +import { ActivityIndicator } from "react-native-paper"; +import Toast from "react-native-toast-message"; + +interface IFormData { + id_Portofolio: string; + namaBisnis: string; + alamatKantor: string; + tlpn: string; + deskripsi: string; + masterBidangBisnisId: string; + subBidang: any[]; +} + +interface IListSubBidangSelected { + id: string; + MasterSubBidangBisnis?: { + id?: string; + name?: string; + masterBidangBisnisId?: string; + }; +} + +export default function PortofolioEdit() { + const { id } = useLocalSearchParams(); + const [isLoading, setIsLoading] = useState(false); + const [data, setData] = useState({}); + + const [selectedCountry, setSelectedCountry] = useState(null); + const [bidangBisnis, setBidangBisnis] = useState< + IMasterBidangBisnis[] | null + >(null); + const [subBidangBisnis, setSubBidangBisnis] = useState< + IMasterSubBidangBisnis[] | null + >(null); + const [selectedSubBidang, setSelectedSubBidang] = useState([]); + const [listSubBidangSelected, setListSubBidangSelected] = useState< + IListSubBidangSelected[] + >([]); + + function handleInputValue(phoneNumber: string) { + setData({ ...data, tlpn: phoneNumber }); + } + + function handleSelectedCountry(country: ICountry) { + setSelectedCountry(country); + } + + const onLoadMasterBidang = async () => { + try { + const response = await apiMasterBidangBisnis(); + setBidangBisnis(response.data); + + return response.success; + } catch (error) { + setBidangBisnis([]); + console.log("Error onLoadMasterBidangBisnis", error); + } + }; + + async function onLoadMasterSubBidangBisnis() { + try { + const response = await apiMasterSubBidangBisnis({}); + setSubBidangBisnis(response.data); + + return response.success; + } catch (error) { + console.error("Error on load master sub bidang bisnis", error); + } + } + + const handleLoadMaster = async (id: string) => { + const loadBidang = await onLoadMasterBidang(); + const loadSubBidang = await onLoadMasterSubBidangBisnis(); + + if (!loadBidang || !loadSubBidang) { + return; + } + + onLoadData(id); + }; + + useEffect(() => { + handleLoadMaster(id as any); + }, [id]); + + const onLoadData = async (id: string) => { + const response = await apiGetOnePortofolio({ id: id }); + + if (response.success) { + const fixNumber = response.data.tlpn.replace("62", ""); + setData({ ...response.data, tlpn: fixNumber }); + + // Cek apakah ada sub bidang bisnis yang terpilih + const prevSubBidang = response.data.Portofolio_BidangDanSubBidangBisnis; + if (prevSubBidang && prevSubBidang.length > 0) { + setListSubBidangSelected(prevSubBidang); + } else { + // Jika tidak ada sub bidang yang terpilih sebelumnya, tetap inisialisasi dengan array kosong + setListSubBidangSelected([ + { + id: "", + MasterSubBidangBisnis: { + id: "", + name: "", + }, + }, + ]); + } + + const bisnisId = response.data.masterBidangBisnisId; + handleLoadSelectedSubBidang({ + id: bisnisId, + }); + } + }; + + // Handler untuk saat komponen pertama kali load + const handleLoadSelectedSubBidang = ({ id }: { id: string }) => { + if (!subBidangBisnis) return; + + const filteredSubBidang: any = subBidangBisnis.filter((item) => { + return item.masterBidangBisnisId === id; + }); + setSelectedSubBidang(filteredSubBidang); + }; + + // Handler untuk menambah sub bidang bisnis + const handleAddSubBidang = () => { + setListSubBidangSelected([ + ...listSubBidangSelected, + { + id: "", + MasterSubBidangBisnis: { id: "", name: "" }, + }, + ]); + }; + + // Handler untuk menghapus sub bidang bisnis + const handleRemoveSubBidang = (index: number) => { + if (listSubBidangSelected.length <= 1) return; + + const updatedList = [...listSubBidangSelected]; + updatedList.splice(index, 1); + setListSubBidangSelected(updatedList); + }; + + // Handler untuk perubahan bidang bisnis + const handleBidangBisnisChange = (val: string) => { + const isSameBidang = data?.MasterBidangBisnis?.id === val; + + setData({ ...(data as any), masterBidangBisnisId: val }); + + // Reset sub bidang jika ganti bidang + if (!isSameBidang) { + setListSubBidangSelected([ + { + id: "", + MasterSubBidangBisnis: { id: "", name: "" }, + }, + ]); + } + + handleLoadSelectedSubBidang({ id: val }); + }; + + // Handler untuk update sub bidang + const handleSubBidangChange = (value: string, index: number) => { + const select = selectedSubBidang.find((sub: any) => sub.id === value); + const list: any = _.cloneDeep(listSubBidangSelected); + list[index] = { + id: "", + MasterSubBidangBisnis: select || { + id: value, + name: "", + masterBidangBisnisId: "", + }, + }; + setListSubBidangSelected(list); + }; + + useEffect(() => { + if (subBidangBisnis?.length !== undefined && data.masterBidangBisnisId) { + handleLoadSelectedSubBidang({ + id: data.masterBidangBisnisId, + }); + } + }, [subBidangBisnis, data.masterBidangBisnisId]); + + function validateData(data: any) { + if ( + !data.namaBisnis || + !data.alamatKantor || + !data.tlpn || + !data.deskripsi || + !data.masterBidangBisnisId + ) { + return false; + } + + return true; + } + + function validateDataSubBidang(dataArray: any[]) { + return !dataArray.some( + (item: any) => + !item.MasterSubBidangBisnis.id || + item.MasterSubBidangBisnis.id.trim() === "" + ); + } + + const handleSubmitUpdate = async () => { + const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || ""; + const fixNumber = data.tlpn.replace(/\s+/g, ""); + const realNumber = callingCode + fixNumber; + + const newData: IFormData = { + id_Portofolio: data.id_Portofolio, + namaBisnis: data.namaBisnis, + alamatKantor: data.alamatKantor, + tlpn: realNumber, + deskripsi: data.deskripsi, + masterBidangBisnisId: data.masterBidangBisnisId, + subBidang: listSubBidangSelected, + }; + + if (!validateData(newData)) { + return Toast.show({ + type: "error", + text1: "Harap lengkapi data", + }); + } + + if (!validateDataSubBidang(listSubBidangSelected as any)) { + return Toast.show({ + type: "error", + text1: "Harap lengkapi sub bidang", + }); + } + + try { + setIsLoading(true); + + const response = await apiUpdatePortofolio({ + id: id as string, + data: newData, + category: "detail", + }); + + if (!response.success) { + Toast.show({ + type: "info", + text1: "Info", + text2: response.message, + }); + + return; + } + + Toast.show({ + type: "success", + text1: "Sukses", + text2: "Data terupdate", + }); + + router.back(); + } catch (error) { + console.log("Error handleSubmitUpdate", error); + } finally { + setIsLoading(false); + } + }; + + const buttonUpdate = ( + + + Update + + + ); + + if (!bidangBisnis || !subBidangBisnis) { + return ( + <> + + + + + ); + } + + return ( + <> + + + + setData({ ...data, namaBisnis: value }) + } + /> + + ({ + label: item.name, + value: item.id, + }))} + value={data.masterBidangBisnisId} + onChange={(value: any) => { + handleBidangBisnisChange(value); + }} + /> + + {listSubBidangSelected.map((item, index) => { + // Filter data untuk select sub bidang, menghilangkan yang sudah dipilih kecuali untuk item ini sendiri + const selectedIds = listSubBidangSelected + .filter((_, i) => i !== index) + .map((s) => s.MasterSubBidangBisnis?.id) + .filter((id) => id); // Filter hanya yang memiliki id (tidak kosong) + + const availableSubBidangOptions = (selectedSubBidang || []) + .filter((sub: any) => { + // Tampilkan jika ini adalah opsi yang dipilih saat ini atau belum dipilih di sub bidang lainnya + + return ( + sub.id === item.MasterSubBidangBisnis?.id || + !selectedIds.includes(sub.id) + ); + }) + .map((sub: any) => ({ + value: sub.id, + label: sub.name, + })); + + return ( + { + handleSubBidangChange(value, index); + }} + /> + ); + })} + + + + { + handleAddSubBidang(); + }} + icon={ + + } + size="xl" + /> + { + handleRemoveSubBidang(listSubBidangSelected.length - 1); + }} + icon={ + + } + size="xl" + /> + + + + + + + + Nomor Telepon + + * + + + + + + + + setData({ ...data, alamatKantor: value }) + } + /> + + + setData({ ...data, deskripsi: value }) + } + autosize + minRows={2} + maxRows={5} + required + showCount + maxLength={1000} + /> + + + + + ); +} diff --git a/app/(application)/(user)/portofolio/[id]/index.tsx b/app/(application)/(user)/portofolio/[id]/index.tsx new file mode 100644 index 0000000..70a4307 --- /dev/null +++ b/app/(application)/(user)/portofolio/[id]/index.tsx @@ -0,0 +1,213 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + ButtonCustom, + DrawerCustom, + DummyLandscapeImage, + LoaderCustom, + Spacing, + StackCustom, + TextCustom, +} from "@/components"; +import LeftButtonCustom from "@/components/Button/BackButton"; +import GridTwoView from "@/components/_ShareComponent/GridTwoView"; +import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +import { useAuth } from "@/hooks/use-auth"; +import Portofolio_BusinessLocation from "@/screens/Portofolio/BusinessLocationSection"; +import Portofolio_ButtonDelete from "@/screens/Portofolio/ButtonDelete"; +import Portofolio_Data from "@/screens/Portofolio/DataPortofolio"; +import { drawerItemsPortofolio } from "@/screens/Portofolio/ListPage"; +import Portofolio_MenuDrawerSection from "@/screens/Portofolio/MenuDrawer"; +import Portofolio_SocialMediaSection from "@/screens/Portofolio/SocialMediaSection"; +import { apiGetOnePortofolio } from "@/service/api-client/api-portofolio"; +import { apiUser } from "@/service/api-client/api-user"; +import { GStyles } from "@/styles/global-styles"; +import { openInDeviceMaps } from "@/utils/openInDeviceMaps"; +import { FontAwesome, Ionicons } from "@expo/vector-icons"; +import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import { TouchableOpacity } from "react-native"; + +export default function Portofolio() { + const { user } = useAuth(); + const { id } = useLocalSearchParams(); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [isLoadingDelete, setIsLoadingDelete] = useState(false); + const [data, setData] = useState(); + const [profileId, setProfileId] = useState(); + const [openDrawerLocation, setOpenDrawerLocation] = useState(false); + + const openDrawer = () => { + setIsDrawerOpen(true); + }; + const closeDrawer = () => { + setIsDrawerOpen(false); + }; + + useFocusEffect( + useCallback(() => { + onLoadData(id as string); + onLoadUserByToken(); + }, [id]) + ); + + async function onLoadData(id: string) { + const response = await apiGetOnePortofolio({ id: id }); + + setData(response.data); + } + + const onLoadUserByToken = async () => { + const response = await apiUser(user?.id as string); + + setProfileId(response?.data?.Profile?.id); + }; + + return ( + <> + {/* Header */} + , + headerRight: () => + data?.Profile?.id !== profileId ? null : ( + + + + ), + headerStyle: GStyles.headerStyle, + headerTitleStyle: GStyles.headerTitleStyle, + }} + /> + + {!data || !profileId ? ( + + ) : ( + + + + + {data?.Profile?.id !== profileId ? null : ( + + )} + + + )} + + + {/* Drawer Komponen Eksternal */} + + + + + {/* Drawer Lokasi */} + setOpenDrawerLocation(false)} + height={"auto"} + > + + + + + } + rightIcon={{data?.BusinessMaps?.namePin}} + /> + + + } + rightIcon={ + {data?.MasterBidangBisnis?.name} + } + /> + + + } + rightIcon={{data?.tlpn}} + /> + + } + rightIcon={{data?.alamatKantor}} + /> + + + { + openInDeviceMaps({ + latitude: data?.BusinessMaps?.latitude, + longitude: data?.BusinessMaps?.longitude, + title: data?.BusinessMaps?.namePin, + }); + }} + > + Buka Maps + + + + + ); +} diff --git a/app/(application)/(user)/portofolio/[id]/list.tsx b/app/(application)/(user)/portofolio/[id]/list.tsx new file mode 100644 index 0000000..7cd0888 --- /dev/null +++ b/app/(application)/(user)/portofolio/[id]/list.tsx @@ -0,0 +1,28 @@ +import { TextCustom, ViewWrapper } from "@/components"; +import Portofolio_BoxView from "@/screens/Portofolio/BoxPortofolioView"; +import { apiGetPortofolio } from "@/service/api-client/api-portofolio"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; + +export default function ListPortofolio() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState([]); + + useFocusEffect( + useCallback(() => { + onLoadPortofolio(id as string); + }, [id]) + ); + + const onLoadPortofolio = async (id: string) => { + const response = await apiGetPortofolio({ id: id }); + setData(response.data); + }; + return ( + + {data ? data?.map((item: any, index: number) => ( + + )) : Tidak ada portofolio} + + ); +} diff --git a/app/(application)/(user)/portofolio/_layout.tsx b/app/(application)/(user)/portofolio/_layout.tsx new file mode 100644 index 0000000..9e4797e --- /dev/null +++ b/app/(application)/(user)/portofolio/_layout.tsx @@ -0,0 +1,32 @@ +import LeftButtonCustom from "@/components/Button/BackButton"; +import { HeaderStyles } from "@/styles/header-styles"; +import { Stack } from "expo-router"; + +export default function PortofolioLayout() { + return ( + <> + , + }} + > + {/* */} + + + + + + + + ); +} diff --git a/app/(application)/(user)/profile/[id]/edit.tsx b/app/(application)/(user)/profile/[id]/edit.tsx new file mode 100644 index 0000000..9526117 --- /dev/null +++ b/app/(application)/(user)/profile/[id]/edit.tsx @@ -0,0 +1,124 @@ +import { + ButtonCustom, + SelectCustom, + StackCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import BoxButtonOnFooter from "@/components/Box/BoxButtonOnFooter"; +import { apiProfile, apiUpdateProfile } from "@/service/api-client/api-profile"; +import { IProfile } from "@/types/Type-Profile"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import Toast from "react-native-toast-message"; + +export default function ProfileEdit() { + const { id } = useLocalSearchParams(); + + const [data, setData] = useState(); + const [isLoading, setIsLoading] = useState(false); + + const options = [ + { label: "Laki-laki", value: "laki-laki" }, + { label: "Perempuan", value: "perempuan" }, + ]; + + useEffect(() => { + onLoadData(id as string); + }, [id]); + + async function onLoadData(id: string) { + try { + const response = await apiProfile({ id }); + setData(response.data); + } catch (error) { + console.log("error get profile >>", error); + } + } + + const handleUpdate = async () => { + try { + setIsLoading(true); + const response = await apiUpdateProfile({ + id: id as string, + data, + category: "profile", + }); + if (!response.success) { + Toast.show({ + type: "info", + text1: "Info", + text2: response.message, + }); + + return; + } + + + + Toast.show({ + type: "success", + text1: "Sukses", + text2: "Profile berhasil diupdate", + }); + return router.back(); + } catch (error) { + console.log("error update profile >>", error); + } finally { + setIsLoading(false); + } + }; + + return ( + + + Update + + + } + > + + { + setData({ ...data, name: text }); + }} + required + /> + { + setData({ ...data, email: text }); + }} + required + /> + { + setData({ ...data, alamat: text }); + }} + required + /> + { + setData({ ...(data as any), jenisKelamin: value }); + }} + /> + + + ); +} diff --git a/app/(application)/(user)/profile/[id]/index.tsx b/app/(application)/(user)/profile/[id]/index.tsx new file mode 100644 index 0000000..d4a55ca --- /dev/null +++ b/app/(application)/(user)/profile/[id]/index.tsx @@ -0,0 +1,163 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { LoaderCustom } from "@/components"; +import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; +import LeftButtonCustom from "@/components/Button/BackButton"; +import DrawerCustom from "@/components/Drawer/DrawerCustom"; +import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import { drawerItemsProfile } from "@/screens/Profile/ListPage"; +import Profile_MenuDrawerSection from "@/screens/Profile/menuDrawerSection"; +import Profile_PortofolioSection from "@/screens/Profile/PortofolioSection"; +import ProfileSection from "@/screens/Profile/ProfileSection"; +import { apiGetPortofolio } from "@/service/api-client/api-portofolio"; +import { apiProfile } from "@/service/api-client/api-profile"; +import { apiUser } from "@/service/api-client/api-user"; +import { GStyles } from "@/styles/global-styles"; +import { IProfile } from "@/types/Type-Profile"; +import { Ionicons } from "@expo/vector-icons"; +import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; +import React, { useCallback, useState } from "react"; +import { TouchableOpacity } from "react-native"; + +export default function Profile() { + const { id } = useLocalSearchParams(); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [data, setData] = useState(); + const [dataToken, setDataToken] = useState(); + const [listPortofolio, setListPortofolio] = useState(); + + const { token, logout, isAdmin, user, userData } = useAuth(); + + const openDrawer = () => { + setIsDrawerOpen(true); + }; + + const closeDrawer = () => { + setIsDrawerOpen(false); + }; + + useFocusEffect( + useCallback(() => { + onLoadData(id as string); + onLoadPortofolio(id as string); + onLoadUserByToken(); + isUserCheck(); + userData(token as string); + }, [id, token]) + ); + + const isUserCheck = () => { + const userId = id; + const userLoginId = dataToken?.id; + + return userId === userLoginId; + }; + + const onLoadData = async (id: string) => { + const response = await apiProfile({ id: id }); + setData(response.data); + }; + + const onLoadUserByToken = async () => { + const response = await apiUser(user?.id as string); + setDataToken(response?.data?.Profile); + }; + + const onLoadPortofolio = async (id: string) => { + const response = await apiGetPortofolio({ id: id }); + const lastTwoByDate = response.data + .sort( + (a: any, b: any) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ) // urut desc + .slice(0, 2); + setListPortofolio(lastTwoByDate); + }; + + return ( + <> + , + headerRight: () => ( + + ), + headerStyle: GStyles.headerStyle, + headerTitleStyle: GStyles.headerTitleStyle, + }} + /> + {/* Main View */} + + {!data || !dataToken ? ( + + ) : ( + <> + + + + + )} + + + {/* Drawer Komponen Eksternal */} + + + + + ); +} + +const ButtonnDot = ({ + id, + openDrawer, + isUserCheck, + logout, +}: { + id: string; + openDrawer: () => void; + isUserCheck: boolean; + logout: () => Promise; +}) => { + const isId = id === undefined || id === null; + + if (isId) { + return ( + <> + + + + + ); + } + + return ( + <> + {isUserCheck && ( + + + + )} + + ); +}; diff --git a/app/(application)/(user)/profile/[id]/take-picture2.txt b/app/(application)/(user)/profile/[id]/take-picture2.txt new file mode 100644 index 0000000..8483dde --- /dev/null +++ b/app/(application)/(user)/profile/[id]/take-picture2.txt @@ -0,0 +1,190 @@ +// COMPONENT : Jika ingin uoload gambar dan video gunakan component ini + +import { + ButtonCustom, + Spacing, + StackCustom, + ViewWrapper +} from "@/components"; +import AntDesign from "@expo/vector-icons/AntDesign"; +import Feather from "@expo/vector-icons/Feather"; +import FontAwesome6 from "@expo/vector-icons/FontAwesome6"; +import { + CameraMode, + CameraType, + CameraView, + useCameraPermissions, +} from "expo-camera"; +import { Image } from "expo-image"; +import { router } from "expo-router"; +import { useRef, useState } from "react"; +import { Button, Pressable, StyleSheet, Text, View } from "react-native"; + +export default function TakePictureProfile2() { + const [permission, requestPermission] = useCameraPermissions(); + const ref = useRef(null); + const [uri, setUri] = useState(null); + const [mode, setMode] = useState("picture"); + const [facing, setFacing] = useState("back"); + const [recording, setRecording] = useState(false); + + if (!permission?.granted) { + return ( + + + We need your permission to use the camera + +