Compare commits
4 Commits
6768f8d5b7
...
f97b49cbdf
Author | SHA1 | Date | |
---|---|---|---|
f97b49cbdf | |||
2b9391f5e9 | |||
3bdc967873 | |||
899b26b234 |
1
.env
@ -3,6 +3,7 @@ APP_ENV=local
|
|||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_NAME=vat_wms
|
APP_NAME=vat_wms
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
#VITE_DEV_SERVER_URL="http://10.0.2.2:5173"
|
||||||
APP_URL=http://0.0.0.0:8004
|
APP_URL=http://0.0.0.0:8004
|
||||||
ASSET_URL=http://0.0.0.0:8004
|
ASSET_URL=http://0.0.0.0:8004
|
||||||
|
|
||||||
|
2
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
/bootstrap/ssr
|
/bootstrap/ssr
|
||||||
/node_modules
|
/node_modules
|
||||||
/public/build
|
/public/build
|
||||||
|
/public/spa
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
@ -22,3 +23,4 @@ yarn-error.log
|
|||||||
/.nova
|
/.nova
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
|
storage/rector/*
|
||||||
|
101
android/.gitignore
vendored
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||||
|
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||||
|
# release/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio Navigation editor temp files
|
||||||
|
.navigation/
|
||||||
|
|
||||||
|
# Android Studio captures folder
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/libraries
|
||||||
|
# Android Studio 3 in .gitignore file.
|
||||||
|
.idea/caches
|
||||||
|
.idea/modules.xml
|
||||||
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
|
.idea/navEditor.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
|
#*.keystore
|
||||||
|
|
||||||
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
# google-services.json
|
||||||
|
|
||||||
|
# Freeline
|
||||||
|
freeline.py
|
||||||
|
freeline/
|
||||||
|
freeline_project_description.json
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
fastlane/readme.md
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
vcs.xml
|
||||||
|
|
||||||
|
# lint
|
||||||
|
lint/intermediates/
|
||||||
|
lint/generated/
|
||||||
|
lint/outputs/
|
||||||
|
lint/tmp/
|
||||||
|
# lint/reports/
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-android-plugins
|
||||||
|
|
||||||
|
# Copied web assets
|
||||||
|
app/src/main/assets/public
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
app/src/main/assets/capacitor.config.json
|
||||||
|
app/src/main/assets/capacitor.plugins.json
|
||||||
|
app/src/main/res/xml/config.xml
|
3
android/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
6
android/.idea/AndroidProjectSystem.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
android/.idea/compiler.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
android/.idea/deploymentTargetSelector.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="app">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
10
android/.idea/migrations.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
9
android/.idea/misc.xml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
17
android/.idea/runConfigurations.xml
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
2
android/app/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/build/*
|
||||||
|
!/build/.npmkeep
|
54
android/app/build.gradle
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "com.duremote.wms"
|
||||||
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.duremote.wms"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
aaptOptions {
|
||||||
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||||
|
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
flatDir{
|
||||||
|
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
|
implementation project(':capacitor-android')
|
||||||
|
testImplementation "junit:junit:$junitVersion"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
||||||
|
try {
|
||||||
|
def servicesJSON = file('google-services.json')
|
||||||
|
if (servicesJSON.text) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||||
|
}
|
19
android/app/capacitor.build.gradle
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (hasProperty('postBuildExtras')) {
|
||||||
|
postBuildExtras()
|
||||||
|
}
|
21
android/app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
44
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/title_activity_main"
|
||||||
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"></meta-data>
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
|
||||||
|
</manifest>
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.duremote.wms;
|
||||||
|
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {}
|
BIN
android/app/src/main/res/drawable-land-hdpi/splash.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/splash.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable-port-hdpi/splash.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
android/app/src/main/res/drawable-port-mdpi/splash.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png
Normal file
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
170
android/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
BIN
android/app/src/main/res/drawable/splash.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
12
android/app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
7
android/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">vatWMS</string>
|
||||||
|
<string name="title_activity_main">vatWMS</string>
|
||||||
|
<string name="package_name">com.duremote.wms</string>
|
||||||
|
<string name="custom_url_scheme">com.duremote.wms</string>
|
||||||
|
</resources>
|
22
android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
5
android/app/src/main/res/xml/file_paths.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
4
android/app/src/main/res/xml/network_security_config.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<base-config cleartextTrafficPermitted="true" />
|
||||||
|
</network-security-config>
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
29
android/build.gradle
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// 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:8.7.2'
|
||||||
|
classpath 'com.google.gms:google-services:4.4.2'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "variables.gradle"
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
3
android/capacitor.settings.gradle
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
include ':capacitor-android'
|
||||||
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
22
android/gradle.properties
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# 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.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
# 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
|
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
252
android/gradlew
vendored
Executable file
@ -0,0 +1,252 @@
|
|||||||
|
#!/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
|
||||||
|
' "$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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# 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" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# 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" "$@"
|
94
android/gradlew.bat
vendored
Normal file
@ -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=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
: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
|
5
android/settings.gradle
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
include ':app'
|
||||||
|
include ':capacitor-cordova-android-plugins'
|
||||||
|
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||||
|
|
||||||
|
apply from: 'capacitor.settings.gradle'
|
16
android/variables.gradle
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
ext {
|
||||||
|
minSdkVersion = 23
|
||||||
|
compileSdkVersion = 35
|
||||||
|
targetSdkVersion = 35
|
||||||
|
androidxActivityVersion = '1.9.2'
|
||||||
|
androidxAppCompatVersion = '1.7.0'
|
||||||
|
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||||
|
androidxCoreVersion = '1.15.0'
|
||||||
|
androidxFragmentVersion = '1.8.4'
|
||||||
|
coreSplashScreenVersion = '1.0.1'
|
||||||
|
androidxWebkitVersion = '1.12.1'
|
||||||
|
junitVersion = '4.13.2'
|
||||||
|
androidxJunitVersion = '1.2.1'
|
||||||
|
androidxEspressoCoreVersion = '3.6.1'
|
||||||
|
cordovaAndroidVersion = '10.1.1'
|
||||||
|
}
|
112
app/Http/Controllers/Api/FloorLayoutController.php
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\FloorLayout;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class FloorLayoutController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$layouts = FloorLayout::all();
|
||||||
|
return response()->json(['layouts' => $layouts]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* GET /api/floor-layouts/{layoutName}
|
||||||
|
* Returns:
|
||||||
|
* - data.rooms (array of room objects)
|
||||||
|
* - data.bgFile (data-uri string or null)
|
||||||
|
*/
|
||||||
|
public function show(string $room_id)
|
||||||
|
{
|
||||||
|
$layout = FloorLayout::with(['room'])->where(
|
||||||
|
['room_id' => $room_id]
|
||||||
|
)->first();
|
||||||
|
|
||||||
|
$bgFile = null;
|
||||||
|
if ($layout->layout_bg_file) {
|
||||||
|
$mime = 'image/png'; // adjust if you store SVG/jpeg
|
||||||
|
$encoded = $layout->layout_bg_file;
|
||||||
|
$bgFile = "data:{$mime};base64,{$encoded}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'room' => $layout->room,
|
||||||
|
'layoutItems' => $layout->data,
|
||||||
|
'bgFile' => $bgFile,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/floor-layouts/{layoutName}
|
||||||
|
* Accepts multipart/form-data with:
|
||||||
|
* - rooms : JSON array
|
||||||
|
* - bg_file : optional image file
|
||||||
|
*/
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
// Validate rooms array, and optionally a Base64 image string
|
||||||
|
$validated = $request->validate([
|
||||||
|
'contents' => 'required|array',
|
||||||
|
'room_id' => 'required|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Lookup (or new) by the unique layout_name
|
||||||
|
$layout = FloorLayout::where(['room_id' => $validated['room_id']])->first();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Save room JSON, track user, persist
|
||||||
|
$layout->data = $validated['contents'];
|
||||||
|
$layout->user_id = Auth::id();
|
||||||
|
$layout->save(); // will INSERT or UPDATE, with your unique key on layout_name
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'updated_at' => $layout->updated_at->toIso8601String(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
// Validate that we got a layout name, and optionally a Base64‐encoded image
|
||||||
|
$validated = $request->validate([
|
||||||
|
'room_id' => 'required|integer',
|
||||||
|
'name' => 'required|string|max:100',
|
||||||
|
'bgFile' => 'nullable|string', // still expect a Data-URL / Base64 string
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Use the submitted name as the unique layout_name
|
||||||
|
$layout = FloorLayout::firstOrNew(['layout_name' => $validated['name'], 'room_id' => $validated['room_id']]);
|
||||||
|
|
||||||
|
// If they sent us a Data-URL, strip the prefix and store *that* Base64
|
||||||
|
if (!empty($validated['bgFile'])) {
|
||||||
|
if (preg_match('#^data:image/\w+;base64,#i', $validated['bgFile'])) {
|
||||||
|
// keep only the Base64 payload
|
||||||
|
$base64 = substr($validated['bgFile'], strpos($validated['bgFile'], ',') + 1);
|
||||||
|
$layout->layout_bg_file = $base64;
|
||||||
|
} else {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'bgFile must be a Base64 data URL',
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with empty rooms array (you’ll add rooms via the save‐layout endpoint later)
|
||||||
|
$layout->data = [];
|
||||||
|
$layout->user_id = Auth::id();
|
||||||
|
$layout->save(); // INSERT or UPDATE
|
||||||
|
|
||||||
|
// Return the layout_name so the front end can add it into tabs & select it
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'layout' => $layout->layout_name,
|
||||||
|
'created_at' => $layout->created_at->toIso8601String(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -44,7 +44,7 @@ class ScannerController extends Controller {
|
|||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
case 'stock_batch':
|
case 'stock_batch':
|
||||||
// Attempt to load a StockBatch by ID. Adjust with(...) if you have relationships you want to return.
|
// Attempt to load a StockBatch by ID. Adjust with(...) if you have relationships you want to return.
|
||||||
$batch = StockBatch::with(['stockEntries.physicalItem', 'stockEntries.sections.position.shelf.rack.line.room', 'stockEntries.supplier', 'files', 'supplier'])
|
$batch = StockBatch::with(['stockEntries.physicalItem', 'stockEntries.sections.position.shelf.rack.line.room', 'stockEntries.supplier', 'files', 'supplier', 'stockEntries.statusHistory'])
|
||||||
->findOrFail($id);
|
->findOrFail($id);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
@ -41,7 +41,7 @@ class StockBatchController extends Controller
|
|||||||
$query->orderBy($sortField, $sortDirection);
|
$query->orderBy($sortField, $sortDirection);
|
||||||
|
|
||||||
// Paginate
|
// Paginate
|
||||||
$perPage = $request->input('per_page', 10);
|
$perPage = $request->input('per_page', 50);
|
||||||
$page = $request->input('page', 1);
|
$page = $request->input('page', 1);
|
||||||
|
|
||||||
$entries = $query->paginate($perPage, ['*'], 'page', $page);
|
$entries = $query->paginate($perPage, ['*'], 'page', $page);
|
||||||
@ -57,6 +57,15 @@ class StockBatchController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getBatch(Request $request, $id)
|
||||||
|
{
|
||||||
|
$batch = StockBatch::with(['stockEntries.physicalItem', 'stockEntries.sections.position.shelf.rack.line.room', 'stockEntries.supplier', 'files', 'supplier', 'stockEntries.statusHistory.status', 'user'])
|
||||||
|
->findOrFail($id);
|
||||||
|
return response()->json([
|
||||||
|
'batch' => $batch,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a newly created stock batch, with multiple files.
|
* Store a newly created stock batch, with multiple files.
|
||||||
*/
|
*/
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Manufacturer;
|
||||||
use App\Models\OriginCountry;
|
use App\Models\OriginCountry;
|
||||||
use App\Models\StockEntry;
|
use App\Models\StockEntry;
|
||||||
use App\Models\StockEntrySection;
|
use App\Models\StockEntrySection;
|
||||||
@ -31,34 +32,67 @@ class StockEntryController extends Controller
|
|||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = StockEntry::query()
|
$query = StockEntry::query()
|
||||||
->with(['physicalItem', 'supplier', 'sections', 'stockBatch', 'physicalItem.manufacturer', 'countryOfOrigin']);
|
->with([
|
||||||
|
'physicalItem',
|
||||||
|
'supplier',
|
||||||
|
'sections',
|
||||||
|
'stockBatch',
|
||||||
|
'physicalItem.manufacturer',
|
||||||
|
'countryOfOrigin'
|
||||||
|
]);
|
||||||
|
|
||||||
// Apply filters if provided
|
// ---- BACKEND FILTERS ----
|
||||||
if ($request->has('search')) {
|
|
||||||
$search = $request->search;
|
// 1) Free‐text “search” on item name
|
||||||
$query->whereHas('physicalItem', function($q) use ($search) {
|
// if ($request->filled('search')) {
|
||||||
$q->where('name', 'like', "%{$search}%");
|
// $itemIds = PhysicalItem::where('id', $request->physical_item_id)
|
||||||
});
|
// ->pluck('id');
|
||||||
|
// $query->whereIn('physical_item_id', $itemIds->isEmpty() ? [0] : $itemIds);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 2) Supplier filter
|
||||||
|
if ($request->filled('supplier_id')) {
|
||||||
|
$query->whereIn('supplier_id', $request->supplier_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort
|
// 3) Brand/Manufacturer filter
|
||||||
$sortField = $request->input('sort_field', 'updated_at');
|
if ($request->filled('manufacturer_id')) {
|
||||||
|
$mfrId = $request->manufacturer_id;
|
||||||
|
|
||||||
|
// find all physical_item IDs that belong to this manufacturer
|
||||||
|
$itemIds = PhysicalItem::whereIn('manufacturer_id', $mfrId)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
// restrict stock_entries to only those items (or [0] if no matches)
|
||||||
|
$query->whereIn(
|
||||||
|
'physical_item_id',
|
||||||
|
$itemIds->isEmpty() ? [0] : $itemIds->toArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Physical‐item filter (by exact item ID)
|
||||||
|
if ($request->filled('physical_item_id')) {
|
||||||
|
$query->whereIn('physical_item_id', $request->physical_item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- SORT & PAGINATION ----
|
||||||
|
|
||||||
|
$sortField = $request->input('sort_field', 'updated_at');
|
||||||
$sortDirection = $request->input('sort_direction', 'desc');
|
$sortDirection = $request->input('sort_direction', 'desc');
|
||||||
$query->orderBy($sortField, $sortDirection);
|
$query->orderBy($sortField, $sortDirection);
|
||||||
|
|
||||||
// Paginate
|
$perPage = $request->input('per_page', 50);
|
||||||
$perPage = $request->input('per_page', 10);
|
$page = $request->input('page', 1);
|
||||||
$page = $request->input('page', 1);
|
|
||||||
|
|
||||||
$entries = $query->paginate($perPage, ['*'], 'page', $page);
|
$entries = $query->paginate($perPage, ['*'], 'page', $page);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => $entries->items(),
|
'data' => $entries->items(),
|
||||||
'meta' => [
|
'meta' => [
|
||||||
'total' => $entries->total(),
|
'total' => $entries->total(),
|
||||||
'per_page' => $entries->perPage(),
|
'per_page' => $entries->perPage(),
|
||||||
'current_page' => $entries->currentPage(),
|
'current_page' => $entries->currentPage(),
|
||||||
'last_page' => $entries->lastPage(),
|
'last_page' => $entries->lastPage(),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -70,7 +104,7 @@ class StockEntryController extends Controller
|
|||||||
|
|
||||||
|
|
||||||
// Paginate
|
// Paginate
|
||||||
$perPage = $request->input('per_page', 10);
|
$perPage = $request->input('per_page', 50);
|
||||||
$page = $request->input('page', 1);
|
$page = $request->input('page', 1);
|
||||||
|
|
||||||
$entries = $query->paginate($perPage, ['*'], 'page', $page);
|
$entries = $query->paginate($perPage, ['*'], 'page', $page);
|
||||||
@ -110,20 +144,22 @@ class StockEntryController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1) create the main stock entry
|
// 1) create the main stock entry
|
||||||
$entry = StockEntry::create($request->only([
|
// pull in everything except `count`
|
||||||
'physical_item_id',
|
$data = $request->only([
|
||||||
'supplier_id',
|
'physical_item_id',
|
||||||
'count',
|
'supplier_id',
|
||||||
'price',
|
'price',
|
||||||
'bought',
|
'bought',
|
||||||
'description',
|
'description',
|
||||||
'note',
|
'note',
|
||||||
'country_of_origin_id',
|
'country_of_origin_id',
|
||||||
'on_the_way',
|
'on_the_way',
|
||||||
'stock_batch_id',
|
'stock_batch_id',
|
||||||
]) + [
|
]);
|
||||||
'created_by' => auth()->id() ?? 1,
|
|
||||||
]);
|
$data['original_count_invoice'] = $request->input('count');
|
||||||
|
$data['created_by'] = auth()->id() ?? 1;
|
||||||
|
$entry = StockEntry::create($data);
|
||||||
|
|
||||||
// 3) eager-load relations (including the full address hierarchy)
|
// 3) eager-load relations (including the full address hierarchy)
|
||||||
$entry->load([
|
$entry->load([
|
||||||
@ -356,6 +392,17 @@ class StockEntryController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getManufacturers(Request $request)
|
||||||
|
{
|
||||||
|
// Get physical items from warehouse DB
|
||||||
|
$manufacturers = Manufacturer::all();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'manufacturers' => $manufacturers,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function audit(Request $request, $id)
|
public function audit(Request $request, $id)
|
||||||
{
|
{
|
||||||
// 1) Load the entry (so we can get its audits)
|
// 1) Load the entry (so we can get its audits)
|
||||||
@ -408,7 +455,7 @@ class StockEntryController extends Controller
|
|||||||
$entry = StockEntry::findOrFail($payload['entryId']);
|
$entry = StockEntry::findOrFail($payload['entryId']);
|
||||||
|
|
||||||
// 3) If original_count is already set → 409 conflict
|
// 3) If original_count is already set → 409 conflict
|
||||||
if ($entry->original_count !== 0) {
|
if ($entry->counted) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'error' => 'already_counted',
|
'error' => 'already_counted',
|
||||||
|
@ -72,13 +72,13 @@ class StockPositionController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function getPosition(Request $request)
|
public function getPosition(Request $request, int $id)
|
||||||
{
|
{
|
||||||
// 1) Eager‐load the real relationships
|
// 1) Eager‐load the real relationships
|
||||||
$position = StockPosition::with([
|
$position = StockPosition::with([
|
||||||
'sections.entries.physicalItem',
|
'sections.entries.physicalItem',
|
||||||
'shelf.rack.line.room',
|
'shelf.rack.line.room',
|
||||||
])->findOrFail(2);
|
])->findOrFail($id);
|
||||||
|
|
||||||
// 2) Compute the storage address string
|
// 2) Compute the storage address string
|
||||||
$position->storage_address = $position->storageAddress();
|
$position->storage_address = $position->storageAddress();
|
||||||
|
@ -22,9 +22,17 @@ class StockRackController extends Controller
|
|||||||
return response()->json($rack, 201);
|
return response()->json($rack, 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, int $id)
|
||||||
|
{
|
||||||
|
$rack = StockRack::with(['line', 'shelves.positions.sections'])->findOrFail($id);
|
||||||
|
|
||||||
|
return response()->json($rack, 201);
|
||||||
|
}
|
||||||
|
|
||||||
public function update(Request $request, StockRack $rack)
|
public function update(Request $request, StockRack $rack)
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
|
'line_id' => 'sometimes|required|exists:stock_line,line_id',
|
||||||
'rack_symbol' => 'sometimes|required|string|max:50',
|
'rack_symbol' => 'sometimes|required|string|max:50',
|
||||||
'rack_name' => 'sometimes|required|string|max:100',
|
'rack_name' => 'sometimes|required|string|max:100',
|
||||||
]);
|
]);
|
||||||
@ -32,6 +40,7 @@ class StockRackController extends Controller
|
|||||||
$rack->update($data);
|
$rack->update($data);
|
||||||
|
|
||||||
return response()->json($rack);
|
return response()->json($rack);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(StockRack $rack)
|
public function destroy(StockRack $rack)
|
||||||
|
@ -77,4 +77,9 @@ class StockRoomController extends Controller
|
|||||||
$room->delete();
|
$room->delete();
|
||||||
return response()->json(['message'=>'Room deleted']);
|
return response()->json(['message'=>'Room deleted']);
|
||||||
}
|
}
|
||||||
|
public function getList()
|
||||||
|
{
|
||||||
|
$rooms = StockRoom::with(['layout', 'lines.racks.shelves.positions.sections'])->get();
|
||||||
|
return response()->json(['rooms' => $rooms]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
61
app/Models/FloorLayout.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||||
|
use OwenIt\Auditing\Auditable;
|
||||||
|
|
||||||
|
class FloorLayout extends Model
|
||||||
|
{
|
||||||
|
// use Auditable;
|
||||||
|
|
||||||
|
protected $table = 'floor_layouts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mass‐assignable attributes
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'layout_name',
|
||||||
|
'room_id',
|
||||||
|
'layout_bg_file',
|
||||||
|
'data',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast `data` JSON column to array
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'data' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always record the currently authenticated user
|
||||||
|
* for the audit and the user_id column.
|
||||||
|
*/
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::saving(function ($model) {
|
||||||
|
$model->user_id = auth()->id();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship to the user who last saved
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Relationship to the user who last saved
|
||||||
|
*/
|
||||||
|
public function room()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(StockRoom::class, 'room_id');
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,7 @@ class StockEntry extends Model implements AuditableContract
|
|||||||
|
|
||||||
protected $appends = [
|
protected $appends = [
|
||||||
'count_stocked',
|
'count_stocked',
|
||||||
|
'counted',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,9 +52,17 @@ class StockEntry extends Model implements AuditableContract
|
|||||||
protected $casts = [
|
protected $casts = [
|
||||||
'bought' => 'date',
|
'bought' => 'date',
|
||||||
'on_the_way' => 'boolean',
|
'on_the_way' => 'boolean',
|
||||||
|
'counted' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
public function getCountedAttribute(): int
|
||||||
|
{
|
||||||
|
// calls your existing method, which will
|
||||||
|
// loadMissing the relations if needed
|
||||||
|
return $this->statusHistory()->where('stock_entries_status_id', 2)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
public function getCountStockedAttribute(): int
|
public function getCountStockedAttribute(): int
|
||||||
{
|
{
|
||||||
// calls your existing method, which will
|
// calls your existing method, which will
|
||||||
|
@ -18,4 +18,8 @@ class StockRoom extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(StockLine::class, 'room_id', 'room_id');
|
return $this->hasMany(StockLine::class, 'room_id', 'room_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function layout(){
|
||||||
|
return $this->hasOne(FloorLayout::class, 'room_id', 'room_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
capacitor.config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { CapacitorConfig } from '@capacitor/cli';
|
||||||
|
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
appId: 'com.duremote.wms',
|
||||||
|
appName: 'vatWMS',
|
||||||
|
webDir: 'public/spa',
|
||||||
|
server: {
|
||||||
|
url: 'http://10.0.2.2:8004',
|
||||||
|
cleartext: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
@ -21,13 +21,15 @@
|
|||||||
"z38/metzli": "^1.1"
|
"z38/metzli": "^1.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"driftingly/rector-laravel": "^2.0",
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pail": "^1.2.2",
|
||||||
"laravel/pint": "^1.13",
|
"laravel/pint": "^1.13",
|
||||||
"laravel/sail": "^1.41",
|
"laravel/sail": "^1.41",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"phpunit/phpunit": "^11.5.3"
|
"phpunit/phpunit": "^11.5.3",
|
||||||
|
"rector/rector": "^2.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
154
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "2cc047a2ea18870d90d8b51342c3003a",
|
"content-hash": "7fadb707e55d12c787dd0bcb6b66c3ac",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@ -6739,6 +6739,41 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
{
|
||||||
|
"name": "driftingly/rector-laravel",
|
||||||
|
"version": "2.0.5",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/driftingly/rector-laravel.git",
|
||||||
|
"reference": "ac61de4f267c23249d175d7fc9149fd01528567d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/driftingly/rector-laravel/zipball/ac61de4f267c23249d175d7fc9149fd01528567d",
|
||||||
|
"reference": "ac61de4f267c23249d175d7fc9149fd01528567d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4 || ^8.0",
|
||||||
|
"rector/rector": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "rector-extension",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"RectorLaravel\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Rector upgrades rules for Laravel Framework",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/driftingly/rector-laravel/issues",
|
||||||
|
"source": "https://github.com/driftingly/rector-laravel/tree/2.0.5"
|
||||||
|
},
|
||||||
|
"time": "2025-05-14T17:30:41+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "fakerphp/faker",
|
"name": "fakerphp/faker",
|
||||||
"version": "v1.24.1",
|
"version": "v1.24.1",
|
||||||
@ -7491,6 +7526,64 @@
|
|||||||
},
|
},
|
||||||
"time": "2022-02-21T01:04:05+00:00"
|
"time": "2022-02-21T01:04:05+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phpstan/phpstan",
|
||||||
|
"version": "2.1.17",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/phpstan/phpstan.git",
|
||||||
|
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
|
||||||
|
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4|^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"phpstan/phpstan-shim": "*"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"phpstan",
|
||||||
|
"phpstan.phar"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "PHPStan - PHP Static Analysis Tool",
|
||||||
|
"keywords": [
|
||||||
|
"dev",
|
||||||
|
"static analysis"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"docs": "https://phpstan.org/user-guide/getting-started",
|
||||||
|
"forum": "https://github.com/phpstan/phpstan/discussions",
|
||||||
|
"issues": "https://github.com/phpstan/phpstan/issues",
|
||||||
|
"security": "https://github.com/phpstan/phpstan/security/policy",
|
||||||
|
"source": "https://github.com/phpstan/phpstan-src"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/ondrejmirtes",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/phpstan",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-05-21T20:55:28+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-code-coverage",
|
"name": "phpunit/php-code-coverage",
|
||||||
"version": "11.0.9",
|
"version": "11.0.9",
|
||||||
@ -7923,6 +8016,65 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-05-11T06:39:52+00:00"
|
"time": "2025-05-11T06:39:52+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "rector/rector",
|
||||||
|
"version": "2.0.18",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rectorphp/rector.git",
|
||||||
|
"reference": "be3a452085b524a04056e3dfe72d861948711062"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/rectorphp/rector/zipball/be3a452085b524a04056e3dfe72d861948711062",
|
||||||
|
"reference": "be3a452085b524a04056e3dfe72d861948711062",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.4|^8.0",
|
||||||
|
"phpstan/phpstan": "^2.1.17"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"rector/rector-doctrine": "*",
|
||||||
|
"rector/rector-downgrade-php": "*",
|
||||||
|
"rector/rector-phpunit": "*",
|
||||||
|
"rector/rector-symfony": "*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-dom": "To manipulate phpunit.xml via the custom-rule command"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/rector"
|
||||||
|
],
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Instant Upgrade and Automated Refactoring of any PHP code",
|
||||||
|
"keywords": [
|
||||||
|
"automation",
|
||||||
|
"dev",
|
||||||
|
"migration",
|
||||||
|
"refactoring"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/rectorphp/rector/issues",
|
||||||
|
"source": "https://github.com/rectorphp/rector/tree/2.0.18"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/tomasvotruba",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-06-11T11:19:37+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/cli-parser",
|
"name": "sebastian/cli-parser",
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
|
18
index.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!-- index.html (project root) -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0"
|
||||||
|
/>
|
||||||
|
<base href="./" /> <!-- makes all URLs relative -->
|
||||||
|
<title>vatWMS</title>
|
||||||
|
<!-- Vite will transform this import into the proper hashed bundle in production -->
|
||||||
|
<script type="module" src="/resources/js/app.tsx"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -205,3 +205,18 @@ create table stock_entries_status_history
|
|||||||
FOREIGN KEY (stock_entries_status_id) REFERENCES stock_entries_status (id)
|
FOREIGN KEY (stock_entries_status_id) REFERENCES stock_entries_status (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE floor_layouts (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
layout_name VARCHAR(100) NOT NULL,
|
||||||
|
room_id int NOT NULL,
|
||||||
|
layout_bg_file LONGBLOB NULL, -- raw image data
|
||||||
|
data JSON NOT NULL, -- your rooms/racks array
|
||||||
|
user_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY floor_layouts_layout_name_unique (layout_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
2692
package-lock.json
generated
15
package.json
@ -2,10 +2,13 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build && vite build --ssr",
|
"build": "vite build",
|
||||||
"dev": "vite"
|
"dev": "vite",
|
||||||
|
"dev:android": "EMULATOR=1 vite --host 0.0.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor/cli": "^7.4.1",
|
||||||
|
"@capacitor/core": "^7.4.1",
|
||||||
"@prettier/plugin-php": "^0.22.4",
|
"@prettier/plugin-php": "^0.22.4",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/postcss": "^4.0.15",
|
"@tailwindcss/postcss": "^4.0.15",
|
||||||
@ -18,14 +21,15 @@
|
|||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"laravel-vite-plugin": "^1.2.0",
|
"laravel-vite-plugin": "^1.3.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"tailwindcss": "^4.1.6",
|
"tailwindcss": "^4.1.6",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor/android": "^7.4.1",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
@ -44,11 +48,14 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"daisyui": "^5.0.35",
|
"daisyui": "^5.0.35",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"konva": "^9.3.20",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"material-react-table": "^3.2.1",
|
"material-react-table": "^3.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
|
"react-konva": "^18.2.12",
|
||||||
|
"use-image": "^1.1.4",
|
||||||
"use-react-countries": "^2.0.1",
|
"use-react-countries": "^2.0.1",
|
||||||
"ziggy-js": "^2.5.2"
|
"ziggy-js": "^2.5.2"
|
||||||
},
|
},
|
||||||
|
60
resources/js/Components/RackModalDetails.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// components/RackDetails.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import {StockRack, StockPosition} from '@/types';
|
||||||
|
|
||||||
|
interface RackDetailsProps {
|
||||||
|
rack: StockRack | null;
|
||||||
|
onPositionClick: (posId: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RackModalDetails({rack, onPositionClick}: RackDetailsProps) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-auto p-4">
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
{rack.shelves?.map((shelf) => (
|
||||||
|
<div key={shelf.shelf_symbol} className="flex items-start space-x-4 flex-col">
|
||||||
|
{/* Shelf label */}
|
||||||
|
<div className="font-bold">Shelf {shelf.shelf_symbol}</div>
|
||||||
|
|
||||||
|
{/* Positions container */}
|
||||||
|
<div className="flex space-x-4 border border-white dark:border-gray-700 h-max min-h-40 w-full">
|
||||||
|
{shelf.positions.length > 0 ? (
|
||||||
|
shelf.positions.map((position) => (
|
||||||
|
<div
|
||||||
|
key={position.position_symbol}
|
||||||
|
className="border rounded-lg p-3 w-full cursor-pointer"
|
||||||
|
onClick={() => onPositionClick(position.position_id)}
|
||||||
|
>
|
||||||
|
{/* Position label */}
|
||||||
|
<div className="font-semibold mb-2">Pos {position.position_symbol}</div>
|
||||||
|
|
||||||
|
{/* Sections grid */}
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
{position.sections.map((section) => (
|
||||||
|
|
||||||
|
<div
|
||||||
|
key={section.section_symbol}
|
||||||
|
className="border rounded p-1 text-center bg-primary font-bold cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPositionClick(position.position_id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sec {section.section_symbol}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="border rounded-lg p-3 w-full flex items-center justify-center">No
|
||||||
|
positions</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -105,7 +105,7 @@ const CountStockModal: React.FC<Props> = ({ onClose, selectedBatch }) => {
|
|||||||
<div className="dropdown w-full">
|
<div className="dropdown w-full">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn w-full justify-between"
|
className={`btn w-full justify-between ${selectedEntry && selectedEntry?.counted ? "pointer-events-none opacity-50" : "pointer-events-auto opacity-100"}`}
|
||||||
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||||||
>
|
>
|
||||||
{selectedEntry ? (
|
{selectedEntry ? (
|
||||||
@ -116,7 +116,7 @@ const CountStockModal: React.FC<Props> = ({ onClose, selectedBatch }) => {
|
|||||||
className="w-6 h-6 rounded-full"
|
className="w-6 h-6 rounded-full"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{selectedEntry.physical_item.name} ({selectedEntry.count})
|
{selectedEntry.physical_item.name} ({selectedEntry.original_count_invoice})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -141,7 +141,8 @@ const CountStockModal: React.FC<Props> = ({ onClose, selectedBatch }) => {
|
|||||||
<li key={entry.id}>
|
<li key={entry.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center px-2 py-1 hover:bg-gray-100 rounded"
|
disabled={entry.counted}
|
||||||
|
className={`flex items-center px-2 py-1 hover:bg-gray-100 rounded ${entry.counted ? "pointer-events-none opacity-50" : "pointer-events-auto opacity-100"}`}
|
||||||
onClick={() => handleSelect(entry)}
|
onClick={() => handleSelect(entry)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@ -150,7 +151,7 @@ const CountStockModal: React.FC<Props> = ({ onClose, selectedBatch }) => {
|
|||||||
className="w-6 h-6 rounded-full mr-2"
|
className="w-6 h-6 rounded-full mr-2"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{entry.physical_item.name} ({entry.count})
|
{entry.physical_item.name} ({entry.original_count_invoice}) {entry.counted ? " --- (Already counted)" : ""}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -6,7 +6,7 @@ import { StockPosition, StockSection } from "@/types"
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
selectedPosition: () => StockPosition
|
selectedPosition: StockPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditableSection {
|
interface EditableSection {
|
||||||
|
@ -72,6 +72,30 @@ export default function AppLayout({
|
|||||||
>
|
>
|
||||||
Dashboard
|
Dashboard
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
href={route('stock')}
|
||||||
|
active={route().current('stock')}
|
||||||
|
>
|
||||||
|
Stock
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
href={route('batches')}
|
||||||
|
active={route().current('batches')}
|
||||||
|
>
|
||||||
|
Batches
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
href={route('pdaView')}
|
||||||
|
active={route().current('pdaView')}
|
||||||
|
>
|
||||||
|
PDA
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
href={route('floorPlan')}
|
||||||
|
active={route().current('floorPlan')}
|
||||||
|
>
|
||||||
|
Floor plan
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -287,6 +311,31 @@ export default function AppLayout({
|
|||||||
>
|
>
|
||||||
Dashboard
|
Dashboard
|
||||||
</ResponsiveNavLink>
|
</ResponsiveNavLink>
|
||||||
|
<ResponsiveNavLink
|
||||||
|
href={route('stock')}
|
||||||
|
active={route().current('stock')}
|
||||||
|
>
|
||||||
|
Stock
|
||||||
|
</ResponsiveNavLink>
|
||||||
|
<ResponsiveNavLink
|
||||||
|
href={route('batches')}
|
||||||
|
active={route().current('batches')}
|
||||||
|
>
|
||||||
|
Batches
|
||||||
|
</ResponsiveNavLink>
|
||||||
|
<ResponsiveNavLink
|
||||||
|
href={route('pdaView')}
|
||||||
|
active={route().current('pdaView')}
|
||||||
|
>
|
||||||
|
PDA
|
||||||
|
</ResponsiveNavLink>
|
||||||
|
|
||||||
|
<ResponsiveNavLink
|
||||||
|
href={route('floorPlan')}
|
||||||
|
active={route().current('floorPlan')}
|
||||||
|
>
|
||||||
|
Floor plan
|
||||||
|
</ResponsiveNavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <!-- Responsive Settings Options --> */}
|
{/* <!-- Responsive Settings Options --> */}
|
||||||
|
593
resources/js/Pages/FloorPlan.tsx
Normal file
@ -0,0 +1,593 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import AppLayout from '@/Layouts/AppLayout';
|
||||||
|
import { Stage, Layer, Rect, Image as KonvaImage, Text, Transformer } from 'react-konva';
|
||||||
|
import useImage from 'use-image';
|
||||||
|
import axios from 'axios';
|
||||||
|
import {StockRoom, LayoutItem, LayoutLine, StockRack, StockLine, StockPosition} from '@/types';
|
||||||
|
import RackModalDetails from "@/Components/RackModalDetails";
|
||||||
|
import EditStockSections from '@/Components/modals/EditStockSections';
|
||||||
|
|
||||||
|
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||||
|
|
||||||
|
export default function FloorPlan() {
|
||||||
|
|
||||||
|
// at the top of FloorPlan()
|
||||||
|
const [showEditSectionsModal, setShowEditSectionsModal] = useState(false);
|
||||||
|
const [selectedPositionForEditing, setSelectedPositionForEditing] = useState<StockPosition | null>(null);
|
||||||
|
|
||||||
|
// fire this when RackModalDetails tells us “user clicked a position”
|
||||||
|
const handleOpenSections = async (posId: number) => {
|
||||||
|
try {
|
||||||
|
console.log(posId);
|
||||||
|
// 1️⃣ pull down the fresh position (with capacity & sections)
|
||||||
|
const { data: freshPos } = await axios.get<StockPosition>(
|
||||||
|
`/api/stockPositions/${posId}`,
|
||||||
|
{ withCredentials: true }
|
||||||
|
);
|
||||||
|
// 2️⃣ stash it & open the modal
|
||||||
|
setSelectedPositionForEditing(freshPos);
|
||||||
|
setShowEditSectionsModal(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert('Could not load position details. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// pass this into EditStockSections so that onClose we go back to RackModalDetails
|
||||||
|
const handleCloseSections = () => {
|
||||||
|
setShowEditSectionsModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rooms & selected room
|
||||||
|
const [rooms, setRooms] = useState<StockRoom[]>([]);
|
||||||
|
const [selectedRoom, setSelectedRoom] = useState<StockRoom | null>(null);
|
||||||
|
|
||||||
|
// Layout data
|
||||||
|
const [layoutItems, setLayoutItems] = useState<LayoutItem[]>([]);
|
||||||
|
const [layoutLines, setLayoutLines] = useState<LayoutLine[]>([]);
|
||||||
|
const [bgFile, setBgFile] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Track which existing DB items were deleted client-side
|
||||||
|
const [deletedLineDbIds, setDeletedLineDbIds] = useState<number[]>([]);
|
||||||
|
const [deletedRackDbIds, setDeletedRackDbIds] = useState<number[]>([]);
|
||||||
|
|
||||||
|
// New-layout modal state
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [newBgFile, setNewBgFile] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Edit-item modal state
|
||||||
|
const [editModal, setEditModal] = useState({
|
||||||
|
type: 'rack' as 'rack' | 'line' | null,
|
||||||
|
item: null as LayoutItem | LayoutLine | null,
|
||||||
|
item_obj: null as StockLine | StockRack | null,
|
||||||
|
visible: false,
|
||||||
|
name: '',
|
||||||
|
symbol: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Konva refs & selection
|
||||||
|
const layerRef = useRef<any>(null);
|
||||||
|
const transformerRef = useRef<any>(null);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const [selectedType, setSelectedType] = useState<'rack' | 'line' | null>(null);
|
||||||
|
|
||||||
|
// Background image for canvas
|
||||||
|
const [bgImage] = useImage(bgFile || '');
|
||||||
|
|
||||||
|
// Fetch room list on mount
|
||||||
|
useEffect(() => {
|
||||||
|
axios.get('/api/rooms')
|
||||||
|
.then(({ data }) => {
|
||||||
|
setRooms(data.rooms);
|
||||||
|
if (data.rooms.length) setSelectedRoom(data.rooms[0]);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showEditSectionsModal && selectedRoom) {
|
||||||
|
loadLayout(selectedRoom.room_id);
|
||||||
|
}
|
||||||
|
}, [showEditSectionsModal, selectedRoom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showEditSectionsModal && editModal.visible && editModal.type === 'rack' && editModal.item_obj) {
|
||||||
|
// grab the just-saved rack by its ID:
|
||||||
|
axios.get<StockRack>(`/api/stock-racks/${(editModal.item_obj as StockRack).rack_id}`, {
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
.then(({ data: freshRack }) => {
|
||||||
|
setEditModal(m => ({ ...m, item_obj: freshRack }));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Could not refresh rack details:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [showEditSectionsModal, editModal.visible]);
|
||||||
|
|
||||||
|
// Load a room's layout (or show create modal)
|
||||||
|
const loadLayout = async (roomId: number) => {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`/api/floor-layouts/${roomId}`);
|
||||||
|
if (!data.layoutItems) {
|
||||||
|
setShowCreateModal(true);
|
||||||
|
} else {
|
||||||
|
setLayoutItems(data.layoutItems.racks || []);
|
||||||
|
setLayoutLines(data.layoutItems.lines || []);
|
||||||
|
setBgFile(data.bgFile || null);
|
||||||
|
setShowCreateModal(false);
|
||||||
|
// reset deletion trackers
|
||||||
|
setDeletedLineDbIds([]);
|
||||||
|
setDeletedRackDbIds([]);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setShowCreateModal(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reload layout whenever selectedRoom changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRoom) loadLayout(selectedRoom.room_id);
|
||||||
|
}, [selectedRoom]);
|
||||||
|
|
||||||
|
// Hook up the Konva Transformer to the selected shape
|
||||||
|
useEffect(() => {
|
||||||
|
const tr = transformerRef.current;
|
||||||
|
if (!tr) return;
|
||||||
|
if (selectedId) {
|
||||||
|
const node = layerRef.current.findOne(`#${selectedId}`);
|
||||||
|
node ? tr.nodes([node]) : tr.nodes([]);
|
||||||
|
} else {
|
||||||
|
tr.nodes([]);
|
||||||
|
}
|
||||||
|
tr.getLayer().batchDraw();
|
||||||
|
}, [selectedId]);
|
||||||
|
|
||||||
|
// AABB intersection for rack-on-line test
|
||||||
|
const intersects = (r: LayoutItem, l: LayoutLine) =>
|
||||||
|
r.x + r.width >= l.x && r.x <= l.x + l.width &&
|
||||||
|
r.y + r.height >= l.y && r.y <= l.y + l.height;
|
||||||
|
|
||||||
|
// Selection handler
|
||||||
|
const onSelect = (e: any, id: string, type: 'rack' | 'line') => {
|
||||||
|
e.cancelBubble = true;
|
||||||
|
setSelectedId(id);
|
||||||
|
setSelectedType(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag end handler
|
||||||
|
const onDragEnd = (e: any, id: string, type: 'rack' | 'line') => {
|
||||||
|
const { x, y } = e.target.position();
|
||||||
|
if (type === 'line') {
|
||||||
|
setLayoutLines(ls => ls.map(l => l.id === id ? { ...l, x, y } : l));
|
||||||
|
} else {
|
||||||
|
setLayoutItems(is => is.map(i => {
|
||||||
|
if (i.id === id) {
|
||||||
|
const upd = { ...i, x, y };
|
||||||
|
const hit = layoutLines.find(l => intersects(upd, l));
|
||||||
|
upd.lineId = hit?.id ?? null;
|
||||||
|
return upd;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform end handler
|
||||||
|
const onTransformEnd = (e: any, id: string) => {
|
||||||
|
const node = e.target;
|
||||||
|
const scaleX = node.scaleX(), scaleY = node.scaleY();
|
||||||
|
const w = Math.max(5, node.width() * scaleX);
|
||||||
|
const h = Math.max(5, node.height() * scaleY);
|
||||||
|
node.scaleX(1); node.scaleY(1);
|
||||||
|
|
||||||
|
if (selectedType === 'line') {
|
||||||
|
setLayoutLines(ls => ls.map(l => l.id === id ? {
|
||||||
|
...l, x: node.x(), y: node.y(), width: w, height: h
|
||||||
|
} : l));
|
||||||
|
} else {
|
||||||
|
setLayoutItems(is => is.map(i => i.id === id ? {
|
||||||
|
...i, x: node.x(), y: node.y(), width: w, height: h
|
||||||
|
} : i));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helpers for new symbols/numbers
|
||||||
|
const nextLineLetter = () => {
|
||||||
|
const used = new Set<string>();
|
||||||
|
selectedRoom?.lines.forEach(l => used.add(l.line_symbol));
|
||||||
|
layoutLines.forEach(l => used.add(l.symbol));
|
||||||
|
for (let c of ALPHABET) if (!used.has(c)) return c;
|
||||||
|
return ALPHABET[0];
|
||||||
|
};
|
||||||
|
const nextRackNumber = (lineId: string) => {
|
||||||
|
const nums = layoutItems
|
||||||
|
.filter(i => i.lineId === lineId)
|
||||||
|
.map(i => parseInt(i.symbol) || 0);
|
||||||
|
return (nums.length ? Math.max(...nums) : 0) + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add new line
|
||||||
|
const addLine = () => {
|
||||||
|
const letter = nextLineLetter();
|
||||||
|
const id = `line-${Date.now()}`;
|
||||||
|
setLayoutLines(ls => [...ls, {
|
||||||
|
id, dbId: null,
|
||||||
|
name: `Line ${letter}`, symbol: letter,
|
||||||
|
x: 100, y: 100, width: 200, height: 20, fill: 'lightblue'
|
||||||
|
}]);
|
||||||
|
setSelectedId(id);
|
||||||
|
setSelectedType('line');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add new rack
|
||||||
|
const addRack = () => {
|
||||||
|
if (!selectedId || selectedType !== 'line') {
|
||||||
|
return alert('Select a line first');
|
||||||
|
}
|
||||||
|
const line = layoutLines.find(l => l.id === selectedId)!;
|
||||||
|
const num = nextRackNumber(line.id);
|
||||||
|
const id = `rack-${Date.now()}`;
|
||||||
|
setLayoutItems(is => [...is, {
|
||||||
|
id, dbId: null,
|
||||||
|
name: `Rack ${num}`, symbol: String(num),
|
||||||
|
x: line.x + 10, y: line.y + line.height + 10,
|
||||||
|
width: 60, height: 60, fill: 'yellow',
|
||||||
|
lineId: line.id
|
||||||
|
}]);
|
||||||
|
setSelectedId(id);
|
||||||
|
setSelectedType('rack');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open edit modal
|
||||||
|
const openEditModal = (item: any, type: 'rack' | 'line') => {
|
||||||
|
const item_obj = type === 'line'
|
||||||
|
? selectedRoom?.lines?.find(l => l.line_id === item.dbId) ?? null
|
||||||
|
: selectedRoom
|
||||||
|
?.lines
|
||||||
|
?.flatMap(l => l.racks || [])
|
||||||
|
.find(r => r.rack_id === item.dbId) ?? null;
|
||||||
|
|
||||||
|
setEditModal({
|
||||||
|
type,
|
||||||
|
item,
|
||||||
|
item_obj,
|
||||||
|
visible: true,
|
||||||
|
name: item.name,
|
||||||
|
symbol: item.symbol
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save name/symbol edits
|
||||||
|
const saveEdit = async () => {
|
||||||
|
if (!editModal.item) return;
|
||||||
|
const { type, item, name, symbol } = editModal;
|
||||||
|
try {
|
||||||
|
if (type === 'line') {
|
||||||
|
let res;
|
||||||
|
if (item.dbId) {
|
||||||
|
res = await axios.put(`/api/stock-lines/${item.dbId}`, {
|
||||||
|
line_name: name,
|
||||||
|
line_symbol: symbol
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res = await axios.post('/api/stock-lines', {
|
||||||
|
room_id: selectedRoom?.room_id,
|
||||||
|
line_name: name,
|
||||||
|
line_symbol: symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const dbId = res.data.line_id;
|
||||||
|
setLayoutLines(ls => ls.map(l =>
|
||||||
|
l.id === item.id ? { ...l, name, symbol, dbId } : l
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
await axios.put(`/api/stock-racks/${item.dbId}`, {
|
||||||
|
rack_name: name,
|
||||||
|
rack_symbol: symbol
|
||||||
|
});
|
||||||
|
setLayoutItems(is => is.map(i =>
|
||||||
|
i.id === item.id ? { ...i, name, symbol } : i
|
||||||
|
));
|
||||||
|
}
|
||||||
|
setEditModal(m => ({ ...m, visible: false }));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert('Error saving');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete rack or line (mark for later DB delete + remove visually)
|
||||||
|
const deleteItem = () => {
|
||||||
|
if (!editModal.item) return;
|
||||||
|
const { type, item } = editModal;
|
||||||
|
|
||||||
|
if (type === 'line') {
|
||||||
|
// if it existed in DB, queue it
|
||||||
|
if (item.dbId) {
|
||||||
|
setDeletedLineDbIds(ids => [...ids, item.dbId!]);
|
||||||
|
// also queue any racks under this line
|
||||||
|
layoutItems
|
||||||
|
.filter(r => r.lineId === item.id && r.dbId)
|
||||||
|
.forEach(r => setDeletedRackDbIds(ids => [...ids, r.dbId!]));
|
||||||
|
}
|
||||||
|
// remove the line and its racks visually
|
||||||
|
setLayoutLines(ls => ls.filter(l => l.id !== item.id));
|
||||||
|
setLayoutItems(is => is.filter(i => i.lineId !== item.id));
|
||||||
|
} else {
|
||||||
|
if (item.dbId) {
|
||||||
|
setDeletedRackDbIds(ids => [...ids, item.dbId!]);
|
||||||
|
}
|
||||||
|
setLayoutItems(is => is.filter(i => i.id !== item.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditModal(m => ({ ...m, visible: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new layout
|
||||||
|
const createLayout = async () => {
|
||||||
|
if (!selectedRoom) return;
|
||||||
|
try {
|
||||||
|
const defaultName = `${selectedRoom.room_name} layout`;
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('room_id', String(selectedRoom.room_id));
|
||||||
|
form.append('name', defaultName);
|
||||||
|
if (newBgFile) form.append('bgFile', newBgFile);
|
||||||
|
|
||||||
|
await axios.post('/api/floor-layouts/create', form, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setNewBgFile(null);
|
||||||
|
await loadLayout(selectedRoom.room_id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert('Error creating layout');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ENHANCED saveLayout: handle creates, deletes, then update layout record ---
|
||||||
|
const saveLayout = async () => {
|
||||||
|
if (layoutItems.some(r => !r.lineId)) {
|
||||||
|
return alert('Place all racks on lines before saving.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1️⃣ Create any new lines
|
||||||
|
await Promise.all(layoutLines.map(async line => {
|
||||||
|
if (!line.dbId) {
|
||||||
|
const { data } = await axios.post('/api/stock-lines', {
|
||||||
|
room_id: selectedRoom!.room_id,
|
||||||
|
line_symbol: line.symbol,
|
||||||
|
line_name: line.name
|
||||||
|
});
|
||||||
|
line.dbId = data.line_id;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 2️⃣ Create any new racks
|
||||||
|
await Promise.all(layoutItems.map(async rack => {
|
||||||
|
if (!rack.dbId) {
|
||||||
|
const parentLine = layoutLines.find(l => l.id === rack.lineId)!;
|
||||||
|
const { data } = await axios.post('/api/stock-racks', {
|
||||||
|
line_id: parentLine.dbId,
|
||||||
|
rack_symbol: rack.symbol,
|
||||||
|
rack_name: rack.name
|
||||||
|
});
|
||||||
|
rack.dbId = data.rack_id;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 3️⃣ Update any existing rack whose parent‐line changed
|
||||||
|
await Promise.all( layoutItems
|
||||||
|
.filter(r => r.dbId !== null)
|
||||||
|
.map(async rack => {
|
||||||
|
const parent = layoutLines.find(l => l.id === rack.lineId)!;
|
||||||
|
// send only if rack.line_id in DB !== parent.dbId
|
||||||
|
// (you could cache original in state, but simplest to just PUT all)
|
||||||
|
await axios.put(`/api/stock-racks/${rack.dbId}`, {
|
||||||
|
line_id: parent.dbId,
|
||||||
|
// you can also include name/symbol if you like:
|
||||||
|
rack_name: rack.name,
|
||||||
|
rack_symbol: rack.symbol,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3️⃣ Delete any queued lines/racks
|
||||||
|
await Promise.all([
|
||||||
|
...deletedLineDbIds.map(id => axios.delete(`/api/stock-lines/${id}`)),
|
||||||
|
...deletedRackDbIds.map(id => axios.delete(`/api/stock-racks/${id}`))
|
||||||
|
]);
|
||||||
|
|
||||||
|
// reset the deletion queues
|
||||||
|
setDeletedLineDbIds([]);
|
||||||
|
setDeletedRackDbIds([]);
|
||||||
|
|
||||||
|
// 4️⃣ Persist the full geometry
|
||||||
|
const { data } = await axios.post('/api/floor-layouts/update', {
|
||||||
|
room_id: selectedRoom!.room_id,
|
||||||
|
contents: {
|
||||||
|
lines: layoutLines,
|
||||||
|
racks: layoutItems
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
alert(`Layout saved at ${new Date(data.updated_at).toLocaleTimeString()}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert('Error saving layout');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout title="Floor Plan">
|
||||||
|
<div className="p-6 flex flex-col items-center space-y-4">
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="w-full flex justify-between items-center mb-4">
|
||||||
|
<div className="tabs">
|
||||||
|
{rooms.map(r => (
|
||||||
|
<button
|
||||||
|
key={r.room_id}
|
||||||
|
className={`tab ${selectedRoom?.room_id === r.room_id ? 'tab-active' : ''}`}
|
||||||
|
onClick={() => setSelectedRoom(r)}
|
||||||
|
>
|
||||||
|
{r.room_name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button className="btn mr-2" onClick={addLine}>Add Line</button>
|
||||||
|
<button className="btn mr-2" onClick={addRack}>Add Rack</button>
|
||||||
|
<button className="btn mr-2" onClick={() => setShowCreateModal(true)}>New Layout</button>
|
||||||
|
<button className="btn" onClick={saveLayout}>Save Layout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Canvas */}
|
||||||
|
<Stage width={1300} height={900} onClick={() => setSelectedId(null)}>
|
||||||
|
<Layer ref={layerRef}>
|
||||||
|
<Rect x={0} y={0} width={1300} height={900} fill="white" />
|
||||||
|
{bgImage && (
|
||||||
|
<KonvaImage
|
||||||
|
image={bgImage}
|
||||||
|
x={0} y={0}
|
||||||
|
width={1300} height={900}
|
||||||
|
listening={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lines */}
|
||||||
|
{layoutLines.map(line => (
|
||||||
|
<React.Fragment key={line.id}>
|
||||||
|
<Rect
|
||||||
|
id={line.id}
|
||||||
|
x={line.x} y={line.y}
|
||||||
|
width={line.width} height={line.height}
|
||||||
|
fill={line.fill} stroke="black"
|
||||||
|
draggable
|
||||||
|
onClick={e => { onSelect(e, line.id, 'line'); openEditModal(line, 'line'); }}
|
||||||
|
onDragEnd={e => onDragEnd(e, line.id, 'line')}
|
||||||
|
onTransformEnd={e => onTransformEnd(e, line.id)}
|
||||||
|
/>
|
||||||
|
<Text text={line.name} x={line.x + 5} y={line.y + 2} fontSize={14} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Racks */}
|
||||||
|
{layoutItems.map(item => (
|
||||||
|
<React.Fragment key={item.id}>
|
||||||
|
<Rect
|
||||||
|
id={item.id}
|
||||||
|
x={item.x} y={item.y}
|
||||||
|
width={item.width} height={item.height}
|
||||||
|
fill={item.fill}
|
||||||
|
stroke={item.lineId ? 'black' : 'red'}
|
||||||
|
strokeWidth={item.lineId ? 1 : 2}
|
||||||
|
draggable
|
||||||
|
onClick={e => { onSelect(e, item.id, 'rack'); openEditModal(item, 'rack'); }}
|
||||||
|
onDragEnd={e => onDragEnd(e, item.id, 'rack')}
|
||||||
|
onTransformEnd={e => onTransformEnd(e, item.id)}
|
||||||
|
/>
|
||||||
|
<Text text={item.name} x={item.x + 5} y={item.y + 5} fontSize={14} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Transformer ref={transformerRef} />
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
|
||||||
|
{/* Edit Modal */}
|
||||||
|
{editModal.visible && (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box max-w-[50%]">
|
||||||
|
<h3 className="font-bold text-lg">Edit {editModal.type}</h3>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input w-full my-2"
|
||||||
|
value={editModal.name}
|
||||||
|
onChange={e => setEditModal(m => ({ ...m, name: e.target.value }))}
|
||||||
|
placeholder="Name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input w-full my-2"
|
||||||
|
value={editModal.symbol}
|
||||||
|
onChange={e => setEditModal(m => ({ ...m, symbol: e.target.value }))}
|
||||||
|
placeholder="Symbol"
|
||||||
|
/>
|
||||||
|
{editModal.type === 'rack' && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<h4 className="font-bold mb-2">Shelves & Sections</h4>
|
||||||
|
<RackModalDetails rack={editModal.item_obj} onPositionClick={handleOpenSections} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="modal-action">
|
||||||
|
<button className="btn" onClick={saveEdit}>Save</button>
|
||||||
|
<button className="btn btn-error" onClick={deleteItem}>Delete</button>
|
||||||
|
<button className="btn btn-outline" onClick={() => setEditModal(m => ({ ...m, visible: false }))}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* === Sections Editor Nested Modal === */}
|
||||||
|
{showEditSectionsModal && selectedPositionForEditing && (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box max-w-[50%]">
|
||||||
|
<EditStockSections
|
||||||
|
selectedPosition={selectedPositionForEditing}
|
||||||
|
onClose={handleCloseSections}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Create Layout Modal */}
|
||||||
|
{showCreateModal && (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">New Layout for {selectedRoom?.room_name}</h3>
|
||||||
|
<h3 className="text-secondary">Layout does not exist yet, upload floorplan and create</h3>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="file-input w-full my-2"
|
||||||
|
onChange={e => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => setNewBgFile(reader.result as string);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{newBgFile && (
|
||||||
|
<img src={newBgFile} alt="Preview" className="w-full mb-2 rounded" />
|
||||||
|
)}
|
||||||
|
<div className="modal-action">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={createLayout}
|
||||||
|
disabled={!newBgFile}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setNewBgFile(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
@ -162,7 +162,26 @@ export default function PdaView({closeParent}: PdaViewProps) {
|
|||||||
const selectedSectionRef = React.useRef<StockSection | null>(null)
|
const selectedSectionRef = React.useRef<StockSection | null>(null)
|
||||||
const selectedPositionRef = React.useRef<StockPosition | null>(null)
|
const selectedPositionRef = React.useRef<StockPosition | null>(null)
|
||||||
|
|
||||||
const closeModal = () => setActiveModal(null)
|
const closeModal = () => {
|
||||||
|
setActiveModal(null);
|
||||||
|
if(selectedBatch) {
|
||||||
|
refetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const refetchData = async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/stockBatches/' + selectedBatch?.id);
|
||||||
|
console.log(res.data);
|
||||||
|
setSelectedBatch(res.data.batch);
|
||||||
|
} catch {
|
||||||
|
toast.error('Unable to reload batch data');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// const closeModal = () => {
|
// const closeModal = () => {
|
||||||
// setActiveModal(null)
|
// setActiveModal(null)
|
||||||
|
@ -271,6 +271,11 @@ export default function StockBatches() {
|
|||||||
});
|
});
|
||||||
setEntries(res.data.data);
|
setEntries(res.data.data);
|
||||||
setEntriesCount(size(res.data.data));
|
setEntriesCount(size(res.data.data));
|
||||||
|
setSelectedBatch((b) =>
|
||||||
|
b && b.id === batchId
|
||||||
|
? { ...b, stock_entries: res.data.data }
|
||||||
|
: b
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Cannot fetch entries');
|
toast.error('Cannot fetch entries');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -668,7 +673,10 @@ export default function StockBatches() {
|
|||||||
>
|
>
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
onChange={(e) => setItemQuery(e.target.value)}
|
onChange={(e) => setItemQuery(e.target.value)}
|
||||||
displayValue={(id) => (physicalItems.find((i) => i.id === id)?.name + " - " + physicalItems.find((i) => i.id === id)?.type._name) || ''}
|
displayValue={id => {
|
||||||
|
const itm = physicalItems.find(i => i.id === id)
|
||||||
|
return itm ? `${itm.name} - ${itm.type._name}` : ''
|
||||||
|
}}
|
||||||
placeholder="Select item..."
|
placeholder="Select item..."
|
||||||
className="input"
|
className="input"
|
||||||
/>
|
/>
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
import React, {useState, useEffect, useMemo, useRef} from 'react';
|
||||||
import { Head } from '@inertiajs/react';
|
import {Head} from '@inertiajs/react';
|
||||||
import AppLayout from '@/Layouts/AppLayout';
|
import AppLayout from '@/Layouts/AppLayout';
|
||||||
import {
|
import {
|
||||||
MaterialReactTable,
|
MaterialReactTable,
|
||||||
type MRT_ColumnDef,
|
type MRT_ColumnDef,
|
||||||
type MRT_PaginationState,
|
type MRT_PaginationState,
|
||||||
type MRT_SortingState,
|
type MRT_SortingState,
|
||||||
|
type MRT_ColumnFiltersState,
|
||||||
} from 'material-react-table';
|
} from 'material-react-table';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { toast } from 'react-hot-toast';
|
import {toast} from 'react-hot-toast';
|
||||||
import { Combobox } from '@headlessui/react';
|
import {Combobox} from '@headlessui/react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||||
import { faChevronDown, faCheck } from '@fortawesome/free-solid-svg-icons';
|
import {faChevronDown, faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {Autocomplete, TextField} from '@mui/material';
|
||||||
|
|
||||||
// --- Interfaces ---
|
// --- Interfaces ---
|
||||||
interface DropdownOption {
|
interface DropdownOption {
|
||||||
@ -78,6 +80,7 @@ export default function StockEntries() {
|
|||||||
// Dropdowns
|
// Dropdowns
|
||||||
const [stockPositions, setStockPositions] = useState<DropdownOption[]>([]);
|
const [stockPositions, setStockPositions] = useState<DropdownOption[]>([]);
|
||||||
const [suppliers, setSuppliers] = useState<DropdownOption[]>([]);
|
const [suppliers, setSuppliers] = useState<DropdownOption[]>([]);
|
||||||
|
const [manufacturers, setManufacturers] = useState<DropdownOption[]>([]);
|
||||||
const [physicalItems, setPhysicalItems] = useState<DropdownOption[]>([]);
|
const [physicalItems, setPhysicalItems] = useState<DropdownOption[]>([]);
|
||||||
const [originCountries, setOriginCountries] = useState<DropdownOption[]>([]);
|
const [originCountries, setOriginCountries] = useState<DropdownOption[]>([]);
|
||||||
|
|
||||||
@ -101,9 +104,12 @@ export default function StockEntries() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Pagination/sorting/filtering
|
// Pagination/sorting/filtering
|
||||||
const [pagination, setPagination] = useState<MRT_PaginationState>({ pageIndex: 0, pageSize: 10 });
|
const [pagination, setPagination] = useState<MRT_PaginationState>({pageIndex: 0, pageSize: 50});
|
||||||
const [sorting, setSorting] = useState<MRT_SortingState>([{ id: 'updated_at', desc: true }]);
|
const [sorting, setSorting] = useState<MRT_SortingState>([{id: 'updated_at', desc: true}]);
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
|
const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>([])
|
||||||
|
// holds the full objects for whatever the user has chosen
|
||||||
|
const [physicalItemFilterOptions, setPhysicalItemFilterOptions] = useState<DropdownOption[]>([]);
|
||||||
|
|
||||||
// Modal ref
|
// Modal ref
|
||||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
@ -113,22 +119,184 @@ export default function StockEntries() {
|
|||||||
// Columns
|
// Columns
|
||||||
const columns = useMemo<MRT_ColumnDef<StockEntry>[]>(
|
const columns = useMemo<MRT_ColumnDef<StockEntry>[]>(
|
||||||
() => [
|
() => [
|
||||||
{ accessorKey: 'id', header: 'ID', size: 80 },
|
{accessorKey: 'id', header: 'ID', size: 80},
|
||||||
{ accessorKey: 'physical_item.name', header: 'Physical Item', size: 200 },
|
{
|
||||||
{ accessorKey: 'supplier.name', header: 'Supplier', size: 150 },
|
id: 'physical_item_id',
|
||||||
{ accessorKey: 'physical_item.manufacturer.name', header: 'Brand', size: 150 },
|
header: 'Physical Item',
|
||||||
{ accessorKey: 'count', header: 'Count', size: 100 },
|
|
||||||
{ accessorKey: 'price', header: 'Price', size: 100, Cell: ({ cell }) => cell.getValue<number>()?.toFixed(2) || '-' },
|
// normal table‐cell display
|
||||||
{ accessorKey: 'bought', header: 'Bought Date', size: 120 },
|
accessorFn: row => row.physical_item?.name ?? '-',
|
||||||
{ accessorKey: 'on_the_way', header: 'On The Way', size: 100, Cell: ({ cell }) => cell.getValue<boolean>() ? 'Yes' : 'No' },
|
cell: ({row}) => row.original.physical_item?.name ?? '-',
|
||||||
{ accessorKey: 'stock_position', header: 'Position', size: 150, Cell: ({ row }) => {
|
|
||||||
|
enableColumnFilter: true,
|
||||||
|
Filter: ({column}) => {
|
||||||
|
// the numeric IDs MRT is storing under the hood
|
||||||
|
const selectedIds = (column.getFilterValue() as number[]) ?? [];
|
||||||
|
|
||||||
|
// 1) our “persistent” list of chosen DropdownOption objects
|
||||||
|
const selectedOptions = physicalItemFilterOptions;
|
||||||
|
|
||||||
|
// 2) the transient search results
|
||||||
|
// (you already have itemQuery & filteredItems above)
|
||||||
|
const suggestionOptions = filteredItems
|
||||||
|
.filter(item => !selectedIds.includes(item.id));
|
||||||
|
|
||||||
|
// merge them so selected stay in the list:
|
||||||
|
const options = [...selectedOptions, ...suggestionOptions];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{/* Show chosen items as chips */}
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{selectedOptions.map(item => (
|
||||||
|
<span
|
||||||
|
key={item.id}
|
||||||
|
className="badge badge-sm badge-primary cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
// remove from both MRT and our state
|
||||||
|
{
|
||||||
|
const newIds = selectedIds.filter(id => id !== item.id);
|
||||||
|
column.setFilterValue(newIds);
|
||||||
|
setPhysicalItemFilterOptions(opts =>
|
||||||
|
opts.filter(o => o.id !== item.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.name} ×
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* The multi‐select Combobox */}
|
||||||
|
<Combobox
|
||||||
|
multiple
|
||||||
|
value={selectedOptions}
|
||||||
|
onChange={(newSelection: DropdownOption[]) => {
|
||||||
|
// 1) tell MRT about the new array of IDs
|
||||||
|
column.setFilterValue(newSelection.map(i => i.id));
|
||||||
|
// 2) persist the objects for future renders
|
||||||
|
setPhysicalItemFilterOptions(newSelection);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Combobox.Input
|
||||||
|
className="input w-full"
|
||||||
|
placeholder="Type to search…"
|
||||||
|
onChange={e => setItemQuery(e.target.value)}
|
||||||
|
displayValue={() => ''} // keep the input box empty
|
||||||
|
/>
|
||||||
|
<Combobox.Options
|
||||||
|
className="dropdown-content menu p-2 shadow bg-base-100 rounded-box max-h-60 overflow-auto">
|
||||||
|
{options.map(item => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={item.id}
|
||||||
|
value={item}
|
||||||
|
className="cursor-pointer p-2 hover:bg-gray-200 flex justify-between items-center"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
{selectedIds.includes(item.id) && <FontAwesomeIcon icon={faCheck}/>}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
filterFn: (row, _columnId, filterValue: number[]) =>
|
||||||
|
filterValue.length === 0 ||
|
||||||
|
filterValue.includes(row.original.physical_item_id!),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 2) Supplier
|
||||||
|
{
|
||||||
|
id: 'supplier_id',
|
||||||
|
header: 'Supplier',
|
||||||
|
accessorFn: row => row.supplier?.name ?? '-',
|
||||||
|
cell: ({row}) => row.original.supplier?.name ?? '-',
|
||||||
|
|
||||||
|
enableColumnFilter: true,
|
||||||
|
Filter: ({column}) => (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
size="small"
|
||||||
|
options={suppliers}
|
||||||
|
getOptionLabel={opt => opt.name}
|
||||||
|
isOptionEqualToValue={(opt, val) => opt.id === val.id}
|
||||||
|
value={
|
||||||
|
suppliers.filter(s =>
|
||||||
|
(column.getFilterValue() as number[] || []).includes(s.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChange={(_, v) =>
|
||||||
|
column.setFilterValue(v.map(s => s.id))
|
||||||
|
}
|
||||||
|
renderInput={params => (
|
||||||
|
<TextField {...params} variant="standard" placeholder="Filter suppliers…"/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
filterFn: (row, _columnId, filterValue: number[]) =>
|
||||||
|
filterValue.length === 0 || filterValue.includes(row.original.supplier_id!),
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3) Brand / Manufacturer
|
||||||
|
{
|
||||||
|
id: 'manufacturer_id',
|
||||||
|
header: 'Brand',
|
||||||
|
accessorFn: row => row.physical_item?.manufacturer?.name ?? '-',
|
||||||
|
cell: ({row}) => row.original.physical_item?.manufacturer?.name ?? '-',
|
||||||
|
|
||||||
|
enableColumnFilter: true,
|
||||||
|
Filter: ({column}) => (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
size="small"
|
||||||
|
options={manufacturers}
|
||||||
|
getOptionLabel={opt => opt.name}
|
||||||
|
isOptionEqualToValue={(opt, val) => opt.id === val.id}
|
||||||
|
value={
|
||||||
|
manufacturers.filter(m =>
|
||||||
|
(column.getFilterValue() as number[] || []).includes(m.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChange={(_, v) =>
|
||||||
|
column.setFilterValue(v.map(m => m.id))
|
||||||
|
}
|
||||||
|
renderInput={params => (
|
||||||
|
<TextField {...params} variant="standard" placeholder="Filter brands…"/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
filterFn: (row, _columnId, filterValue: number[]) =>
|
||||||
|
filterValue.length === 0 ||
|
||||||
|
filterValue.includes(row.original.physical_item?.manufacturer?.id!),
|
||||||
|
},
|
||||||
|
{accessorKey: 'count', header: 'Count', size: 100},
|
||||||
|
{
|
||||||
|
accessorKey: 'price',
|
||||||
|
header: 'Price',
|
||||||
|
size: 100,
|
||||||
|
Cell: ({cell}) => cell.getValue<number>()?.toFixed(2) || '-'
|
||||||
|
},
|
||||||
|
{accessorKey: 'bought', header: 'Bought Date', size: 120},
|
||||||
|
{
|
||||||
|
accessorKey: 'on_the_way',
|
||||||
|
header: 'On The Way',
|
||||||
|
size: 100,
|
||||||
|
Cell: ({cell}) => cell.getValue<boolean>() ? 'Yes' : 'No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'stock_position', header: 'Position', size: 150, Cell: ({row}) => {
|
||||||
const pos = row.original.stock_position;
|
const pos = row.original.stock_position;
|
||||||
return pos ? `${pos.line}-${pos.rack}-${pos.shelf}-${pos.position}` : '-';
|
return pos ? `${pos.line}-${pos.rack}-${pos.shelf}-${pos.position}` : '-';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ accessorKey: 'updated_at', header: 'Last Updated', size: 150 },
|
{accessorKey: 'updated_at', header: 'Last Updated', size: 150},
|
||||||
],
|
],
|
||||||
[],
|
[suppliers, manufacturers, physicalItems, itemQuery, physicalItemFilterOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Batch columns
|
// Batch columns
|
||||||
@ -137,7 +305,7 @@ export default function StockEntries() {
|
|||||||
{
|
{
|
||||||
accessorKey: 'select',
|
accessorKey: 'select',
|
||||||
header: 'Select',
|
header: 'Select',
|
||||||
Cell: ({ row }) => {
|
Cell: ({row}) => {
|
||||||
const id = row.original.id;
|
const id = row.original.id;
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
@ -150,36 +318,78 @@ export default function StockEntries() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ accessorKey: 'id', header: 'ID' },
|
{accessorKey: 'id', header: 'ID'},
|
||||||
{ accessorKey: 'physical_item.name', header: 'Item' },
|
{accessorKey: 'physical_item.name', header: 'Item'},
|
||||||
{ accessorKey: 'supplier.name', header: 'Supplier' },
|
{accessorKey: 'supplier.name', header: 'Supplier'},
|
||||||
],
|
],
|
||||||
[batchSelections],
|
[batchSelections],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch options & data
|
// Fetch options & data
|
||||||
useEffect(() => { fetchData(); }, [pagination.pageIndex, pagination.pageSize, sorting, globalFilter]);
|
useEffect(() => {
|
||||||
useEffect(() => { fetchOptions(); }, []);
|
fetchData();
|
||||||
|
}, [pagination.pageIndex, pagination.pageSize, sorting, globalFilter, columnFilters]);
|
||||||
|
useEffect(() => {
|
||||||
|
fetchOptions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPagination((old) => ({...old, pageIndex: 0}));
|
||||||
|
}, [globalFilter, columnFilters]);
|
||||||
|
|
||||||
|
|
||||||
async function fetchOptions() {
|
async function fetchOptions() {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get('/api/stockData/options');
|
const [
|
||||||
setStockPositions(data.stockPositions);
|
{data: opts},
|
||||||
setSuppliers(data.suppliers);
|
{data: mfrData},
|
||||||
setOriginCountries(data.countriesOrigin);
|
] = await Promise.all([
|
||||||
|
axios.get('/api/stockData/options'),
|
||||||
|
axios.get('/api/stockData/manufacturers'), // ← new endpoint
|
||||||
|
]);
|
||||||
|
setStockPositions(opts.stockPositions);
|
||||||
|
setSuppliers(opts.suppliers);
|
||||||
|
setOriginCountries(opts.countriesOrigin);
|
||||||
|
setManufacturers(mfrData.manufacturers); // ← set manufacturers
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to load form options');
|
toast.error('Failed to load form options');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
|
// trigger loading states
|
||||||
setIsLoading(!data.length);
|
setIsLoading(!data.length);
|
||||||
setIsRefetching(!!data.length);
|
setIsRefetching(!!data.length);
|
||||||
|
|
||||||
|
const supplierFilter = columnFilters.find(f => f.id === 'supplier_id')?.value
|
||||||
|
const brandFilter = columnFilters.find(f => f.id === 'manufacturer_id')?.value
|
||||||
|
const itemFilter = columnFilters.find(f => f.id === 'physical_item_id')?.value
|
||||||
|
console.log(columnFilters);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/stockData');
|
const res = await axios.get('/api/stockData', {
|
||||||
|
params: {
|
||||||
|
page: pagination.pageIndex + 1,
|
||||||
|
per_page: pagination.pageSize,
|
||||||
|
sort_field: sorting[0]?.id || 'updated_at',
|
||||||
|
sort_direction: sorting[0]?.desc ? 'desc' : 'asc',
|
||||||
|
search: globalFilter,
|
||||||
|
supplier_id: supplierFilter ?? undefined,
|
||||||
|
manufacturer_id: brandFilter ?? undefined,
|
||||||
|
physical_item_id: itemFilter ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// update rows and total count
|
||||||
setData(res.data.data);
|
setData(res.data.data);
|
||||||
setRowCount(res.data.meta.total);
|
setRowCount(res.data.meta.total);
|
||||||
} catch {
|
|
||||||
|
// (optional) keep your pagination widget in sync
|
||||||
|
// setPagination(old => ({
|
||||||
|
// ...old,
|
||||||
|
// pageIndex: res.data.meta.current_page - 1,
|
||||||
|
// }));
|
||||||
|
} catch (error) {
|
||||||
setIsError(true);
|
setIsError(true);
|
||||||
toast.error('Failed to fetch stock entries');
|
toast.error('Failed to fetch stock entries');
|
||||||
} finally {
|
} finally {
|
||||||
@ -190,12 +400,13 @@ export default function StockEntries() {
|
|||||||
|
|
||||||
async function fetchPhysicalItems(query: string) {
|
async function fetchPhysicalItems(query: string) {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/stockData/options/items', { params: { item_name: query } });
|
const res = await axios.get('/api/stockData/options/items', {params: {item_name: query}});
|
||||||
setPhysicalItems(res.data.physicalItems);
|
setPhysicalItems(res.data.physicalItems);
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Failed to load items');
|
toast.error('Failed to load items');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const delay = setTimeout(() => itemQuery && fetchPhysicalItems(itemQuery), 500);
|
const delay = setTimeout(() => itemQuery && fetchPhysicalItems(itemQuery), 500);
|
||||||
return () => clearTimeout(delay);
|
return () => clearTimeout(delay);
|
||||||
@ -203,7 +414,7 @@ export default function StockEntries() {
|
|||||||
|
|
||||||
// Input change
|
// Input change
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
const { name, value, type } = e.target;
|
const {name, value, type} = e.target;
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: type === 'checkbox'
|
[name]: type === 'checkbox'
|
||||||
@ -230,7 +441,7 @@ export default function StockEntries() {
|
|||||||
if (isBatchMode) {
|
if (isBatchMode) {
|
||||||
// Add to batch and keep modal open
|
// Add to batch and keep modal open
|
||||||
setBatchSelections(prev => [...prev, newEntry.id]);
|
setBatchSelections(prev => [...prev, newEntry.id]);
|
||||||
setFormData({ ...defaultForm, on_the_way: true });
|
setFormData({...defaultForm, on_the_way: true});
|
||||||
} else {
|
} else {
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
@ -244,7 +455,7 @@ export default function StockEntries() {
|
|||||||
const handleBatchSubmit = async (e: React.FormEvent) => {
|
const handleBatchSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/stockData/batch', { ids: batchSelections });
|
await axios.post('/api/stockData/batch', {ids: batchSelections});
|
||||||
toast.success('Batch created');
|
toast.success('Batch created');
|
||||||
closeModal();
|
closeModal();
|
||||||
fetchData();
|
fetchData();
|
||||||
@ -255,7 +466,28 @@ export default function StockEntries() {
|
|||||||
|
|
||||||
const handleEdit = (entry: StockEntry) => {
|
const handleEdit = (entry: StockEntry) => {
|
||||||
setEditingEntry(entry);
|
setEditingEntry(entry);
|
||||||
setFormData({ ...entry });
|
setFormData({
|
||||||
|
physical_item_id: entry.physical_item_id,
|
||||||
|
supplier_id: entry.supplier_id,
|
||||||
|
count: entry.count,
|
||||||
|
price: entry.price,
|
||||||
|
bought: entry.bought,
|
||||||
|
description: entry.description,
|
||||||
|
note: entry.note,
|
||||||
|
stock_position_id: entry.stock_position_id,
|
||||||
|
country_of_origin_id: entry.country_of_origin_id,
|
||||||
|
on_the_way: entry.on_the_way,
|
||||||
|
stock_batch_id: entry.stock_batch_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// make sure our combobox can render the current item
|
||||||
|
if (entry.physical_item) {
|
||||||
|
setPhysicalItems(prev =>
|
||||||
|
prev.some(i => i.id === entry.physical_item!.id)
|
||||||
|
? prev
|
||||||
|
: [...prev, entry.physical_item!]
|
||||||
|
);
|
||||||
|
}
|
||||||
openModal();
|
openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -281,19 +513,15 @@ export default function StockEntries() {
|
|||||||
setIsBatchMode(true);
|
setIsBatchMode(true);
|
||||||
setEditingEntry(null);
|
setEditingEntry(null);
|
||||||
setBatchSelections([]);
|
setBatchSelections([]);
|
||||||
setFormData({ ...defaultForm, on_the_way: true });
|
setFormData({...defaultForm, on_the_way: true});
|
||||||
openModal();
|
openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout title="Stock Entries" renderHeader={() => (
|
<AppLayout title="Stock Entries">
|
||||||
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
<Head title="Stock Entries"/>
|
||||||
Stock Entries
|
<div className="py-6">
|
||||||
</h2>
|
<div className="mx-auto sm:px-6 lg:px-8">
|
||||||
)}>
|
|
||||||
<Head title="Stock Entries" />
|
|
||||||
<div className="py-12">
|
|
||||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
||||||
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-6">
|
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||||
<div className="mb-4 flex justify-between items-center">
|
<div className="mb-4 flex justify-between items-center">
|
||||||
<h1 className="text-2xl font-bold">Stock Entries</h1>
|
<h1 className="text-2xl font-bold">Stock Entries</h1>
|
||||||
@ -306,19 +534,32 @@ export default function StockEntries() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={data}
|
data={data}
|
||||||
enableTopToolbar
|
enableTopToolbar
|
||||||
manualPagination
|
columnFilters={columnFilters}
|
||||||
manualSorting
|
onColumnFiltersChange={setColumnFilters}
|
||||||
manualFiltering
|
|
||||||
onPaginationChange={setPagination}
|
onPaginationChange={setPagination}
|
||||||
onSortingChange={setSorting}
|
onSortingChange={setSorting}
|
||||||
onGlobalFilterChange={setGlobalFilter}
|
onGlobalFilterChange={setGlobalFilter}
|
||||||
|
manualPagination
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
state={{ isLoading, pagination, showProgressBars: isRefetching, sorting, globalFilter }}
|
state={{
|
||||||
|
isLoading,
|
||||||
|
pagination,
|
||||||
|
showProgressBars: isRefetching,
|
||||||
|
sorting,
|
||||||
|
globalFilter,
|
||||||
|
columnFilters
|
||||||
|
}}
|
||||||
enableRowActions
|
enableRowActions
|
||||||
renderRowActionMenuItems={({ row }) => [
|
renderRowActionMenuItems={({row}) => [
|
||||||
<button key="edit" onClick={() => handleEdit(row.original)} className="menu-item">Edit</button>,
|
<button key="edit" onClick={() => handleEdit(row.original)}
|
||||||
<button key="delete" onClick={() => handleDelete(row.original.id)} className="menu-item text-red-600">Delete</button>,
|
className="menu-item">Edit</button>,
|
||||||
|
<button key="delete" onClick={() => handleDelete(row.original.id)}
|
||||||
|
className="menu-item text-red-600">Delete</button>,
|
||||||
]}
|
]}
|
||||||
|
muiTablePaginationProps={{
|
||||||
|
rowsPerPageOptions: [25, 50, 100, 200, 500, 1000, 2000, 5000],
|
||||||
|
rowsPerPage: pagination.pageSize,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -326,8 +567,11 @@ export default function StockEntries() {
|
|||||||
|
|
||||||
{/* Modal (Add/Edit & Batch) */}
|
{/* Modal (Add/Edit & Batch) */}
|
||||||
<dialog ref={dialogRef} className="modal">
|
<dialog ref={dialogRef} className="modal">
|
||||||
<form onSubmit={handleSubmit} className={`modal-box flex space-x-4 p-6 ${isBatchMode ? 'max-w-full' : ''}`}>
|
<form onSubmit={handleSubmit}
|
||||||
<button type="button" onClick={closeModal} className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
className={`modal-box flex space-x-4 p-6 ${isBatchMode ? 'max-w-full' : ''}`}>
|
||||||
|
<button type="button" onClick={closeModal}
|
||||||
|
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕
|
||||||
|
</button>
|
||||||
{isBatchMode && (
|
{isBatchMode && (
|
||||||
<div className="w-2/3">
|
<div className="w-2/3">
|
||||||
<h3 className="font-bold text-lg mb-2">Select Incoming Items</h3>
|
<h3 className="font-bold text-lg mb-2">Select Incoming Items</h3>
|
||||||
@ -345,19 +589,23 @@ export default function StockEntries() {
|
|||||||
<h3 className="font-bold text-lg mb-4">{isBatchMode ? 'New Batch Entry' : (editingEntry ? 'Edit Stock Entry' : 'New Stock Entry')}</h3>
|
<h3 className="font-bold text-lg mb-4">{isBatchMode ? 'New Batch Entry' : (editingEntry ? 'Edit Stock Entry' : 'New Stock Entry')}</h3>
|
||||||
{/* Physical Item */}
|
{/* Physical Item */}
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<Combobox value={formData.physical_item_id} onChange={val => setFormData(prev => ({ ...prev, physical_item_id: val }))}>
|
<Combobox value={formData.physical_item_id}
|
||||||
|
onChange={val => setFormData(prev => ({...prev, physical_item_id: val}))}>
|
||||||
<Combobox.Input
|
<Combobox.Input
|
||||||
onChange={e => setItemQuery(e.target.value)}
|
onChange={e => setItemQuery(e.target.value)}
|
||||||
displayValue={id => physicalItems.find(i => i.id === id)?.name || ''}
|
displayValue={id => physicalItems.find(i => i.id === id)?.name || ''}
|
||||||
placeholder="Select item..."
|
placeholder="Select item..."
|
||||||
className="input"
|
className="input"
|
||||||
/>
|
/>
|
||||||
<Combobox.Options className="dropdown-content menu p-2 shadow bg-base-100 rounded-box max-h-60 overflow-auto">
|
<Combobox.Options
|
||||||
|
className="dropdown-content menu p-2 shadow bg-base-100 rounded-box max-h-60 overflow-auto">
|
||||||
{filteredItems.map(item => (
|
{filteredItems.map(item => (
|
||||||
<Combobox.Option key={item.id} value={item.id} className="cursor-pointer p-2 hover:bg-gray-200">
|
<Combobox.Option key={item.id} value={item.id}
|
||||||
|
className="cursor-pointer p-2 hover:bg-gray-200">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span>{item.name}</span>
|
<span>{item.name}</span>
|
||||||
{formData.physical_item_id === item.id && <FontAwesomeIcon icon={faCheck} />}
|
{formData.physical_item_id === item.id &&
|
||||||
|
<FontAwesomeIcon icon={faCheck}/>}
|
||||||
</div>
|
</div>
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))}
|
))}
|
||||||
@ -370,7 +618,8 @@ export default function StockEntries() {
|
|||||||
<label className="label" htmlFor="supplier_id">
|
<label className="label" htmlFor="supplier_id">
|
||||||
<span className="label-text">Supplier</span>
|
<span className="label-text">Supplier</span>
|
||||||
</label>
|
</label>
|
||||||
<select id="supplier_id" name="supplier_id" value={formData.supplier_id || ''} onChange={handleInputChange} className="select">
|
<select id="supplier_id" name="supplier_id" value={formData.supplier_id || ''}
|
||||||
|
onChange={handleInputChange} className="select">
|
||||||
<option value="">Select supplier...</option>
|
<option value="">Select supplier...</option>
|
||||||
{suppliers.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
{suppliers.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
@ -379,34 +628,46 @@ export default function StockEntries() {
|
|||||||
{/* Count, Price, Bought Date */}
|
{/* Count, Price, Bought Date */}
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label" htmlFor="count"><span className="label-text">Count</span></label>
|
<label className="label" htmlFor="count"><span
|
||||||
<input id="count" name="count" type="number" value={formData.count} onChange={handleInputChange} className="input" />
|
className="label-text">Count</span></label>
|
||||||
|
<input id="count" name="count" type="number" value={formData.count}
|
||||||
|
onChange={handleInputChange} className="input"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label" htmlFor="price"><span className="label-text">Price</span></label>
|
<label className="label" htmlFor="price"><span
|
||||||
<input id="price" name="price" type="number" step="0.01" value={formData.price || ''} onChange={handleInputChange} className="input" />
|
className="label-text">Price</span></label>
|
||||||
|
<input id="price" name="price" type="number" step="0.01" value={formData.price || ''}
|
||||||
|
onChange={handleInputChange} className="input"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label" htmlFor="bought"><span className="label-text">Bought Date</span></label>
|
<label className="label" htmlFor="bought"><span
|
||||||
<input id="bought" name="bought" type="date" value={formData.bought || ''} onChange={handleInputChange} className="input" />
|
className="label-text">Bought Date</span></label>
|
||||||
|
<input id="bought" name="bought" type="date" value={formData.bought || ''}
|
||||||
|
onChange={handleInputChange} className="input"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description & Note */}
|
{/* Description & Note */}
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label" htmlFor="description"><span className="label-text">Description</span></label>
|
<label className="label" htmlFor="description"><span
|
||||||
<textarea id="description" name="description" value={formData.description || ''} onChange={handleInputChange} className="textarea" />
|
className="label-text">Description</span></label>
|
||||||
|
<textarea id="description" name="description" value={formData.description || ''}
|
||||||
|
onChange={handleInputChange} className="textarea"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label" htmlFor="note"><span className="label-text">Note</span></label>
|
<label className="label" htmlFor="note"><span className="label-text">Note</span></label>
|
||||||
<textarea id="note" name="note" value={formData.note || ''} onChange={handleInputChange} className="textarea" />
|
<textarea id="note" name="note" value={formData.note || ''} onChange={handleInputChange}
|
||||||
|
className="textarea"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stock Position & Country & On The Way */}
|
{/* Stock Position & Country & On The Way */}
|
||||||
<div className="grid grid-cols-3 gap-4 items-end">
|
<div className="grid grid-cols-3 gap-4 items-end">
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label" htmlFor="stock_position_id"><span className="label-text">Position</span></label>
|
<label className="label" htmlFor="stock_position_id"><span
|
||||||
<select id="stock_position_id" name="stock_position_id" value={formData.stock_position_id || ''} onChange={handleInputChange} className="select">
|
className="label-text">Position</span></label>
|
||||||
|
<select id="stock_position_id" name="stock_position_id"
|
||||||
|
value={formData.stock_position_id || ''} onChange={handleInputChange}
|
||||||
|
className="select">
|
||||||
<option value="">Select...</option>
|
<option value="">Select...</option>
|
||||||
{stockPositions.map(pos => <option key={pos.id} value={pos.id}>{pos.name}</option>)}
|
{stockPositions.map(pos => <option key={pos.id} value={pos.id}>{pos.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
@ -415,14 +676,17 @@ export default function StockEntries() {
|
|||||||
<label className="label" htmlFor="supplier_id">
|
<label className="label" htmlFor="supplier_id">
|
||||||
<span className="label-text">Country</span>
|
<span className="label-text">Country</span>
|
||||||
</label>
|
</label>
|
||||||
<select id="country_of_origin_id" name="country_of_origin_id" value={formData.country_of_origin_id || ''} onChange={handleInputChange} className="select">
|
<select id="country_of_origin_id" name="country_of_origin_id"
|
||||||
|
value={formData.country_of_origin_id || ''} onChange={handleInputChange}
|
||||||
|
className="select">
|
||||||
<option value="">Select country...</option>
|
<option value="">Select country...</option>
|
||||||
{originCountries.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
{originCountries.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-control flex items-center">
|
<div className="form-control flex items-center">
|
||||||
<label className="label cursor-pointer">
|
<label className="label cursor-pointer">
|
||||||
<input type="checkbox" name="on_the_way" checked={formData.on_the_way} onChange={handleInputChange} className="checkbox mr-2" />
|
<input type="checkbox" name="on_the_way" checked={formData.on_the_way}
|
||||||
|
onChange={handleInputChange} className="checkbox mr-2"/>
|
||||||
<span className="label-text">On The Way</span>
|
<span className="label-text">On The Way</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
4273
resources/js/floorplan/floorplan_1p.svg
Normal file
After Width: | Height: | Size: 548 KiB |
@ -141,6 +141,8 @@ export interface StockEntry {
|
|||||||
physical_item_id: number;
|
physical_item_id: number;
|
||||||
supplier_id: number;
|
supplier_id: number;
|
||||||
count: number;
|
count: number;
|
||||||
|
original_count: number;
|
||||||
|
original_count_invoice: number;
|
||||||
price: number | null;
|
price: number | null;
|
||||||
bought: string | null;
|
bought: string | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Api\ExpediceController;
|
use App\Http\Controllers\Api\ExpediceController;
|
||||||
|
use App\Http\Controllers\Api\FloorLayoutController;
|
||||||
use App\Http\Controllers\Api\StockBatchController;
|
use App\Http\Controllers\Api\StockBatchController;
|
||||||
use App\Http\Controllers\Api\StockSectionController;
|
use App\Http\Controllers\Api\StockSectionController;
|
||||||
use App\Http\Controllers\Api\ScannerController;
|
use App\Http\Controllers\Api\ScannerController;
|
||||||
@ -28,6 +29,7 @@ Route::middleware([EnsureFrontendRequestsAreStateful::class, 'auth:sanctum'])->g
|
|||||||
// Route::group(['middleware' => ['role:admin']], function () {
|
// Route::group(['middleware' => ['role:admin']], function () {
|
||||||
// Stock Entry endpoints
|
// Stock Entry endpoints
|
||||||
Route::get('/stockData/options', [StockEntryController::class, 'getOptions']);
|
Route::get('/stockData/options', [StockEntryController::class, 'getOptions']);
|
||||||
|
Route::get('/stockData/manufacturers', [StockEntryController::class, 'getManufacturers']);
|
||||||
Route::get('/stockData/options/items', [StockEntryController::class, 'getItems']);
|
Route::get('/stockData/options/items', [StockEntryController::class, 'getItems']);
|
||||||
Route::get('stockData', [StockEntryController::class, 'index']);
|
Route::get('stockData', [StockEntryController::class, 'index']);
|
||||||
Route::get('stockData/audit/{id}', [StockEntryController::class, 'audit']);
|
Route::get('stockData/audit/{id}', [StockEntryController::class, 'audit']);
|
||||||
@ -36,6 +38,7 @@ Route::post('stockData', [StockEntryController::class, 'addData']);
|
|||||||
Route::put('stockData/{id}', [StockEntryController::class, 'updateData']);
|
Route::put('stockData/{id}', [StockEntryController::class, 'updateData']);
|
||||||
|
|
||||||
Route::get('stockBatches', [StockBatchController::class, 'index']);
|
Route::get('stockBatches', [StockBatchController::class, 'index']);
|
||||||
|
Route::get('stockBatches/{id}', [StockBatchController::class, 'getBatch']);
|
||||||
Route::post('stockBatches', [StockBatchController::class, 'addData']);
|
Route::post('stockBatches', [StockBatchController::class, 'addData']);
|
||||||
Route::put('stockBatches/{id}', [StockBatchController::class, 'updateData']);
|
Route::put('stockBatches/{id}', [StockBatchController::class, 'updateData']);
|
||||||
Route::get('stockBatches/{id}/entries', [StockBatchController::class, 'getEntries']);
|
Route::get('stockBatches/{id}/entries', [StockBatchController::class, 'getEntries']);
|
||||||
@ -123,13 +126,14 @@ Route::get('/stockStatusList', [StockEntryController::class, 'getStatusList']);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
Route::get('/pdaView/getStockPosition', [StockPositionController::class, 'getPosition']);
|
Route::get('/pdaView/getStockPosition/{id}', [StockPositionController::class, 'getPosition']);
|
||||||
Route::post('/pdaView/moveStockSection', [StockSectionController::class, 'movePosition']);
|
Route::post('/pdaView/moveStockSection', [StockSectionController::class, 'movePosition']);
|
||||||
Route::post('/pdaView/changeCount', [StockSectionController::class, 'changeCount']);
|
Route::post('/pdaView/changeCount', [StockSectionController::class, 'changeCount']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Route::put('/stockPositions/{id}', [StockPositionController::class, 'update']);
|
Route::put('/stockPositions/{id}', [StockPositionController::class, 'update']);
|
||||||
|
Route::get('/stockPositions/{id}', [StockPositionController::class, 'getPosition']);
|
||||||
Route::get('/stockSections/{id}', [StockSectionController::class, 'getSection']);
|
Route::get('/stockSections/{id}', [StockSectionController::class, 'getSection']);
|
||||||
Route::post('/stockSections', [StockSectionController::class, 'store']);
|
Route::post('/stockSections', [StockSectionController::class, 'store']);
|
||||||
Route::put('/stockSections/{id}', [StockSectionController::class, 'update']);
|
Route::put('/stockSections/{id}', [StockSectionController::class, 'update']);
|
||||||
@ -150,6 +154,7 @@ Route::get('/stockStatusList', [StockEntryController::class, 'getStatusList']);
|
|||||||
Route::delete ('/stock-lines/{line}', [StockLineController::class, 'destroy']);
|
Route::delete ('/stock-lines/{line}', [StockLineController::class, 'destroy']);
|
||||||
|
|
||||||
// Racks
|
// Racks
|
||||||
|
Route::get ('/stock-racks/{rack}', [StockRackController::class, 'show']);
|
||||||
Route::post ('/stock-racks', [StockRackController::class, 'store']);
|
Route::post ('/stock-racks', [StockRackController::class, 'store']);
|
||||||
Route::put ('/stock-racks/{rack}', [StockRackController::class, 'update']);
|
Route::put ('/stock-racks/{rack}', [StockRackController::class, 'update']);
|
||||||
Route::delete ('/stock-racks/{rack}', [StockRackController::class, 'destroy']);
|
Route::delete ('/stock-racks/{rack}', [StockRackController::class, 'destroy']);
|
||||||
@ -166,6 +171,11 @@ Route::get('/stockStatusList', [StockEntryController::class, 'getStatusList']);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Route::get('/rooms', [StockRoomController::class, 'getList']);
|
||||||
|
Route::get('/floor-layouts', [FloorLayoutController::class, 'index']);
|
||||||
|
Route::get('/floor-layouts/{room_id}', [FloorLayoutController::class, 'show']);
|
||||||
|
Route::post('/floor-layouts/update', [FloorLayoutController::class, 'update']);
|
||||||
|
Route::post('/floor-layouts/create', [FloorLayoutController::class, 'create']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,4 +77,11 @@ Route::middleware([
|
|||||||
|
|
||||||
})->name('storageSetup');
|
})->name('storageSetup');
|
||||||
|
|
||||||
|
Route::get('/floorPlan', function () {
|
||||||
|
return Inertia::render('FloorPlan',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
})->name('floorPlan');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,22 +1,54 @@
|
|||||||
import { defineConfig } from 'vite';
|
// vite.config.ts
|
||||||
import laravel from 'laravel-vite-plugin';
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react';
|
import laravel from 'laravel-vite-plugin'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ command }) => {
|
||||||
plugins: [
|
const isDev = command === 'serve'
|
||||||
laravel({
|
const isBuild = command === 'build'
|
||||||
input: 'resources/js/app.tsx',
|
// set EMULATOR=1 in your npm script when you want to work on the Android build
|
||||||
// ssr: 'resources/js/ssr.tsx',
|
const isEmulator = !!process.env.EMULATOR
|
||||||
refresh: true,
|
|
||||||
}),
|
return {
|
||||||
react(),
|
// for web‐dev we load from “/”, for the Capacitor build we need “./”
|
||||||
],
|
base: isBuild ? './' : '/',
|
||||||
resolve: {
|
|
||||||
alias: {
|
plugins: [
|
||||||
'@': '/resources/js',
|
laravel({
|
||||||
},
|
input: 'resources/js/app.tsx',
|
||||||
},
|
refresh: true,
|
||||||
// ssr: {
|
}),
|
||||||
// noExternal: ['@inertiajs/server'],
|
react(),
|
||||||
// },
|
],
|
||||||
});
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': '/resources/js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// only spin up a dev server when running `vite` or `npm run dev`
|
||||||
|
...(isDev && {
|
||||||
|
server: {
|
||||||
|
// if you do `--host`, Vite will override this to 0.0.0.0
|
||||||
|
host: isEmulator ? '0.0.0.0' : 'localhost',
|
||||||
|
port: 5173,
|
||||||
|
strictPort: true,
|
||||||
|
cors: true,
|
||||||
|
hmr: {
|
||||||
|
// emulator needs 10.0.2.2, browser just localhost
|
||||||
|
host: isEmulator ? '10.0.2.2' : 'localhost',
|
||||||
|
port: 5173,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// only emit your Capacitor build when running `vite build` or `npm run build`
|
||||||
|
...(isBuild && {
|
||||||
|
build: {
|
||||||
|
outDir: 'public/spa',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|