feat(scanner): Mobile Scanner App mit echten Produktionsaufträgen
Scanner-App Tauri v2 Android App mit Login, Barcode-Scanner, Consume/Move/Book-Flows und Aufgabenliste. TasksPage lädt echte RELEASED/IN_PROGRESS Produktionsaufträge via API, Consume-Flow nutzt den konkreten Auftrag für korrekte Rezept-Skalierung statt blind den ersten IN_PROGRESS-Auftrag zu nehmen. Backend: FindStockByBatchId Use Case + REST-Endpoint für Stock-Lookup per Chargennummer.
|
|
@ -0,0 +1,28 @@
|
|||
package de.effigenix.application.inventory;
|
||||
|
||||
import de.effigenix.domain.inventory.Stock;
|
||||
import de.effigenix.domain.inventory.StockError;
|
||||
import de.effigenix.domain.inventory.StockRepository;
|
||||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FindStockByBatchId {
|
||||
|
||||
private final StockRepository stockRepository;
|
||||
|
||||
public FindStockByBatchId(StockRepository stockRepository) {
|
||||
this.stockRepository = stockRepository;
|
||||
}
|
||||
|
||||
public Result<StockError, List<Stock>> execute(String batchId) {
|
||||
if (batchId == null || batchId.isBlank()) {
|
||||
return Result.failure(new StockError.InvalidBatchReference("Batch ID must not be blank"));
|
||||
}
|
||||
return switch (stockRepository.findAllByBatchId(batchId)) {
|
||||
case Result.Failure(var err) -> Result.failure(new StockError.RepositoryFailure(err.message()));
|
||||
case Result.Success(var stocks) -> Result.success(stocks);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import de.effigenix.application.inventory.BookProductionOutput;
|
|||
import de.effigenix.application.inventory.ConfirmReservation;
|
||||
import de.effigenix.application.inventory.RecordStockMovement;
|
||||
import de.effigenix.application.inventory.ActivateStorageLocation;
|
||||
import de.effigenix.application.inventory.FindStockByBatchId;
|
||||
import de.effigenix.application.inventory.AddStockBatch;
|
||||
import de.effigenix.application.inventory.BlockStockBatch;
|
||||
import de.effigenix.application.inventory.CheckStockExpiry;
|
||||
|
|
@ -99,6 +100,11 @@ public class InventoryUseCaseConfiguration {
|
|||
return new ListStocks(stockRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FindStockByBatchId findStockByBatchId(StockRepository stockRepository) {
|
||||
return new FindStockByBatchId(stockRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AddStockBatch addStockBatch(StockRepository stockRepository, UnitOfWork unitOfWork) {
|
||||
return new AddStockBatch(stockRepository, unitOfWork);
|
||||
|
|
|
|||
|
|
@ -264,6 +264,24 @@ public class JdbcStockRepository implements StockRepository {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Stock>> findAllByBatchId(String batchId) {
|
||||
try {
|
||||
var stocks = jdbc.sql("""
|
||||
SELECT DISTINCT s.* FROM stocks s
|
||||
JOIN stock_batches b ON b.stock_id = s.id
|
||||
WHERE b.batch_id = :batchId
|
||||
""")
|
||||
.param("batchId", batchId)
|
||||
.query(this::mapStockRow)
|
||||
.list();
|
||||
return Result.success(loadChildrenForAll(stocks));
|
||||
} catch (Exception e) {
|
||||
logger.trace("Database error in findAllByBatchId", e);
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Void> save(Stock stock) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package de.effigenix.infrastructure.inventory.web.controller;
|
||||
|
||||
import de.effigenix.application.inventory.FindStockByBatchId;
|
||||
import de.effigenix.application.inventory.AddStockBatch;
|
||||
import de.effigenix.application.inventory.BlockStockBatch;
|
||||
import de.effigenix.application.inventory.ConfirmReservation;
|
||||
|
|
@ -63,6 +64,7 @@ public class StockController {
|
|||
private final GetStock getStock;
|
||||
private final ListStocks listStocks;
|
||||
private final ListStocksBelowMinimum listStocksBelowMinimum;
|
||||
private final FindStockByBatchId findStockByBatchId;
|
||||
private final AddStockBatch addStockBatch;
|
||||
private final RemoveStockBatch removeStockBatch;
|
||||
private final BlockStockBatch blockStockBatch;
|
||||
|
|
@ -72,7 +74,7 @@ public class StockController {
|
|||
private final ConfirmReservation confirmReservation;
|
||||
|
||||
public StockController(CreateStock createStock, UpdateStock updateStock, GetStock getStock, ListStocks listStocks,
|
||||
ListStocksBelowMinimum listStocksBelowMinimum,
|
||||
ListStocksBelowMinimum listStocksBelowMinimum, FindStockByBatchId findStockByBatchId,
|
||||
AddStockBatch addStockBatch, RemoveStockBatch removeStockBatch,
|
||||
BlockStockBatch blockStockBatch, UnblockStockBatch unblockStockBatch,
|
||||
ReserveStock reserveStock, ReleaseReservation releaseReservation,
|
||||
|
|
@ -82,6 +84,7 @@ public class StockController {
|
|||
this.getStock = getStock;
|
||||
this.listStocks = listStocks;
|
||||
this.listStocksBelowMinimum = listStocksBelowMinimum;
|
||||
this.findStockByBatchId = findStockByBatchId;
|
||||
this.addStockBatch = addStockBatch;
|
||||
this.removeStockBatch = removeStockBatch;
|
||||
this.blockStockBatch = blockStockBatch;
|
||||
|
|
@ -109,6 +112,20 @@ public class StockController {
|
|||
};
|
||||
}
|
||||
|
||||
@GetMapping("/by-batch/{batchId}")
|
||||
@PreAuthorize("hasAuthority('STOCK_READ')")
|
||||
public ResponseEntity<List<StockResponse>> findByBatchId(@PathVariable String batchId) {
|
||||
return switch (findStockByBatchId.execute(batchId)) {
|
||||
case Result.Failure(var err) -> throw new StockDomainErrorException(err);
|
||||
case Result.Success(var stocks) -> {
|
||||
var responses = stocks.stream()
|
||||
.map(StockResponse::from)
|
||||
.toList();
|
||||
yield ResponseEntity.ok(responses);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// NOTE: Must be declared before /{id} to avoid Spring matching "below-minimum" as path variable
|
||||
@GetMapping("/below-minimum")
|
||||
@PreAuthorize("hasAuthority('STOCK_READ')")
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ public class SecurityConfig {
|
|||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(allowedOrigins);
|
||||
configuration.setAllowedOriginPatterns(List.of("*"));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ logging:
|
|||
# CORS Configuration
|
||||
effigenix:
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000}
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:1420,tauri://localhost}
|
||||
inventory:
|
||||
expiry-check-cron: "0 0 6 * * *"
|
||||
|
||||
|
|
|
|||
|
|
@ -153,5 +153,6 @@ class ListStocksBelowMinimumTest {
|
|||
@Override public Result<RepositoryError, List<Stock>> findAllWithExpiryRelevantBatches(LocalDate referenceDate) { return Result.success(List.of()); }
|
||||
@Override public Result<RepositoryError, List<Stock>> findAllByBatchId(String batchId) { return Result.success(List.of()); }
|
||||
@Override public Result<RepositoryError, Void> save(Stock stock) { return Result.success(null); }
|
||||
@Override public Result<RepositoryError, List<Stock>> findAllByBatchId(String batchId) { return Result.success(List.of()); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
55
flake.nix
|
|
@ -14,7 +14,13 @@
|
|||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
config = {
|
||||
allowUnfree = true;
|
||||
android_sdk.accept_license = true;
|
||||
};
|
||||
};
|
||||
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" "rust-analyzer" ];
|
||||
|
|
@ -25,6 +31,23 @@
|
|||
"x86_64-linux-android"
|
||||
];
|
||||
};
|
||||
|
||||
# ─── Android SDK ──────────────────────────
|
||||
androidComposition = pkgs.androidenv.composeAndroidPackages {
|
||||
cmdLineToolsVersion = "8.0";
|
||||
platformToolsVersion = "35.0.1";
|
||||
buildToolsVersions = [ "34.0.0" "35.0.0" ];
|
||||
platformVersions = [ "34" "36" ];
|
||||
abiVersions = [ "x86_64" ];
|
||||
includeNDK = true;
|
||||
ndkVersions = [ "27.0.12077973" ];
|
||||
includeSystemImages = true;
|
||||
systemImageTypes = [ "google_apis_playstore" ];
|
||||
includeEmulator = true;
|
||||
useGoogleAPIs = true;
|
||||
};
|
||||
androidSdk = androidComposition.androidsdk;
|
||||
androidHome = "${androidSdk}/libexec/android-sdk";
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
|
|
@ -52,6 +75,13 @@
|
|||
harfbuzz
|
||||
atk
|
||||
|
||||
# ─── Backend ─────────────────────────
|
||||
jdk21
|
||||
|
||||
# ─── Android ───────────────────────────
|
||||
androidSdk
|
||||
jdk17
|
||||
|
||||
# ─── Tools ─────────────────────────────
|
||||
just
|
||||
jq
|
||||
|
|
@ -68,13 +98,36 @@
|
|||
openssl
|
||||
]);
|
||||
|
||||
ANDROID_HOME = androidHome;
|
||||
ANDROID_SDK_ROOT = androidHome;
|
||||
ANDROID_NDK_ROOT = "${androidHome}/ndk/27.0.12077973";
|
||||
JAVA_HOME = "${pkgs.jdk21}";
|
||||
|
||||
# NixOS: Gradle's aapt2 von Maven ist dynamisch gelinkt und läuft nicht.
|
||||
# Wir zeigen auf das aapt2 aus den Nix-bereitgestellten build-tools.
|
||||
GRADLE_OPTS = "-Dandroid.aapt2FromMavenOverride=${androidHome}/build-tools/35.0.0/aapt2";
|
||||
|
||||
shellHook = ''
|
||||
echo "Node $(node --version)"
|
||||
echo "pnpm $(pnpm --version)"
|
||||
echo "rustc $(rustc --version)"
|
||||
echo "cargo $(cargo --version)"
|
||||
echo "Java $(java --version 2>&1 | head -1)"
|
||||
echo ""
|
||||
echo "ANDROID_HOME=$ANDROID_HOME"
|
||||
echo ""
|
||||
echo "just --list für alle Befehle"
|
||||
|
||||
# Tauri mobile dev: Ports 1420 (Vite) + 1421 (HMR) in der NixOS-Firewall öffnen
|
||||
if [[ "''${EFFIGENIX_OPEN_FW:-}" == "1" ]]; then
|
||||
echo ""
|
||||
echo "Opening firewall ports 1420/1421 for Tauri mobile dev..."
|
||||
sudo iptables -C nixos-fw -p tcp --dport 1420 -j ACCEPT 2>/dev/null \
|
||||
|| sudo iptables -I nixos-fw 3 -p tcp --dport 1420 -j ACCEPT
|
||||
sudo iptables -C nixos-fw -p tcp --dport 1421 -j ACCEPT 2>/dev/null \
|
||||
|| sudo iptables -I nixos-fw 3 -p tcp --dport 1421 -j ACCEPT
|
||||
echo "Done."
|
||||
fi
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@
|
|||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#c06b3a" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Effigenix Scanner</title>
|
||||
</head>
|
||||
<body class="bg-warm-50 text-warm-900 font-sans antialiased">
|
||||
<body class="bg-warm-50 text-warm-800 font-sans antialiased">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -12,22 +12,24 @@
|
|||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@effigenix/api-client": "workspace:*",
|
||||
"@effigenix/config": "workspace:*",
|
||||
"@effigenix/types": "workspace:*",
|
||||
"@effigenix/ui": "workspace:*",
|
||||
"@effigenix/validation": "workspace:*",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-barcode-scanner": "^2.0.0",
|
||||
"@effigenix/ui": "workspace:*",
|
||||
"@effigenix/api-client": "workspace:*",
|
||||
"@effigenix/types": "workspace:*",
|
||||
"@effigenix/validation": "workspace:*",
|
||||
"@effigenix/config": "workspace:*"
|
||||
"@tauri-apps/plugin-http": "^2.5.7",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^6.0.0"
|
||||
|
|
|
|||
512
frontend/apps/scanner/src-tauri/Cargo.lock
generated
|
|
@ -300,6 +300,12 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
|
|
@ -334,10 +340,57 @@ version = "0.18.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"document-features",
|
||||
"idna",
|
||||
"log",
|
||||
"publicsuffix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"document-features",
|
||||
"idna",
|
||||
"log",
|
||||
"publicsuffix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
|
|
@ -361,7 +414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
|
|
@ -374,7 +427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
@ -505,6 +558,12 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
|
|
@ -626,6 +685,15 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dom_query"
|
||||
version = "0.27.0"
|
||||
|
|
@ -686,6 +754,7 @@ dependencies = [
|
|||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-barcode-scanner",
|
||||
"tauri-plugin-http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -708,6 +777,15 @@ version = "1.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -1037,8 +1115,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1048,9 +1128,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1214,6 +1296,25 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap 2.13.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
|
|
@ -1324,6 +1425,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
|
|
@ -1335,6 +1437,23 @@ dependencies = [
|
|||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
|
|
@ -1353,9 +1472,11 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1721,6 +1842,12 @@ version = "0.8.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
|
|
@ -1736,6 +1863,12 @@ version = "0.4.29"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2454,6 +2587,22 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||
|
||||
[[package]]
|
||||
name = "publicsuffix"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
|
|
@ -2463,6 +2612,61 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
|
|
@ -2509,6 +2713,16 @@ dependencies = [
|
|||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
|
|
@ -2529,6 +2743,16 @@ dependencies = [
|
|||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
|
|
@ -2547,6 +2771,15 @@ dependencies = [
|
|||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
|
|
@ -2640,6 +2873,49 @@ version = "0.8.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"cookie_store 0.22.1",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.2"
|
||||
|
|
@ -2674,6 +2950,20 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
|
|
@ -2689,12 +2979,53 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
|
|
@ -2903,6 +3234,18 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.18.0"
|
||||
|
|
@ -3141,6 +3484,12 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
|
|
@ -3194,6 +3543,27 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
|
|
@ -3215,7 +3585,7 @@ checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb"
|
|||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dispatch2",
|
||||
|
|
@ -3292,7 +3662,7 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"reqwest 0.13.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
|
|
@ -3407,6 +3777,52 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-fs"
|
||||
version = "2.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"glob",
|
||||
"percent-encoding",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8f069451c4e87e7e2636b7f065a4c52866c4ce5e60e2d53fa1038edb6d184dc"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cookie_store 0.21.1",
|
||||
"data-url",
|
||||
"http",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.10.1"
|
||||
|
|
@ -3609,6 +4025,21 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.50.0"
|
||||
|
|
@ -3620,9 +4051,31 @@ dependencies = [
|
|||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
|
|
@ -3904,6 +4357,12 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
|
|
@ -4150,6 +4609,16 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_atoms"
|
||||
version = "0.2.3"
|
||||
|
|
@ -4206,6 +4675,15 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
|
|
@ -4391,6 +4869,17 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
|
|
@ -4436,6 +4925,15 @@ dependencies = [
|
|||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
|
|
@ -4924,6 +5422,12 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ tauri-build = { version = "2", features = [] }
|
|||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri-plugin-http = "2"
|
||||
|
||||
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
|
||||
tauri-plugin-barcode-scanner = "2"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,19 @@
|
|||
"description": "Default capabilities for the scanner app",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
"core:default",
|
||||
"barcode-scanner:allow-scan",
|
||||
"barcode-scanner:allow-cancel",
|
||||
"barcode-scanner:allow-check-permissions",
|
||||
"barcode-scanner:allow-request-permissions",
|
||||
"barcode-scanner:allow-open-app-settings",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "url": "http://192.168.0.166:8080/**" },
|
||||
{ "url": "http://192.168.0.*:8080/**" },
|
||||
{ "url": "https://api.effigenix.de" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
12
frontend/apps/scanner/src-tauri/gen/android/.editorconfig
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
19
frontend/apps/scanner/src-tauri/gen/android/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
6
frontend/apps/scanner/src-tauri/gen/android/app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/src/main/**/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("rust")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "de.effigenix.scanner"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "de.effigenix.scanner"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
rust {
|
||||
rootDirRel = "../../../"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
21
frontend/apps/scanner/src-tauri/gen/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,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.effigenix_scanner"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_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" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package de.effigenix.scanner
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
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="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
|
|
@ -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:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.effigenix_scanner" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Effigenix Scanner</string>
|
||||
<string name="main_activity_title">Effigenix Scanner</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.effigenix_scanner" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -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>
|
||||
22
frontend/apps/scanner/src-tauri/gen/android/build.gradle.kts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("clean").configure {
|
||||
delete("build")
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("pluginsForCoolKids") {
|
||||
id = "rust"
|
||||
implementationClass = "RustPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(gradleApi())
|
||||
implementation("com.android.tools.build:gradle:8.11.0")
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import java.io.File
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.logging.LogLevel
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
|
||||
open class BuildTask : DefaultTask() {
|
||||
@Input
|
||||
var rootDirRel: String? = null
|
||||
@Input
|
||||
var target: String? = null
|
||||
@Input
|
||||
var release: Boolean? = null
|
||||
|
||||
@TaskAction
|
||||
fun assemble() {
|
||||
val executable = """pnpm""";
|
||||
try {
|
||||
runTauriCli(executable)
|
||||
} catch (e: Exception) {
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
// Try different Windows-specific extensions
|
||||
val fallbacks = listOf(
|
||||
"$executable.exe",
|
||||
"$executable.cmd",
|
||||
"$executable.bat",
|
||||
)
|
||||
|
||||
var lastException: Exception = e
|
||||
for (fallback in fallbacks) {
|
||||
try {
|
||||
runTauriCli(fallback)
|
||||
return
|
||||
} catch (fallbackException: Exception) {
|
||||
lastException = fallbackException
|
||||
}
|
||||
}
|
||||
throw lastException
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runTauriCli(executable: String) {
|
||||
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||
val target = target ?: throw GradleException("target cannot be null")
|
||||
val release = release ?: throw GradleException("release cannot be null")
|
||||
val args = listOf("tauri", "android", "android-studio-script");
|
||||
|
||||
project.exec {
|
||||
workingDir(File(project.projectDir, rootDirRel))
|
||||
executable(executable)
|
||||
args(args)
|
||||
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||
args("-vv")
|
||||
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||
args("-v")
|
||||
}
|
||||
if (release) {
|
||||
args("--release")
|
||||
}
|
||||
args(listOf("--target", target))
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.get
|
||||
|
||||
const val TASK_GROUP = "rust"
|
||||
|
||||
open class Config {
|
||||
lateinit var rootDirRel: String
|
||||
}
|
||||
|
||||
open class RustPlugin : Plugin<Project> {
|
||||
private lateinit var config: Config
|
||||
|
||||
override fun apply(project: Project) = with(project) {
|
||||
config = extensions.create("rust", Config::class.java)
|
||||
|
||||
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||
|
||||
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||
|
||||
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
@Suppress("UnstableApiUsage")
|
||||
flavorDimensions.add("abi")
|
||||
productFlavors {
|
||||
create("universal") {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters += abiList
|
||||
}
|
||||
}
|
||||
defaultArchList.forEachIndexed { index, arch ->
|
||||
create(arch) {
|
||||
dimension = "abi"
|
||||
ndk {
|
||||
abiFilters.add(defaultAbiList[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
for (profile in listOf("debug", "release")) {
|
||||
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||
val buildTask = tasks.maybeCreate(
|
||||
"rustBuildUniversal$profileCapitalized",
|
||||
DefaultTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for all targets"
|
||||
}
|
||||
|
||||
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||
|
||||
for (targetPair in targetsList.withIndex()) {
|
||||
val targetName = targetPair.value
|
||||
val targetArch = archList[targetPair.index]
|
||||
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||
val targetBuildTask = project.tasks.maybeCreate(
|
||||
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||
BuildTask::class.java
|
||||
).apply {
|
||||
group = TASK_GROUP
|
||||
description = "Build dynamic library in $profile mode for $targetArch"
|
||||
rootDirRel = config.rootDirRel
|
||||
target = targetName
|
||||
release = profile == "release"
|
||||
}
|
||||
|
||||
buildTask.dependsOn(targetBuildTask)
|
||||
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||
targetBuildTask
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# 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=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# 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
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
# Use Nix-provided aapt2 instead of Maven-downloaded one (NixOS compatibility)
|
||||
android.aapt2FromMavenOverride=/nix/store/n6f18hhmk8c1mf9d2p4ks6ljw3nnzaqy-androidsdk/libexec/android-sdk/build-tools/35.0.0/aapt2
|
||||
6
frontend/apps/scanner/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#Tue May 10 19:22:52 CST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
185
frontend/apps/scanner/src-tauri/gen/android/gradlew
vendored
Executable file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# 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"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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
|
||||
;;
|
||||
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"
|
||||
which java >/dev/null 2>&1 || 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
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
frontend/apps/scanner/src-tauri/gen/android/gradlew.bat
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
@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
|
||||
|
||||
@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=.
|
||||
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%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
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%"=="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!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
include ':app'
|
||||
|
||||
apply from: 'tauri.settings.gradle'
|
||||
2526
frontend/apps/scanner/src-tauri/gen/schemas/android-schema.json
Normal file
|
|
@ -1 +1 @@
|
|||
{"default":{"identifier":"default","description":"Default capabilities for the scanner app","local":true,"windows":["main"],"permissions":["core:default"]}}
|
||||
{"default":{"identifier":"default","description":"Default capabilities for the scanner app","local":true,"windows":["main"],"permissions":["core:default","barcode-scanner:allow-scan","barcode-scanner:allow-cancel","barcode-scanner:allow-check-permissions","barcode-scanner:allow-request-permissions","barcode-scanner:allow-open-app-settings",{"identifier":"http:default","allow":[{"url":"http://192.168.0.166:8080/**"},{"url":"http://192.168.0.*:8080/**"},{"url":"https://api.effigenix.de"}]}]}}
|
||||
2526
frontend/apps/scanner/src-tauri/gen/schemas/mobile-schema.json
Normal file
|
|
@ -1,11 +1,24 @@
|
|||
use tauri::Manager;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let builder = tauri::Builder::default();
|
||||
let builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_http::init());
|
||||
|
||||
#[cfg(mobile)]
|
||||
let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
|
||||
|
||||
builder
|
||||
.setup(|app| {
|
||||
// Desktop: Zoom-Faktor erhöhen, damit die Mobile-UI
|
||||
// auf einem 1× Bildschirm nicht winzig aussieht.
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
let webview = app.get_webview_window("main").unwrap();
|
||||
let _ = webview.set_zoom(1.5);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@
|
|||
"windows": [
|
||||
{
|
||||
"title": "Effigenix Scanner",
|
||||
"width": 400,
|
||||
"height": 700,
|
||||
"resizable": true
|
||||
"width": 645,
|
||||
"height": 1305,
|
||||
"resizable": true,
|
||||
"zoomHotkeysEnabled": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,66 @@
|
|||
import { Button, Card } from '@effigenix/ui';
|
||||
import { NavigationProvider, useNavigation, type Page } from './navigation';
|
||||
import { FlowStateProvider } from './flow-state';
|
||||
import { ApiProvider } from './api';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { HomePage } from './pages/HomePage';
|
||||
import { ScannerPage } from './pages/ScannerPage';
|
||||
import { ManualEntryPage } from './pages/ManualEntryPage';
|
||||
import { BatchDetailPage } from './pages/BatchDetailPage';
|
||||
import { MoveFlowPage } from './pages/MoveFlowPage';
|
||||
import { MoveConfirmPage } from './pages/MoveConfirmPage';
|
||||
import { ConsumeFlowPage } from './pages/ConsumeFlowPage';
|
||||
import { ConsumeQtyPage } from './pages/ConsumeQtyPage';
|
||||
import { ConsumeConfirmPage } from './pages/ConsumeConfirmPage';
|
||||
import { BookFlowPage } from './pages/BookFlowPage';
|
||||
import { BookConfirmPage } from './pages/BookConfirmPage';
|
||||
import { TasksPage } from './pages/TasksPage';
|
||||
import { HistoryPage } from './pages/HistoryPage';
|
||||
|
||||
const pages: Record<Page, () => JSX.Element> = {
|
||||
login: () => <LoginPage />,
|
||||
home: () => <HomePage />,
|
||||
scan: () => <ScannerPage />,
|
||||
'manual-entry': () => <ManualEntryPage />,
|
||||
'batch-detail': () => <BatchDetailPage />,
|
||||
'move-flow': () => <MoveFlowPage />,
|
||||
'move-confirm': () => <MoveConfirmPage />,
|
||||
'consume-flow': () => <ConsumeFlowPage />,
|
||||
'consume-qty': () => <ConsumeQtyPage />,
|
||||
'consume-confirm': () => <ConsumeConfirmPage />,
|
||||
'book-flow': () => <BookFlowPage />,
|
||||
'book-confirm': () => <BookConfirmPage />,
|
||||
tasks: () => <TasksPage />,
|
||||
history: () => <HistoryPage />,
|
||||
};
|
||||
|
||||
function Router() {
|
||||
const { page } = useNavigation();
|
||||
const render = pages[page];
|
||||
return render ? render() : <LoginPage />;
|
||||
}
|
||||
|
||||
function StatusBar() {
|
||||
return (
|
||||
<div
|
||||
className="sticky top-0 z-50 bg-warm-50"
|
||||
style={{ height: 'env(safe-area-inset-top, 0px)' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<div className="min-h-screen p-6">
|
||||
<div className="mx-auto max-w-md space-y-6">
|
||||
<h1 className="text-2xl font-bold text-brand-700">Effigenix Scanner</h1>
|
||||
|
||||
<Card>
|
||||
<h2 className="mb-4 text-lg font-semibold">Chargen-Scanner</h2>
|
||||
<p className="mb-4 text-sm text-warm-600">
|
||||
QR-Code oder Barcode scannen, um Chargen-Details anzuzeigen.
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
// TODO: Barcode-Scanner Integration
|
||||
console.log('Scan gestartet');
|
||||
}}
|
||||
>
|
||||
Scan starten
|
||||
</Button>
|
||||
</Card>
|
||||
<ApiProvider>
|
||||
<NavigationProvider>
|
||||
<FlowStateProvider>
|
||||
<div className="h-dvh flex flex-col bg-warm-50">
|
||||
<StatusBar />
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<Router />
|
||||
</div>
|
||||
</div>
|
||||
</FlowStateProvider>
|
||||
</NavigationProvider>
|
||||
</ApiProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
98
frontend/apps/scanner/src/api.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { createContext, useContext, useMemo, useState, useCallback, type ReactNode } from 'react';
|
||||
import { createEffigenixClient, type EffigenixClient, type TokenProvider } from '@effigenix/api-client';
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
|
||||
/**
|
||||
* Axios adapter that delegates to Tauri's HTTP plugin.
|
||||
* Bypasses CORS restrictions on mobile webviews.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const tauriAdapter = async (config: any) => {
|
||||
const url = new URL(config.url || '', config.baseURL);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (config.headers) {
|
||||
const raw = typeof config.headers.toJSON === 'function' ? config.headers.toJSON(false) : config.headers;
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
if (value != null) headers[key] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await tauriFetch(url.toString(), {
|
||||
method: (config.method || 'GET').toUpperCase(),
|
||||
headers,
|
||||
body: config.data != null
|
||||
? (typeof config.data === 'string' ? config.data : JSON.stringify(config.data))
|
||||
: undefined,
|
||||
});
|
||||
|
||||
let data: unknown;
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
config,
|
||||
};
|
||||
};
|
||||
|
||||
interface ApiState {
|
||||
client: EffigenixClient;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
const ApiContext = createContext<ApiState | null>(null);
|
||||
|
||||
export function ApiProvider({ children }: { children: ReactNode }) {
|
||||
const [tokens, setTokens] = useState<{ access: string; refresh: string } | null>(null);
|
||||
|
||||
const tokenProvider = useMemo<TokenProvider>(() => ({
|
||||
getAccessToken: async () => tokens?.access ?? null,
|
||||
getRefreshToken: async () => tokens?.refresh ?? null,
|
||||
saveTokens: async (accessToken, refreshToken) => {
|
||||
setTokens({ access: accessToken, refresh: refreshToken });
|
||||
},
|
||||
clearTokens: async () => setTokens(null),
|
||||
}), [tokens]);
|
||||
|
||||
const client = useMemo(() => createEffigenixClient(tokenProvider, {
|
||||
baseUrl: API_BASE,
|
||||
adapter: tauriAdapter,
|
||||
}), [tokenProvider]);
|
||||
|
||||
const login = useCallback(async (username: string, password: string) => {
|
||||
const res = await tauriFetch(`${API_BASE}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`${res.status}: ${text}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
setTokens({ access: data.accessToken, refresh: data.refreshToken });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ApiContext.Provider value={{ client, login, isAuthenticated: tokens !== null }}>
|
||||
{children}
|
||||
</ApiContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useApi() {
|
||||
const ctx = useContext(ApiContext);
|
||||
if (!ctx) throw new Error('useApi must be used within ApiProvider');
|
||||
return ctx;
|
||||
}
|
||||
68
frontend/apps/scanner/src/components/BottomNav.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import {
|
||||
Home,
|
||||
ClipboardList,
|
||||
ScanLine,
|
||||
History,
|
||||
Settings,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@effigenix/ui';
|
||||
import { useNavigation, type Page } from '../navigation';
|
||||
|
||||
interface NavItem {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
page: Page;
|
||||
}
|
||||
|
||||
const items: NavItem[] = [
|
||||
{ icon: Home, label: 'Start', page: 'home' },
|
||||
{ icon: ClipboardList, label: 'Aufgaben', page: 'tasks' },
|
||||
// scan button goes here (rendered separately)
|
||||
{ icon: History, label: 'Verlauf', page: 'history' },
|
||||
{ icon: Settings, label: 'Mehr', page: 'home' },
|
||||
];
|
||||
|
||||
export function BottomNav() {
|
||||
const { page, navigate, setFlowType } = useNavigation();
|
||||
|
||||
function handleScan() {
|
||||
setFlowType('info');
|
||||
navigate('scan');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sticky bottom-0 bg-white border-t border-warm-200 px-4 pb-[env(safe-area-inset-bottom)] pt-1 flex items-end justify-around">
|
||||
{items.slice(0, 2).map((item) => (
|
||||
<NavTab key={item.page} item={item} active={page === item.page} onPress={() => navigate(item.page)} />
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={handleScan}
|
||||
className="w-14 h-14 rounded-full bg-gradient-to-br from-brand-500 to-brand-700 text-white flex items-center justify-center shadow-brand-lg -mt-5 border-3 border-warm-50 active:scale-[0.92] transition-transform"
|
||||
>
|
||||
<ScanLine className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{items.slice(2).map((item) => (
|
||||
<NavTab key={item.label} item={item} active={page === item.page} onPress={() => navigate(item.page)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavTab({ item, active, onPress }: { item: NavItem; active: boolean; onPress: () => void }) {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<button
|
||||
onClick={onPress}
|
||||
className={cn(
|
||||
'flex-1 flex flex-col items-center gap-0.5 py-1.5 text-2xs font-medium transition-colors',
|
||||
active ? 'text-brand-600' : 'text-warm-400'
|
||||
)}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
15
frontend/apps/scanner/src/components/PageActions.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import { cn } from '@effigenix/ui';
|
||||
|
||||
interface PageActionsProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageActions({ children, className }: PageActionsProps) {
|
||||
return (
|
||||
<div className={cn('px-5 py-8 space-y-2.5 shrink-0', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
frontend/apps/scanner/src/components/PageHeader.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
subtitle?: ReactNode;
|
||||
right?: ReactNode;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, subtitle, right, onBack }: PageHeaderProps) {
|
||||
const { goBack } = useNavigation();
|
||||
|
||||
return (
|
||||
<div className="px-5 pt-6 pb-3 flex items-center gap-3">
|
||||
<button
|
||||
onClick={onBack ?? goBack}
|
||||
className="w-9 h-9 rounded-full bg-warm-100 flex items-center justify-center shrink-0 active:bg-warm-200 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-4.5 h-4.5 text-warm-600" />
|
||||
</button>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-lg font-bold text-warm-800">{title}</h1>
|
||||
{subtitle && <p className="text-xs text-warm-500">{subtitle}</p>}
|
||||
</div>
|
||||
{right}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
frontend/apps/scanner/src/components/StepIndicator.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { Check } from 'lucide-react';
|
||||
import { cn } from '@effigenix/ui';
|
||||
|
||||
export interface Step {
|
||||
label: string;
|
||||
state: 'done' | 'active' | 'pending';
|
||||
}
|
||||
|
||||
interface StepIndicatorProps {
|
||||
steps: Step[];
|
||||
}
|
||||
|
||||
export function StepIndicator({ steps }: StepIndicatorProps) {
|
||||
return (
|
||||
<div className="px-8 mb-2">
|
||||
<div className="flex items-center gap-0 mb-1">
|
||||
{steps.map((step, i) => (
|
||||
<div key={i} className="contents">
|
||||
<div
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all',
|
||||
step.state === 'done' && 'bg-success-100 text-success-700',
|
||||
step.state === 'active' && 'bg-brand-600 text-white shadow-brand',
|
||||
step.state === 'pending' && 'bg-warm-200 text-warm-500'
|
||||
)}
|
||||
>
|
||||
{step.state === 'done' ? <Check className="w-3.5 h-3.5" /> : i + 1}
|
||||
</div>
|
||||
{i < steps.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 h-0.5 transition-colors',
|
||||
step.state === 'done' ? 'bg-success-300' : 'bg-warm-200'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between text-2xs font-semibold uppercase px-0.5">
|
||||
{steps.map((step, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={cn(
|
||||
step.state === 'done' && 'text-success-600',
|
||||
step.state === 'active' && 'text-brand-600',
|
||||
step.state === 'pending' && 'text-warm-500'
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
frontend/apps/scanner/src/components/SuccessScreen.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { CheckCircle2 } from 'lucide-react';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
interface SuccessScreenProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children?: ReactNode;
|
||||
actions: ReactNode;
|
||||
}
|
||||
|
||||
export function SuccessScreen({ title, subtitle, children, actions }: SuccessScreenProps) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center px-6 animate-fade-in">
|
||||
<div className="text-center">
|
||||
<div className="w-20 h-20 rounded-full bg-success-100 flex items-center justify-center mx-auto mb-5">
|
||||
<CheckCircle2 className="w-10 h-10 text-success-500" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-warm-800 mb-1">{title}</h1>
|
||||
<p className="text-warm-500 text-sm mb-6">{subtitle}</p>
|
||||
{children && <div className="mb-6">{children}</div>}
|
||||
<div className="space-y-2.5 max-w-xs mx-auto">{actions}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
frontend/apps/scanner/src/components/SummaryCard.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { Card } from '@effigenix/ui';
|
||||
|
||||
interface SummaryRow {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
highlight?: 'success' | 'danger';
|
||||
}
|
||||
|
||||
interface SummaryCardProps {
|
||||
rows: SummaryRow[];
|
||||
}
|
||||
|
||||
export function SummaryCard({ rows }: SummaryCardProps) {
|
||||
return (
|
||||
<Card className="p-4 text-left mx-auto max-w-xs">
|
||||
<div className="space-y-2 text-sm">
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="flex justify-between">
|
||||
<span className="text-warm-500">{row.label}</span>
|
||||
<span
|
||||
className={[
|
||||
row.mono && 'font-mono',
|
||||
row.highlight === 'success' && 'font-semibold text-success-600',
|
||||
row.highlight === 'danger' && 'font-semibold text-danger-600',
|
||||
!row.highlight && 'font-medium',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
76
frontend/apps/scanner/src/flow-state.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { createContext, useContext, useCallback, useState, type ReactNode } from 'react';
|
||||
|
||||
export interface ConsumeItem {
|
||||
name: string;
|
||||
batch: string;
|
||||
location: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
required: number | null;
|
||||
requiredUnit: string | null;
|
||||
}
|
||||
|
||||
export interface ScaledIngredient {
|
||||
articleId: string;
|
||||
quantity: number;
|
||||
uom: string;
|
||||
}
|
||||
|
||||
export interface ProductionOrderContext {
|
||||
orderId: string;
|
||||
recipeId: string;
|
||||
batchNumber: string | null;
|
||||
articleName: string;
|
||||
plannedQuantity: number;
|
||||
plannedQuantityUnit: string;
|
||||
}
|
||||
|
||||
interface FlowState {
|
||||
consumeItems: ConsumeItem[];
|
||||
consumeScanIndex: number;
|
||||
scaledIngredients: ScaledIngredient[];
|
||||
productionOrder: ProductionOrderContext | null;
|
||||
addConsumeItem: (item: ConsumeItem) => void;
|
||||
nextConsumeScan: () => void;
|
||||
resetConsume: () => void;
|
||||
setScaledIngredients: (ingredients: ScaledIngredient[]) => void;
|
||||
setProductionOrder: (order: ProductionOrderContext) => void;
|
||||
}
|
||||
|
||||
const FlowStateContext = createContext<FlowState | null>(null);
|
||||
|
||||
export function FlowStateProvider({ children }: { children: ReactNode }) {
|
||||
const [consumeItems, setConsumeItems] = useState<ConsumeItem[]>([]);
|
||||
const [consumeScanIndex, setConsumeScanIndex] = useState(0);
|
||||
const [scaledIngredients, setScaledIngredients] = useState<ScaledIngredient[]>([]);
|
||||
const [productionOrder, setProductionOrder] = useState<ProductionOrderContext | null>(null);
|
||||
|
||||
const addConsumeItem = useCallback((item: ConsumeItem) => {
|
||||
setConsumeItems((prev) => [...prev, item]);
|
||||
}, []);
|
||||
|
||||
const nextConsumeScan = useCallback(() => {
|
||||
setConsumeScanIndex((i) => i + 1);
|
||||
}, []);
|
||||
|
||||
const resetConsume = useCallback(() => {
|
||||
setConsumeItems([]);
|
||||
setConsumeScanIndex(0);
|
||||
setScaledIngredients([]);
|
||||
setProductionOrder(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<FlowStateContext.Provider
|
||||
value={{ consumeItems, consumeScanIndex, scaledIngredients, productionOrder, addConsumeItem, nextConsumeScan, resetConsume, setScaledIngredients, setProductionOrder }}
|
||||
>
|
||||
{children}
|
||||
</FlowStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useFlowState() {
|
||||
const ctx = useContext(FlowStateContext);
|
||||
if (!ctx) throw new Error('useFlowState must be used within FlowStateProvider');
|
||||
return ctx;
|
||||
}
|
||||
|
|
@ -1,4 +1,79 @@
|
|||
@import 'tailwindcss';
|
||||
@import '@effigenix/ui/theme.css';
|
||||
|
||||
@source '../../packages/ui/src/**/*.{ts,tsx}';
|
||||
@source '../../../packages/ui/src/**/*.{ts,tsx}';
|
||||
|
||||
/*
|
||||
* Mobile Typography Scale
|
||||
*
|
||||
* Überschreibt die Tailwind-Defaults für Touch-optimierte Lesbarkeit.
|
||||
* Gilt für die gesamte Scanner-App inkl. shared UI-Komponenten.
|
||||
*
|
||||
* | Klasse | Default | Mobile | Verwendung |
|
||||
* |-----------|---------|--------|-----------------------------------|
|
||||
* | text-2xs | – | 11px | Timestamps, Mikro-Labels, % |
|
||||
* | text-xs | 12px | 13px | Captions, Subtitles |
|
||||
* | text-sm | 14px | 15px | Body, Listentext, Badges |
|
||||
* | text-base | 16px | 16px | Card-Titel, Inputs |
|
||||
* | text-lg | 18px | 20px | Seitenüberschriften |
|
||||
* | text-xl | 20px | 24px | Große Überschriften |
|
||||
* | text-2xl | 24px | 28px | Hero / Login-Titel |
|
||||
*/
|
||||
@theme {
|
||||
--text-2xs: 0.6875rem;
|
||||
--text-2xs--line-height: 1rem;
|
||||
|
||||
--text-xs: 0.8125rem;
|
||||
--text-xs--line-height: 1.125rem;
|
||||
|
||||
--text-sm: 0.9375rem;
|
||||
--text-sm--line-height: 1.375rem;
|
||||
|
||||
--text-base: 1rem;
|
||||
--text-base--line-height: 1.5rem;
|
||||
|
||||
--text-lg: 1.25rem;
|
||||
--text-lg--line-height: 1.625rem;
|
||||
|
||||
--text-xl: 1.5rem;
|
||||
--text-xl--line-height: 1.75rem;
|
||||
|
||||
--text-2xl: 1.75rem;
|
||||
--text-2xl--line-height: 2rem;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes scan-line {
|
||||
0% { top: 10%; }
|
||||
50% { top: 85%; }
|
||||
100% { top: 10%; }
|
||||
}
|
||||
|
||||
@utility animate-fade-in {
|
||||
animation: fade-in 0.2s ease;
|
||||
}
|
||||
|
||||
@utility animate-scan-line {
|
||||
animation: scan-line 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Mobile defaults */
|
||||
html, body {
|
||||
background-color: #faf9f7;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
overscroll-behavior: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
|
|
|||
111
frontend/apps/scanner/src/mock-data.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
export interface MockBatch {
|
||||
id: string;
|
||||
article: string;
|
||||
articleCode: string;
|
||||
category: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
location: string;
|
||||
bestBefore: string;
|
||||
status: 'planned' | 'in-production' | 'completed';
|
||||
}
|
||||
|
||||
export interface MockActivity {
|
||||
type: 'move' | 'consume' | 'production-start' | 'production-complete' | 'goods-receipt';
|
||||
batchId: string;
|
||||
description: string;
|
||||
detail: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface MockMaterial {
|
||||
name: string;
|
||||
batch: string;
|
||||
location: string;
|
||||
available: number;
|
||||
unit: string;
|
||||
recipeQty: number;
|
||||
}
|
||||
|
||||
export interface MockLocation {
|
||||
name: string;
|
||||
temperature: string;
|
||||
icon: 'cold' | 'frozen' | 'scan';
|
||||
}
|
||||
|
||||
export const BATCH: MockBatch = {
|
||||
id: 'P-2026-03-19-003',
|
||||
article: 'Wiener Würstchen',
|
||||
articleCode: 'ART-001',
|
||||
category: 'Wurstwaren',
|
||||
quantity: 118,
|
||||
unit: 'kg',
|
||||
location: 'Kühlhaus 1',
|
||||
bestBefore: '29.03.2026',
|
||||
status: 'completed',
|
||||
};
|
||||
|
||||
export const ACTIVITIES: MockActivity[] = [
|
||||
{
|
||||
type: 'move',
|
||||
batchId: 'P-003',
|
||||
description: 'P-2026-03-19-003 → Kühlhaus 1',
|
||||
detail: 'Umlagerung · 118 kg · vor 23 min',
|
||||
time: '11:05',
|
||||
},
|
||||
{
|
||||
type: 'consume',
|
||||
batchId: 'P-003',
|
||||
description: '54 kg Schweinefleisch S1 → P-003',
|
||||
detail: 'Verbrauch · vor 1 Std',
|
||||
time: '10:00',
|
||||
},
|
||||
{
|
||||
type: 'goods-receipt',
|
||||
batchId: 'WE-001',
|
||||
description: 'WE-2026-03-18-001 → Kühlhaus 1',
|
||||
detail: 'Wareneingang · 500 kg · vor 3 Std',
|
||||
time: '08:30',
|
||||
},
|
||||
];
|
||||
|
||||
export const MATERIALS: MockMaterial[] = [
|
||||
{ name: 'Schweinefleisch S1', batch: 'WE-2026-03-18-001', location: 'Kühlhaus 1', available: 145, unit: 'kg', recipeQty: 32 },
|
||||
{ name: 'Schweinefett (Speck)', batch: 'WE-2026-03-17-003', location: 'Kühlhaus 1', available: 80, unit: 'kg', recipeQty: 20 },
|
||||
{ name: 'Leberkäse-Gewürz', batch: 'WE-2026-03-10-014', location: 'Trockenlager', available: 8, unit: 'kg', recipeQty: 1.4 },
|
||||
];
|
||||
|
||||
export const LOCATIONS: MockLocation[] = [
|
||||
{ name: 'Kühlhaus 1', temperature: '2–4 °C', icon: 'cold' },
|
||||
{ name: 'Kühlhaus 2', temperature: '0–2 °C', icon: 'cold' },
|
||||
{ name: 'Tiefkühler A', temperature: '-18 bis -22 °C', icon: 'frozen' },
|
||||
];
|
||||
|
||||
export const HISTORY_ENTRIES: Array<{ time: string; entries: MockActivity[] }> = [
|
||||
{
|
||||
time: '11:05',
|
||||
entries: [
|
||||
{ type: 'move', batchId: 'P-003', description: 'Umlagerung', detail: 'P-003 · 118 kg → KH 2', time: '11:05' },
|
||||
],
|
||||
},
|
||||
{
|
||||
time: '10:45',
|
||||
entries: [
|
||||
{ type: 'production-complete', batchId: 'P-003', description: 'Produktion abgeschlossen', detail: 'P-003 · Wiener · 118 kg', time: '10:45' },
|
||||
],
|
||||
},
|
||||
{
|
||||
time: '09:15',
|
||||
entries: [
|
||||
{ type: 'consume', batchId: 'P-003', description: 'Verbrauch', detail: '54 kg Schweinefleisch → P-003', time: '09:15' },
|
||||
{ type: 'consume', batchId: 'P-003', description: 'Verbrauch', detail: '24 kg Speck → P-003', time: '09:15' },
|
||||
{ type: 'consume', batchId: 'P-003', description: 'Verbrauch', detail: '2.2 kg Nitritpökelsalz → P-003', time: '09:15' },
|
||||
],
|
||||
},
|
||||
{
|
||||
time: '07:30',
|
||||
entries: [
|
||||
{ type: 'production-start', batchId: 'P-003', description: 'Produktion gestartet', detail: 'P-003 · Wiener Würstchen', time: '07:30' },
|
||||
],
|
||||
},
|
||||
];
|
||||
77
frontend/apps/scanner/src/navigation.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { createContext, useContext, useCallback, useState, type ReactNode } from 'react';
|
||||
|
||||
export type Page =
|
||||
| 'login'
|
||||
| 'home'
|
||||
| 'scan'
|
||||
| 'manual-entry'
|
||||
| 'batch-detail'
|
||||
| 'move-flow'
|
||||
| 'move-confirm'
|
||||
| 'consume-flow'
|
||||
| 'consume-qty'
|
||||
| 'consume-confirm'
|
||||
| 'book-flow'
|
||||
| 'book-confirm'
|
||||
| 'tasks'
|
||||
| 'history';
|
||||
|
||||
export type FlowType = 'move' | 'consume' | 'book' | 'info';
|
||||
|
||||
interface NavEntry {
|
||||
page: Page;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface NavigationState {
|
||||
page: Page;
|
||||
params: Record<string, unknown>;
|
||||
flowType: FlowType;
|
||||
navigate: (page: Page, params?: Record<string, unknown>) => void;
|
||||
goBack: () => void;
|
||||
setFlowType: (flow: FlowType) => void;
|
||||
resetTo: (page: Page) => void;
|
||||
}
|
||||
|
||||
const NavigationContext = createContext<NavigationState | null>(null);
|
||||
|
||||
export function NavigationProvider({ children }: { children: ReactNode }) {
|
||||
const [history, setHistory] = useState<NavEntry[]>([{ page: 'login', params: {} }]);
|
||||
const [flowType, setFlowType] = useState<FlowType>('info');
|
||||
|
||||
const current = history[history.length - 1];
|
||||
|
||||
const navigate = useCallback((page: Page, params: Record<string, unknown> = {}) => {
|
||||
setHistory((h) => [...h, { page, params }]);
|
||||
}, []);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
setHistory((h) => (h.length > 1 ? h.slice(0, -1) : h));
|
||||
}, []);
|
||||
|
||||
const resetTo = useCallback((page: Page) => {
|
||||
setHistory([{ page, params: {} }]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider
|
||||
value={{
|
||||
page: current.page,
|
||||
params: current.params,
|
||||
flowType,
|
||||
navigate,
|
||||
goBack,
|
||||
setFlowType,
|
||||
resetTo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useNavigation() {
|
||||
const ctx = useContext(NavigationContext);
|
||||
if (!ctx) throw new Error('useNavigation must be used within NavigationProvider');
|
||||
return ctx;
|
||||
}
|
||||
117
frontend/apps/scanner/src/pages/BatchDetailPage.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { Beef, Check, FlaskConical, Calendar, ArrowRightLeft, PackageMinus } from 'lucide-react';
|
||||
import { Card, Badge, Button } from '@effigenix/ui';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { BATCH } from '../mock-data';
|
||||
|
||||
export function BatchDetailPage() {
|
||||
const { navigate, setFlowType } = useNavigation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col animate-fade-in">
|
||||
<PageHeader
|
||||
title="Chargendetail"
|
||||
subtitle={BATCH.id}
|
||||
right={<Badge variant="success">Abgeschlossen</Badge>}
|
||||
/>
|
||||
|
||||
<div className="px-5 space-y-4 flex-1 pb-4">
|
||||
{/* Product Info */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-11 h-11 rounded-xl bg-brand-50 flex items-center justify-center">
|
||||
<Beef className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-warm-800">{BATCH.article}</div>
|
||||
<div className="text-xs text-warm-500">
|
||||
{BATCH.articleCode} · {BATCH.category}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<StatBox label="Menge" value={`${BATCH.quantity} ${BATCH.unit}`} />
|
||||
<StatBox label="MHD" value="29.03." />
|
||||
<StatBox label="Lagerort" value="KH 1" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Status Timeline */}
|
||||
<Card className="p-4">
|
||||
<h3 className="text-xs font-bold text-warm-500 uppercase tracking-wider mb-3">Status</h3>
|
||||
<div className="space-y-3">
|
||||
<TimelineEntry icon={Check} color="success" title="Produktion abgeschlossen" detail="19.03. 10:32 · Lisa Meier" />
|
||||
<TimelineEntry icon={Check} color="success" title="Eingelagert: Kühlhaus 1" detail="19.03. 10:45 · Stefan Koch" />
|
||||
<TimelineEntry icon={FlaskConical} color="info" title="Produktion gestartet" detail="19.03. 07:30 · Lisa Meier" />
|
||||
<TimelineEntry icon={Calendar} color="warm" title="Geplant" detail="18.03. 14:00 · Max Huber" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="space-y-2.5">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setFlowType('move');
|
||||
navigate('move-flow');
|
||||
}}
|
||||
>
|
||||
<ArrowRightLeft className="w-4 h-4 text-info-600" />
|
||||
Umlagern
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setFlowType('consume');
|
||||
navigate('consume-flow');
|
||||
}}
|
||||
>
|
||||
<PackageMinus className="w-4 h-4 text-warning-600" />
|
||||
Als Verbrauch buchen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatBox({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="text-center p-2 bg-warm-50 rounded-lg">
|
||||
<div className="text-2xs text-warm-500 font-semibold uppercase">{label}</div>
|
||||
<div className="text-sm font-bold text-warm-800 mt-0.5">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const colorClasses = {
|
||||
success: { bg: 'bg-success-100', text: 'text-success-600' },
|
||||
info: { bg: 'bg-info-100', text: 'text-info-600' },
|
||||
warm: { bg: 'bg-warm-100', text: 'text-warm-500' },
|
||||
};
|
||||
|
||||
function TimelineEntry({
|
||||
icon: Icon,
|
||||
color,
|
||||
title,
|
||||
detail,
|
||||
}: {
|
||||
icon: typeof Check;
|
||||
color: keyof typeof colorClasses;
|
||||
title: string;
|
||||
detail: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-7 h-7 rounded-full ${colorClasses[color].bg} flex items-center justify-center`}>
|
||||
<Icon className={`w-3.5 h-3.5 ${colorClasses[color].text}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-warm-800">{title}</div>
|
||||
<div className="text-xs text-warm-500">{detail}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
frontend/apps/scanner/src/pages/BookConfirmPage.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { ScanLine, Home } from 'lucide-react';
|
||||
import { Button } from '@effigenix/ui';
|
||||
import { SuccessScreen } from '../components/SuccessScreen';
|
||||
import { SummaryCard } from '../components/SummaryCard';
|
||||
import { useNavigation } from '../navigation';
|
||||
|
||||
export function BookConfirmPage() {
|
||||
const { navigate, setFlowType } = useNavigation();
|
||||
|
||||
return (
|
||||
<SuccessScreen
|
||||
title="Charge abgeschlossen"
|
||||
subtitle="P-2026-03-19-004 → Kühlhaus 1"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setFlowType('book');
|
||||
navigate('scan');
|
||||
}}
|
||||
>
|
||||
<ScanLine className="w-4 h-4" />
|
||||
Nächste Charge
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full" onClick={() => navigate('home')}>
|
||||
<Home className="w-4 h-4" />
|
||||
Zurück zum Start
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SummaryCard
|
||||
rows={[
|
||||
{ label: 'Charge', value: 'P-2026-03-19-004', mono: true },
|
||||
{ label: 'Artikel', value: 'Leberkäse Bayerisch' },
|
||||
{ label: 'Ist-Menge', value: '78 kg', mono: true },
|
||||
{ label: 'Ausschuss', value: '2.1 kg', mono: true },
|
||||
{ label: 'Ausbeute', value: '97.5%', highlight: 'success' },
|
||||
{ label: 'Eingelagert', value: 'Kühlhaus 1' },
|
||||
{ label: 'MHD', value: '26.03.2026', mono: true },
|
||||
]}
|
||||
/>
|
||||
</SuccessScreen>
|
||||
);
|
||||
}
|
||||
102
frontend/apps/scanner/src/pages/BookFlowPage.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { FlaskConical, PackageCheck } from 'lucide-react';
|
||||
import { Card, Badge, Button, Input } from '@effigenix/ui';
|
||||
import { PageActions } from '../components/PageActions';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { useFlowState } from '../flow-state';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function BookFlowPage() {
|
||||
const { navigate, goBack } = useNavigation();
|
||||
const { productionOrder } = useFlowState();
|
||||
const [actualQty, setActualQty] = useState('78');
|
||||
const [waste, setWaste] = useState('2.1');
|
||||
|
||||
const plannedQty = productionOrder?.plannedQuantity ?? 0;
|
||||
const plannedUnit = productionOrder?.plannedQuantityUnit ?? 'kg';
|
||||
const target = parseFloat(actualQty) || 0;
|
||||
const yieldPct = plannedQty > 0 ? ((target / plannedQty) * 100).toFixed(1) : '0';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col animate-fade-in">
|
||||
<PageHeader title="Charge abschließen" />
|
||||
|
||||
<div className="px-5 space-y-4 flex-1">
|
||||
{/* Batch card */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-11 h-11 rounded-xl bg-brand-50 flex items-center justify-center">
|
||||
<FlaskConical className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-warm-800">{productionOrder?.batchNumber ?? '–'}</div>
|
||||
<div className="text-xs text-warm-500">
|
||||
{productionOrder?.articleName ?? '–'} · Soll: {plannedQty} {plannedUnit}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="info">In Produktion</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-warm-500">3 Verbräuche erfasst</div>
|
||||
</Card>
|
||||
|
||||
{/* Actual quantity */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1.5">Ist-Menge ({plannedUnit}) *</label>
|
||||
<Input
|
||||
type="number"
|
||||
className="font-mono text-lg text-center"
|
||||
value={actualQty}
|
||||
onChange={(e) => setActualQty(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center justify-between mt-1.5 text-xs text-warm-500">
|
||||
<span>Soll: {plannedQty} {plannedUnit}</span>
|
||||
<span>
|
||||
Ausbeute: <span className="font-semibold text-success-600">{yieldPct}%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Waste */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1.5">Ausschuss (kg)</label>
|
||||
<Input
|
||||
type="number"
|
||||
className="font-mono text-lg text-center"
|
||||
value={waste}
|
||||
onChange={(e) => setWaste(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Target location */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1.5">Ziel-Lagerort</label>
|
||||
<select className="w-full rounded-xl border border-warm-300 bg-white px-3 py-2 text-sm text-warm-900 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 appearance-none bg-[url('data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2216%22%20height=%2216%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%236d6459%22%20stroke-width=%222%22%3E%3Cpath%20d=%22m6%209%206%206%206-6%22/%3E%3C/svg%3E')] bg-no-repeat bg-[position:right_0.75rem_center] pr-10">
|
||||
<option>Kühlhaus 1 (2–4 °C)</option>
|
||||
<option>Kühlhaus 2 (0–2 °C)</option>
|
||||
<option>Regal Ladentheke</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1.5">Bemerkung</label>
|
||||
<textarea
|
||||
className="w-full rounded-xl border border-warm-300 bg-white px-3 py-2 text-sm text-warm-900 placeholder:text-warm-400 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
|
||||
rows={2}
|
||||
placeholder="Optional…"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PageActions>
|
||||
<Button variant="success" size="lg" className="w-full" onClick={() => navigate('book-confirm')}>
|
||||
<PackageCheck className="w-5 h-5" />
|
||||
Charge abschließen & einlagern
|
||||
</Button>
|
||||
<Button variant="ghost" className="w-full text-warm-500" onClick={goBack}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
</PageActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
frontend/apps/scanner/src/pages/ConsumeConfirmPage.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { ScanLine, PackageCheck, Home } from 'lucide-react';
|
||||
import { Button } from '@effigenix/ui';
|
||||
import { SuccessScreen } from '../components/SuccessScreen';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { useFlowState } from '../flow-state';
|
||||
|
||||
export function ConsumeConfirmPage() {
|
||||
const { navigate, setFlowType } = useNavigation();
|
||||
const { consumeItems, resetConsume, productionOrder } = useFlowState();
|
||||
const count = consumeItems.length;
|
||||
|
||||
return (
|
||||
<SuccessScreen
|
||||
title="Verbrauch gebucht"
|
||||
subtitle={`${count} Materialien → ${productionOrder?.batchNumber ?? '–'}`}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
resetConsume();
|
||||
setFlowType('consume');
|
||||
navigate('scan');
|
||||
}}
|
||||
>
|
||||
<ScanLine className="w-4 h-4" />
|
||||
Weitere Materialien scannen
|
||||
</Button>
|
||||
<Button variant="success" className="w-full" onClick={() => navigate('book-flow')}>
|
||||
<PackageCheck className="w-4 h-4" />
|
||||
Charge abschließen
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
resetConsume();
|
||||
navigate('home');
|
||||
}}
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
Zurück zum Start
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
119
frontend/apps/scanner/src/pages/ConsumeFlowPage.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { FlaskConical, ScanLine, Check, PackageSearch } from 'lucide-react';
|
||||
import { Card, Button } from '@effigenix/ui';
|
||||
import { PageActions } from '../components/PageActions';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { StepIndicator } from '../components/StepIndicator';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { useFlowState } from '../flow-state';
|
||||
|
||||
export function ConsumeFlowPage() {
|
||||
const { navigate, setFlowType } = useNavigation();
|
||||
const { consumeItems, productionOrder } = useFlowState();
|
||||
const hasItems = consumeItems.length > 0;
|
||||
|
||||
if (!productionOrder) {
|
||||
navigate('tasks');
|
||||
return null;
|
||||
}
|
||||
|
||||
function startScan() {
|
||||
setFlowType('consume');
|
||||
navigate('scan');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col animate-fade-in">
|
||||
<PageHeader
|
||||
title="Verbrauch erfassen"
|
||||
subtitle={
|
||||
<span>
|
||||
Produktionscharge:{' '}
|
||||
<span className="font-mono font-medium text-brand-600">{productionOrder.batchNumber ?? '–'}</span>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
<StepIndicator
|
||||
steps={[
|
||||
{ label: 'Prod.-Charge', state: 'done' },
|
||||
{ label: 'Materialien', state: hasItems ? 'done' : 'active' },
|
||||
{ label: 'Bestätigen', state: hasItems ? 'active' : 'pending' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Production batch */}
|
||||
<div className="px-5 mb-2">
|
||||
<Card className="p-2.5 flex items-center gap-3 border-brand-200 bg-brand-50">
|
||||
<div className="w-8 h-8 rounded-lg bg-brand-100 flex items-center justify-center">
|
||||
<FlaskConical className="w-4 h-4 text-brand-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-warm-800">
|
||||
{productionOrder.batchNumber ?? '–'} · {productionOrder.articleName}
|
||||
</div>
|
||||
<div className="text-xs text-warm-500">
|
||||
Soll: {productionOrder.plannedQuantity} {productionOrder.plannedQuantityUnit} · In Produktion
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Scanned materials – scrollable area */}
|
||||
<div className="px-5 flex-1 min-h-0 overflow-y-auto space-y-2">
|
||||
<h2 className="text-xs font-bold text-warm-500 uppercase tracking-wider mb-2">
|
||||
Gescannte Materialien ({consumeItems.length})
|
||||
</h2>
|
||||
|
||||
{consumeItems.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{consumeItems.map((item, i) => (
|
||||
<Card key={i} className="p-2.5 flex items-center gap-3 animate-fade-in">
|
||||
<div className="w-2 h-10 rounded-full bg-brand-400 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-warm-800">{item.name}</div>
|
||||
<div className="text-xs text-warm-500">
|
||||
{item.batch} · {item.location}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div className="font-mono font-semibold text-sm text-warm-800">
|
||||
{item.quantity} {item.unit}
|
||||
</div>
|
||||
{item.required != null && (
|
||||
<div className="text-2xs text-warm-400">
|
||||
von {item.required} {item.requiredUnit ?? item.unit}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="p-5 text-center">
|
||||
<div className="inline-flex items-center justify-center w-11 h-11 rounded-xl bg-warm-100 mb-2">
|
||||
<PackageSearch className="w-5 h-5 text-warm-400" />
|
||||
</div>
|
||||
<p className="text-sm text-warm-500 mb-0.5">Noch keine Materialien erfasst</p>
|
||||
<p className="text-xs text-warm-400">
|
||||
Chargen-Etiketten der Eingangsmaterialien scannen.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
<Button variant="secondary" size='lg' className="w-full" onClick={startScan}>
|
||||
<ScanLine className="w-4 h-4" />
|
||||
{consumeItems.length === 0 ? 'Erste Zutat scannen' : 'Nächste Zutat scannen'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PageActions>
|
||||
{hasItems && (
|
||||
<Button variant="primary" size='lg' className="w-full" onClick={() => navigate('consume-confirm')}>
|
||||
<Check className="w-5 h-5" />
|
||||
{consumeItems.length} Verbrauch{consumeItems.length !== 1 ? 'e' : ''} buchen
|
||||
</Button>
|
||||
)}
|
||||
</PageActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
254
frontend/apps/scanner/src/pages/ConsumeQtyPage.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import { CheckCircle2, Check, X, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { Card, Button, Input } from '@effigenix/ui';
|
||||
import { PageActions } from '../components/PageActions';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { useFlowState, type ScaledIngredient } from '../flow-state';
|
||||
import { useApi } from '../api';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import type { StockDTO, ArticleDTO, StorageLocationDTO } from '@effigenix/types';
|
||||
|
||||
interface BatchLookup {
|
||||
stock: StockDTO;
|
||||
batch: StockDTO['batches'][number];
|
||||
article: ArticleDTO;
|
||||
location: StorageLocationDTO;
|
||||
scaledIngredient: ScaledIngredient | null;
|
||||
}
|
||||
|
||||
type LookupState =
|
||||
| { status: 'loading' }
|
||||
| { status: 'error'; message: string }
|
||||
| { status: 'ok'; data: BatchLookup };
|
||||
|
||||
export function ConsumeQtyPage() {
|
||||
const { navigate, params } = useNavigation();
|
||||
const { addConsumeItem, nextConsumeScan, scaledIngredients, setScaledIngredients, productionOrder } = useFlowState();
|
||||
const { client } = useApi();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [quantity, setQuantity] = useState('');
|
||||
const [lookup, setLookup] = useState<LookupState>({ status: 'loading' });
|
||||
|
||||
const batchId = params.batchId as string | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!batchId) {
|
||||
setLookup({ status: 'error', message: 'Keine Chargennummer übergeben' });
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Fetch stock + scaled ingredients in parallel
|
||||
const [stocks, ingredients] = await Promise.all([
|
||||
client.stocks.findByBatchId(batchId),
|
||||
scaledIngredients.length > 0
|
||||
? Promise.resolve(scaledIngredients)
|
||||
: loadScaledIngredients(),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
|
||||
if (stocks.length === 0) {
|
||||
setLookup({ status: 'error', message: `Charge "${batchId}" nicht im Bestand gefunden` });
|
||||
return;
|
||||
}
|
||||
|
||||
const stock = stocks[0];
|
||||
const batch = stock.batches.find((b) => b.batchId === batchId);
|
||||
if (!batch) {
|
||||
setLookup({ status: 'error', message: `Charge "${batchId}" nicht im Bestand gefunden` });
|
||||
return;
|
||||
}
|
||||
|
||||
const [article, location] = await Promise.all([
|
||||
client.articles.getById(stock.articleId),
|
||||
client.storageLocations.getById(stock.storageLocationId),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
|
||||
const scaledIngredient = ingredients.find((i) => i.articleId === stock.articleId) ?? null;
|
||||
setLookup({ status: 'ok', data: { stock, batch, article, location, scaledIngredient } });
|
||||
setTimeout(() => inputRef.current?.focus(), 300);
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
setLookup({ status: 'error', message: 'Fehler beim Laden der Chargendaten' });
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [batchId]);
|
||||
|
||||
async function loadScaledIngredients(): Promise<ScaledIngredient[]> {
|
||||
try {
|
||||
if (!productionOrder?.recipeId) return [];
|
||||
|
||||
const recipe = await client.recipes.getById(productionOrder.recipeId);
|
||||
const outputQty = parseFloat(recipe.outputQuantity) || 1;
|
||||
const factor = productionOrder.plannedQuantity / outputQty;
|
||||
|
||||
const result = recipe.ingredients.map((i) => ({
|
||||
articleId: i.articleId,
|
||||
quantity: Math.round(parseFloat(i.quantity) * factor * 100) / 100,
|
||||
uom: i.uom,
|
||||
}));
|
||||
setScaledIngredients(result);
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (lookup.status !== 'ok') return;
|
||||
const { batch, article, location, scaledIngredient } = lookup.data;
|
||||
const qty = parseFloat(quantity) || batch.quantityAmount || 0;
|
||||
addConsumeItem({
|
||||
name: article.name,
|
||||
batch: batch.batchId ?? '',
|
||||
location: location.name,
|
||||
quantity: qty,
|
||||
unit: batch.quantityUnit ?? 'kg',
|
||||
required: scaledIngredient?.quantity ?? null,
|
||||
requiredUnit: scaledIngredient?.uom ?? null,
|
||||
});
|
||||
nextConsumeScan();
|
||||
navigate('consume-flow');
|
||||
}
|
||||
|
||||
if (lookup.status === 'loading') {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center animate-fade-in">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 text-brand-500 mx-auto mb-3 animate-spin" />
|
||||
<p className="text-sm text-warm-500">Charge wird gesucht…</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (lookup.status === 'error') {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col animate-fade-in">
|
||||
<div className="px-5 pt-4 pb-2 flex items-center gap-3 shrink-0">
|
||||
<button
|
||||
onClick={() => navigate('consume-flow')}
|
||||
className="w-9 h-9 rounded-full bg-warm-100 flex items-center justify-center shrink-0 active:bg-warm-200"
|
||||
>
|
||||
<X className="w-4.5 h-4.5 text-warm-600" />
|
||||
</button>
|
||||
<h1 className="text-lg font-bold text-warm-800">Charge nicht gefunden</h1>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center px-5">
|
||||
<Card className="p-5 text-center w-full border-danger-200 bg-danger-50">
|
||||
<AlertCircle className="w-8 h-8 text-danger-500 mx-auto mb-2" />
|
||||
<p className="text-sm text-warm-700">{lookup.message}</p>
|
||||
{batchId && (
|
||||
<p className="text-xs text-warm-500 font-mono mt-1">{batchId}</p>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
<PageActions>
|
||||
<Button variant="secondary" className="w-full" onClick={() => navigate('consume-flow')}>
|
||||
Zurück
|
||||
</Button>
|
||||
</PageActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { batch, article, location, scaledIngredient } = lookup.data;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col animate-fade-in">
|
||||
<div className="px-5 pt-4 pb-2 flex items-center gap-3 shrink-0">
|
||||
<button
|
||||
onClick={() => navigate('consume-flow')}
|
||||
className="w-9 h-9 rounded-full bg-warm-100 flex items-center justify-center shrink-0 active:bg-warm-200"
|
||||
>
|
||||
<X className="w-4.5 h-4.5 text-warm-600" />
|
||||
</button>
|
||||
<h1 className="text-lg font-bold text-warm-800">Menge erfassen</h1>
|
||||
</div>
|
||||
|
||||
<div className="px-5 flex-1 min-h-0 flex flex-col">
|
||||
{/* Scanned material info */}
|
||||
<Card className="p-3 mb-4 border-success-200 bg-success-50 shrink-0">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<CheckCircle2 className="w-4 h-4 text-success-600" />
|
||||
<span className="text-xs font-semibold text-success-700 uppercase tracking-wider">
|
||||
Charge erkannt
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-10 rounded-full bg-brand-400 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-semibold text-warm-800">{article.name}</div>
|
||||
<div className="text-xs text-warm-500 font-mono">{batch.batchId}</div>
|
||||
<div className="text-xs text-warm-500">
|
||||
{location.name} · Verfügbar:{' '}
|
||||
<span className="font-semibold">
|
||||
{batch.quantityAmount} {batch.quantityUnit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quantity input */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center">
|
||||
<label className="text-sm font-medium text-warm-600 mb-2">Verbrauchte Menge</label>
|
||||
<div className="flex items-end gap-2 mb-2">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="number"
|
||||
step="0.1"
|
||||
className="font-mono text-center text-4xl font-bold py-2 w-40 border-2"
|
||||
placeholder="0"
|
||||
value={quantity}
|
||||
onKeyUp={(e) => e.key === 'Enter' && inputRef.current?.blur()}
|
||||
onChange={(e) => setQuantity(e.target.value)}
|
||||
/>
|
||||
<span className="text-lg text-warm-500 font-medium pb-2">{batch.quantityUnit}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 mb-4">
|
||||
{[5, 10, 20, 25].map((val) => (
|
||||
<Button
|
||||
key={val}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="px-3.5"
|
||||
onClick={() => setQuantity(String(val))}
|
||||
>
|
||||
{val}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{scaledIngredient ? (
|
||||
<p className="text-xs text-warm-400 text-center">
|
||||
Soll:{' '}
|
||||
<span className="font-semibold text-brand-600">
|
||||
{scaledIngredient.quantity} {scaledIngredient.uom}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-warm-400 text-center">
|
||||
Verfügbar:{' '}
|
||||
<span className="font-semibold text-warm-600">
|
||||
{batch.quantityAmount} {batch.quantityUnit}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PageActions>
|
||||
<Button size="lg" className="w-full" onClick={confirm}>
|
||||
<Check className="w-5 h-5" />
|
||||
Menge übernehmen
|
||||
</Button>
|
||||
</PageActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
77
frontend/apps/scanner/src/pages/HistoryPage.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import {
|
||||
ArrowRightLeft,
|
||||
PackageCheck,
|
||||
PackageMinus,
|
||||
PlayCircle,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { Card, Button } from '@effigenix/ui';
|
||||
import { BottomNav } from '../components/BottomNav';
|
||||
import { HISTORY_ENTRIES, type MockActivity } from '../mock-data';
|
||||
import { useState } from 'react';
|
||||
|
||||
const iconMap: Record<MockActivity['type'], { icon: LucideIcon; bg: string; color: string }> = {
|
||||
move: { icon: ArrowRightLeft, bg: 'bg-info-50', color: 'text-info-600' },
|
||||
consume: { icon: PackageMinus, bg: 'bg-warning-50', color: 'text-warning-600' },
|
||||
'production-complete': { icon: PackageCheck, bg: 'bg-success-50', color: 'text-success-600' },
|
||||
'production-start': { icon: PlayCircle, bg: 'bg-info-50', color: 'text-info-600' },
|
||||
'goods-receipt': { icon: ArrowRightLeft, bg: 'bg-info-50', color: 'text-info-600' },
|
||||
};
|
||||
|
||||
type TimeFilter = 'today' | 'yesterday' | 'week';
|
||||
|
||||
export function HistoryPage() {
|
||||
const [filter, setFilter] = useState<TimeFilter>('today');
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col animate-fade-in">
|
||||
<div className="px-5 pt-6 pb-4">
|
||||
<h1 className="text-lg font-bold text-warm-800">Verlauf</h1>
|
||||
<p className="text-xs text-warm-500">Alle Buchungen heute</p>
|
||||
</div>
|
||||
|
||||
<div className="px-5 mb-3">
|
||||
<div className="flex gap-2">
|
||||
{(['today', 'yesterday', 'week'] as const).map((f) => (
|
||||
<Button
|
||||
key={f}
|
||||
variant={filter === f ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => setFilter(f)}
|
||||
>
|
||||
{f === 'today' ? 'Heute' : f === 'yesterday' ? 'Gestern' : 'Woche'}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-5 flex-1 space-y-2">
|
||||
{HISTORY_ENTRIES.map((group) => (
|
||||
<div key={group.time}>
|
||||
<div className="text-2xs font-bold text-warm-400 uppercase tracking-wider mt-3 first:mt-0 mb-1.5">
|
||||
{group.time}
|
||||
</div>
|
||||
{group.entries.map((entry, i) => {
|
||||
const mapping = iconMap[entry.type];
|
||||
const Icon = mapping.icon;
|
||||
return (
|
||||
<Card key={i} className="p-3 flex items-center gap-3 mb-2">
|
||||
<div className={`w-8 h-8 rounded-lg ${mapping.bg} flex items-center justify-center`}>
|
||||
<Icon className={`w-4 h-4 ${mapping.color}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-warm-800">{entry.description}</div>
|
||||
<div className="text-xs text-warm-500">{entry.detail}</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
frontend/apps/scanner/src/pages/HomePage.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import {
|
||||
Radio,
|
||||
ArrowRightLeft,
|
||||
PackageMinus,
|
||||
PackageCheck,
|
||||
Search,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@effigenix/ui';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { BottomNav } from '../components/BottomNav';
|
||||
import { ACTIVITIES, type MockActivity } from '../mock-data';
|
||||
|
||||
export function HomePage() {
|
||||
const { navigate, setFlowType } = useNavigation();
|
||||
|
||||
function startFlow(flow: 'move' | 'consume' | 'book' | 'info') {
|
||||
setFlowType(flow);
|
||||
navigate('scan');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-6 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-warm-800">Hallo, Lisa</h1>
|
||||
<p className="text-xs text-warm-500">Mi, 19. März 2026 · Hauptbetrieb</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center text-brand-700 font-bold text-sm">
|
||||
LM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scanner Status */}
|
||||
<div className="mx-5 mb-4">
|
||||
<Card className="p-3 flex items-center gap-3 border-brand-200 bg-brand-50">
|
||||
<div className="w-9 h-9 rounded-lg bg-brand-100 flex items-center justify-center shrink-0">
|
||||
<Radio className="w-4.5 h-4.5 text-brand-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-brand-800">Scanner verbunden</div>
|
||||
<div className="text-xs text-brand-600">Zebra DS3678 · Bluetooth</div>
|
||||
</div>
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-success-400 shrink-0" />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="px-5 mb-5">
|
||||
<h2 className="text-xs font-bold text-warm-500 uppercase tracking-wider mb-3">
|
||||
Schnellaktionen
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<ActionCard icon={ArrowRightLeft} label="Umlagerung" sublabel="Charge → Lagerort" color="info" onPress={() => startFlow('move')} />
|
||||
<ActionCard icon={PackageMinus} label="Verbrauch" sublabel="Material → Produktion" color="warning" onPress={() => startFlow('consume')} />
|
||||
<ActionCard icon={PackageCheck} label="Produktion buchen" sublabel="Charge abschließen" color="success" onPress={() => startFlow('book')} />
|
||||
<ActionCard icon={Search} label="Charge prüfen" sublabel="Info & Historie" color="warm" onPress={() => startFlow('info')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="px-5 flex-1">
|
||||
<h2 className="text-xs font-bold text-warm-500 uppercase tracking-wider mb-3">
|
||||
Letzte Buchungen
|
||||
</h2>
|
||||
<div className="space-y-2.5">
|
||||
{ACTIVITIES.map((activity, i) => (
|
||||
<ActivityRow key={i} activity={activity} onPress={() => navigate('batch-detail')} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
info: { bg: 'bg-info-50', icon: 'text-info-600' },
|
||||
warning: { bg: 'bg-warning-50', icon: 'text-warning-600' },
|
||||
success: { bg: 'bg-success-50', icon: 'text-success-600' },
|
||||
warm: { bg: 'bg-warm-100', icon: 'text-warm-600' },
|
||||
};
|
||||
|
||||
function ActionCard({
|
||||
icon: Icon,
|
||||
label,
|
||||
sublabel,
|
||||
color,
|
||||
onPress,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
sublabel: string;
|
||||
color: keyof typeof colorMap;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
className="p-4 flex flex-col items-center gap-2 text-center cursor-pointer active:scale-[0.97] active:bg-warm-50 transition"
|
||||
onClick={onPress}
|
||||
>
|
||||
<div className={`w-11 h-11 rounded-xl ${colorMap[color].bg} flex items-center justify-center`}>
|
||||
<Icon className={`w-5 h-5 ${colorMap[color].icon}`} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-warm-700">{label}</span>
|
||||
<span className="text-2xs text-warm-500">{sublabel}</span>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const activityIconMap: Record<MockActivity['type'], { icon: LucideIcon; bg: string; color: string }> = {
|
||||
'move': { icon: CheckCircle2, bg: 'bg-success-50', color: 'text-success-600' },
|
||||
'consume': { icon: PackageMinus, bg: 'bg-warning-50', color: 'text-warning-600' },
|
||||
'goods-receipt': { icon: ArrowRightLeft, bg: 'bg-info-50', color: 'text-info-600' },
|
||||
'production-start': { icon: ArrowRightLeft, bg: 'bg-info-50', color: 'text-info-600' },
|
||||
'production-complete': { icon: PackageCheck, bg: 'bg-success-50', color: 'text-success-600' },
|
||||
};
|
||||
|
||||
function ActivityRow({ activity, onPress }: { activity: MockActivity; onPress: () => void }) {
|
||||
const mapping = activityIconMap[activity.type];
|
||||
const Icon = mapping.icon;
|
||||
return (
|
||||
<Card className="p-3 flex items-center gap-3 cursor-pointer" onClick={onPress}>
|
||||
<div className={`w-9 h-9 rounded-lg ${mapping.bg} flex items-center justify-center shrink-0`}>
|
||||
<Icon className={`w-4 h-4 ${mapping.color}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-warm-800 truncate">{activity.description}</div>
|
||||
<div className="text-xs text-warm-500">{activity.detail}</div>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-warm-300 shrink-0" />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
87
frontend/apps/scanner/src/pages/LoginPage.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { useState } from 'react';
|
||||
import { ScanLine, LogIn, Building2, AlertCircle } from 'lucide-react';
|
||||
import { Button, Input } from '@effigenix/ui';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { useApi } from '../api';
|
||||
|
||||
export function LoginPage() {
|
||||
const { navigate } = useNavigation();
|
||||
const { login } = useApi();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleLogin() {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(username, password);
|
||||
navigate('home');
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center px-6 bg-gradient-to-b from-warm-50 to-brand-100 animate-fade-in">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl mb-4 shadow-lg bg-gradient-to-br from-brand-500 to-brand-700">
|
||||
<ScanLine className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-warm-800 tracking-tight">Effigenix Mobile</h1>
|
||||
<p className="text-warm-500 text-sm mt-1">Chargen & Lager</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1">Betrieb</label>
|
||||
<div className="flex items-center gap-2 w-full rounded-xl border border-warm-300 bg-white px-3 py-2 text-sm text-warm-700">
|
||||
<Building2 className="w-4 h-4 text-warm-400 shrink-0" />
|
||||
Metzgerei Huber – Hauptbetrieb
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1">Benutzer</label>
|
||||
<Input
|
||||
placeholder="Benutzername"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1">Passwort</label>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 text-danger-600 text-sm">
|
||||
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button size="lg" className="w-full mt-2" onClick={handleLogin} disabled={loading}>
|
||||
<LogIn className="w-5 h-5" />
|
||||
{loading ? 'Anmelden…' : 'Anmelden'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-warm-400 text-xs mt-8">
|
||||
Verbunden mit Effigenix ERP · v2.0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
frontend/apps/scanner/src/pages/ManualEntryPage.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { Search, Info } from 'lucide-react';
|
||||
import { Button, Input, Card } from '@effigenix/ui';
|
||||
import { PageActions } from '../components/PageActions';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function ManualEntryPage() {
|
||||
const { navigate } = useNavigation();
|
||||
const [codeType, setCodeType] = useState<'qr' | 'ean' | 'datamatrix'>('qr');
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col animate-fade-in">
|
||||
<PageHeader title="Manuelle Eingabe" />
|
||||
|
||||
<div className="px-5 space-y-4 flex-1">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1.5">Chargennummer</label>
|
||||
<Input className="font-mono text-lg" placeholder="P-2026-03-19-003" defaultValue="P-2026-03-19-003" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1.5">Code-Typ</label>
|
||||
<div className="flex gap-2">
|
||||
{(['qr', 'ean', 'datamatrix'] as const).map((type) => (
|
||||
<Button
|
||||
key={type}
|
||||
variant={codeType === type ? 'primary' : 'secondary'}
|
||||
className="flex-1"
|
||||
onClick={() => setCodeType(type)}
|
||||
>
|
||||
{type === 'qr' ? 'QR-Code' : type === 'ean' ? 'EAN / GTIN' : 'DataMatrix'}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-3 bg-info-50 border-info-200 flex items-start gap-2.5">
|
||||
<Info className="w-4 h-4 text-info-600 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-info-700">
|
||||
Sie können auch Lieferschein-Nummern, Artikel-Nummern oder interne Referenzen eingeben.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<PageActions>
|
||||
<Button size="lg" className="w-full" onClick={() => navigate('batch-detail')}>
|
||||
<Search className="w-5 h-5" />
|
||||
Charge suchen
|
||||
</Button>
|
||||
</PageActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
frontend/apps/scanner/src/pages/MoveConfirmPage.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { ScanLine, Home } from 'lucide-react';
|
||||
import { Button } from '@effigenix/ui';
|
||||
import { SuccessScreen } from '../components/SuccessScreen';
|
||||
import { SummaryCard } from '../components/SummaryCard';
|
||||
import { useNavigation } from '../navigation';
|
||||
|
||||
export function MoveConfirmPage() {
|
||||
const { navigate, setFlowType } = useNavigation();
|
||||
|
||||
return (
|
||||
<SuccessScreen
|
||||
title="Umlagerung gebucht"
|
||||
subtitle="P-2026-03-19-003 → Kühlhaus 2"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setFlowType('move');
|
||||
navigate('scan');
|
||||
}}
|
||||
>
|
||||
<ScanLine className="w-4 h-4" />
|
||||
Nächste Charge scannen
|
||||
</Button>
|
||||
<Button variant="secondary" className="w-full" onClick={() => navigate('home')}>
|
||||
<Home className="w-4 h-4" />
|
||||
Zurück zum Start
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SummaryCard
|
||||
rows={[
|
||||
{ label: 'Charge', value: 'P-2026-03-19-003', mono: true },
|
||||
{ label: 'Artikel', value: 'Wiener Würstchen' },
|
||||
{ label: 'Menge', value: '118 kg', mono: true },
|
||||
{ label: 'Von', value: 'Kühlhaus 1' },
|
||||
{ label: 'Nach', value: 'Kühlhaus 2', highlight: 'success' },
|
||||
{ label: 'Zeitpunkt', value: '19.03. 11:05' },
|
||||
]}
|
||||
/>
|
||||
</SuccessScreen>
|
||||
);
|
||||
}
|
||||
107
frontend/apps/scanner/src/pages/MoveFlowPage.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { Check, ArrowDown, ThermometerSnowflake, Snowflake, ScanLine, ArrowRightLeft } from 'lucide-react';
|
||||
import { Card, Button, Input, cn } from '@effigenix/ui';
|
||||
import { PageActions } from '../components/PageActions';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
import { StepIndicator } from '../components/StepIndicator';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { BATCH, LOCATIONS } from '../mock-data';
|
||||
import { useState } from 'react';
|
||||
|
||||
const locationIcons = {
|
||||
cold: ThermometerSnowflake,
|
||||
frozen: Snowflake,
|
||||
scan: ScanLine,
|
||||
};
|
||||
|
||||
export function MoveFlowPage() {
|
||||
const { navigate } = useNavigation();
|
||||
const [selected, setSelected] = useState(1);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col animate-fade-in">
|
||||
<PageHeader title="Umlagerung" />
|
||||
|
||||
<StepIndicator
|
||||
steps={[
|
||||
{ label: 'Charge', state: 'done' },
|
||||
{ label: 'Ziel', state: 'active' },
|
||||
{ label: 'Bestätigen', state: 'pending' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="px-5 space-y-4 flex-1">
|
||||
{/* Scanned batch */}
|
||||
<Card className="p-3 flex items-center gap-3 border-success-200 bg-success-50">
|
||||
<div className="w-9 h-9 rounded-lg bg-success-100 flex items-center justify-center">
|
||||
<Check className="w-4 h-4 text-success-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold text-warm-800">{BATCH.id}</div>
|
||||
<div className="text-xs text-warm-500">
|
||||
{BATCH.article} · {BATCH.quantity} {BATCH.unit}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-warm-500">KH 1</span>
|
||||
</Card>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="flex items-center gap-2 text-warm-400">
|
||||
<div className="flex-1 h-px bg-warm-200" />
|
||||
<ArrowDown className="w-4 h-4" />
|
||||
<div className="flex-1 h-px bg-warm-200" />
|
||||
</div>
|
||||
|
||||
{/* Target location */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-2">Ziel-Lagerort</label>
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
{LOCATIONS.map((loc, i) => {
|
||||
const Icon = locationIcons[loc.icon];
|
||||
return (
|
||||
<Card
|
||||
key={loc.name}
|
||||
className={cn(
|
||||
'p-3 text-center cursor-pointer border-2 transition',
|
||||
selected === i ? 'border-brand-400 bg-brand-50' : 'border-transparent hover:border-brand-300'
|
||||
)}
|
||||
onClick={() => setSelected(i)}
|
||||
>
|
||||
<Icon className="w-5 h-5 text-info-500 mx-auto mb-1" />
|
||||
<div className="text-sm font-semibold text-warm-800">{loc.name}</div>
|
||||
<div className="text-2xs text-warm-500">{loc.temperature}</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<Card
|
||||
className="p-3 text-center cursor-pointer border-2 border-transparent hover:border-brand-300 transition"
|
||||
>
|
||||
<ScanLine className="w-5 h-5 text-warm-400 mx-auto mb-1" />
|
||||
<div className="text-sm font-semibold text-warm-800">Scannen</div>
|
||||
<div className="text-2xs text-warm-500">Lagerort-QR</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Partial quantity */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-warm-700 mb-1.5">
|
||||
Teilmenge (optional)
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input className="font-mono text-center text-lg max-w-[120px]" placeholder="118" />
|
||||
<span className="text-sm text-warm-500 font-medium">
|
||||
von {BATCH.quantity} {BATCH.unit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PageActions>
|
||||
<Button size="lg" className="w-full" onClick={() => navigate('move-confirm')}>
|
||||
<ArrowRightLeft className="w-5 h-5" />
|
||||
Umlagerung bestätigen
|
||||
</Button>
|
||||
</PageActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
151
frontend/apps/scanner/src/pages/ScannerPage.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { X, Flashlight, ScanLine, Keyboard } from 'lucide-react';
|
||||
import { Button } from '@effigenix/ui';
|
||||
import { PageActions } from '../components/PageActions';
|
||||
import { useNavigation, type FlowType, type Page } from '../navigation';
|
||||
import {
|
||||
scan,
|
||||
cancel,
|
||||
Format,
|
||||
checkPermissions,
|
||||
requestPermissions,
|
||||
} from '@tauri-apps/plugin-barcode-scanner';
|
||||
|
||||
const flowLabels: Record<FlowType, string> = {
|
||||
move: 'Charge zum Umlagern scannen',
|
||||
consume: 'Material scannen',
|
||||
book: 'Produktionscharge scannen',
|
||||
info: 'Charge scannen',
|
||||
};
|
||||
|
||||
const flowTargets: Record<FlowType, Page> = {
|
||||
move: 'batch-detail',
|
||||
consume: 'consume-qty',
|
||||
book: 'book-flow',
|
||||
info: 'batch-detail',
|
||||
};
|
||||
|
||||
export function ScannerPage() {
|
||||
const { navigate, goBack, flowType } = useNavigation();
|
||||
const [nativeAvailable, setNativeAvailable] = useState<boolean | null>(null);
|
||||
|
||||
function handleResult(content: string) {
|
||||
navigate(flowTargets[flowType], { batchId: content });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let scanning = false;
|
||||
|
||||
(async () => {
|
||||
// 1. Check if plugin is available (throws on desktop)
|
||||
try {
|
||||
let perm = await checkPermissions();
|
||||
if (cancelled) return;
|
||||
if (perm !== 'granted') perm = await requestPermissions();
|
||||
if (cancelled) return;
|
||||
if (perm !== 'granted') {
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setNativeAvailable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Start native scan — only if not cancelled by StrictMode cleanup
|
||||
scanning = true;
|
||||
try {
|
||||
const result = await scan({
|
||||
formats: [
|
||||
Format.QRCode,
|
||||
Format.DataMatrix,
|
||||
Format.Code128,
|
||||
Format.EAN13,
|
||||
Format.EAN8,
|
||||
],
|
||||
});
|
||||
scanning = false;
|
||||
if (!cancelled) handleResult(result.content);
|
||||
} catch {
|
||||
scanning = false;
|
||||
if (!cancelled) goBack();
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (scanning) cancel().catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Native scan in progress – show minimal placeholder
|
||||
if (nativeAvailable !== false) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center bg-warm-900">
|
||||
<div className="text-center">
|
||||
<ScanLine className="w-10 h-10 text-white/30 mx-auto mb-3 animate-pulse" />
|
||||
<p className="text-white/50 text-sm">{flowLabels[flowType]}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop fallback: simulated viewfinder
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-warm-900 animate-fade-in">
|
||||
<div className="flex-1 min-h-0 relative flex flex-col">
|
||||
{/* Top bar */}
|
||||
<div className="absolute top-0 inset-x-0 z-10 flex items-center justify-between px-5 pt-4">
|
||||
<button
|
||||
onClick={goBack}
|
||||
className="w-10 h-10 rounded-full bg-black/30 backdrop-blur flex items-center justify-center"
|
||||
>
|
||||
<X className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
<div className="px-3 py-1.5 rounded-full bg-black/30 backdrop-blur text-white text-xs font-semibold">
|
||||
{flowLabels[flowType]}
|
||||
</div>
|
||||
<button className="w-10 h-10 rounded-full bg-black/30 backdrop-blur flex items-center justify-center">
|
||||
<Flashlight className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Camera simulation */}
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center relative"
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, #1a1611 0%, #2e2820 50%, #1a1611 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="w-64 h-64 relative">
|
||||
<div className="absolute inset-0 border-2 border-white/20 rounded-2xl" />
|
||||
<div className="absolute top-0 left-0 w-8 h-8 border-t-[3px] border-l-[3px] border-white rounded-tl-2xl" />
|
||||
<div className="absolute top-0 right-0 w-8 h-8 border-t-[3px] border-r-[3px] border-white rounded-tr-2xl" />
|
||||
<div className="absolute bottom-0 left-0 w-8 h-8 border-b-[3px] border-l-[3px] border-white rounded-bl-2xl" />
|
||||
<div className="absolute bottom-0 right-0 w-8 h-8 border-b-[3px] border-r-[3px] border-white rounded-br-2xl" />
|
||||
<div className="absolute left-3 right-3 h-0.5 bg-brand-400 rounded-full animate-scan-line shadow-[0_0_12px_oklch(0.75_0.16_70/0.6)]" />
|
||||
</div>
|
||||
<p className="absolute bottom-8 text-white/50 text-sm font-medium text-center px-8">
|
||||
QR-Code, DataMatrix oder Barcode in den Rahmen halten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PageActions>
|
||||
<Button size="lg" className="w-full" onClick={() => handleResult('P-2026-03-19-003')}>
|
||||
<ScanLine className="w-5 h-5" />
|
||||
Scan simulieren
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full bg-white/10 border-white/20 text-white hover:bg-white/20"
|
||||
onClick={() => navigate('manual-entry')}
|
||||
>
|
||||
<Keyboard className="w-4 h-4" />
|
||||
Manuell eingeben
|
||||
</Button>
|
||||
</PageActions>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
163
frontend/apps/scanner/src/pages/TasksPage.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { Card, Badge } from '@effigenix/ui';
|
||||
import { Loader2, AlertCircle, ClipboardList } from 'lucide-react';
|
||||
import { BottomNav } from '../components/BottomNav';
|
||||
import { useNavigation } from '../navigation';
|
||||
import { useFlowState } from '../flow-state';
|
||||
import { useApi } from '../api';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { ProductionOrderDTO } from '@effigenix/types';
|
||||
|
||||
const UOM_SHORT: Record<string, string> = {
|
||||
KILOGRAM: 'kg',
|
||||
GRAM: 'g',
|
||||
LITER: 'L',
|
||||
MILLILITER: 'mL',
|
||||
PIECE: 'Stk',
|
||||
METER: 'm',
|
||||
};
|
||||
|
||||
function uomLabel(uom: string | undefined): string {
|
||||
return (uom && UOM_SHORT[uom]) ?? uom ?? '';
|
||||
}
|
||||
|
||||
const priorityConfig: Record<string, { variant: 'danger' | 'warning' | 'neutral'; label: string }> = {
|
||||
URGENT: { variant: 'danger', label: 'Dringend' },
|
||||
HIGH: { variant: 'warning', label: 'Hoch' },
|
||||
NORMAL: { variant: 'neutral', label: 'Normal' },
|
||||
LOW: { variant: 'neutral', label: 'Niedrig' },
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
RELEASED: 'Freigegeben',
|
||||
IN_PROGRESS: 'In Produktion',
|
||||
};
|
||||
|
||||
interface EnrichedOrder {
|
||||
order: ProductionOrderDTO;
|
||||
articleName: string;
|
||||
}
|
||||
|
||||
type LoadState =
|
||||
| { status: 'loading' }
|
||||
| { status: 'error'; message: string }
|
||||
| { status: 'ok'; data: EnrichedOrder[] };
|
||||
|
||||
export function TasksPage() {
|
||||
const { navigate, setFlowType } = useNavigation();
|
||||
const { setProductionOrder } = useFlowState();
|
||||
const { client } = useApi();
|
||||
const [state, setState] = useState<LoadState>({ status: 'loading' });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const [released, inProgress] = await Promise.all([
|
||||
client.productionOrders.list({ status: 'RELEASED' }),
|
||||
client.productionOrders.list({ status: 'IN_PROGRESS' }),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
|
||||
const orders = [...inProgress, ...released];
|
||||
|
||||
// Enrich: recipe → article name
|
||||
const enriched = await Promise.all(
|
||||
orders.map(async (order): Promise<EnrichedOrder> => {
|
||||
try {
|
||||
if (!order.recipeId) return { order, articleName: '–' };
|
||||
const recipe = await client.recipes.getById(order.recipeId);
|
||||
const article = await client.articles.getById(recipe.articleId);
|
||||
return { order, articleName: article.name };
|
||||
} catch {
|
||||
return { order, articleName: '–' };
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (cancelled) return;
|
||||
|
||||
setState({ status: 'ok', data: enriched });
|
||||
} catch {
|
||||
if (!cancelled) setState({ status: 'error', message: 'Aufträge konnten nicht geladen werden' });
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [client]);
|
||||
|
||||
function openOrder(e: EnrichedOrder) {
|
||||
const { order } = e;
|
||||
if (order.status !== 'IN_PROGRESS') return;
|
||||
setProductionOrder({
|
||||
orderId: order.id!,
|
||||
recipeId: order.recipeId!,
|
||||
batchNumber: order.batchNumber ?? null,
|
||||
articleName: e.articleName,
|
||||
plannedQuantity: parseFloat(order.plannedQuantity ?? '0'),
|
||||
plannedQuantityUnit: order.plannedQuantityUnit ?? '',
|
||||
});
|
||||
setFlowType('consume');
|
||||
navigate('consume-flow');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col animate-fade-in">
|
||||
<div className="px-5 pt-6 pb-4">
|
||||
<h1 className="text-lg font-bold text-warm-800">Aufgaben</h1>
|
||||
<p className="text-xs text-warm-500">Offene Produktionsaufträge</p>
|
||||
</div>
|
||||
|
||||
<div className="px-5 flex-1 space-y-2.5">
|
||||
{state.status === 'loading' && (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2 className="w-7 h-7 text-brand-500 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state.status === 'error' && (
|
||||
<Card className="p-5 text-center border-danger-200 bg-danger-50">
|
||||
<AlertCircle className="w-7 h-7 text-danger-500 mx-auto mb-2" />
|
||||
<p className="text-sm text-warm-700">{state.message}</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{state.status === 'ok' && state.data.length === 0 && (
|
||||
<Card className="p-5 text-center">
|
||||
<ClipboardList className="w-7 h-7 text-warm-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-warm-500">Keine offenen Aufträge</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{state.status === 'ok' && state.data.map((e) => {
|
||||
const { order } = e;
|
||||
const prio = priorityConfig[order.priority ?? 'NORMAL'] ?? priorityConfig.NORMAL;
|
||||
const isClickable = order.status === 'IN_PROGRESS';
|
||||
return (
|
||||
<Card
|
||||
key={order.id}
|
||||
className={`p-4 ${isClickable ? 'cursor-pointer active:bg-warm-50' : 'opacity-75'}`}
|
||||
onClick={() => isClickable && openOrder(e)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Badge variant={prio.variant}>{prio.label}</Badge>
|
||||
<span className="text-xs text-warm-500">
|
||||
{statusLabels[order.status ?? ''] ?? order.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="font-semibold text-warm-800 text-sm">
|
||||
{e.articleName}
|
||||
</div>
|
||||
<div className="text-xs text-warm-500 mt-0.5">
|
||||
{order.plannedQuantity} {uomLabel(order.plannedQuantityUnit)}
|
||||
{order.plannedDate && ` · ${order.plannedDate}`}
|
||||
{order.batchNumber && ` · ${order.batchNumber}`}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,11 +6,14 @@ const host = process.env.TAURI_DEV_HOST;
|
|||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), react()],
|
||||
resolve: {
|
||||
conditions: ['source'],
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
host: host || '0.0.0.0',
|
||||
hmr: host
|
||||
? {
|
||||
protocol: 'ws',
|
||||
|
|
@ -18,5 +21,11 @@ export default defineConfig({
|
|||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.VITE_API_URL || 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Base axios client for the Effigenix API
|
||||
*/
|
||||
|
||||
import axios, { type AxiosInstance } from 'axios';
|
||||
import axios, { type AxiosAdapter, type AxiosInstance } from 'axios';
|
||||
import { type ApiConfig, DEFAULT_API_CONFIG } from '@effigenix/config';
|
||||
import { setupAuthInterceptor } from './interceptors/auth-interceptor.js';
|
||||
import { setupRefreshInterceptor } from './interceptors/refresh-interceptor.js';
|
||||
|
|
@ -30,6 +30,7 @@ export function createApiClient(
|
|||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
...(resolvedConfig.adapter ? { adapter: resolvedConfig.adapter as AxiosAdapter } : {}),
|
||||
});
|
||||
|
||||
setupAuthInterceptor(client, tokenProvider);
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ export function createStocksResource(client: AxiosInstance) {
|
|||
return res.data;
|
||||
},
|
||||
|
||||
async findByBatchId(batchId: string): Promise<StockDTO[]> {
|
||||
const res = await client.get<StockDTO[]>(`${BASE}/by-batch/${encodeURIComponent(batchId)}`);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async create(request: CreateStockRequest): Promise<CreateStockResponse> {
|
||||
const res = await client.post<CreateStockResponse>(BASE, request);
|
||||
return res.data;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ export interface ApiConfig {
|
|||
baseUrl: string;
|
||||
timeoutMs: number;
|
||||
retries: number;
|
||||
/** Custom axios adapter (e.g. Tauri HTTP plugin fetch). */
|
||||
adapter?: unknown;
|
||||
}
|
||||
|
||||
export const DEFAULT_API_CONFIG: ApiConfig = {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"source": "./src/index.ts",
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { forwardRef, type ButtonHTMLAttributes } from 'react';
|
||||
import { cn } from '../lib/cn';
|
||||
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
|
||||
type ButtonSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
|
|
@ -13,16 +13,18 @@ const variantStyles: Record<ButtonVariant, string> = {
|
|||
primary:
|
||||
'bg-brand-600 text-white hover:bg-brand-700 active:bg-brand-800 focus-visible:ring-brand-500',
|
||||
secondary:
|
||||
'bg-warm-100 text-warm-800 hover:bg-warm-200 active:bg-warm-300 focus-visible:ring-warm-400 border border-warm-300',
|
||||
'bg-white text-warm-800 hover:bg-warm-50 active:bg-warm-100 focus-visible:ring-warm-400 border border-warm-300',
|
||||
ghost: 'text-warm-700 hover:bg-warm-100 active:bg-warm-200 focus-visible:ring-warm-400',
|
||||
danger:
|
||||
'bg-danger-600 text-white hover:bg-danger-700 active:bg-danger-800 focus-visible:ring-danger-500',
|
||||
success:
|
||||
'bg-success-600 text-white hover:bg-success-700 active:bg-success-800 focus-visible:ring-success-500',
|
||||
};
|
||||
|
||||
const sizeStyles: Record<ButtonSize, string> = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-5 py-2.5 text-base',
|
||||
sm: 'px-4 py-2 text-sm',
|
||||
md: 'px-6 py-3 text-sm',
|
||||
lg: 'px-8 py-4 text-base',
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
|
|
@ -31,9 +33,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
<button
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center font-medium rounded-md transition-colors',
|
||||
'inline-flex items-center justify-center gap-2 font-semibold rounded-xl transition-colors',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||
'disabled:opacity-50 disabled:pointer-events-none',
|
||||
'active:scale-[0.97]',
|
||||
variantStyles[variant],
|
||||
sizeStyles[size],
|
||||
className
|
||||
|
|
|
|||
|
|
@ -79,10 +79,16 @@
|
|||
|
||||
/* Font */
|
||||
--font-sans: 'Poppins', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
||||
|
||||
/* Radius */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-xs: 0 1px 3px oklch(0 0 0 / 0.04);
|
||||
--shadow-brand: 0 2px 8px oklch(0.65 0.19 55 / 0.3);
|
||||
--shadow-brand-lg: 0 4px 16px oklch(0.65 0.19 55 / 0.4);
|
||||
}
|
||||
|
|
|
|||
22
frontend/pnpm-lock.yaml
generated
|
|
@ -108,6 +108,12 @@ importers:
|
|||
'@tauri-apps/plugin-barcode-scanner':
|
||||
specifier: ^2.0.0
|
||||
version: 2.4.4
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: ^2.5.7
|
||||
version: 2.5.7
|
||||
lucide-react:
|
||||
specifier: ^0.577.0
|
||||
version: 0.577.0(react@18.3.1)
|
||||
react:
|
||||
specifier: ^18.2.0
|
||||
version: 18.3.1
|
||||
|
|
@ -1458,6 +1464,9 @@ packages:
|
|||
'@tauri-apps/plugin-barcode-scanner@2.4.4':
|
||||
resolution: {integrity: sha512-uXvyMI8UgQjSrGxzTU5isNoQarMGRxFmTmb4TsgiWZHf/g7LsIyAQCwoFShjax0fXCK5mdVKDOvlkfOr21fo6g==}
|
||||
|
||||
'@tauri-apps/plugin-http@2.5.7':
|
||||
resolution: {integrity: sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A==}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||
|
||||
|
|
@ -2391,6 +2400,11 @@ packages:
|
|||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.577.0:
|
||||
resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
|
|
@ -4085,6 +4099,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
'@tauri-apps/plugin-http@2.5.7':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.2
|
||||
|
|
@ -5087,6 +5105,10 @@ snapshots:
|
|||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.577.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
|
|
|||
10
justfile
|
|
@ -26,6 +26,16 @@ dev-scanner:
|
|||
dev-ui:
|
||||
cd frontend && pnpm run --filter @effigenix/ui dev
|
||||
|
||||
# ─── Android ─────────────────────────────────────────────
|
||||
|
||||
# Android-Emulator starten (Pixel 3 XL, API 34)
|
||||
android-emulator:
|
||||
emulator -avd effigenix -gpu swiftshader_indirect
|
||||
|
||||
# Scanner auf Android-Emulator starten (Emulator muss laufen)
|
||||
dev-scanner-android:
|
||||
cd frontend/apps/scanner && pnpm tauri android dev
|
||||
|
||||
# ─── Build ────────────────────────────────────────────────
|
||||
|
||||
# Frontend komplett bauen
|
||||
|
|
|
|||