wip warehouse and inc stock workflow
This commit is contained in:
parent
e637d26842
commit
6768f8d5b7
4
.env
4
.env
@ -12,11 +12,13 @@ DB_HOST=127.0.0.1
|
|||||||
DB_PORT=3311
|
DB_PORT=3311
|
||||||
DB_DATABASE=vat_wms
|
DB_DATABASE=vat_wms
|
||||||
DB_DATABASE_WAREHOUSE=vat_warehouse
|
DB_DATABASE_WAREHOUSE=vat_warehouse
|
||||||
|
DB_DATABASE_SHIPPING=shipping
|
||||||
DB_USERNAME=vat_wms
|
DB_USERNAME=vat_wms
|
||||||
DB_PASSWORD=vat_wms
|
DB_PASSWORD=vat_wms
|
||||||
|
|
||||||
|
|
||||||
APP_KEY=base64:yptRdaDOEfbK3K+eheSvnvbD7JFYK/GUedXzVM4U2qs=
|
APP_KEY=base64:yptRdaDOEfbK3K+eheSvnvbD7JFYK/GUedXzVM4U2qs=
|
||||||
|
SHIPPING_API_KEY=4|mNZYZOhSB2gcCf0G9vJr38j9im7AtbM5Xj5BdcMr04903057
|
||||||
|
SHIPPPING_APP_URL=https://shipping.dalkove-ovladace.cz
|
||||||
#OCTANE_SERVER=roadrunner
|
#OCTANE_SERVER=roadrunner
|
||||||
|
|
||||||
|
109
app/Http/Controllers/Api/ExpediceController.php
Normal file
109
app/Http/Controllers/Api/ExpediceController.php
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\StockEntry;
|
||||||
|
use App\Models\StockEntryStatusHistory;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ExpediceController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function batchListWMS(Request $request) {
|
||||||
|
// Instantiate Guzzle client
|
||||||
|
$client = new Client([
|
||||||
|
'base_uri' => env('SHIPPPING_APP_URL'),
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer ' . env('SHIPPING_API_KEY'),
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
// Optionally set a timeout
|
||||||
|
'timeout' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Prepare query parameters
|
||||||
|
$query = [
|
||||||
|
'user_id' => 5
|
||||||
|
// 'user_id' => $request->user()->id,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Send GET request to /api/batchListWMS with user_id as query parameter
|
||||||
|
$response = $client->request('GET', '/api/batchListWMS', [
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$batches = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
// Return decoded JSON (or raw body, depending on your needs)
|
||||||
|
return response()->json([
|
||||||
|
"batches" => $batches['batches'],
|
||||||
|
]);
|
||||||
|
// return json_decode($response->getBody()->getContents(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function expediceListWMS(Request $request) {
|
||||||
|
// Instantiate Guzzle client
|
||||||
|
$client = new Client([
|
||||||
|
'base_uri' => env('SHIPPPING_APP_URL'),
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer ' . env('SHIPPING_API_KEY'),
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
// Optionally set a timeout
|
||||||
|
'timeout' => 40,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Prepare payload
|
||||||
|
$payload = [
|
||||||
|
'batch_id' => $request->input('batch_id'), // or wherever you pull batch_id from
|
||||||
|
];
|
||||||
|
|
||||||
|
// Send POST request with JSON body
|
||||||
|
$response = $client->request('POST', '/api/warehouseExpediceListWMS', [
|
||||||
|
'json' => $payload,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$batch_items = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
|
||||||
|
// Return decoded JSON (or raw body, depending on your needs)
|
||||||
|
return response()->json([
|
||||||
|
"batch_items" => $batch_items,
|
||||||
|
]);
|
||||||
|
// return json_decode($response->getBody()->getContents(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductImage(Request $request) {
|
||||||
|
// Instantiate Guzzle client
|
||||||
|
$client = new Client([
|
||||||
|
'base_uri' => env('SHIPPPING_APP_URL'),
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer ' . env('SHIPPING_API_KEY'),
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
// Optionally set a timeout
|
||||||
|
'timeout' => 40,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Prepare query parameters instead of JSON body
|
||||||
|
$query = [
|
||||||
|
'selectedShipmentID' => $request->input('selectedShipmentID'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Send GET request with query parameters
|
||||||
|
$response = $client->request('GET', '/api/warehouseExpedice/getImage', [
|
||||||
|
'query' => $query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$batch_items = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
|
||||||
|
// Return decoded JSON (or raw body, depending on your needs)
|
||||||
|
return response()->json($batch_items);
|
||||||
|
// return json_decode($response->getBody()->getContents(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
155
app/Http/Controllers/Api/ScannerController.php
Normal file
155
app/Http/Controllers/Api/ScannerController.php
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\StockPosition;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
// Replace these with your actual model classes
|
||||||
|
use App\Models\StockBatch;
|
||||||
|
use App\Models\StockSection;
|
||||||
|
use Metzli\Encoder\Encoder;
|
||||||
|
use Metzli\Renderer\PngRenderer;
|
||||||
|
|
||||||
|
class ScannerController extends Controller {
|
||||||
|
|
||||||
|
|
||||||
|
public function barcodeScan(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Validate the incoming JSON structure
|
||||||
|
// {"barcode_type": "stock_batch","payload":{"id": 7}}
|
||||||
|
try {
|
||||||
|
$data = $request->validate([
|
||||||
|
'barcode_type' => 'required|string|in:stock_batch,stock_section,stock_position,label_scanned,carrier_scanned',
|
||||||
|
'payload' => 'required|array',
|
||||||
|
'payload.id' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $e->errors(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $data['barcode_type'];
|
||||||
|
$id = $data['payload']['id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($type) {
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
case 'stock_batch':
|
||||||
|
// Attempt to load a StockBatch by ID. Adjust with(...) if you have relationships you want to return.
|
||||||
|
$batch = StockBatch::with(['stockEntries.physicalItem', 'stockEntries.sections.position.shelf.rack.line.room', 'stockEntries.supplier', 'files', 'supplier'])
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $batch,
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
case 'stock_section':
|
||||||
|
// Attempt to load a StockSection by ID.
|
||||||
|
$section = StockSection::with(['position.sections.entries.physicalItem', 'entries.physicalItem'])
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $section,
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
case 'stock_position':
|
||||||
|
// Attempt to load a StockPosition by ID.
|
||||||
|
$position = StockPosition::with(['shelf', 'sections.entries.physicalItem'])
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $position,
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
case 'label_scanned':
|
||||||
|
// Attempt to load a Label by ID. Replace with your real model/relations.
|
||||||
|
$label = Label::with(['product', 'batch'])
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $label,
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
case 'carrier_scanned':
|
||||||
|
// “Do something” for carrier scans. For example, mark the carrier as checked‐in.
|
||||||
|
// Here we just look up a Carrier and flip a boolean. Adjust to your real logic.
|
||||||
|
$carrier = Carrier::findOrFail($id);
|
||||||
|
$carrier->last_scanned_at = now();
|
||||||
|
$carrier->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
default:
|
||||||
|
// This will never run because our validator already limits barcode_type to the four values.
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'invalid_barcode_type',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
// Any of the findOrFail(...) calls threw a ModelNotFoundException
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => "No record found for ID {$id} under type '{$type}'.",
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the exception so you can inspect it later
|
||||||
|
Log::error('Error in BarcodeController@scan', [
|
||||||
|
'barcode_type' => $type,
|
||||||
|
'id' => $id,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'An unexpected error occurred.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function batchBarcodePrint(Request $request) {
|
||||||
|
|
||||||
|
$code = Encoder::encode(json_encode(
|
||||||
|
[
|
||||||
|
"barcode_type" => "stock_batch",
|
||||||
|
"payload" => [
|
||||||
|
'id' => $request->input('id'),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
));
|
||||||
|
$renderer = new PngRenderer();
|
||||||
|
|
||||||
|
$aztec_code = $renderer->render($code);
|
||||||
|
|
||||||
|
$base64_aztec_img = base64_encode($aztec_code);
|
||||||
|
$base64_aztec_src = "data:image/png;base64,{$base64_aztec_img}";
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $base64_aztec_src,
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -24,7 +24,7 @@ class StockBatchController extends Controller
|
|||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$query = StockBatch::query()
|
$query = StockBatch::query()
|
||||||
->with(['user', 'supplier', 'stockEntries.statusHistory.status', 'files']);
|
->with(['user', 'supplier', 'stockEntries.statusHistory.status', 'stockEntries.sections','stockEntries.physicalItem', 'files']);
|
||||||
|
|
||||||
// Apply filters if provided
|
// Apply filters if provided
|
||||||
if ($request->has('search')) {
|
if ($request->has('search')) {
|
||||||
@ -33,6 +33,7 @@ class StockBatchController extends Controller
|
|||||||
$q->where('name', 'like', "%{$search}%");
|
$q->where('name', 'like', "%{$search}%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
$query->where('default_batch', 0);
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
$sortField = $request->input('sort_field', 'updated_at');
|
$sortField = $request->input('sort_field', 'updated_at');
|
||||||
@ -65,12 +66,14 @@ class StockBatchController extends Controller
|
|||||||
|
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
'supplier_id' => 'nullable|integer',
|
'supplier_id' => 'nullable|integer',
|
||||||
'tracking_number' => 'nullable|string',
|
'tracking_number' => 'nullable|string',
|
||||||
'arrival_date' => 'nullable|date',
|
'arrival_date' => 'nullable|date',
|
||||||
'files.*' => 'file',
|
'files.*' => 'file'
|
||||||
'file_types' => 'array',
|
.'|mimes:jpeg,png,jpg,heic,heif,pdf,doc,docx,xls,xlsx,txt'
|
||||||
'file_types.*' => 'in:invoice,label,other',
|
.'|max:20480', // max size in KB (20 MB)
|
||||||
|
'file_types' => 'array',
|
||||||
|
'file_types.*' => 'in:invoice,label,other',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
|
@ -7,13 +7,17 @@ use App\Models\OriginCountry;
|
|||||||
use App\Models\StockEntry;
|
use App\Models\StockEntry;
|
||||||
use App\Models\StockEntrySection;
|
use App\Models\StockEntrySection;
|
||||||
use App\Models\StockEntryStatus;
|
use App\Models\StockEntryStatus;
|
||||||
|
use App\Models\StockEntryStatusHistory;
|
||||||
use App\Models\StockPosition;
|
use App\Models\StockPosition;
|
||||||
use App\Models\PhysicalItem;
|
use App\Models\PhysicalItem;
|
||||||
use App\Models\StockSection;
|
use App\Models\StockSection;
|
||||||
use App\Models\Supplier;
|
use App\Models\Supplier;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use OwenIt\Auditing\Models\Audit;
|
use OwenIt\Auditing\Models\Audit;
|
||||||
|
|
||||||
class StockEntryController extends Controller
|
class StockEntryController extends Controller
|
||||||
@ -98,23 +102,6 @@ class StockEntryController extends Controller
|
|||||||
'on_the_way' => 'boolean',
|
'on_the_way' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
// condition for requiring section + count
|
|
||||||
$needsSection = function() use ($request) {
|
|
||||||
return ! $request->boolean('on_the_way')
|
|
||||||
&& !is_null($request->input('stock_batch_id'));
|
|
||||||
};
|
|
||||||
|
|
||||||
// add conditional rules
|
|
||||||
$rules['stock_position_id'] = [
|
|
||||||
Rule::requiredIf($needsSection),
|
|
||||||
'integer',
|
|
||||||
'exists:stock_section,section_id'
|
|
||||||
];
|
|
||||||
$rules['section_count'] = [
|
|
||||||
Rule::requiredIf($needsSection),
|
|
||||||
'integer',
|
|
||||||
'min:1'
|
|
||||||
];
|
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), $rules);
|
$validator = Validator::make($request->all(), $rules);
|
||||||
|
|
||||||
@ -138,14 +125,6 @@ class StockEntryController extends Controller
|
|||||||
'created_by' => auth()->id() ?? 1,
|
'created_by' => auth()->id() ?? 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2) only attach to section pivot if needed
|
|
||||||
if ($needsSection()) {
|
|
||||||
$entry->sections()->attach(
|
|
||||||
$request->input('stock_position_id'),
|
|
||||||
['count' => $request->input('section_count')]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) eager-load relations (including the full address hierarchy)
|
// 3) eager-load relations (including the full address hierarchy)
|
||||||
$entry->load([
|
$entry->load([
|
||||||
'physicalItem',
|
'physicalItem',
|
||||||
@ -367,8 +346,9 @@ class StockEntryController extends Controller
|
|||||||
public function getItems(Request $request)
|
public function getItems(Request $request)
|
||||||
{
|
{
|
||||||
// Get physical items from warehouse DB
|
// Get physical items from warehouse DB
|
||||||
$physicalItems = PhysicalItem::select('id', 'name')
|
$physicalItems = PhysicalItem::with(['type', 'manufacturer'])
|
||||||
->where('name', 'like', '%' . $request->input('item_name', '') . '%')->limit(100)
|
->where('name', 'like', '%' . $request->input('item_name', '') . '%')
|
||||||
|
->limit(100)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
@ -405,4 +385,85 @@ class StockEntryController extends Controller
|
|||||||
'statuses' => $entry,
|
'statuses' => $entry,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countStock(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Validate input
|
||||||
|
try {
|
||||||
|
$payload = $request->validate([
|
||||||
|
'entryId' => 'required|integer|exists:stock_entries,id',
|
||||||
|
'quantity' => 'required|integer|min:0',
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
// Return 422 + a JSON body with errors
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $e->errors(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2) Fetch the StockEntry (will throw ModelNotFoundException if not exists)
|
||||||
|
$entry = StockEntry::findOrFail($payload['entryId']);
|
||||||
|
|
||||||
|
// 3) If original_count is already set → 409 conflict
|
||||||
|
if ($entry->original_count !== 0) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'already_counted',
|
||||||
|
'message' => 'This stock entry has already been counted.',
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$quantity = $payload['quantity'];
|
||||||
|
// Determine which status to record
|
||||||
|
if ($entry->original_count_invoice === $quantity) {
|
||||||
|
StockEntryStatusHistory::create([
|
||||||
|
'stock_entries_id' => $entry->id,
|
||||||
|
'stock_entries_status_id' => StockEntryStatus::NEW_GOODS_COUNTED,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$statusId = $entry->original_count_invoice > $quantity
|
||||||
|
? StockEntryStatus::NEW_GOODS_MISSING
|
||||||
|
: StockEntryStatus::NEW_GOODS_SURPLUS;
|
||||||
|
|
||||||
|
StockEntryStatusHistory::create([
|
||||||
|
'stock_entries_id' => $entry->id,
|
||||||
|
'stock_entries_status_id' => $statusId,
|
||||||
|
'status_note' => 'Count: ' . ($quantity - $entry->original_count_invoice)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$entry->update([
|
||||||
|
'original_count' => $quantity,
|
||||||
|
'count' => $quantity,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
]); // HTTP 200
|
||||||
|
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'Stock entry not found.',
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the exception for debugging
|
||||||
|
Log::error('countStock error', [
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'An unexpected error occurred.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
42
app/Http/Controllers/Api/StockLineController.php
Normal file
42
app/Http/Controllers/Api/StockLineController.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
use App\Models\StockLine;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class StockLineController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'room_id' => 'required|exists:stock_room,room_id',
|
||||||
|
'line_symbol' => 'required|string|max:50',
|
||||||
|
'line_name' => 'required|string|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$line = StockLine::create($data);
|
||||||
|
|
||||||
|
return response()->json($line, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, StockLine $line)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'line_symbol' => 'sometimes|required|string|max:50',
|
||||||
|
'line_name' => 'sometimes|required|string|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$line->update($data);
|
||||||
|
|
||||||
|
return response()->json($line);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(StockLine $line)
|
||||||
|
{
|
||||||
|
$line->delete();
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
}
|
123
app/Http/Controllers/Api/StockPositionController.php
Normal file
123
app/Http/Controllers/Api/StockPositionController.php
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\StockPosition;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
|
||||||
|
class StockPositionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the capacity of a given StockPosition.
|
||||||
|
*
|
||||||
|
* Request payload:
|
||||||
|
* {
|
||||||
|
* "capacity": <integer> // new capacity for the position
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* - Validates that capacity ≥ 0.
|
||||||
|
* - Ensures the new capacity is not less than the sum of existing section capacities.
|
||||||
|
* - Returns JSON { success: true } on success,
|
||||||
|
* or { success: false, error: "<code>", message: "<details>" } on failure.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
// 1) Find the position
|
||||||
|
$position = StockPosition::find($id);
|
||||||
|
if (!$position) {
|
||||||
|
return response()
|
||||||
|
->json(['success' => false, 'error' => 'not_found', 'message' => 'Position not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Validate incoming payload
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'capacity' => 'required|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $validator->errors()->first(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newCapacity = $request->input('capacity');
|
||||||
|
|
||||||
|
// 3) Check that new capacity ≥ sum of all existing (non-deleted) sections' capacities
|
||||||
|
// (we assume “sections” relationship returns the live StockSection models)
|
||||||
|
$currentSectionsSum = $position
|
||||||
|
->sections()
|
||||||
|
->sum('capacity');
|
||||||
|
|
||||||
|
if ($newCapacity < $currentSectionsSum) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'insufficient_capacity',
|
||||||
|
'message' => "New capacity ({$newCapacity}) is less than total of existing section capacities ({$currentSectionsSum}).",
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) All good → update and save
|
||||||
|
$position->capacity = $newCapacity;
|
||||||
|
$position->save();
|
||||||
|
|
||||||
|
return response()->json(['success' => true], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getPosition(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Eager‐load the real relationships
|
||||||
|
$position = StockPosition::with([
|
||||||
|
'sections.entries.physicalItem',
|
||||||
|
'shelf.rack.line.room',
|
||||||
|
])->findOrFail(2);
|
||||||
|
|
||||||
|
// 2) Compute the storage address string
|
||||||
|
$position->storage_address = $position->storageAddress();
|
||||||
|
|
||||||
|
// 3) Return the model itself (now including a top‐level "storage_address" field)
|
||||||
|
return response()->json($position);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'shelf_id' => 'required|exists:stock_shelf,shelf_id',
|
||||||
|
'position_symbol' => 'required|string|max:50',
|
||||||
|
'position_name' => 'required|string|max:100',
|
||||||
|
'capacity' => 'required|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pos = StockPosition::create($data);
|
||||||
|
|
||||||
|
return response()->json($pos, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateBasic(Request $request, StockPosition $pos)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'position_symbol' => 'sometimes|required|string|max:50',
|
||||||
|
'position_name' => 'sometimes|required|string|max:100',
|
||||||
|
'capacity' => 'sometimes|required|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pos->update($data);
|
||||||
|
|
||||||
|
return response()->json($pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(StockPosition $pos)
|
||||||
|
{
|
||||||
|
$pos->delete();
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
}
|
42
app/Http/Controllers/Api/StockRackController.php
Normal file
42
app/Http/Controllers/Api/StockRackController.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
use App\Models\StockRack;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class StockRackController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'line_id' => 'required|exists:stock_line,line_id',
|
||||||
|
'rack_symbol' => 'required|string|max:50',
|
||||||
|
'rack_name' => 'required|string|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rack = StockRack::create($data);
|
||||||
|
|
||||||
|
return response()->json($rack, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, StockRack $rack)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'rack_symbol' => 'sometimes|required|string|max:50',
|
||||||
|
'rack_name' => 'sometimes|required|string|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rack->update($data);
|
||||||
|
|
||||||
|
return response()->json($rack);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(StockRack $rack)
|
||||||
|
{
|
||||||
|
$rack->delete();
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
}
|
80
app/Http/Controllers/Api/StockRoomController.php
Normal file
80
app/Http/Controllers/Api/StockRoomController.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Models\StockRoom;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class StockRoomController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'room_symbol' => 'required|string',
|
||||||
|
'room_name' => 'required|string',
|
||||||
|
'lines' => 'required|integer|min:1',
|
||||||
|
'racks_per_line' => 'required|integer|min:1',
|
||||||
|
'shelves_per_rack' => 'required|integer|min:1',
|
||||||
|
'positions_per_shelf' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
\DB::transaction(function() use($data) {
|
||||||
|
$room = StockRoom::create([
|
||||||
|
'room_symbol' => $data['room_symbol'],
|
||||||
|
'room_name' => $data['room_name'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
for ($i = 1; $i <= $data['lines']; $i++) {
|
||||||
|
$line = $room->lines()->create([
|
||||||
|
'line_symbol' => $i,
|
||||||
|
'line_name' => 'Line ' . $i,
|
||||||
|
]);
|
||||||
|
for ($j = 1; $j <= $data['racks_per_line']; $j++) {
|
||||||
|
$rack = $line->racks()->create([
|
||||||
|
'rack_symbol' => $j,
|
||||||
|
'rack_name' => 'Rack ' . $j,
|
||||||
|
]);
|
||||||
|
for ($k = 1; $k <= $data['shelves_per_rack']; $k++) {
|
||||||
|
$shelf = $rack->shelves()->create([
|
||||||
|
'shelf_symbol' => $k,
|
||||||
|
'shelf_name' => 'Shelf ' . $k,
|
||||||
|
]);
|
||||||
|
for ($p = 1; $p <= $data['positions_per_shelf']; $p++) {
|
||||||
|
$shelf->positions()->create([
|
||||||
|
'position_symbol' => $p,
|
||||||
|
'position_name' => 'Position ' . $p,
|
||||||
|
'capacity' => 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json(['message' => 'Room created'], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(StockRoom $room)
|
||||||
|
{
|
||||||
|
$room->load('lines.racks.shelves.positions.sections');
|
||||||
|
return response()->json($room);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, StockRoom $room)
|
||||||
|
{
|
||||||
|
// You can use a single JSON payload with full tree edits, or
|
||||||
|
// issue individual calls per entity (lines, racks, etc.)
|
||||||
|
// For brevity, let’s assume front-end will call your existing modals
|
||||||
|
// to update positions or sections via your other APIs.
|
||||||
|
$room->update($request->only('room_symbol','room_name'));
|
||||||
|
return response()->json(['message'=>'Room updated']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(StockRoom $room)
|
||||||
|
{
|
||||||
|
$room->delete();
|
||||||
|
return response()->json(['message'=>'Room deleted']);
|
||||||
|
}
|
||||||
|
}
|
711
app/Http/Controllers/Api/StockSectionController.php
Normal file
711
app/Http/Controllers/Api/StockSectionController.php
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\StockBatch;
|
||||||
|
use App\Models\StockEntries2Section;
|
||||||
|
use App\Models\StockEntry;
|
||||||
|
use App\Models\StockPosition;
|
||||||
|
use App\Models\StockSection;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
|
||||||
|
class StockSectionController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function getSection(Request $request, int $id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return response()->json([
|
||||||
|
'section' => StockSection::with(['position', 'entries.physicalItem'])->findOrFail($id)
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'StockSection not found.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function setSection(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Validate input
|
||||||
|
try {
|
||||||
|
$data = $request->validate([
|
||||||
|
'section_id' => 'required|integer|exists:stock_section,section_id',
|
||||||
|
'entry_id' => 'required|integer|exists:stock_entries,id',
|
||||||
|
'count_to_be_stored' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $e->errors(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sectionId = $data['section_id'];
|
||||||
|
$entryId = $data['entry_id'];
|
||||||
|
$count = $data['count_to_be_stored'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2) Locate section and entry (404 if not found)
|
||||||
|
$section = StockSection::findOrFail($sectionId);
|
||||||
|
$entry = StockEntry::findOrFail($entryId);
|
||||||
|
|
||||||
|
// 3) If the section is already occupied → 409 Conflict
|
||||||
|
if ($section->occupied()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'section_occupied',
|
||||||
|
'message' => 'That section is already occupied.',
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Check capacity: count_to_be_stored must be <= section’s capacity
|
||||||
|
if ($count > $section->capacity) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'insufficient_capacity',
|
||||||
|
'message' => 'Not enough capacity in this section.',
|
||||||
|
'available_capacity' => $section->capacity,
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Create the pivot record and mark section as occupied
|
||||||
|
StockEntries2Section::create([
|
||||||
|
'entry_id' => $entry->id,
|
||||||
|
'section_id' => $section->section_id,
|
||||||
|
'count' => $count,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Entry assigned to section successfully.',
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
// Either the section or the entry was not found
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'StockSection or StockEntry not found.',
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log unexpected exceptions
|
||||||
|
Log::error('Error in setSection', [
|
||||||
|
'section_id' => $sectionId,
|
||||||
|
'entry_id' => $entryId,
|
||||||
|
'count' => $count,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'An unexpected error occurred.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeStock(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Validate input
|
||||||
|
try {
|
||||||
|
$data = $request->validate([
|
||||||
|
'section_id' => 'required|integer|exists:stock_section,section_id', // new section
|
||||||
|
'current_section' => 'required|integer|exists:stock_section,section_id', // old section
|
||||||
|
'entry_id' => 'required|integer|exists:stock_entries,id',
|
||||||
|
'count_to_be_stored' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $e->errors(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newSectionId = $data['section_id'];
|
||||||
|
$currentSectionId = $data['current_section'];
|
||||||
|
$entryId = $data['entry_id'];
|
||||||
|
$moveCount = $data['count_to_be_stored'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use (
|
||||||
|
$newSectionId,
|
||||||
|
$currentSectionId,
|
||||||
|
$entryId,
|
||||||
|
$moveCount
|
||||||
|
) {
|
||||||
|
// 2) Load sections and entry
|
||||||
|
$currentSection = StockSection::findOrFail($currentSectionId);
|
||||||
|
$newSection = StockSection::findOrFail($newSectionId);
|
||||||
|
$entry = StockEntry::findOrFail($entryId);
|
||||||
|
|
||||||
|
// 3) Check occupation: allow if occupied only by this same entry
|
||||||
|
$existingInNew = StockEntries2Section::where('section_id', $newSectionId)
|
||||||
|
->where('entry_id', $entryId)
|
||||||
|
->first();
|
||||||
|
if ($newSection->occupied() && ! $existingInNew) {
|
||||||
|
abort(409, json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'section_occupied',
|
||||||
|
'message' => 'Target section is already occupied by another item.',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Locate pivot in current section
|
||||||
|
$pivot = StockEntries2Section::where('section_id', $currentSectionId)
|
||||||
|
->where('entry_id', $entryId)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
// 5) Ensure there is enough count in current section
|
||||||
|
if ($pivot->count < $moveCount) {
|
||||||
|
abort(409, json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'insufficient_count',
|
||||||
|
'message' => 'Not enough items in the current section to move.',
|
||||||
|
'available' => $pivot->count,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Decrement or remove pivot in current section
|
||||||
|
$remaining = $pivot->count - $moveCount;
|
||||||
|
if ($remaining > 0) {
|
||||||
|
DB::table('stock_entries2section')
|
||||||
|
->where('entry_id', $entryId)
|
||||||
|
->where('section_id', $currentSectionId)
|
||||||
|
->limit(1)
|
||||||
|
->update(['count' => $remaining]);
|
||||||
|
} else {
|
||||||
|
DB::table('stock_entries2section')
|
||||||
|
->where('entry_id', $entryId)
|
||||||
|
->where('section_id', $currentSectionId)
|
||||||
|
->limit(1)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
// if no more entries in this section, mark it unoccupied
|
||||||
|
if (! DB::table('stock_entries2section')->where('section_id', $currentSectionId)->exists()) {
|
||||||
|
$currentSection->update(['occupied' => false]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) Upsert into new section, merging counts if already present
|
||||||
|
// lockForUpdate to avoid race conditions
|
||||||
|
$existing = DB::table('stock_entries2section')
|
||||||
|
->where('entry_id', $entryId)
|
||||||
|
->where('section_id', $newSectionId)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$currentInNew = $existing->count ?? 0;
|
||||||
|
$totalAfter = $currentInNew + $moveCount;
|
||||||
|
|
||||||
|
// 8) Ensure capacity in new section
|
||||||
|
if ($totalAfter > $newSection->capacity) {
|
||||||
|
abort(409, json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'insufficient_capacity',
|
||||||
|
'message' => 'Not enough capacity in target section.',
|
||||||
|
'available_capacity' => $newSection->capacity - $currentInNew,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
DB::table('stock_entries2section')
|
||||||
|
->where('entry_id', $entryId)
|
||||||
|
->where('section_id', $newSectionId)
|
||||||
|
->limit(1)
|
||||||
|
->update(['count' => $totalAfter]);
|
||||||
|
} else {
|
||||||
|
StockEntries2Section::create([
|
||||||
|
'entry_id' => $entry->id,
|
||||||
|
'section_id' => $newSectionId,
|
||||||
|
'count' => $moveCount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9) Mark new section occupied
|
||||||
|
$newSection->update(['occupied' => true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// success response
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Moved stock successfully.',
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'StockSection or StockEntry not found.',
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
||||||
|
$payload = json_decode($e->getMessage(), true);
|
||||||
|
return response()->json($payload, $e->getStatusCode());
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error in storeStock', [
|
||||||
|
'current_section' => $currentSectionId,
|
||||||
|
'new_section' => $newSectionId,
|
||||||
|
'entry_id' => $entryId,
|
||||||
|
'count' => $moveCount,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'An unexpected error occurred.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSectionForBatch(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Validate input
|
||||||
|
try {
|
||||||
|
$data = $request->validate([
|
||||||
|
'position_id' => 'required|integer|exists:stock_position,position_id',
|
||||||
|
'batch_id' => 'required|integer|exists:stock_batch,id',
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $e->errors(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$positionId = $data['position_id'];
|
||||||
|
$batchId = $data['batch_id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2) Locate section and entry (404 if not found)
|
||||||
|
$position = StockPosition::findOrFail($positionId);
|
||||||
|
$batch = StockBatch::with(['stockEntries'])->findOrFail($batchId);
|
||||||
|
|
||||||
|
$anyOccupied = $position->sections->contains(function($section) {
|
||||||
|
return $section->occupied();
|
||||||
|
});
|
||||||
|
// 3) If the section is already occupied → 409 Conflict
|
||||||
|
if ($anyOccupied) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'position_occupied',
|
||||||
|
'message' => 'That position is already occupied.',
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$position->temporary) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'section_not_tmp',
|
||||||
|
'message' => 'That section is not suitable for entire batch temporary storage.',
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all existing sections for this position
|
||||||
|
StockSection::where('position_id', $position->position_id)->delete();
|
||||||
|
|
||||||
|
// Create new sections for each entry
|
||||||
|
$sectionCounter = 1;
|
||||||
|
foreach ($batch->stockEntries as $entry) {
|
||||||
|
// Create new section
|
||||||
|
$section = StockSection::create([
|
||||||
|
'position_id' => $position->position_id,
|
||||||
|
'section_name' => (string)$sectionCounter,
|
||||||
|
'section_symbol' => (string)$sectionCounter,
|
||||||
|
'capacity' => $entry->count,
|
||||||
|
'retrievable' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create mapping
|
||||||
|
StockEntries2Section::create([
|
||||||
|
'entry_id' => $entry->id,
|
||||||
|
'section_id' => $section->section_id,
|
||||||
|
'count' => $entry->count
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sectionCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Batch stored to position successfully.',
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
// Either the section or the entry was not found
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'StockSection or StockEntry not found.',
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log unexpected exceptions
|
||||||
|
Log::error('Error in setSectionForBatch', [
|
||||||
|
'position_id' => $positionId,
|
||||||
|
'batch_id' => $batchId,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'An unexpected error occurred.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function movePosition(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Validate input
|
||||||
|
try {
|
||||||
|
$data = $request->validate([
|
||||||
|
'section_id' => 'required|integer|exists:stock_section,section_id',
|
||||||
|
'new_section_id' => 'required|integer|exists:stock_section,section_id',
|
||||||
|
'entry_id' => 'required|integer|exists:stock_entries,id',
|
||||||
|
'count' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $e->errors(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sectionId = $data['section_id'];
|
||||||
|
$newSectionId = $data['new_section_id'];
|
||||||
|
$entryId = $data['entry_id'];
|
||||||
|
$count = $data['count'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2) Locate section and entry (404 if not found)
|
||||||
|
$new_section = StockSection::findOrFail($newSectionId);
|
||||||
|
$entry = StockEntry::findOrFail($entryId);
|
||||||
|
|
||||||
|
// 3) If the section is already occupied → 409 Conflict
|
||||||
|
if ($new_section->occupied()) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'section_occupied',
|
||||||
|
'message' => 'That section is already occupied.',
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Check capacity: count_to_be_stored must be <= section’s capacity
|
||||||
|
if ($count > $new_section->capacity) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'insufficient_capacity',
|
||||||
|
'message' => 'Not enough capacity in this section.',
|
||||||
|
'available_capacity' => $new_section->capacity,
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use query builder because of composite PK
|
||||||
|
DB::table('stock_entries2section')
|
||||||
|
->where('entry_id', $entryId)
|
||||||
|
->where('section_id', $sectionId)
|
||||||
|
->limit(1) // ensure only one row is touched
|
||||||
|
->update([
|
||||||
|
'section_id' => $new_section->section_id,
|
||||||
|
'count' => $count,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Section changed successfully.',
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
// Either the section or the entry was not found
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'StockSection or StockEntry not found.',
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log unexpected exceptions
|
||||||
|
Log::error('Error in setSection', [
|
||||||
|
'section_id' => $sectionId,
|
||||||
|
'new_section_id' => $newSectionId,
|
||||||
|
'entry_id' => $entryId,
|
||||||
|
'count' => $count,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'An unexpected error occurred.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changeCount(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Validate input
|
||||||
|
try {
|
||||||
|
$data = $request->validate([
|
||||||
|
'section_id' => 'required|integer|exists:stock_section,section_id',
|
||||||
|
'entry_id' => 'required|integer|exists:stock_entries,id',
|
||||||
|
'count' => 'required|integer|min:1',
|
||||||
|
'new_count' => 'required|integer|min:1',
|
||||||
|
]);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $e->errors(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sectionId = $data['section_id'];
|
||||||
|
$entryId = $data['entry_id'];
|
||||||
|
$count = $data['count'];
|
||||||
|
$new_count = $data['new_count'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2) Locate section and entry (404 if not found)
|
||||||
|
$section = StockSection::findOrFail($sectionId);
|
||||||
|
$entry = StockEntry::findOrFail($entryId);
|
||||||
|
|
||||||
|
|
||||||
|
// Check capacity: count_to_be_stored must be <= section’s capacity
|
||||||
|
if ($new_count > $section->capacity) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'insufficient_capacity',
|
||||||
|
'message' => 'Not enough capacity in this section.',
|
||||||
|
'available_capacity' => $section->capacity,
|
||||||
|
], 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use query builder because of composite PK
|
||||||
|
DB::table('stock_entries2section')
|
||||||
|
->where('entry_id', $entryId)
|
||||||
|
->where('section_id', $sectionId)
|
||||||
|
->limit(1) // ensure only one row is touched
|
||||||
|
->update([
|
||||||
|
'count' => $new_count,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Section changed successfully.',
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (ModelNotFoundException $e) {
|
||||||
|
// Either the section or the entry was not found
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'StockSection or StockEntry not found.',
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log unexpected exceptions
|
||||||
|
Log::error('Error in setSection', [
|
||||||
|
'section_id' => $sectionId,
|
||||||
|
'new_section_id' => $newSectionId,
|
||||||
|
'entry_id' => $entryId,
|
||||||
|
'count' => $count,
|
||||||
|
'exception' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'server_error',
|
||||||
|
'message' => 'An unexpected error occurred.',
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
// 1) Validate incoming payload
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'position_id' => 'required|integer|exists:stock_position,position_id',
|
||||||
|
'section_symbol' => 'required|string|max:5',
|
||||||
|
'section_name' => 'required|string|max:32',
|
||||||
|
'capacity' => 'required|integer|min:0',
|
||||||
|
'retrievable' => 'required|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $validator->errors()->first(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Find the parent position
|
||||||
|
$position = StockPosition::find($request->input('position_id'));
|
||||||
|
if (!$position) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'Position not found.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newCapacity = $request->input('capacity');
|
||||||
|
|
||||||
|
// 3) Check that (existing sections sum) + newCapacity ≤ position.capacity
|
||||||
|
$existingSum = $position
|
||||||
|
->sections()
|
||||||
|
->sum('capacity');
|
||||||
|
|
||||||
|
if ($existingSum + $newCapacity > $position->capacity) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'insufficient_capacity',
|
||||||
|
'message' => "Creating this section would push total capacity ({$existingSum} + {$newCapacity}) over position capacity ({$position->capacity}).",
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Create the new section
|
||||||
|
$section = new StockSection();
|
||||||
|
$section->position_id = $position->position_id;
|
||||||
|
$section->section_symbol = $request->input('section_symbol');
|
||||||
|
$section->section_name = $request->input('section_name');
|
||||||
|
$section->capacity = $newCapacity;
|
||||||
|
$section->retrievable = $request->input('retrievable');
|
||||||
|
$section->save();
|
||||||
|
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => true,
|
||||||
|
'section_id' => $section->section_id,
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing StockSection.
|
||||||
|
*
|
||||||
|
* Endpoint: PUT /api/stockSections/{id}
|
||||||
|
*
|
||||||
|
* Request payload:
|
||||||
|
* {
|
||||||
|
* "section_name": <string>,
|
||||||
|
* "capacity": <integer ≥ 0>,
|
||||||
|
* "retrievable": <boolean>
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Responses:
|
||||||
|
* • 200 → { success: true }
|
||||||
|
* • 404 → { success: false, error: "not_found", message: "Section not found." }
|
||||||
|
* • 422 → { success: false, error: "validation_failed" / "insufficient_capacity", message: "..." }
|
||||||
|
*/
|
||||||
|
public function update(Request $request, $id)
|
||||||
|
{
|
||||||
|
// 1) Find the section
|
||||||
|
$section = StockSection::find($id);
|
||||||
|
if (!$section) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'Section not found.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Validate incoming payload
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'section_name' => 'required|string|max:255',
|
||||||
|
'capacity' => 'required|integer|min:0',
|
||||||
|
'retrievable' => 'required|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'validation_failed',
|
||||||
|
'message' => $validator->errors()->first(),
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedCapacity = $request->input('capacity');
|
||||||
|
$position = $section->position; // via relation
|
||||||
|
|
||||||
|
// 3) Check that sum of *other* sections + updatedCapacity ≤ position.capacity
|
||||||
|
$otherSectionsSum = $position
|
||||||
|
->sections()
|
||||||
|
->where('section_id', '!=', $id)
|
||||||
|
->sum('capacity');
|
||||||
|
|
||||||
|
if ($otherSectionsSum + $updatedCapacity > $position->capacity) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'insufficient_capacity',
|
||||||
|
'message' => "Updating to capacity {$updatedCapacity} would push total of other sections ({$otherSectionsSum}) over position capacity ({$position->capacity}).",
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Apply changes
|
||||||
|
$section->section_name = $request->input('section_name');
|
||||||
|
$section->capacity = $updatedCapacity;
|
||||||
|
$section->retrievable = $request->input('retrievable');
|
||||||
|
$section->save();
|
||||||
|
|
||||||
|
return response()->json(['success' => true], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a StockSection by its ID.
|
||||||
|
*
|
||||||
|
* Endpoint: DELETE /api/stockSections/{id}
|
||||||
|
*
|
||||||
|
* Responses:
|
||||||
|
* • 200 → { success: true }
|
||||||
|
* • 404 → { success: false, error: "not_found", message: "Section not found." }
|
||||||
|
*/
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$section = StockSection::find($id);
|
||||||
|
if (!$section) {
|
||||||
|
return response()
|
||||||
|
->json([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'not_found',
|
||||||
|
'message' => 'Section not found.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$section->delete();
|
||||||
|
|
||||||
|
return response()->json(['success' => true], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
app/Http/Controllers/Api/StockShelfController.php
Normal file
42
app/Http/Controllers/Api/StockShelfController.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
use App\Models\StockShelf;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class StockShelfController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'rack_id' => 'required|exists:stock_rack,rack_id',
|
||||||
|
'shelf_symbol' => 'required|string|max:50',
|
||||||
|
'shelf_name' => 'required|string|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shelf = StockShelf::create($data);
|
||||||
|
|
||||||
|
return response()->json($shelf, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, StockShelf $shelf)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'shelf_symbol' => 'sometimes|required|string|max:50',
|
||||||
|
'shelf_name' => 'sometimes|required|string|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shelf->update($data);
|
||||||
|
|
||||||
|
return response()->json($shelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(StockShelf $shelf)
|
||||||
|
{
|
||||||
|
$shelf->delete();
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
}
|
102
app/Http/Controllers/Api/StorageController.php
Normal file
102
app/Http/Controllers/Api/StorageController.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use App\Models\StockRoom;
|
||||||
|
use App\Models\StockLine;
|
||||||
|
use App\Models\StockRack;
|
||||||
|
use App\Models\StockShelf;
|
||||||
|
use App\Models\StockPosition;
|
||||||
|
use App\Models\StockSection;
|
||||||
|
|
||||||
|
class StorageController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle initial bulk‐creation of a new storage layout.
|
||||||
|
*
|
||||||
|
* Expected JSON payload:
|
||||||
|
* {
|
||||||
|
* "room_symbol": "RM1",
|
||||||
|
* "room_name": "Main Storeroom",
|
||||||
|
* "num_lines": 5, // minimum 1
|
||||||
|
* "racks_per_line": 5, // minimum 1
|
||||||
|
* "shelves_per_rack": 5, // minimum 1
|
||||||
|
* "positions_per_shelf": 4, // minimum 1
|
||||||
|
* "sections_per_position": 1 // minimum 1
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function setup(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'room_symbol' => 'required|string|max:50',
|
||||||
|
'room_name' => 'required|string|max:100',
|
||||||
|
'num_lines' => 'nullable|integer|min:1',
|
||||||
|
'racks_per_line' => 'nullable|integer|min:1',
|
||||||
|
'shelves_per_rack' => 'nullable|integer|min:1',
|
||||||
|
'positions_per_shelf' => 'nullable|integer|min:1',
|
||||||
|
'sections_per_position'=> 'nullable|integer|min:1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Apply defaults if any count is missing or invalid:
|
||||||
|
$numLines = $data['num_lines'] ?? 1;
|
||||||
|
$racksPerLine = $data['racks_per_line'] ?? 1;
|
||||||
|
$shelvesPerRack= $data['shelves_per_rack'] ?? 1;
|
||||||
|
$positionsPerShelf = $data['positions_per_shelf'] ?? 1;
|
||||||
|
$sectionsPerPosition= $data['sections_per_position'] ?? 1;
|
||||||
|
|
||||||
|
// 1) Create the Room
|
||||||
|
$room = StockRoom::create([
|
||||||
|
'room_symbol' => $data['room_symbol'],
|
||||||
|
'room_name' => $data['room_name'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2) Create N lines
|
||||||
|
for ($i = 1; $i <= $numLines; $i++) {
|
||||||
|
$line = $room->lines()->create([
|
||||||
|
'line_symbol' => $room->room_symbol . '_L' . $i,
|
||||||
|
'line_name' => $room->room_name . ' Line ' . $i,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 3) For each line, create M racks
|
||||||
|
for ($j = 1; $j <= $racksPerLine; $j++) {
|
||||||
|
$rack = $line->racks()->create([
|
||||||
|
'rack_symbol' => $line->line_symbol . '_R' . $j,
|
||||||
|
'rack_name' => $line->line_name . ' Rack ' . $j,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4) For each rack, create P shelves
|
||||||
|
for ($k = 1; $k <= $shelvesPerRack; $k++) {
|
||||||
|
$shelf = $rack->shelves()->create([
|
||||||
|
'shelf_symbol' => $rack->rack_symbol . '_S' . $k,
|
||||||
|
'shelf_name' => $rack->rack_name . ' Shelf ' . $k,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5) For each shelf, create Q positions
|
||||||
|
for ($m = 1; $m <= $positionsPerShelf; $m++) {
|
||||||
|
$position = $shelf->positions()->create([
|
||||||
|
'position_symbol' => $shelf->shelf_symbol . '_P' . $m,
|
||||||
|
'position_name' => $shelf->shelf_name . ' Position ' . $m,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 6) For each position, create R sections
|
||||||
|
for ($n = 1; $n <= $sectionsPerPosition; $n++) {
|
||||||
|
$position->sections()->create([
|
||||||
|
'section_symbol' => $position->position_symbol . '_SEC' . $n,
|
||||||
|
'section_name' => $position->position_name . ' Section ' . $n,
|
||||||
|
'capacity' => 0,
|
||||||
|
'retrievable' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Storage layout created successfully.',
|
||||||
|
'room_id' => $room->room_id,
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class PhysicalItem extends Model
|
class PhysicalItem extends Model
|
||||||
@ -47,4 +48,9 @@ class PhysicalItem extends Model
|
|||||||
return $this->hasOne(Manufacturer::class, 'id', 'manufacturer_id');
|
return $this->hasOne(Manufacturer::class, 'id', 'manufacturer_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function type(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PhysicalItemType::class, 'physical_item_type_id');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
70
app/Models/PhysicalItemType.php
Normal file
70
app/Models/PhysicalItemType.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class PhysicalItemType extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
// Use the same connection as your other warehouse models
|
||||||
|
protected $connection = 'vat_warehouse';
|
||||||
|
protected $table = 'physical_item_type';
|
||||||
|
|
||||||
|
// Allow mass assignment on these fields
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'code_suffix',
|
||||||
|
'priority',
|
||||||
|
'weight',
|
||||||
|
'volume',
|
||||||
|
'title_template_id',
|
||||||
|
'description_template_id',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all physical items of this type.
|
||||||
|
*/
|
||||||
|
public function items(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PhysicalItem::class, 'physical_item_type_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title template associated with this item type.
|
||||||
|
*/
|
||||||
|
public function titleTemplate(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Template::class, 'title_template_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The description template associated with this item type.
|
||||||
|
*/
|
||||||
|
public function descriptionTemplate(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Template::class, 'description_template_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user who created this record.
|
||||||
|
*/
|
||||||
|
public function createdBy(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user who last updated this record.
|
||||||
|
*/
|
||||||
|
public function updatedBy(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ class StockEntry extends Model implements AuditableContract
|
|||||||
'supplier_id',
|
'supplier_id',
|
||||||
'count',
|
'count',
|
||||||
'original_count',
|
'original_count',
|
||||||
|
'original_count_invoice',
|
||||||
'price',
|
'price',
|
||||||
'bought',
|
'bought',
|
||||||
'description',
|
'description',
|
||||||
@ -38,6 +39,10 @@ class StockEntry extends Model implements AuditableContract
|
|||||||
'updated_by',
|
'updated_by',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'count_stocked',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be cast.
|
* The attributes that should be cast.
|
||||||
*
|
*
|
||||||
@ -48,6 +53,14 @@ class StockEntry extends Model implements AuditableContract
|
|||||||
'on_the_way' => 'boolean',
|
'on_the_way' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
public function getCountStockedAttribute(): int
|
||||||
|
{
|
||||||
|
// calls your existing method, which will
|
||||||
|
// loadMissing the relations if needed
|
||||||
|
return $this->countStockedItems();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the physical item associated with the stock entry.
|
* Get the physical item associated with the stock entry.
|
||||||
*/
|
*/
|
||||||
@ -141,4 +154,12 @@ class StockEntry extends Model implements AuditableContract
|
|||||||
})
|
})
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function countStockedItems(): int
|
||||||
|
{
|
||||||
|
return (int) $this
|
||||||
|
->sections()
|
||||||
|
->sum('stock_entries2section.count');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ class StockEntryStatus extends Model
|
|||||||
public const NEW_GOODS_MISSING = 7;
|
public const NEW_GOODS_MISSING = 7;
|
||||||
public const NEW_GOODS_SURPLUS = 8;
|
public const NEW_GOODS_SURPLUS = 8;
|
||||||
public const STOCK_MISSING = 9;
|
public const STOCK_MISSING = 9;
|
||||||
|
|
||||||
public const STOCK_DISCARDED = 10;
|
public const STOCK_DISCARDED = 10;
|
||||||
public const STOCK_RETURNED = 11;
|
public const STOCK_RETURNED = 11;
|
||||||
|
|
||||||
|
@ -13,15 +13,52 @@ class StockPosition extends Model
|
|||||||
'position_symbol',
|
'position_symbol',
|
||||||
'position_name',
|
'position_name',
|
||||||
'shelf_id',
|
'shelf_id',
|
||||||
|
'capacity',
|
||||||
|
'temporary'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'storage_address',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getStorageAddressAttribute(): string
|
||||||
|
{
|
||||||
|
// calls your existing method, which will
|
||||||
|
// loadMissing the relations if needed
|
||||||
|
return $this->storageAddress();
|
||||||
|
}
|
||||||
|
|
||||||
public function shelf()
|
public function shelf()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(StockShelf::class, 'shelf_id', 'shelf_id');
|
return $this->belongsTo(StockShelf::class, 'shelf_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sections()
|
public function sections()
|
||||||
{
|
{
|
||||||
return $this->hasMany(StockSection::class, 'position_id', 'position_id');
|
return $this->hasMany(StockSection::class, 'position_id', 'position_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function storageAddress(): string
|
||||||
|
{
|
||||||
|
// eager-load the whole hierarchy if not already
|
||||||
|
$this->loadMissing('shelf.rack.line.room');
|
||||||
|
|
||||||
|
|
||||||
|
$shelf = $this->shelf;
|
||||||
|
$rack = $shelf->rack;
|
||||||
|
$line = $rack->line;
|
||||||
|
$room = $line->room;
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%s-%s-%s-%s-%s',
|
||||||
|
$room->room_symbol,
|
||||||
|
$line->line_symbol,
|
||||||
|
$rack->rack_symbol,
|
||||||
|
$shelf->shelf_symbol,
|
||||||
|
$this->position_symbol
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,10 @@ class StockSection extends Model
|
|||||||
'retrievable'
|
'retrievable'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $appends = [
|
||||||
|
'storage_address',
|
||||||
|
];
|
||||||
|
|
||||||
public function position()
|
public function position()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(StockPosition::class, 'position_id', 'position_id');
|
return $this->belongsTo(StockPosition::class, 'position_id', 'position_id');
|
||||||
@ -40,4 +44,36 @@ class StockSection extends Model
|
|||||||
return $this->entries()->exists();
|
return $this->entries()->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getStorageAddressAttribute(): string
|
||||||
|
{
|
||||||
|
// calls your existing method, which will
|
||||||
|
// loadMissing the relations if needed
|
||||||
|
return $this->storageAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storageAddress(): string
|
||||||
|
{
|
||||||
|
// eager-load the whole hierarchy if not already
|
||||||
|
$this->loadMissing('position.shelf.rack.line.room');
|
||||||
|
|
||||||
|
|
||||||
|
$position = $this->position;
|
||||||
|
$shelf = $position->shelf;
|
||||||
|
$rack = $shelf->rack;
|
||||||
|
$line = $rack->line;
|
||||||
|
$room = $line->room;
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%s-%s-%s-%s-%s-%s',
|
||||||
|
$room->room_symbol,
|
||||||
|
$line->line_symbol,
|
||||||
|
$rack->rack_symbol,
|
||||||
|
$shelf->shelf_symbol,
|
||||||
|
$position->position_symbol,
|
||||||
|
$this->section_symbol
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,14 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"inertiajs/inertia-laravel": "^2.0",
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
|
"intervention/image": "^3.11",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/jetstream": "^5.3",
|
"laravel/jetstream": "^5.3",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"owen-it/laravel-auditing": "^14.0",
|
"owen-it/laravel-auditing": "^14.0",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0",
|
||||||
|
"z38/metzli": "^1.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
198
composer.lock
generated
198
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "87a6c86e9f85c22d4b6e67d73bb8823d",
|
"content-hash": "2cc047a2ea18870d90d8b51342c3003a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@ -1226,6 +1226,150 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-04-10T15:08:36+00:00"
|
"time": "2025-04-10T15:08:36+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "intervention/gif",
|
||||||
|
"version": "4.2.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Intervention/gif.git",
|
||||||
|
"reference": "5999eac6a39aa760fb803bc809e8909ee67b451a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Intervention/gif/zipball/5999eac6a39aa760fb803bc809e8909ee67b451a",
|
||||||
|
"reference": "5999eac6a39aa760fb803bc809e8909ee67b451a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^2.1",
|
||||||
|
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
|
||||||
|
"slevomat/coding-standard": "~8.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.8"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Intervention\\Gif\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Oliver Vogel",
|
||||||
|
"email": "oliver@intervention.io",
|
||||||
|
"homepage": "https://intervention.io/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Native PHP GIF Encoder/Decoder",
|
||||||
|
"homepage": "https://github.com/intervention/gif",
|
||||||
|
"keywords": [
|
||||||
|
"animation",
|
||||||
|
"gd",
|
||||||
|
"gif",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Intervention/gif/issues",
|
||||||
|
"source": "https://github.com/Intervention/gif/tree/4.2.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://paypal.me/interventionio",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Intervention",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://ko-fi.com/interventionphp",
|
||||||
|
"type": "ko_fi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-03-29T07:46:21+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "intervention/image",
|
||||||
|
"version": "3.11.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Intervention/image.git",
|
||||||
|
"reference": "d0f097b8a3fa8fb758efc9440b513aa3833cda17"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Intervention/image/zipball/d0f097b8a3fa8fb758efc9440b513aa3833cda17",
|
||||||
|
"reference": "d0f097b8a3fa8fb758efc9440b513aa3833cda17",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"intervention/gif": "^4.2",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"phpstan/phpstan": "^2.1",
|
||||||
|
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
|
||||||
|
"slevomat/coding-standard": "~8.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.8"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-exif": "Recommended to be able to read EXIF data properly."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Intervention\\Image\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Oliver Vogel",
|
||||||
|
"email": "oliver@intervention.io",
|
||||||
|
"homepage": "https://intervention.io/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP image manipulation",
|
||||||
|
"homepage": "https://image.intervention.io/",
|
||||||
|
"keywords": [
|
||||||
|
"gd",
|
||||||
|
"image",
|
||||||
|
"imagick",
|
||||||
|
"resize",
|
||||||
|
"thumbnail",
|
||||||
|
"watermark"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Intervention/image/issues",
|
||||||
|
"source": "https://github.com/Intervention/image/tree/3.11.3"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://paypal.me/interventionio",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Intervention",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://ko-fi.com/interventionphp",
|
||||||
|
"type": "ko_fi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-05-22T17:26:23+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/fortify",
|
"name": "laravel/fortify",
|
||||||
"version": "v1.25.4",
|
"version": "v1.25.4",
|
||||||
@ -6540,6 +6684,58 @@
|
|||||||
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
|
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
|
||||||
},
|
},
|
||||||
"time": "2022-06-03T18:03:27+00:00"
|
"time": "2022-06-03T18:03:27+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "z38/metzli",
|
||||||
|
"version": "v1.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/z38/metzli.git",
|
||||||
|
"reference": "ff8e22c492d02ba33dce8c039ffc6733cb877f63"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/z38/metzli/zipball/ff8e22c492d02ba33dce8c039ffc6733cb877f63",
|
||||||
|
"reference": "ff8e22c492d02ba33dce8c039ffc6733cb877f63",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.6.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "~5 || ~7",
|
||||||
|
"setasign/fpdf": "^1.8"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Metzli": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"Apache-2.0"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "z38",
|
||||||
|
"homepage": "https://github.com/z38",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP library to generate Aztec barcodes",
|
||||||
|
"homepage": "https://github.com/z38/metzli",
|
||||||
|
"keywords": [
|
||||||
|
"2d-barcode",
|
||||||
|
"aztec",
|
||||||
|
"barcode",
|
||||||
|
"library"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/z38/metzli/issues",
|
||||||
|
"source": "https://github.com/z38/metzli/tree/master"
|
||||||
|
},
|
||||||
|
"time": "2018-05-14T08:34:22+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
@ -17,7 +17,7 @@ return [
|
|||||||
|
|
||||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||||
'%s%s',
|
'%s%s',
|
||||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1, localhost:8004',
|
||||||
Sanctum::currentApplicationUrlWithPort()
|
Sanctum::currentApplicationUrlWithPort()
|
||||||
))),
|
))),
|
||||||
|
|
||||||
@ -79,5 +79,6 @@ return [
|
|||||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||||
],
|
],
|
||||||
|
'prefix' => 'api'
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -56,6 +56,8 @@ CREATE TABLE stock_position (
|
|||||||
position_symbol VARCHAR(5) NOT NULL,
|
position_symbol VARCHAR(5) NOT NULL,
|
||||||
position_name VARCHAR(32) DEFAULT NULL,
|
position_name VARCHAR(32) DEFAULT NULL,
|
||||||
shelf_id INT NOT NULL,
|
shelf_id INT NOT NULL,
|
||||||
|
capacity int default 0,
|
||||||
|
temporary bool default false,
|
||||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||||
updated_at DATETIME NULL ON UPDATE NOW(),
|
updated_at DATETIME NULL ON UPDATE NOW(),
|
||||||
FOREIGN KEY (shelf_id) REFERENCES stock_shelf(shelf_id)
|
FOREIGN KEY (shelf_id) REFERENCES stock_shelf(shelf_id)
|
||||||
@ -91,6 +93,7 @@ create table stock_batch
|
|||||||
supplier_id int default null,
|
supplier_id int default null,
|
||||||
tracking_number varchar(256) default null,
|
tracking_number varchar(256) default null,
|
||||||
arrival_date DATETIME DEFAULT null,
|
arrival_date DATETIME DEFAULT null,
|
||||||
|
default_batch bool default false,
|
||||||
created_at DATETIME DEFAULT now(),
|
created_at DATETIME DEFAULT now(),
|
||||||
updated_at DATETIME DEFAULT NULL ON UPDATE now(),
|
updated_at DATETIME DEFAULT NULL ON UPDATE now(),
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
@ -103,6 +106,7 @@ create table stock_entries
|
|||||||
supplier_id int unsigned not null,
|
supplier_id int unsigned not null,
|
||||||
count int default 0 not null, #needitovatelny ve formulari / jen odpis, inventura
|
count int default 0 not null, #needitovatelny ve formulari / jen odpis, inventura
|
||||||
original_count int default 0 not null,
|
original_count int default 0 not null,
|
||||||
|
original_count_invoice int default 0 not null,
|
||||||
price double null,
|
price double null,
|
||||||
bought date default null,
|
bought date default null,
|
||||||
description text null,
|
description text null,
|
||||||
@ -196,7 +200,7 @@ create table stock_entries_status_history
|
|||||||
status_note text default null,
|
status_note text default null,
|
||||||
created_at DATETIME DEFAULT now(),
|
created_at DATETIME DEFAULT now(),
|
||||||
updated_at DATETIME DEFAULT NULL ON UPDATE now(),
|
updated_at DATETIME DEFAULT NULL ON UPDATE now(),
|
||||||
FOREIGN KEY (stock_entries_id) REFERENCES stock_entries (id),
|
FOREIGN KEY (stock_entries_id) REFERENCES stock_entries (id) on delete cascade,
|
||||||
FOREIGN KEY (section_id) REFERENCES stock_section (section_id),
|
FOREIGN KEY (section_id) REFERENCES stock_section (section_id),
|
||||||
FOREIGN KEY (stock_entries_status_id) REFERENCES stock_entries_status (id)
|
FOREIGN KEY (stock_entries_status_id) REFERENCES stock_entries_status (id)
|
||||||
);
|
);
|
||||||
|
263
package-lock.json
generated
263
package-lock.json
generated
@ -12,8 +12,10 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"@inertiajs/react": "^2.0.0",
|
"@inertiajs/react": "^2.0.0",
|
||||||
"@inertiajs/server": "^0.1.0",
|
"@inertiajs/server": "^0.1.0",
|
||||||
|
"@material-tailwind/react": "^2.1.10",
|
||||||
"@mui/icons-material": "^7.1.0",
|
"@mui/icons-material": "^7.1.0",
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.0",
|
||||||
"@mui/x-date-pickers": "^8.3.0",
|
"@mui/x-date-pickers": "^8.3.0",
|
||||||
@ -1056,6 +1058,15 @@
|
|||||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@heroicons/react": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@inertiajs/core": {
|
"node_modules/@inertiajs/core": {
|
||||||
"version": "2.0.9",
|
"version": "2.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.0.9.tgz",
|
||||||
@ -1171,6 +1182,159 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@material-tailwind/react": {
|
||||||
|
"version": "2.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@material-tailwind/react/-/react-2.1.10.tgz",
|
||||||
|
"integrity": "sha512-xGU/mLDKDBp/qZ8Dp2XR7fKcTpDuFeZEBqoL9Bk/29kakKxNxjUGYSRHEFLsyOFf4VIhU6WGHdIS7tOA3QGJHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react": "0.19.0",
|
||||||
|
"classnames": "2.3.2",
|
||||||
|
"deepmerge": "4.2.2",
|
||||||
|
"framer-motion": "6.5.1",
|
||||||
|
"material-ripple-effects": "2.0.1",
|
||||||
|
"prop-types": "15.8.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"tailwind-merge": "1.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16 || ^17 || ^18",
|
||||||
|
"react-dom": "^16 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@material-tailwind/react/node_modules/@floating-ui/react": {
|
||||||
|
"version": "0.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.19.0.tgz",
|
||||||
|
"integrity": "sha512-fgYvN4ksCi5OvmPXkyOT8o5a8PSKHMzPHt+9mR6KYWdF16IAjWRLZPAAziI2sznaWT23drRFrYw64wdvYqqaQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^1.2.2",
|
||||||
|
"aria-hidden": "^1.1.3",
|
||||||
|
"tabbable": "^6.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@material-tailwind/react/node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.2.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@material-tailwind/react/node_modules/classnames": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@material-tailwind/react/node_modules/deepmerge": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@material-tailwind/react/node_modules/react": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@material-tailwind/react/node_modules/react-dom": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/animation": {
|
||||||
|
"version": "10.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz",
|
||||||
|
"integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/easing": "^10.18.0",
|
||||||
|
"@motionone/types": "^10.17.1",
|
||||||
|
"@motionone/utils": "^10.18.0",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/dom": {
|
||||||
|
"version": "10.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz",
|
||||||
|
"integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/animation": "^10.12.0",
|
||||||
|
"@motionone/generators": "^10.12.0",
|
||||||
|
"@motionone/types": "^10.12.0",
|
||||||
|
"@motionone/utils": "^10.12.0",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/easing": {
|
||||||
|
"version": "10.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz",
|
||||||
|
"integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/utils": "^10.18.0",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/generators": {
|
||||||
|
"version": "10.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz",
|
||||||
|
"integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/types": "^10.17.1",
|
||||||
|
"@motionone/utils": "^10.18.0",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/types": {
|
||||||
|
"version": "10.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz",
|
||||||
|
"integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@motionone/utils": {
|
||||||
|
"version": "10.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz",
|
||||||
|
"integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/types": "^10.17.1",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mui/core-downloads-tracker": {
|
"node_modules/@mui/core-downloads-tracker": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
|
||||||
@ -2759,6 +2923,18 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-hidden": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@ -3431,6 +3607,53 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "6.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz",
|
||||||
|
"integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@motionone/dom": "10.12.0",
|
||||||
|
"framesync": "6.0.1",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"popmotion": "11.0.3",
|
||||||
|
"style-value-types": "5.0.0",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "^0.8.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": ">=16.8 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/framer-motion/node_modules/@emotion/is-prop-valid": {
|
||||||
|
"version": "0.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
|
||||||
|
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/memoize": "0.7.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/framer-motion/node_modules/@emotion/memoize": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/framesync": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@ -3597,6 +3820,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hey-listen": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/highlight-words": {
|
"node_modules/highlight-words": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/highlight-words/-/highlight-words-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/highlight-words/-/highlight-words-2.0.0.tgz",
|
||||||
@ -4119,6 +4348,12 @@
|
|||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/material-ripple-effects": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/material-ripple-effects/-/material-ripple-effects-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hHlUkZAuXbP94lu02VgrPidbZ3hBtgXBtjlwR8APNqOIgDZMV8MCIcsclL8FmGJQHvnORyvoQgC965vPsiyXLQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -4369,6 +4604,18 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/popmotion": {
|
||||||
|
"version": "11.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz",
|
||||||
|
"integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"framesync": "6.0.1",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"style-value-types": "5.0.0",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||||
@ -4792,6 +5039,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-value-types": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stylis": {
|
"node_modules/stylis": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
||||||
@ -4832,6 +5089,12 @@
|
|||||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwind-merge": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-+fflfPxvHFr81hTJpQ3MIwtqgvefHZFUHFiIHpVIRXvG/nX9+gu2P7JNlFu2bfDMJ+uHhi/pUgzaYacMoXv+Ww==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz",
|
||||||
|
@ -33,8 +33,10 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"@inertiajs/react": "^2.0.0",
|
"@inertiajs/react": "^2.0.0",
|
||||||
"@inertiajs/server": "^0.1.0",
|
"@inertiajs/server": "^0.1.0",
|
||||||
|
"@material-tailwind/react": "^2.1.10",
|
||||||
"@mui/icons-material": "^7.1.0",
|
"@mui/icons-material": "^7.1.0",
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.0",
|
||||||
"@mui/x-date-pickers": "^8.3.0",
|
"@mui/x-date-pickers": "^8.3.0",
|
||||||
|
@ -2,47 +2,12 @@
|
|||||||
|
|
||||||
@plugin '@tailwindcss/forms';
|
@plugin '@tailwindcss/forms';
|
||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
@plugin "daisyui";
|
|
||||||
|
|
||||||
|
@plugin "daisyui" {
|
||||||
|
themes: light --default, dracula --prefersdark;
|
||||||
@plugin "daisyui/theme" {
|
|
||||||
name: "insanedestroyer";
|
|
||||||
default: false;
|
|
||||||
prefersdark: false;
|
|
||||||
color-scheme: "dark";
|
|
||||||
--color-base-100: oklch(20% 0.042 265.755);
|
|
||||||
--color-base-200: oklch(21% 0.034 264.665);
|
|
||||||
--color-base-300: oklch(27% 0.033 256.848);
|
|
||||||
--color-base-content: oklch(96% 0.003 264.542);
|
|
||||||
--color-primary: oklch(44% 0.043 257.281);
|
|
||||||
--color-primary-content: oklch(98% 0.003 247.858);
|
|
||||||
--color-secondary: oklch(64% 0.2 131.684);
|
|
||||||
--color-secondary-content: oklch(98% 0.031 120.757);
|
|
||||||
--color-accent: oklch(59% 0.145 163.225);
|
|
||||||
--color-accent-content: oklch(97% 0.021 166.113);
|
|
||||||
--color-neutral: oklch(37% 0.034 259.733);
|
|
||||||
--color-neutral-content: oklch(98% 0.002 247.839);
|
|
||||||
--color-info: oklch(62% 0.214 259.815);
|
|
||||||
--color-info-content: oklch(97% 0.014 254.604);
|
|
||||||
--color-success: oklch(72% 0.219 149.579);
|
|
||||||
--color-success-content: oklch(98% 0.018 155.826);
|
|
||||||
--color-warning: oklch(70% 0.213 47.604);
|
|
||||||
--color-warning-content: oklch(98% 0.016 73.684);
|
|
||||||
--color-error: oklch(63% 0.237 25.331);
|
|
||||||
--color-error-content: oklch(97% 0.013 17.38);
|
|
||||||
--radius-selector: 0.25rem;
|
|
||||||
--radius-field: 2rem;
|
|
||||||
--radius-box: 0.5rem;
|
|
||||||
--size-selector: 0.25rem;
|
|
||||||
--size-field: 0.25rem;
|
|
||||||
--border: 1px;
|
|
||||||
--depth: 0;
|
|
||||||
--noise: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||||
@source '../../vendor/laravel/jetstream/**/*.blade.php';
|
@source '../../vendor/laravel/jetstream/**/*.blade.php';
|
||||||
|
|
||||||
|
209
resources/js/Components/BatchInfoWindow.tsx
Normal file
209
resources/js/Components/BatchInfoWindow.tsx
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
// components/BatchInfoWindow.tsx
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
|
import {
|
||||||
|
MaterialReactTable,
|
||||||
|
type MRT_ColumnDef,
|
||||||
|
type MRT_PaginationState,
|
||||||
|
type MRT_SortingState,
|
||||||
|
} from 'material-react-table'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faQrcode, faListOl } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { NextRouter } from 'next/router'
|
||||||
|
import { StockBatch, StockEntry, Supplier } from '@/types'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { size } from 'lodash'
|
||||||
|
|
||||||
|
import {downloadBatchBarcode} from "@/functions/functions"
|
||||||
|
|
||||||
|
interface Status {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchInfoWindowProps {
|
||||||
|
selectedBatch: StockBatch | null
|
||||||
|
formatDate?: (value: string, withTime?: boolean) => string
|
||||||
|
router: NextRouter
|
||||||
|
route: (name: string, params: Record<string, any>) => string
|
||||||
|
|
||||||
|
/** We now allow `entries` to be null, but internally we coalesce to [] */
|
||||||
|
entries: StockEntry[] | null
|
||||||
|
|
||||||
|
/** Any other props you had (for pagination / sorting, etc.) */
|
||||||
|
entryColumns?: MRT_ColumnDef<StockEntry>[]
|
||||||
|
entriesCount?: number
|
||||||
|
entriesLoading?: boolean
|
||||||
|
entriesPagination?: MRT_PaginationState
|
||||||
|
entriesSorting?: MRT_SortingState
|
||||||
|
entriesFilter?: string
|
||||||
|
setEntriesPagination?: (updater: MRT_PaginationState) => void
|
||||||
|
setEntriesSorting?: (sorter: MRT_SortingState) => void
|
||||||
|
setEntriesFilter?: (filter: string) => void
|
||||||
|
|
||||||
|
openEntry?: (entry: StockEntry) => void
|
||||||
|
recountEnabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFormatter = (value: string, withTime = true) => {
|
||||||
|
const d = new Date(value)
|
||||||
|
const dd = String(d.getDate()).padStart(2, '0')
|
||||||
|
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const yyyy = d.getFullYear()
|
||||||
|
if (!withTime) return `${dd}-${mm}-${yyyy}`
|
||||||
|
const hh = String(d.getHours()).padStart(2, '0')
|
||||||
|
const mi = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
return `${dd}-${mm}-${yyyy} ${hh}:${mi}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusIds = [2, 6, 7, 8]
|
||||||
|
|
||||||
|
export default function BatchInfoWindow({
|
||||||
|
selectedBatch,
|
||||||
|
formatDate = defaultFormatter,
|
||||||
|
router,
|
||||||
|
route,
|
||||||
|
openEntry,
|
||||||
|
recountEnabled
|
||||||
|
}: BatchInfoWindowProps) {
|
||||||
|
const [statuses, setStatuses] = useState<Status[]>([])
|
||||||
|
/** Always keep an array here (never null). */
|
||||||
|
|
||||||
|
/** Fetch statuses once */
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(selectedBatch);
|
||||||
|
axios
|
||||||
|
.get('/api/stockStatusList')
|
||||||
|
.then((res) => setStatuses(res.data.statuses))
|
||||||
|
.catch(() => toast.error('Failed to load status list'))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/** Example helper to calculate ratios */
|
||||||
|
function calculateStatusRatio(entries: StockEntry[], statusId: number) {
|
||||||
|
const total = entries.length
|
||||||
|
const count = entries.filter((e) =>
|
||||||
|
e.status_history?.some((h: any) => h.stock_entries_status_id === statusId)
|
||||||
|
).length
|
||||||
|
const ratio = total > 0 ? count / total : 0
|
||||||
|
return { count, total, ratio }
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryColumnsMemo = useMemo<MRT_ColumnDef<StockEntry>[]>(
|
||||||
|
() => [
|
||||||
|
{ accessorKey: 'id', header: 'ID', size: 80 },
|
||||||
|
{ accessorKey: 'physical_item.name', header: 'Physical Item', size: 200 },
|
||||||
|
{ accessorKey: 'supplier.name', header: 'Supplier', size: 150 },
|
||||||
|
{
|
||||||
|
accessorKey: 'price',
|
||||||
|
header: 'Price',
|
||||||
|
size: 100,
|
||||||
|
Cell: ({ cell }) => cell.getValue<number>()?.toFixed(2) || '-',
|
||||||
|
},
|
||||||
|
// …add the rest of your columns here…
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasNonCountedStatus = selectedBatch?.stock_entries?.some((stockEntry) => {
|
||||||
|
const nullSectionStatuses =
|
||||||
|
stockEntry.status_history?.filter((h: any) => h.section_id === null) ?? []
|
||||||
|
if (nullSectionStatuses.length === 0) return true
|
||||||
|
const latest = nullSectionStatuses.reduce((prev, curr) =>
|
||||||
|
new Date(prev.created_at) > new Date(curr.created_at) ? prev : curr
|
||||||
|
)
|
||||||
|
return latest.stock_entries_status_id !== 2
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:space-x-4">
|
||||||
|
{/* Left: Batch Details & Ratios */}
|
||||||
|
<div className="sm:w-1/3 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-lg mb-2">Batch Details</h3>
|
||||||
|
|
||||||
|
{selectedBatch ? (
|
||||||
|
<ul className="list-disc list-inside">
|
||||||
|
<li>
|
||||||
|
<strong>ID:</strong> {selectedBatch.id}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Supplier:</strong> {selectedBatch.supplier.name}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Tracking #:</strong> {selectedBatch.tracking_number}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Arrival:</strong>{' '}
|
||||||
|
{formatDate(selectedBatch.arrival_date, false)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500">No batch selected.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedBatch && (
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
<h3 className="font-semibold text-md mb-2">Status Ratios</h3>
|
||||||
|
{statusIds.map((id) => {
|
||||||
|
const { count, total, ratio } = calculateStatusRatio(
|
||||||
|
selectedBatch.stock_entries,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
const statusData = statuses.find((s) => s.id === id)
|
||||||
|
return (
|
||||||
|
<p key={id} className="text-sm">
|
||||||
|
{statusData?.name}: {count} / {total} (
|
||||||
|
{(ratio * 100).toFixed(1)}%)
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex space-x-2">
|
||||||
|
<div className="tooltip" data-tip="Batch QR kód">
|
||||||
|
<button className="btn bg-[#666666] text-[19px]" onClick={() => downloadBatchBarcode(selectedBatch?.id)}>
|
||||||
|
<FontAwesomeIcon icon={faQrcode} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasNonCountedStatus && selectedBatch && recountEnabled && (
|
||||||
|
<div className="tooltip" data-tip="Přepočítat">
|
||||||
|
<button
|
||||||
|
className="btn btn-warning text-[19px]"
|
||||||
|
onClick={() =>
|
||||||
|
router.get(
|
||||||
|
route('batchCounting', { selectedBatch: selectedBatch.id })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faListOl} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Stock Entries Table (never pass null!) */}
|
||||||
|
<div className="sm:w-2/3 flex flex-col sm:pl-6">
|
||||||
|
<h3 className="font-bold text-lg mb-2">Stock Entries</h3>
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<MaterialReactTable
|
||||||
|
columns={entryColumnsMemo}
|
||||||
|
data={selectedBatch?.stock_entries} // ← guaranteed array, never null
|
||||||
|
manualPagination
|
||||||
|
manualSorting
|
||||||
|
enableGlobalFilter
|
||||||
|
initialState={{ pagination: { pageSize: 100 } }}
|
||||||
|
muiTableBodyRowProps={({ row }) => ({
|
||||||
|
onClick: () => openEntry?.(row.original),
|
||||||
|
style: { cursor: 'pointer' },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -12,13 +12,14 @@ const ModalPDA: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal modal-open">
|
<div className="modal modal-open">
|
||||||
<div className="modal-box relative">
|
<div className="modal-box max-w-6xl flex flex-col space-x-6 p-6 relative">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-circle absolute right-2 top-2"
|
className="btn btn-sm btn-circle absolute right-2 top-2"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
292
resources/js/Components/ShipmentItemsAccordion.tsx
Normal file
292
resources/js/Components/ShipmentItemsAccordion.tsx
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
import React, { useState, useRef } from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faBoxesStacked,
|
||||||
|
faCheckCircle,
|
||||||
|
faCheckToSlot,
|
||||||
|
faClipboardQuestion,
|
||||||
|
faShuffle,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { ShipmentRequest } from "@/interfaces/interfaces";
|
||||||
|
|
||||||
|
interface ShipmentItemsAccordionProps {
|
||||||
|
selectedShipment: ShipmentRequest;
|
||||||
|
buttonStates: { [itemId: number]: any };
|
||||||
|
setButtonStates: React.Dispatch<React.SetStateAction<{ [itemId: number]: any }>>;
|
||||||
|
parentModalHandle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShipmentItemsAccordion({
|
||||||
|
selectedShipment,
|
||||||
|
buttonStates,
|
||||||
|
setButtonStates,
|
||||||
|
parentModalHandle,
|
||||||
|
}: ShipmentItemsAccordionProps) {
|
||||||
|
// Track which accordion item (by id) is open. Default to first item.
|
||||||
|
const [openAccordion, setOpenAccordion] = useState<number | null>(
|
||||||
|
selectedShipment.items[0]?.id || null
|
||||||
|
);
|
||||||
|
// Track completed items by their id.
|
||||||
|
const [completedItems, setCompletedItems] = useState<{ [id: number]: boolean }>({});
|
||||||
|
// Ref to store a hold timer for touch devices.
|
||||||
|
const holdTimerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// Modal state
|
||||||
|
const [modalPositionOpen, setModalPositionOpen] = useState(false);
|
||||||
|
const [currentItem, setCurrentItem] = useState<any>(null);
|
||||||
|
const [fromPosition, setFromPosition] = useState<string>("");
|
||||||
|
const [toPosition, setToPosition] = useState<string>("");
|
||||||
|
// modalMode: "full" or "select-only"
|
||||||
|
const [modalMode, setModalMode] = useState<"full" | "select-only">("full");
|
||||||
|
// modalKey: "neniNaPozici" | "doskladnit" | "zmenitPozici"
|
||||||
|
const [modalKey, setModalKey] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Helper to get or initialize the button state for an item
|
||||||
|
const getItemState = (itemId: number) =>
|
||||||
|
buttonStates[itemId] || {
|
||||||
|
neniNaPozici: { value: false, details: [] },
|
||||||
|
doskladnit: { value: false, details: [] },
|
||||||
|
zmenitPozici: { value: false, details: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generic update function for button state for a given item and key.
|
||||||
|
const updateButtonState = (itemId: number, key: string, details: any[] = []) => {
|
||||||
|
setButtonStates((prev) => {
|
||||||
|
const currentState = getItemState(itemId);
|
||||||
|
const currentButtonState = currentState[key] || { value: false, details: [] };
|
||||||
|
const toggledValue = !currentButtonState.value;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[itemId]: {
|
||||||
|
...currentState,
|
||||||
|
[key]: { value: toggledValue, details: toggledValue ? details : [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the modal with appropriate mode and key
|
||||||
|
const handleOpenModal = (
|
||||||
|
item: any,
|
||||||
|
key: "neniNaPozici" | "doskladnit" | "zmenitPozici",
|
||||||
|
mode: "full" | "select-only"
|
||||||
|
) => {
|
||||||
|
setCurrentItem(item);
|
||||||
|
setModalKey(key);
|
||||||
|
setModalMode(mode);
|
||||||
|
setModalPositionOpen(true);
|
||||||
|
// Optionally notify parent modal
|
||||||
|
// parentModalHandle();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Confirm handler for the modal
|
||||||
|
const handleConfirmModal = () => {
|
||||||
|
if (currentItem && modalKey) {
|
||||||
|
if (modalMode === "full") {
|
||||||
|
updateButtonState(currentItem.id, modalKey, [
|
||||||
|
{ from: fromPosition, to: toPosition },
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// select-only: only pass the "from" detail
|
||||||
|
updateButtonState(currentItem.id, modalKey, [{ position: fromPosition }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reset modal state
|
||||||
|
setModalPositionOpen(false);
|
||||||
|
setCurrentItem(null);
|
||||||
|
setFromPosition("");
|
||||||
|
setToPosition("");
|
||||||
|
setModalMode("full");
|
||||||
|
setModalKey(null);
|
||||||
|
// Optionally notify parent modal
|
||||||
|
// parentModalHandle();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle the open state of an accordion item.
|
||||||
|
const handleToggle = (id: number) => {
|
||||||
|
if (openAccordion === id) {
|
||||||
|
setOpenAccordion(null);
|
||||||
|
} else {
|
||||||
|
setOpenAccordion(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark an item as completed.
|
||||||
|
const handleComplete = (id: number) => {
|
||||||
|
setCompletedItems((prev) => ({ ...prev, [id]: true }));
|
||||||
|
if (openAccordion === id) {
|
||||||
|
setOpenAccordion(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Touch event handlers for long press.
|
||||||
|
const onTouchStartHandler = (id: number) => {
|
||||||
|
holdTimerRef.current = window.setTimeout(() => {
|
||||||
|
handleComplete(id);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchEndHandler = () => {
|
||||||
|
if (holdTimerRef.current) {
|
||||||
|
clearTimeout(holdTimerRef.current);
|
||||||
|
holdTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{selectedShipment.items.map((item) => {
|
||||||
|
const itemState = getItemState(item.id);
|
||||||
|
const isOpen = openAccordion === item.id;
|
||||||
|
const isCompleted = completedItems[item.id];
|
||||||
|
// If an item is not open, it appears "greyed out"
|
||||||
|
const headerOpacity = isOpen ? "opacity-100" : "opacity-50";
|
||||||
|
const buttonFunctionality = isOpen ? "" : "pointer-events-none";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.id} className="space-y-2">
|
||||||
|
<div
|
||||||
|
className={`collapse collapse-arrow border border-t-2 border-warning border-b-0 border-l-0 border-r-0 bg-base-100 ${headerOpacity}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isOpen}
|
||||||
|
onChange={() => handleToggle(item.id)}
|
||||||
|
className="peer"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="collapse-title flex justify-between items-center text-base-content px-4 py-2 cursor-pointer"
|
||||||
|
onDoubleClick={() => handleComplete(item.id)}
|
||||||
|
onTouchStart={() => onTouchStartHandler(item.id)}
|
||||||
|
onTouchEnd={onTouchEndHandler}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-base-content">
|
||||||
|
{item.quantity}× {item.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-base-content">{item.item_note}</p>
|
||||||
|
<div className="flex justify-between text-sm text-base-content mt-1">
|
||||||
|
<span>{item.model_number}</span>
|
||||||
|
<span>
|
||||||
|
{(item.price / item.quantity).toFixed(2)} {selectedShipment.currency}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isCompleted && (
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} className="text-success" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="collapse-content bg-base-200 text-base-content p-4">
|
||||||
|
<div className="flex flex-wrap gap-4 border border-base-300 rounded bg-base-100 p-3">
|
||||||
|
{Object.entries(item.stockData).flatMap(([stockName, stockArray]) =>
|
||||||
|
stockArray.map((stock, idx) => (
|
||||||
|
<div key={`${stockName}-${idx}`} className="text-sm">
|
||||||
|
<p className="font-semibold">{stockName}</p>
|
||||||
|
<p>Location: {stock.location}</p>
|
||||||
|
<p>Count: {stock.count} ks</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`flex flex-wrap items-center gap-2 text-base-content ${headerOpacity}`}>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenModal(item, "neniNaPozici", "select-only")
|
||||||
|
}
|
||||||
|
className={`btn btn-outline btn-sm ${!isOpen ? "pointer-events-none" : ""} ${itemState.neniNaPozici.value ? "btn-success" : ""} w-full sm:w-auto`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faClipboardQuestion} className="mr-2" />
|
||||||
|
Není na pozici
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenModal(item, "doskladnit", "select-only")
|
||||||
|
}
|
||||||
|
className={`btn btn-outline btn-sm ${!isOpen ? "pointer-events-none" : ""} ${itemState.doskladnit.value ? "btn-success" : ""} w-full sm:w-auto`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBoxesStacked} className="mr-2" />
|
||||||
|
Doplnit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenModal(item, "zmenitPozici", "full")
|
||||||
|
}
|
||||||
|
className={`btn btn-outline btn-sm ${!isOpen ? "pointer-events-none" : ""} ${itemState.zmenitPozici.value ? "btn-success" : ""} w-full sm:w-auto`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faShuffle} className="mr-2" />
|
||||||
|
Změnit pozici
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleComplete(item.id)}
|
||||||
|
className={`btn btn-outline btn-sm ml-auto ${buttonFunctionality} w-full sm:w-auto`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheckToSlot} className="text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Modal for changing position */}
|
||||||
|
<div className={`modal ${modalPositionOpen ? "modal-open" : ""}`}>
|
||||||
|
<div className="modal-box max-w-md">
|
||||||
|
<h3 className="font-bold text-lg">Change Position</h3>
|
||||||
|
<div className="space-y-4 mt-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-base-content mb-1">
|
||||||
|
From Position
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered w-full"
|
||||||
|
value={fromPosition}
|
||||||
|
onChange={(e) => setFromPosition(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Select position</option>
|
||||||
|
{currentItem &&
|
||||||
|
Object.entries(currentItem.stockData).flatMap(
|
||||||
|
([stockName, stockArray]) =>
|
||||||
|
stockArray.map((stock, idx) => (
|
||||||
|
<option
|
||||||
|
key={`${stockName}-${idx}`}
|
||||||
|
value={`${stockName}--${stock.location}`}
|
||||||
|
>
|
||||||
|
<strong>{stockName}</strong> --- {stock.location}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{modalMode === "full" && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-base-content mb-1">
|
||||||
|
To Position
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
value={toPosition}
|
||||||
|
onChange={(e) => setToPosition(e.target.value)}
|
||||||
|
placeholder="Enter new position"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="modal-action mt-4 flex justify-end gap-2">
|
||||||
|
<button className="btn btn-primary btn-sm" onClick={handleConfirmModal}>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline btn-sm"
|
||||||
|
onClick={() => setModalPositionOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
265
resources/js/Components/WarehouseExpediceDialog.tsx
Normal file
265
resources/js/Components/WarehouseExpediceDialog.tsx
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faCircleInfo, faCubesStacked, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { ShipmentRequest } from "@/interfaces/interfaces";
|
||||||
|
import ShipmentItemsAccordion from "@/Components/ShipmentItemsAccordion";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
interface WarehouseExpediceDialogProps {
|
||||||
|
selectedShipment: ShipmentRequest;
|
||||||
|
isDialogOpen: boolean;
|
||||||
|
closeDialog: () => void;
|
||||||
|
handleDialogProcess: () => void;
|
||||||
|
dialogToggleClose: () => void;
|
||||||
|
selectedType: "parcels" | "processed" | null;
|
||||||
|
buttonStates: { [itemId: number]: any };
|
||||||
|
setButtonStates: React.Dispatch<React.SetStateAction<{ [itemId: number]: any }>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WarehouseExpediceDialog({
|
||||||
|
selectedShipment,
|
||||||
|
isDialogOpen,
|
||||||
|
closeDialog,
|
||||||
|
handleDialogProcess,
|
||||||
|
dialogToggleClose,
|
||||||
|
selectedType,
|
||||||
|
buttonStates,
|
||||||
|
setButtonStates,
|
||||||
|
}: WarehouseExpediceDialogProps) {
|
||||||
|
// Track which tab is active: "stock" or "details"
|
||||||
|
const [activeTab, setActiveTab] = useState<"stock" | "details">("stock");
|
||||||
|
const [imageArray, setImageArray] = useState<Record<number, { url: string }>>({});
|
||||||
|
|
||||||
|
const fetchItemImages = () => {
|
||||||
|
axios
|
||||||
|
.get("/api/expediceListWMS/getImage", {
|
||||||
|
params: { selectedShipmentID: selectedShipment.id },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setImageArray(response.data.imageArray || {});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching image:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Whenever user clicks the Details tab, fetch images
|
||||||
|
const handleDetailsClick = () => {
|
||||||
|
setActiveTab("details");
|
||||||
|
fetchItemImages();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`modal ${isDialogOpen ? "modal-open" : ""}`}>
|
||||||
|
{/* Modal backdrop + box */}
|
||||||
|
<div className="modal-box max-w-4xl max-h-[90vh] overflow-hidden p-0">
|
||||||
|
{/* Header with Tabs and Close button */}
|
||||||
|
<div className="flex items-center justify-between border-b border-base-300 px-4 py-3">
|
||||||
|
<div className="tabs">
|
||||||
|
<a
|
||||||
|
className={`tab ${activeTab === "stock" ? "tab-active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("stock")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faCubesStacked} />
|
||||||
|
<span>Items</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className={`tab ${activeTab === "details" ? "tab-active" : ""}`}
|
||||||
|
onClick={handleDetailsClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} />
|
||||||
|
<span>Details</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={closeDialog}
|
||||||
|
className="btn btn-ghost btn-circle text-base-content hover:text-error"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="overflow-y-auto p-4 bg-base-100">
|
||||||
|
{activeTab === "details" && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Reference */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold text-base-content">
|
||||||
|
Ref. # {selectedShipment.shipment_reference}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Items Section */}
|
||||||
|
<div className="bg-base-200 p-4 rounded-lg shadow">
|
||||||
|
<h3 className="text-lg font-semibold text-base-content mb-2">Items</h3>
|
||||||
|
{selectedShipment.items.map((item) => (
|
||||||
|
<div key={item.id} className="collapse collapse-arrow border border-base-300 rounded-lg mb-4">
|
||||||
|
<input type="checkbox" className="peer" />
|
||||||
|
<div className="collapse-title flex items-center justify-between bg-base-100 text-base-content px-4 py-2">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href={imageArray[item.id]?.url || "#"}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-24 h-36 bg-base-300 rounded overflow-hidden flex-shrink-0 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{imageArray[item.id]?.url ? (
|
||||||
|
<img src={imageArray[item.id].url} alt="Item" className="w-full h-full object-center" />
|
||||||
|
) : (
|
||||||
|
<div className="w-auto h-full bg-base-200 flex items-center justify-center text-sm text-base-content">
|
||||||
|
No Image
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold">{item.quantity}× {item.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-base-content">
|
||||||
|
{(item.price / item.quantity).toFixed(2)} {selectedShipment.currency}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="collapse-content bg-base-100 text-base-content p-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Model Number:</p>
|
||||||
|
<p>{item.model_number}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Origin:</p>
|
||||||
|
<p>{item.originCountry}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Weight:</p>
|
||||||
|
<p>{item.weight}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{item.stockData && Object.keys(item.stockData).length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<p className="font-semibold mb-2">Stock Information:</p>
|
||||||
|
{Object.entries(item.stockData).flatMap(([stockName, stockArray]) =>
|
||||||
|
stockArray.map((stock, idx) => (
|
||||||
|
<div
|
||||||
|
key={`${stockName}-${idx}`}
|
||||||
|
className="border-b border-base-300 pb-2 mb-2 last:border-0"
|
||||||
|
>
|
||||||
|
<p className="font-bold">{stockName}</p>
|
||||||
|
<p>Location: {stock.location}</p>
|
||||||
|
<p>Count: {stock.count} ks</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delivery Address */}
|
||||||
|
<div className="bg-base-200 p-4 rounded-lg shadow">
|
||||||
|
<h3 className="text-lg font-semibold text-base-content mb-2">Delivery Address</h3>
|
||||||
|
<div className="space-y-1 text-base-content">
|
||||||
|
<p>
|
||||||
|
<span className="font-semibold">Name:</span> {selectedShipment.delivery_address_name}
|
||||||
|
</p>
|
||||||
|
{selectedShipment.delivery_address_company_name && (
|
||||||
|
<p>
|
||||||
|
<span className="font-semibold">Company:</span> {selectedShipment.delivery_address_company_name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<span className="font-semibold">Street:</span> {selectedShipment.delivery_address_street_name}{" "}
|
||||||
|
{selectedShipment.delivery_address_street_number}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-semibold">City:</span> {selectedShipment.delivery_address_city}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-semibold">ZIP:</span> {selectedShipment.delivery_address_zip}
|
||||||
|
</p>
|
||||||
|
{selectedShipment.delivery_address_state_iso && (
|
||||||
|
<p>
|
||||||
|
<span className="font-semibold">State:</span> {selectedShipment.delivery_address_state_iso}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<span className="font-semibold">Country:</span> {selectedShipment.delivery_address_country_iso}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Carrier & Shipment Info */}
|
||||||
|
<div className="bg-base-200 p-4 rounded-lg shadow">
|
||||||
|
<h3 className="text-lg font-semibold text-base-content mb-2">Carrier & Shipment Info</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-base-content">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Carrier:</p>
|
||||||
|
<p>{selectedShipment.carrier.carrier_name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Tracking Number:</p>
|
||||||
|
<p>{selectedShipment.shipment?.tracking_number || ""}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Shipment Price:</p>
|
||||||
|
<p>
|
||||||
|
{selectedShipment.currency} {selectedShipment.shipment_price}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Shipment Value:</p>
|
||||||
|
<p>{selectedShipment.shipment_value}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Weight:</p>
|
||||||
|
<p>{selectedShipment.weight}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Dimensions:</p>
|
||||||
|
<p>
|
||||||
|
{selectedShipment.length} × {selectedShipment.width} × {selectedShipment.height}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "stock" && (
|
||||||
|
<div className="text-base-content">
|
||||||
|
<ShipmentItemsAccordion
|
||||||
|
selectedShipment={selectedShipment}
|
||||||
|
buttonStates={buttonStates}
|
||||||
|
setButtonStates={setButtonStates}
|
||||||
|
parentModalHandle={dialogToggleClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer with action button */}
|
||||||
|
<div className="modal-action border-t border-base-300 p-4 flex justify-end gap-2">
|
||||||
|
{selectedType === "parcels" && (
|
||||||
|
<button className="btn btn-primary" onClick={handleDialogProcess}>
|
||||||
|
Mark as Processed
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{selectedType === "processed" && (
|
||||||
|
<button className="btn btn-error" onClick={handleDialogProcess}>
|
||||||
|
Mark as Unprocessed
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="btn btn-ghost" onClick={closeDialog}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
35
resources/js/Components/modals/BatchInfoModal.tsx
Normal file
35
resources/js/Components/modals/BatchInfoModal.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// components/modals/BatchInfoModal.tsx
|
||||||
|
import React from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { StockBatch, StockEntry } from "@/types"
|
||||||
|
import BatchInfoWindow from "@/Components/BatchInfoWindow";
|
||||||
|
import {router} from "@inertiajs/react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void
|
||||||
|
selectedBatch: () => StockBatch
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const BatchInfoModal: React.FC<Props> = ({ onClose, selectedBatch }) => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-bold text-lg">Batch # {selectedBatch.id}</h3>
|
||||||
|
|
||||||
|
<BatchInfoWindow
|
||||||
|
selectedBatch={selectedBatch}
|
||||||
|
router={router}
|
||||||
|
route={(name, params) => route(name, params)}
|
||||||
|
entries={null}
|
||||||
|
openEntry={() => {}}
|
||||||
|
recountEnabled={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BatchInfoModal
|
210
resources/js/Components/modals/ChangeCountModal.tsx
Normal file
210
resources/js/Components/modals/ChangeCountModal.tsx
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { StockPosition, StockEntry } from '@/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void
|
||||||
|
selectedPosition: StockPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChangeCountModal: React.FC<Props> = ({ onClose, selectedPosition }) => {
|
||||||
|
const [selectedEntry, setSelectedEntry] = React.useState<StockEntry | null>(null)
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false)
|
||||||
|
const [newCount, setNewCount] = React.useState<number | null>(null)
|
||||||
|
const [loading, setLoading] = React.useState(false)
|
||||||
|
|
||||||
|
|
||||||
|
const handleChangeCount = async () => {
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
const toastId = toast.loading('Changing count…')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(
|
||||||
|
'/api/pdaView/changeCount',
|
||||||
|
{
|
||||||
|
entry_id: selectedEntry.id,
|
||||||
|
section_id: selectedEntry.pivot.section_id,
|
||||||
|
count: selectedEntry.pivot.count,
|
||||||
|
new_count: newCount,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
switch (data.error) {
|
||||||
|
case 'validation_failed':
|
||||||
|
toast.error('Validation failed. Check inputs.')
|
||||||
|
break
|
||||||
|
case 'not_found':
|
||||||
|
toast.error('Entry or section not found.')
|
||||||
|
break
|
||||||
|
case 'section_occupied':
|
||||||
|
toast.error('Target section is already occupied.')
|
||||||
|
break
|
||||||
|
case 'insufficient_capacity':
|
||||||
|
toast.error('Not enough capacity in target section.')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
toast.error(data.message ?? 'Server error during count change.')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
toast.success('Count changed on position.')
|
||||||
|
resetAndClose()
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.dismiss()
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
const payload = err.response.data
|
||||||
|
if (payload.error === 'validation_failed') {
|
||||||
|
toast.error('Validation failed. Check inputs.')
|
||||||
|
} else if (payload.error === 'not_found') {
|
||||||
|
toast.error('Entry or section not found.')
|
||||||
|
} else if (payload.error === 'section_occupied') {
|
||||||
|
toast.error('Target section is already occupied.')
|
||||||
|
} else if (payload.error === 'insufficient_capacity') {
|
||||||
|
toast.error('Not enough capacity in target section.')
|
||||||
|
} else {
|
||||||
|
toast.error(payload.message || 'Unknown error occurred.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('Network error. Please try again.')
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetAndClose = () => {
|
||||||
|
setSelectedEntry(null)
|
||||||
|
setNewCount(null)
|
||||||
|
setLoading(false)
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build flat list of entries grouped by section
|
||||||
|
const entriesList = React.useMemo<StockEntry[]>(() => {
|
||||||
|
if (!selectedPosition.sections) return []
|
||||||
|
return selectedPosition.sections.flatMap((section) =>
|
||||||
|
section.entries.map((entry) => ({ ...entry, pivot: entry.pivot, _section: section }))
|
||||||
|
)
|
||||||
|
}, [selectedPosition])
|
||||||
|
console.log(entriesList);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-bold text-lg">Change item count</h3>
|
||||||
|
|
||||||
|
{/* Entry Dropdown */}
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Select Item</span>
|
||||||
|
</label>
|
||||||
|
<div className="dropdown w-full">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn w-full justify-between"
|
||||||
|
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{selectedEntry ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
src={selectedEntry?.physical_item?.image_url}
|
||||||
|
alt={selectedEntry?.physical_item?.name}
|
||||||
|
className="w-6 h-6 rounded-full"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{selectedEntry?.physical_item?.name} (Count: {selectedEntry?.pivot.count}) in Sect.{' '}
|
||||||
|
{entriesList.find(e => e.id === selectedEntry.id)._section.section_symbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Select item...'
|
||||||
|
)}
|
||||||
|
<svg
|
||||||
|
className="fill-current w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M5.516 7.548a.75.75 0 0 1 1.06 0L10 10.972l3.424-3.424a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className={`dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full mt-1 ${
|
||||||
|
isDropdownOpen ? 'block' : 'hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{entriesList.map((entry) => (
|
||||||
|
<li key={`${entry.id}-${entry.pivot.section_id}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center px-2 py-1 hover:bg-gray-100 rounded"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedEntry(entry)
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={entry.physical_item?.image_url}
|
||||||
|
alt={entry.physical_item?.name}
|
||||||
|
className="w-6 h-6 rounded-full mr-2"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{entry.physical_item?.name} ({entry.pivot?.count} ks) — Section {entry._section?.section_symbol}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{entriesList.length === 0 && (
|
||||||
|
<li>
|
||||||
|
<span className="px-2 py-1 text-gray-500">No items in this position</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{selectedEntry ? (
|
||||||
|
<label className="input">
|
||||||
|
<input type="number" className="grow" placeholder="123" onChange={(e) => setNewCount(parseInt(e.target.value))} />
|
||||||
|
<kbd className="kbd kbd-sm">123</kbd>
|
||||||
|
</label>
|
||||||
|
): ""}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simulate Scan Button */}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleChangeCount}
|
||||||
|
disabled={!selectedEntry || loading}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Waiting for scan indicator */}
|
||||||
|
{selectedEntry && newCount === null && !loading && (
|
||||||
|
<div className="flex items-center space-x-2 mt-2">
|
||||||
|
<span>Waiting for section scan...</span>
|
||||||
|
<span className="loading loading-spinner text-primary"></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cancel Button */}
|
||||||
|
<div className="modal-action flex justify-end pt-2">
|
||||||
|
<button type="button" className="btn" onClick={resetAndClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangeCountModal
|
@ -2,43 +2,192 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { StockBatch, StockEntry } from "@/types"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
selectedBatch: () => StockBatch
|
||||||
}
|
}
|
||||||
|
|
||||||
const CountStockModal: React.FC<Props> = ({ onClose }) => {
|
const CountStockModal: React.FC<Props> = ({ onClose, selectedBatch }) => {
|
||||||
const [quantity, setQuantity] = React.useState(0)
|
const [quantity, setQuantity] = React.useState("")
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false)
|
||||||
|
const [selectedEntry, setSelectedEntry] = React.useState<StockEntry | null>(null)
|
||||||
|
|
||||||
|
const handleSelect = (entry: StockEntry) => {
|
||||||
|
setSelectedEntry(entry)
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!selectedEntry) {
|
||||||
|
toast.error("Please select a product first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/set-stock', { quantity }, { withCredentials: true })
|
const { data } = await axios.post(
|
||||||
toast.success('Stock set!')
|
"/api/pdaView/countStock",
|
||||||
|
{
|
||||||
|
entryId: selectedEntry.id,
|
||||||
|
quantity,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the HTTP status was 200 but success===false,
|
||||||
|
// inspect data.error to decide which toast to show
|
||||||
|
if (!data.success) {
|
||||||
|
switch (data.error) {
|
||||||
|
case "already_counted":
|
||||||
|
toast.error("This item has already been counted.")
|
||||||
|
break
|
||||||
|
case "validation_failed":
|
||||||
|
toast.error("Validation failed. Please check your inputs.")
|
||||||
|
break
|
||||||
|
case "not_found":
|
||||||
|
toast.error("Could not find that stock entry.")
|
||||||
|
break
|
||||||
|
case "server_error":
|
||||||
|
default:
|
||||||
|
// show the message from the server if provided, otherwise fallback
|
||||||
|
toast.error(
|
||||||
|
data.message ?? "Something went wrong on the server."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// success === true:
|
||||||
|
toast.success("Stock counted!")
|
||||||
onClose()
|
onClose()
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
toast.error('Failed to set stock')
|
// If the request itself failed (e.g. network or HTTP 500 that didn't return JSON):
|
||||||
|
// You can inspect err.response.status if you want, e.g. 409 → extract JSON, etc.
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
// Attempt to read the server’s JSON error payload
|
||||||
|
const payload = err.response.data
|
||||||
|
if (payload.error === "already_counted") {
|
||||||
|
toast.error("This item has already been counted.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (payload.error === "validation_failed") {
|
||||||
|
toast.error("Validation failed. Please check your inputs.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (payload.error === "not_found") {
|
||||||
|
toast.error("Could not find that stock entry.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Fallback to any message string
|
||||||
|
toast.error(payload.message || "Unknown error occurred.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, a true “network” or unexpected error:
|
||||||
|
toast.error("Failed to count stock. Please try again.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<h3 className="font-bold text-lg">Set Stock</h3>
|
<h3 className="font-bold text-lg">Count Stock</h3>
|
||||||
<label className="label">
|
|
||||||
<span className="label-text">Quantity</span>
|
{/* Product Dropdown */}
|
||||||
</label>
|
<div className="form-control w-full">
|
||||||
<input
|
<label className="label">
|
||||||
type="number"
|
<span className="label-text">Product</span>
|
||||||
value={quantity}
|
</label>
|
||||||
onChange={e => setQuantity(+e.target.value)}
|
|
||||||
className="input input-bordered w-full"
|
<div className="dropdown w-full">
|
||||||
required
|
<button
|
||||||
/>
|
type="button"
|
||||||
<div className="modal-action">
|
className="btn w-full justify-between"
|
||||||
|
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{selectedEntry ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
src={selectedEntry.physical_item.image_url}
|
||||||
|
alt={selectedEntry.physical_item.name}
|
||||||
|
className="w-6 h-6 rounded-full"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{selectedEntry.physical_item.name} ({selectedEntry.count})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Select product ..."
|
||||||
|
)}
|
||||||
|
<svg
|
||||||
|
className="fill-current w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M5.516 7.548a.75.75 0 0 1 1.06 0L10 10.972l3.424-3.424a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className={`dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full mt-1 ${
|
||||||
|
isDropdownOpen ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedBatch.stock_entries.map((entry) => (
|
||||||
|
<li key={entry.id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center px-2 py-1 hover:bg-gray-100 rounded"
|
||||||
|
onClick={() => handleSelect(entry)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={entry.physical_item.image_url}
|
||||||
|
alt={entry.physical_item.name}
|
||||||
|
className="w-6 h-6 rounded-full mr-2"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{entry.physical_item.name} ({entry.count})
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{selectedBatch.stock_entries.length === 0 && (
|
||||||
|
<li>
|
||||||
|
<span className="px-2 py-1 text-gray-500">No items available</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quantity Input */}
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Quantity</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={quantity}
|
||||||
|
onChange={(e) => setQuantity(+e.target.value)}
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="modal-action flex justify-end space-x-2">
|
||||||
<button type="button" className="btn" onClick={onClose}>
|
<button type="button" className="btn" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-primary">
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={!selectedEntry || quantity < 0}
|
||||||
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
170
resources/js/Components/modals/EditRoomModal.tsx
Normal file
170
resources/js/Components/modals/EditRoomModal.tsx
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { StockRoom } from '@/types';
|
||||||
|
import EditStockSections from '@/Components/modals/EditStockSections';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
room: StockRoom;
|
||||||
|
onClose: () => void;
|
||||||
|
onUpdated: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditRoomModal({ room, onClose, onUpdated }: Props) {
|
||||||
|
const [data, setData] = useState<StockRoom>(room);
|
||||||
|
const [activePosition, setActivePosition] = useState<() => any | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData(room);
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
|
const reload = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await axios.get<StockRoom>(`/api/stock-rooms/${room.room_id}`, { withCredentials: true });
|
||||||
|
setData(resp.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to reload room');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addLine = async () => {
|
||||||
|
try {
|
||||||
|
const newIndex = data.lines.length + 1;
|
||||||
|
await axios.post('/api/stock-lines', {
|
||||||
|
room_id: room.room_id,
|
||||||
|
line_symbol: `${newIndex}`,
|
||||||
|
line_name: `Line ${newIndex}`
|
||||||
|
}, { withCredentials: true });
|
||||||
|
await reload();
|
||||||
|
toast.success('Line added');
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to add line');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRack = async (line_id: number, line_symbol: string, line_name: string) => {
|
||||||
|
try {
|
||||||
|
const line = data.lines.find(l => l.line_id === line_id);
|
||||||
|
const newIndex = line?.racks.length! + 1;
|
||||||
|
await axios.post('/api/stock-racks', {
|
||||||
|
line_id,
|
||||||
|
rack_symbol: `${newIndex}`,
|
||||||
|
rack_name: `Rack ${newIndex}`
|
||||||
|
}, { withCredentials: true });
|
||||||
|
await reload();
|
||||||
|
toast.success('Rack added');
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to add rack');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addShelf = async (rack_id: number, rack_symbol: string, rack_name: string) => {
|
||||||
|
try {
|
||||||
|
const rack = data.lines
|
||||||
|
.flatMap(l => l.racks)
|
||||||
|
.find(r => r.rack_id === rack_id);
|
||||||
|
const newIndex = rack?.shelves.length! + 1;
|
||||||
|
await axios.post('/api/stock-shelves', {
|
||||||
|
rack_id,
|
||||||
|
shelf_symbol: `${newIndex}`,
|
||||||
|
shelf_name: `Shelf ${newIndex}`
|
||||||
|
}, { withCredentials: true });
|
||||||
|
await reload();
|
||||||
|
toast.success('Shelf added');
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to add shelf');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPosition = async (shelf_id: number, shelf_symbol: string, shelf_name: string) => {
|
||||||
|
try {
|
||||||
|
const shelf = data.lines
|
||||||
|
.flatMap(l => l.racks)
|
||||||
|
.flatMap(r => r.shelves)
|
||||||
|
.find(s => s.shelf_id === shelf_id);
|
||||||
|
const newIndex = shelf?.positions.length! + 1;
|
||||||
|
await axios.post('/api/stock-positions', {
|
||||||
|
shelf_id,
|
||||||
|
position_symbol: `${newIndex}`,
|
||||||
|
position_name: `Position ${newIndex}`,
|
||||||
|
capacity: 0
|
||||||
|
}, { withCredentials: true });
|
||||||
|
await reload();
|
||||||
|
toast.success('Position added');
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to add position');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-box max-w-6xl p-6 relative">
|
||||||
|
<h3 className="text-xl font-bold mb-4 flex justify-between items-center">
|
||||||
|
<span>Edit Room: {data.room_name}</span>
|
||||||
|
<button className="btn btn-sm btn-outline" onClick={addLine}>+ Add Line</button>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2 max-h-[60vh] overflow-y-auto">
|
||||||
|
{data.lines.map(line => (
|
||||||
|
<details key={line.line_id} className="border rounded p-2">
|
||||||
|
<summary className="flex justify-between items-center font-semibold">
|
||||||
|
<span>{line.line_name}</span>
|
||||||
|
<button className="btn btn-xs btn-outline" onClick={() => addRack(line.line_id, line.line_symbol, line.line_name)}>
|
||||||
|
+ Add Rack
|
||||||
|
</button>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
{line.racks.map(rack => (
|
||||||
|
<details key={rack.rack_id} className="ml-4 border rounded p-2">
|
||||||
|
<summary className="flex justify-between items-center">
|
||||||
|
<span>{rack.rack_name}</span>
|
||||||
|
<button className="btn btn-xxs btn-outline" onClick={() => addShelf(rack.rack_id, rack.rack_symbol, rack.rack_name)}>
|
||||||
|
+ Add Shelf
|
||||||
|
</button>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
{rack.shelves.map(shelf => (
|
||||||
|
<details key={shelf.shelf_id} className="ml-4 border rounded p-2">
|
||||||
|
<summary className="flex justify-between items-center">
|
||||||
|
<span>{shelf.shelf_name}</span>
|
||||||
|
<button className="btn btn-xxs btn-outline" onClick={() => addPosition(shelf.shelf_id, shelf.shelf_symbol, shelf.shelf_name)}>
|
||||||
|
+ Add Position
|
||||||
|
</button>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<ul className="ml-4">
|
||||||
|
{shelf.positions.map(pos => (
|
||||||
|
<li key={pos.position_id} className="flex justify-between items-center py-1">
|
||||||
|
<span>{pos.position_name}</span>
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-outline"
|
||||||
|
onClick={() => setActivePosition(() => () => pos)}
|
||||||
|
>
|
||||||
|
Edit Sections
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-2 mt-4">
|
||||||
|
<button onClick={onClose} className="btn btn-outline">Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activePosition && (
|
||||||
|
<EditStockSections
|
||||||
|
selectedPosition={activePosition}
|
||||||
|
onClose={() => {
|
||||||
|
setActivePosition(null);
|
||||||
|
reload();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
347
resources/js/Components/modals/EditStockSections.tsx
Normal file
347
resources/js/Components/modals/EditStockSections.tsx
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
// components/modals/EditStockSections.tsx
|
||||||
|
import React from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { StockPosition, StockSection } from "@/types"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void
|
||||||
|
selectedPosition: () => StockPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditableSection {
|
||||||
|
// Mirror of StockSection, with local-only flags
|
||||||
|
section_id?: number
|
||||||
|
section_symbol: string
|
||||||
|
section_name: string
|
||||||
|
capacity: number
|
||||||
|
retrievable: boolean
|
||||||
|
markedForDeletion: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditStockSections: React.FC<Props> = ({ onClose, selectedPosition }) => {
|
||||||
|
|
||||||
|
// Local state for position capacity and editable sections
|
||||||
|
const [positionCapacity, setPositionCapacity] = React.useState<number>(selectedPosition.capacity)
|
||||||
|
const [sections, setSections] = React.useState<EditableSection[]>([])
|
||||||
|
const [loading, setLoading] = React.useState<boolean>(false)
|
||||||
|
const [validationError, setValidationError] = React.useState<string>("")
|
||||||
|
|
||||||
|
// Initialize sections state whenever the selected position changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
const initialSections: EditableSection[] = selectedPosition.sections.map((sec) => ({
|
||||||
|
section_id: sec.section_id,
|
||||||
|
section_symbol: sec.section_symbol,
|
||||||
|
section_name: sec.section_name,
|
||||||
|
capacity: sec.capacity,
|
||||||
|
retrievable: sec.retrievable,
|
||||||
|
markedForDeletion: false,
|
||||||
|
}))
|
||||||
|
setSections(initialSections)
|
||||||
|
setPositionCapacity(selectedPosition.capacity)
|
||||||
|
setValidationError("")
|
||||||
|
}, [selectedPosition])
|
||||||
|
|
||||||
|
// Handler: change position capacity
|
||||||
|
const handlePositionCapacityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const val = parseInt(e.target.value, 10)
|
||||||
|
if (!isNaN(val) && val >= 0) {
|
||||||
|
setPositionCapacity(val)
|
||||||
|
// Clear any existing validation error to re-validate
|
||||||
|
setValidationError("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler: change a section's field (by index)
|
||||||
|
const handleSectionChange = (
|
||||||
|
index: number,
|
||||||
|
field: keyof Omit<EditableSection, "section_id" | "markedForDeletion">,
|
||||||
|
value: string | boolean
|
||||||
|
) => {
|
||||||
|
setSections((prev) => {
|
||||||
|
const updated = [...prev]
|
||||||
|
if (field === "capacity") {
|
||||||
|
const num = parseInt(value as string, 10)
|
||||||
|
updated[index].capacity = isNaN(num) ? 0 : num
|
||||||
|
} else if (field === "retrievable") {
|
||||||
|
updated[index].retrievable = value as boolean
|
||||||
|
} else {
|
||||||
|
updated[index][field] = value as string
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
setValidationError("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler: add a brand-new empty section
|
||||||
|
const handleAddSection = () => {
|
||||||
|
setSections((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
section_symbol: `${prev.length + 1}`,
|
||||||
|
section_name: `Section ${prev.length + 1}`,
|
||||||
|
capacity: 0,
|
||||||
|
retrievable: true,
|
||||||
|
markedForDeletion: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
setValidationError("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler: mark a section for deletion (or directly remove if never saved)
|
||||||
|
const handleRemoveSection = (index: number) => {
|
||||||
|
setSections((prev) => {
|
||||||
|
const updated = [...prev]
|
||||||
|
if (updated[index].section_id) {
|
||||||
|
// If it exists on the server, mark for deletion
|
||||||
|
updated[index].markedForDeletion = true
|
||||||
|
} else {
|
||||||
|
// If it's a new unsaved section, just drop it
|
||||||
|
updated.splice(index, 1)
|
||||||
|
}
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
setValidationError("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute total capacity of non-deleted sections
|
||||||
|
const totalSectionsCapacity = sections
|
||||||
|
.filter((s) => !s.markedForDeletion)
|
||||||
|
.reduce((sum, s) => sum + s.capacity, 0)
|
||||||
|
|
||||||
|
// Validation: total section capacity must not exceed position capacity
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (totalSectionsCapacity > positionCapacity) {
|
||||||
|
setValidationError(
|
||||||
|
`Total section capacity (${totalSectionsCapacity}) exceeds position capacity (${positionCapacity}).`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setValidationError("")
|
||||||
|
}
|
||||||
|
}, [totalSectionsCapacity, positionCapacity])
|
||||||
|
|
||||||
|
// Handler: save all changes (position + sections)
|
||||||
|
const handleSave = async () => {
|
||||||
|
// Prevent saving if there's a validation error
|
||||||
|
if (validationError) {
|
||||||
|
toast.error(validationError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
const toastId = toast.loading("Saving changes…")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) Update the StockPosition's capacity if it has changed
|
||||||
|
if (positionCapacity !== selectedPosition.capacity) {
|
||||||
|
await axios.put(
|
||||||
|
`/api/stockPositions/${selectedPosition.position_id}`,
|
||||||
|
{
|
||||||
|
capacity: positionCapacity,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Process each section: create, update, or delete
|
||||||
|
for (const sec of sections) {
|
||||||
|
if (sec.section_id && sec.markedForDeletion) {
|
||||||
|
// DELETE existing section
|
||||||
|
await axios.delete(`/api/stockSections/${sec.section_id}`, {
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
} else if (sec.section_id) {
|
||||||
|
// UPDATE existing section
|
||||||
|
await axios.put(
|
||||||
|
`/api/stockSections/${sec.section_id}`,
|
||||||
|
{
|
||||||
|
section_name: sec.section_name,
|
||||||
|
capacity: sec.capacity,
|
||||||
|
retrievable: sec.retrievable,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
} else if (!sec.section_id && !sec.markedForDeletion) {
|
||||||
|
// CREATE new section
|
||||||
|
await axios.post(
|
||||||
|
`/api/stockSections`,
|
||||||
|
{
|
||||||
|
position_id: selectedPosition.position_id,
|
||||||
|
section_symbol: sec.section_symbol,
|
||||||
|
section_name: sec.section_name,
|
||||||
|
capacity: sec.capacity,
|
||||||
|
retrievable: sec.retrievable,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// If a new section was added then immediately marked for deletion before save,
|
||||||
|
// we do nothing (skip).
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
toast.success("Position and sections updated successfully.")
|
||||||
|
onClose()
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
if (err.response && err.response.data && err.response.data.message) {
|
||||||
|
toast.error(`Error: ${err.response.data.message}`)
|
||||||
|
} else {
|
||||||
|
toast.error("Network or server error. Please try again.")
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-bold text-lg"> Edit<span className="italic opacity-75 border-l-2 pl-2 ml-2">Position {selectedPosition.storage_address}</span> </h3>
|
||||||
|
|
||||||
|
{/* Position Capacity Input */}
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Position Capacity</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={positionCapacity || ""}
|
||||||
|
onChange={handlePositionCapacityChange}
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
min="0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sections List */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h4 className="font-semibold">Sections</h4>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-outline"
|
||||||
|
onClick={handleAddSection}
|
||||||
|
>
|
||||||
|
+ Add Section
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sections.map((sec, idx) => (
|
||||||
|
<div
|
||||||
|
key={sec.section_id ?? `new-${idx}`}
|
||||||
|
className={`border rounded p-3 ${
|
||||||
|
sec.markedForDeletion ? "opacity-50 italic" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="font-medium">
|
||||||
|
{sec.section_id
|
||||||
|
? `Section ID: ${sec.section_id}`
|
||||||
|
: `New Section`}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-error btn-xs"
|
||||||
|
onClick={() => handleRemoveSection(idx)}
|
||||||
|
>
|
||||||
|
{sec.section_id ? "Delete" : "Remove"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{!sec.markedForDeletion && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{/* Section Name */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Name</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={sec.section_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSectionChange(idx, "section_name", e.target.value)
|
||||||
|
}
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Capacity */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Capacity</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={sec.capacity}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSectionChange(idx, "capacity", e.target.value)
|
||||||
|
}
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
min="0"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Retrievable Checkbox */}
|
||||||
|
<div className="form-control flex items-center pt-6">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={sec.retrievable}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSectionChange(idx, "retrievable", e.target.checked)
|
||||||
|
}
|
||||||
|
className="checkbox checkbox-primary mr-2"
|
||||||
|
/>
|
||||||
|
<span className="label-text">Retrievable</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sec.markedForDeletion && (
|
||||||
|
<div className="text-sm text-red-600">
|
||||||
|
This section will be deleted when you save.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{sections.length === 0 && (
|
||||||
|
<div className="text-gray-500 italic">
|
||||||
|
No sections defined for this position.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Error */}
|
||||||
|
{validationError && (
|
||||||
|
<div className="text-red-600 font-medium">{validationError}</div>
|
||||||
|
)}
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
Total of section capacities: <strong>{totalSectionsCapacity}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex space-x-2 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!!validationError || loading}
|
||||||
|
>
|
||||||
|
{loading ? "Saving…" : "Save Changes"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditStockSections
|
136
resources/js/Components/modals/MoveSectionModal.tsx
Normal file
136
resources/js/Components/modals/MoveSectionModal.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { StockPosition, StockSection } from '@/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void
|
||||||
|
fromPosition?: StockPosition | null
|
||||||
|
fromSection?: StockSection | null
|
||||||
|
toPosition?: StockPosition | null
|
||||||
|
toSection?: StockSection | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const MoveSectionModal: React.FC<Props> = ({
|
||||||
|
onClose,
|
||||||
|
fromSection,
|
||||||
|
toSection,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleMove = async () => {
|
||||||
|
if (!fromSection || !toSection) return
|
||||||
|
setLoading(true)
|
||||||
|
const toastId = toast.loading('Moving…')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(
|
||||||
|
'/api/pdaView/moveStockSection',
|
||||||
|
{
|
||||||
|
entry_id: fromSection.entries[0].id,
|
||||||
|
section_id: fromSection.section_id,
|
||||||
|
new_section_id: toSection.section_id,
|
||||||
|
count: fromSection.entries[0].pivot.count,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
switch (data.error) {
|
||||||
|
case 'validation_failed':
|
||||||
|
toast.error('Validation failed. Check inputs.')
|
||||||
|
break
|
||||||
|
case 'not_found':
|
||||||
|
toast.error('Entry or section not found.')
|
||||||
|
break
|
||||||
|
case 'section_occupied':
|
||||||
|
toast.error('Target section is already occupied.')
|
||||||
|
break
|
||||||
|
case 'insufficient_capacity':
|
||||||
|
toast.error('Not enough capacity in target section.')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
toast.error(data.message ?? 'Server error during moving.')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
toast.success('Moved successfully.')
|
||||||
|
onClose()
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.dismiss()
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
const payload = err.response.data
|
||||||
|
if (payload.error === 'validation_failed') {
|
||||||
|
toast.error('Validation failed. Check inputs.')
|
||||||
|
} else if (payload.error === 'not_found') {
|
||||||
|
toast.error('Entry or section not found.')
|
||||||
|
} else if (payload.error === 'section_occupied') {
|
||||||
|
toast.error('Target section is already occupied.')
|
||||||
|
} else if (payload.error === 'insufficient_capacity') {
|
||||||
|
toast.error('Not enough capacity in target section.')
|
||||||
|
} else {
|
||||||
|
toast.error(payload.message || 'Unknown error occurred.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(fromSection.entries?.length <= 0) {
|
||||||
|
toast.error('No items in the FROM section.');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Network error. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error('MoveSectionModal error:', err)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-bold text-lg">Move Stock Item</h3>
|
||||||
|
|
||||||
|
{!fromSection && (
|
||||||
|
<div className="alert alert-info">
|
||||||
|
Please scan the <strong>TO</strong> section…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fromSection && !toSection && (
|
||||||
|
<div className="alert alert-info">
|
||||||
|
Scanned <strong>FROM:</strong> {fromSection.section_id}. Now scan the <strong>TO</strong> section…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fromSection && toSection && (
|
||||||
|
<div className="alert alert-success">
|
||||||
|
Ready to move from {fromSection.section_id} → {toSection.section_id}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleMove}
|
||||||
|
disabled={!fromSection || !toSection || loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Moving…' : 'Confirm Move'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoveSectionModal
|
82
resources/js/Components/modals/NewRoomModal.tsx
Normal file
82
resources/js/Components/modals/NewRoomModal.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// resources/js/Components/Modals/NewRoomModal.tsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
onSaved: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewRoomModal({ onClose, onSaved }: Props) {
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
room_symbol: '',
|
||||||
|
room_name: '',
|
||||||
|
lines: 1,
|
||||||
|
racks_per_line: 1,
|
||||||
|
shelves_per_rack: 1,
|
||||||
|
positions_per_shelf: 1,
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setForm({ ...form, [e.target.name]: e.target.type === 'number'
|
||||||
|
? parseInt(e.target.value)
|
||||||
|
: e.target.value });
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const id = toast.loading('Creating room…');
|
||||||
|
try {
|
||||||
|
await axios.post('/api/stock-rooms', form, { withCredentials: true });
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.success('Room created!');
|
||||||
|
onSaved();
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.error(err.response?.data?.message || 'Error creating room');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-box max-w-6xl flex flex-col space-x-6 p-6 relative">
|
||||||
|
<h3 className="text-xl font-bold">New Room</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{['room_symbol','room_name'].map((field) => (
|
||||||
|
<div key={field} className="form-control">
|
||||||
|
<label className="label"><span className="label-text">{field.replace('_',' ')}</span></label>
|
||||||
|
<input
|
||||||
|
name={field}
|
||||||
|
type="text"
|
||||||
|
value={(form as any)[field]}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="input input-bordered"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{['lines','racks_per_line','shelves_per_rack','positions_per_shelf'].map((field) => (
|
||||||
|
<div key={field} className="form-control">
|
||||||
|
<label className="label"><span className="label-text">{field.replace(/_/g,' ')}</span></label>
|
||||||
|
<input
|
||||||
|
name={field}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={(form as any)[field]}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="input input-bordered"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button onClick={onClose} className="btn btn-outline" disabled={loading}>Cancel</button>
|
||||||
|
<button onClick={save} className="btn btn-primary" disabled={loading}>
|
||||||
|
{loading ? 'Saving…' : 'Create'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
@ -1,48 +1,391 @@
|
|||||||
// components/modals/SetStockModal.tsx
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { toast } from 'react-hot-toast'
|
import {toast} from 'react-hot-toast'
|
||||||
|
import {StockBatch, StockEntry, StockPosition, StockSection} from '@/types'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
|
||||||
|
import {faCheckCircle, faTimesCircle} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
selectedBatch: StockBatch
|
||||||
|
selectedSection: StockSection | null
|
||||||
|
selectedPosition: StockPosition | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const SetStockModal: React.FC<Props> = ({ onClose }) => {
|
const SetStockModal: React.FC<Props> = ({onClose, selectedBatch, selectedSection, selectedPosition}) => {
|
||||||
const [quantity, setQuantity] = React.useState(0)
|
const [quantity, setQuantity] = React.useState<string>('')
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false)
|
||||||
|
const [selectedEntry, setSelectedEntry] = React.useState<StockEntry | null>(null)
|
||||||
|
const [loading, setLoading] = React.useState(false)
|
||||||
|
const [isSuggestionsOpen, setIsSuggestionsOpen] = React.useState(false)
|
||||||
|
const [activeTab, setActiveTab] = React.useState<'batch' | 'item'>('item')
|
||||||
|
const handleSelect = (entry: StockEntry) => {
|
||||||
|
setSelectedEntry(entry)
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedQty = parseInt(quantity, 10)
|
||||||
|
const isNumber = !isNaN(parsedQty)
|
||||||
|
const maxItem = selectedEntry ? selectedEntry.count - selectedEntry.count_stocked : 0
|
||||||
|
const capacity = selectedSection ? selectedSection.capacity : 0
|
||||||
|
const exceedsItem = isNumber && parsedQty > maxItem
|
||||||
|
const exceedsCapacity = isNumber && parsedQty > capacity
|
||||||
|
const hasError = exceedsItem || exceedsCapacity
|
||||||
|
|
||||||
|
const readyToScan =
|
||||||
|
selectedEntry !== null &&
|
||||||
|
isNumber &&
|
||||||
|
parsedQty >= 1 &&
|
||||||
|
!exceedsItem &&
|
||||||
|
!exceedsCapacity
|
||||||
|
|
||||||
|
// Called when “Simulate Scan (ID=3)” is clicked
|
||||||
|
const handleScan = async () => {
|
||||||
|
|
||||||
|
if(activeTab === 'batch') {
|
||||||
|
if (!selectedBatch || !selectedPosition) return
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!selectedEntry || !selectedSection) return
|
||||||
|
if (!readyToScan) {
|
||||||
|
toast.error('Please fix validation errors before confirming.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
const toastId = toast.loading('Storing…')
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/set-stock', { quantity }, { withCredentials: true })
|
let response
|
||||||
toast.success('Stock set!')
|
if (activeTab === 'batch') {
|
||||||
onClose()
|
response = await axios.post(
|
||||||
} catch {
|
'/api/pdaView/setBatchSection',
|
||||||
toast.error('Failed to set stock')
|
{
|
||||||
|
batch_id: selectedBatch.id,
|
||||||
|
position_id: selectedPosition?.position_id,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
response = await axios.post(
|
||||||
|
'/api/pdaView/setStockSection',
|
||||||
|
{
|
||||||
|
entry_id: selectedEntry?.id,
|
||||||
|
section_id: selectedSection?.section_id,
|
||||||
|
count_to_be_stored: parsedQty,
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now 'response' is defined no matter which branch ran
|
||||||
|
const { data } = response
|
||||||
|
console.log('data', data)
|
||||||
|
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
switch (data.error) {
|
||||||
|
case 'validation_failed':
|
||||||
|
toast.error('Validation failed. Check inputs.')
|
||||||
|
break
|
||||||
|
case 'not_found':
|
||||||
|
toast.error('Entry or section not found.')
|
||||||
|
break
|
||||||
|
case 'section_occupied':
|
||||||
|
toast.error('That section is already occupied.')
|
||||||
|
break
|
||||||
|
case 'insufficient_capacity':
|
||||||
|
toast.error('Not enough capacity in this section.')
|
||||||
|
break
|
||||||
|
case 'server_error':
|
||||||
|
default:
|
||||||
|
toast.error(data.message ?? 'Server error during storing.')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
toast.success('Stored successfully.')
|
||||||
|
resetAndClose()
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.dismiss()
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
const payload = err.response.data
|
||||||
|
if (payload.error === 'validation_failed') {
|
||||||
|
toast.error('Validation failed. Check inputs.')
|
||||||
|
} else if (payload.error === 'not_found') {
|
||||||
|
toast.error('Entry or section not found.')
|
||||||
|
} else if (payload.error === 'section_occupied') {
|
||||||
|
toast.error('That section is already occupied.')
|
||||||
|
} else if (payload.error === 'insufficient_capacity') {
|
||||||
|
toast.error('Not enough capacity in this section.')
|
||||||
|
} else {
|
||||||
|
toast.error(payload.message || 'Unknown error occurred.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('Network error. Please try again.')
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetAndClose = () => {
|
||||||
|
setQuantity('')
|
||||||
|
setSelectedEntry(null)
|
||||||
|
setLoading(false)
|
||||||
|
setIsSuggestionsOpen(false)
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<div className="space-y-4">
|
||||||
<h3 className="font-bold text-lg">Set Stock</h3>
|
<h3 className="font-bold text-lg">Store Stock</h3>
|
||||||
<label className="label">
|
|
||||||
<span className="label-text">Quantity</span>
|
<div className="tabs tabs-box">
|
||||||
</label>
|
<button
|
||||||
<input
|
className={`tab ${activeTab === 'batch' ? 'tab-active' : ''}`}
|
||||||
type="number"
|
onClick={() => setActiveTab('batch')}
|
||||||
value={quantity}
|
>
|
||||||
onChange={e => setQuantity(+e.target.value)}
|
Entire batch
|
||||||
className="input input-bordered w-full"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div className="modal-action">
|
|
||||||
<button type="button" className="btn" onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-primary">
|
<button
|
||||||
Submit
|
className={`tab ${activeTab === 'item' ? 'tab-active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('item')}
|
||||||
|
>
|
||||||
|
Individual items
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
{activeTab === 'batch' && (
|
||||||
|
<div className="bg-base-100 border-base-300 p-6">
|
||||||
|
{(selectedSection || selectedPosition) && (
|
||||||
|
<div className="mb-2 flex items-center text-primary">
|
||||||
|
Selected position: {selectedPosition ? selectedPosition.storage_address : selectedSection ? selectedSection.position.storage_address : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2 mt-2">
|
||||||
|
<span>{loading ? 'Storing…' : (selectedSection || selectedPosition) ? 'Waiting for confirm' : 'Waiting for section scan...'}</span>
|
||||||
|
<span className="loading loading-spinner text-primary"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button type="button" className="btn" onClick={() => setIsSuggestionsOpen(true)}>
|
||||||
|
Suggested Sections
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-primary ${loading ? 'loading' : ''}`}
|
||||||
|
onClick={handleScan}
|
||||||
|
disabled={!selectedSection && !selectedPosition}
|
||||||
|
>
|
||||||
|
{loading ? 'Storing…' : selectedSection || selectedPosition ? 'Confirm' : 'Waiting for section scan...'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'item' && (
|
||||||
|
<div className="bg-base-100 border-base-300 p-6">
|
||||||
|
{/* Product Dropdown */}
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Product</span>
|
||||||
|
</label>
|
||||||
|
<div className="dropdown w-full">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn w-full justify-between"
|
||||||
|
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{selectedEntry ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
src={selectedEntry.physical_item.image_url}
|
||||||
|
alt={selectedEntry.physical_item.name}
|
||||||
|
className="w-6 h-6 rounded-full"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{selectedEntry.physical_item.name} ({selectedEntry.count_stocked} uskladneno | {selectedEntry.count-selectedEntry.count_stocked} zbyva) {selectedEntry.sections.length > 0 ? `[${selectedEntry.sections[0].storage_address}]` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Select product ...'
|
||||||
|
)}
|
||||||
|
<svg
|
||||||
|
className="fill-current w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M5.516 7.548a.75.75 0 0 1 1.06 0L10 10.972l3.424-3.424a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className={`dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full mt-1 ${
|
||||||
|
isDropdownOpen ? 'block' : 'hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{selectedBatch.stock_entries.map((entry) => {
|
||||||
|
const stocked = entry.count_stocked
|
||||||
|
const total = entry.count
|
||||||
|
const remaining = total - stocked
|
||||||
|
const selectable = remaining > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={entry.id}
|
||||||
|
className={`${!selectable ? 'opacity-50 filter' : ''}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!selectable}
|
||||||
|
onClick={selectable ? () => handleSelect(entry) : undefined}
|
||||||
|
className={
|
||||||
|
`
|
||||||
|
flex items-center px-2 py-1 rounded
|
||||||
|
hover:border-gray-100
|
||||||
|
${!selectable ? 'cursor-not-allowed' : 'hover:border-gray-100'}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={entry.physical_item.image_url}
|
||||||
|
alt={entry.physical_item.name}
|
||||||
|
className="w-6 h-6 rounded-full mr-2"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{entry.physical_item.name} ({entry.count_stocked} uskladneno | {remaining} zbyva) {entry.sections.length > 0 ? `[${entry.sections[0].storage_address}]` : ''}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{selectedBatch.stock_entries.length === 0 && (
|
||||||
|
<li>
|
||||||
|
<span className="px-2 py-1 text-gray-500">No items available</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quantity Input */}
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Quantity to store</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={quantity}
|
||||||
|
onChange={(e) => setQuantity(e.target.value)}
|
||||||
|
className={`input input-bordered w-full ${hasError ? 'border-red-500' : ''}`}
|
||||||
|
min="1"
|
||||||
|
max={selectedEntry ? maxItem : undefined}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{selectedEntry && (
|
||||||
|
<span className="text-sm text-gray-500 mt-1">
|
||||||
|
Max items: {maxItem} | Capacity: {capacity}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Validation Messages */}
|
||||||
|
{(exceedsItem || exceedsCapacity) && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{exceedsItem && (
|
||||||
|
<div className="flex items-center text-red-600">
|
||||||
|
<FontAwesomeIcon icon={faTimesCircle} className="mr-2" />
|
||||||
|
<span>Item quantity exceeded</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{exceedsCapacity && (
|
||||||
|
<div className="flex items-center text-red-600">
|
||||||
|
<FontAwesomeIcon icon={faTimesCircle} className="mr-2" />
|
||||||
|
<span>Maximum capacity exceeded</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success indicator if valid */}
|
||||||
|
{readyToScan && (
|
||||||
|
<div className="mt-2 flex items-center text-green-600">
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} className="mr-2" />
|
||||||
|
<span>Ready to store</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedSection && (
|
||||||
|
<div className="mt-2 mb-2 flex items-center text-primary">
|
||||||
|
Selected section: {selectedSection.storage_address}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Buttons: Suggested Sections & Simulate Scan */}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button type="button" className="btn" onClick={() => setIsSuggestionsOpen(true)}>
|
||||||
|
Suggested Sections
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-primary ${loading ? 'loading' : ''}`}
|
||||||
|
onClick={handleScan}
|
||||||
|
disabled={!readyToScan || loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Storing…' : selectedSection ? 'Confirm' : 'Waiting for section scan...'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Waiting for scan indicator */}
|
||||||
|
{readyToScan && !loading && (
|
||||||
|
<div className="flex items-center space-x-2 mt-2">
|
||||||
|
<span>{loading ? 'Storing…' : selectedSection ? 'Waiting for confirm' : 'Waiting for section scan...'}</span>
|
||||||
|
<span className="loading loading-spinner text-primary"></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cancel Button */}
|
||||||
|
<div className="modal-action flex justify-end pt-2">
|
||||||
|
<button type="button" className="btn" onClick={resetAndClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Suggested Sections Modal */}
|
||||||
|
{isSuggestionsOpen && (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">Suggested Sections</h3>
|
||||||
|
<ul className="mt-4 space-y-2">
|
||||||
|
<li>Section A (ID: 1)</li>
|
||||||
|
<li>Section B (ID: 2)</li>
|
||||||
|
<li>Section C (ID: 3)</li>
|
||||||
|
<li>Section D (ID: 4)</li>
|
||||||
|
</ul>
|
||||||
|
<div className="modal-action">
|
||||||
|
<button className="btn" onClick={() => setIsSuggestionsOpen(false)}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
342
resources/js/Components/modals/SetStockModalFromTemp.tsx
Normal file
342
resources/js/Components/modals/SetStockModalFromTemp.tsx
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import {toast} from 'react-hot-toast'
|
||||||
|
import {StockBatch, StockEntry, StockPosition, StockSection} from '@/types'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
|
||||||
|
import {faCheckCircle, faTimesCircle} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void
|
||||||
|
selectedSection: StockSection | null
|
||||||
|
selectedPosition: StockPosition | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const SetStockModal: React.FC<Props> = ({onClose, selectedSection, selectedPosition}) => {
|
||||||
|
const [quantity, setQuantity] = React.useState<string>('')
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false)
|
||||||
|
const [selectedEntry, setSelectedEntry] = React.useState<StockEntry | null>(null)
|
||||||
|
const [loading, setLoading] = React.useState(false)
|
||||||
|
const [isSuggestionsOpen, setIsSuggestionsOpen] = React.useState(false)
|
||||||
|
const [activeTab, setActiveTab] = React.useState<'batch' | 'item'>('item')
|
||||||
|
const handleSelect = (entry: StockEntry) => {
|
||||||
|
setSelectedEntry(entry)
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedQty = parseInt(quantity, 10)
|
||||||
|
const isNumber = !isNaN(parsedQty)
|
||||||
|
const capacity = selectedSection ? selectedSection.capacity : 0
|
||||||
|
const exceedsItem = isNumber && parsedQty > selectedEntry?.count
|
||||||
|
const exceedsCapacity = isNumber && parsedQty > capacity
|
||||||
|
const hasError = exceedsItem || exceedsCapacity
|
||||||
|
|
||||||
|
const readyToScan =
|
||||||
|
selectedEntry !== null &&
|
||||||
|
isNumber &&
|
||||||
|
parsedQty >= 1 &&
|
||||||
|
!exceedsItem &&
|
||||||
|
!exceedsCapacity
|
||||||
|
|
||||||
|
// Called when “Simulate Scan (ID=3)” is clicked
|
||||||
|
const handleScan = async () => {
|
||||||
|
|
||||||
|
|
||||||
|
if (!selectedEntry || !selectedSection) return
|
||||||
|
if (!readyToScan) {
|
||||||
|
toast.error('Please fix validation errors before confirming.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
const toastId = toast.loading('Storing…')
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response
|
||||||
|
|
||||||
|
response = await axios.post(
|
||||||
|
'/api/pdaView/storeStock',
|
||||||
|
{
|
||||||
|
entry_id: selectedEntry?.id,
|
||||||
|
section_id: selectedSection?.section_id,
|
||||||
|
current_section: selectedEntry?.sections[0].section_id,
|
||||||
|
count_to_be_stored: parsedQty,
|
||||||
|
},
|
||||||
|
{withCredentials: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
const {data} = response
|
||||||
|
console.log('data', data)
|
||||||
|
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
switch (data.error) {
|
||||||
|
case 'validation_failed':
|
||||||
|
toast.error('Validation failed. Check inputs.')
|
||||||
|
break
|
||||||
|
case 'not_found':
|
||||||
|
toast.error('Entry or section not found.')
|
||||||
|
break
|
||||||
|
case 'section_occupied':
|
||||||
|
toast.error('That section is already occupied.')
|
||||||
|
break
|
||||||
|
case 'insufficient_capacity':
|
||||||
|
toast.error('Not enough capacity in this section.')
|
||||||
|
break
|
||||||
|
case 'server_error':
|
||||||
|
default:
|
||||||
|
toast.error(data.message ?? 'Server error during storing.')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
toast.success('Stored successfully.')
|
||||||
|
resetAndClose()
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.dismiss()
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
const payload = err.response.data
|
||||||
|
if (payload.error === 'validation_failed') {
|
||||||
|
toast.error('Validation failed. Check inputs.')
|
||||||
|
} else if (payload.error === 'not_found') {
|
||||||
|
toast.error('Entry or section not found.')
|
||||||
|
} else if (payload.error === 'section_occupied') {
|
||||||
|
toast.error('That section is already occupied.')
|
||||||
|
} else if (payload.error === 'insufficient_capacity') {
|
||||||
|
toast.error('Not enough capacity in this section.')
|
||||||
|
} else {
|
||||||
|
toast.error(payload.message || 'Unknown error occurred.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error('Network error. Please try again.')
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetAndClose = () => {
|
||||||
|
setQuantity('')
|
||||||
|
setSelectedEntry(null)
|
||||||
|
setLoading(false)
|
||||||
|
setIsSuggestionsOpen(false)
|
||||||
|
setIsDropdownOpen(false)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const sectionEntries = React.useMemo(() => {
|
||||||
|
// 1) Strip out the `entries` field from each section
|
||||||
|
const sectionsWithoutEntries =
|
||||||
|
selectedPosition?.sections.map(({entries, ...sec}) => sec) || []
|
||||||
|
|
||||||
|
// 2) Flatten all entries, and tag each with:
|
||||||
|
// - the cleaned-up sections array
|
||||||
|
// - its own sectionId / sectionAddress
|
||||||
|
return (
|
||||||
|
selectedPosition?.sections.flatMap((section) =>
|
||||||
|
section.entries.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
sections: sectionsWithoutEntries,
|
||||||
|
sectionId: section.section_id,
|
||||||
|
sectionAddress: section.storage_address,
|
||||||
|
}))
|
||||||
|
) || []
|
||||||
|
)
|
||||||
|
}, [selectedPosition])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-bold text-lg">Store Stock</h3>
|
||||||
|
|
||||||
|
<div className="bg-base-100 border-base-300 p-6">
|
||||||
|
{/* Product Dropdown */}
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Product</span>
|
||||||
|
</label>
|
||||||
|
<div className="dropdown w-full">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn w-full justify-between"
|
||||||
|
onClick={() => setIsDropdownOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{selectedEntry ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<img
|
||||||
|
src={selectedEntry.physical_item.image_url}
|
||||||
|
alt={selectedEntry.physical_item.name}
|
||||||
|
className="w-6 h-6 rounded-full"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{selectedEntry.physical_item.name} ({selectedEntry.count_stocked} uskladneno | {selectedEntry.count - selectedEntry.count_stocked} zbyva) [{selectedEntry.sections[0].storage_address}]
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Select product ...'
|
||||||
|
)}
|
||||||
|
<svg
|
||||||
|
className="fill-current w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.516 7.548a.75.75 0 0 1 1.06 0L10 10.972l3.424-3.424a.75.75 0 1 1 1.06 1.06l-4 4a.75.75 0 0 1-1.06 0l-4-4a.75.75 0 0 1 0-1.06z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className={`dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full mt-1 ${
|
||||||
|
isDropdownOpen ? 'block' : 'hidden'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{sectionEntries.map((entry) => {
|
||||||
|
const stocked = entry.count_stocked
|
||||||
|
const total = entry.count
|
||||||
|
const remaining = total - stocked
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={entry.id}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelect(entry)}
|
||||||
|
className="flex items-center px-2 py-1 rounded hover:border-gray-100"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={entry.physical_item.image_url}
|
||||||
|
alt={entry.physical_item.name}
|
||||||
|
className="w-6 h-6 rounded-full mr-2"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{entry.physical_item.name} ({entry.count_stocked} uskladneno | {remaining} zbyva) [{entry.sections[0].storage_address}]
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{sectionEntries.length === 0 && (
|
||||||
|
<li>
|
||||||
|
<span className="px-2 py-1 text-gray-500">No items available</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quantity Input */}
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Quantity to store</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={quantity}
|
||||||
|
onChange={(e) => setQuantity(e.target.value)}
|
||||||
|
className={`input input-bordered w-full ${hasError ? 'border-red-500' : ''}`}
|
||||||
|
min="1"
|
||||||
|
max={selectedEntry?.count}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{selectedEntry && (
|
||||||
|
<span className="text-sm text-gray-500 mt-1">
|
||||||
|
Max items: {selectedEntry?.count} | Capacity: {capacity}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Validation Messages */}
|
||||||
|
{(exceedsItem || exceedsCapacity) && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{exceedsItem && (
|
||||||
|
<div className="flex items-center text-red-600">
|
||||||
|
<FontAwesomeIcon icon={faTimesCircle} className="mr-2"/>
|
||||||
|
<span>Item quantity exceeded</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{exceedsCapacity && (
|
||||||
|
<div className="flex items-center text-red-600">
|
||||||
|
<FontAwesomeIcon icon={faTimesCircle} className="mr-2"/>
|
||||||
|
<span>Maximum capacity exceeded</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success indicator if valid */}
|
||||||
|
{readyToScan && (
|
||||||
|
<div className="mt-2 flex items-center text-green-600">
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} className="mr-2"/>
|
||||||
|
<span>Ready to store</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedSection && (
|
||||||
|
<div className="mt-2 mb-2 flex items-center text-primary">
|
||||||
|
Selected section: {selectedSection.storage_address}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Buttons: Suggested Sections & Simulate Scan */}
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button type="button" className="btn" onClick={() => setIsSuggestionsOpen(true)}>
|
||||||
|
Suggested Sections
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-primary ${loading ? 'loading' : ''}`}
|
||||||
|
onClick={handleScan}
|
||||||
|
disabled={!readyToScan || loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Storing…' : selectedSection ? 'Confirm' : 'Waiting for section scan...'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Waiting for scan indicator */}
|
||||||
|
{readyToScan && !loading && (
|
||||||
|
<div className="flex items-center space-x-2 mt-2">
|
||||||
|
<span>{loading ? 'Storing…' : selectedSection ? 'Waiting for confirm' : 'Waiting for section scan...'}</span>
|
||||||
|
<span className="loading loading-spinner text-primary"></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cancel Button */}
|
||||||
|
<div className="modal-action flex justify-end pt-2">
|
||||||
|
<button type="button" className="btn" onClick={resetAndClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Suggested Sections Modal */}
|
||||||
|
{isSuggestionsOpen && (
|
||||||
|
<div className="modal modal-open">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">Suggested Sections</h3>
|
||||||
|
<ul className="mt-4 space-y-2">
|
||||||
|
<li>Section A (ID: 1)</li>
|
||||||
|
<li>Section B (ID: 2)</li>
|
||||||
|
<li>Section C (ID: 3)</li>
|
||||||
|
<li>Section D (ID: 4)</li>
|
||||||
|
</ul>
|
||||||
|
<div className="modal-action">
|
||||||
|
<button className="btn" onClick={() => setIsSuggestionsOpen(false)}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SetStockModal
|
534
resources/js/Pages/Expedice.tsx
Normal file
534
resources/js/Pages/Expedice.tsx
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
|
import { usePage } from "@inertiajs/react";
|
||||||
|
import axios from "axios";
|
||||||
|
import {
|
||||||
|
faSquareCheck,
|
||||||
|
faTruck,
|
||||||
|
faXmark,
|
||||||
|
faBarcode, faArrowLeft, faQuestionCircle,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { Item, ShipmentRequest } from "@/interfaces/interfaces";
|
||||||
|
import WarehouseExpediceDialog from "@/Components/WarehouseExpediceDialog";
|
||||||
|
import PdaView from "@/Pages/PdaView";
|
||||||
|
|
||||||
|
// Define an interface for the imperative handle exposed by a row.
|
||||||
|
interface RowHandle {
|
||||||
|
triggerAnimation: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- ParcelRow (for marking as processed) -------------------
|
||||||
|
interface ParcelRowProps {
|
||||||
|
shipment: ShipmentRequest;
|
||||||
|
onProcess: (shipment: ShipmentRequest) => void;
|
||||||
|
onOpenDialog: (shipment: ShipmentRequest, type: "parcels") => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ParcelRow = forwardRef<RowHandle, ParcelRowProps>(
|
||||||
|
({ shipment, onProcess, onOpenDialog }, ref) => {
|
||||||
|
// "none" | "bounce" | "fade"
|
||||||
|
const [animationPhase, setAnimationPhase] = useState<
|
||||||
|
"none" | "bounce" | "fade"
|
||||||
|
>("none");
|
||||||
|
|
||||||
|
// Expose a method to trigger the animation from the parent (dialog)
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
triggerAnimation: () => {
|
||||||
|
setAnimationPhase("bounce");
|
||||||
|
setTimeout(() => {
|
||||||
|
setAnimationPhase("fade");
|
||||||
|
}, 1000);
|
||||||
|
setTimeout(() => {
|
||||||
|
onProcess(shipment);
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleRowClick = () => {
|
||||||
|
onOpenDialog(shipment, "parcels");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProcess = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setAnimationPhase("bounce");
|
||||||
|
setTimeout(() => {
|
||||||
|
setAnimationPhase("fade");
|
||||||
|
}, 1000);
|
||||||
|
setTimeout(() => {
|
||||||
|
onProcess(shipment);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Determine classes based on the animation phase:
|
||||||
|
- "none": border-base-300, bg-base-100, text-base-content
|
||||||
|
- "bounce": border-primary, bg-primary, text-primary-content
|
||||||
|
- "fade": border-primary, bg-primary, text-primary-content, opacity-0
|
||||||
|
*/
|
||||||
|
const containerClasses = `
|
||||||
|
p-4 border rounded-lg shadow flex flex-col space-y-3 transition-all duration-1000 cursor-pointer
|
||||||
|
${
|
||||||
|
animationPhase === "none"
|
||||||
|
? "border-base-300 bg-base-100 text-base-content opacity-100"
|
||||||
|
: animationPhase === "bounce"
|
||||||
|
? "border-primary bg-primary text-primary-content opacity-100"
|
||||||
|
: "border-primary bg-primary text-primary-content opacity-0"
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onClick={handleRowClick} className={containerClasses}>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold">{shipment.shipment_reference}</p>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
{shipment.items.map((item) => (
|
||||||
|
<div key={item.id} className="border-t-2 border-warning pt-2 space-y-1">
|
||||||
|
<p className="text-base-content">
|
||||||
|
{item.quantity}× {item.name}
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-between text-sm text-base-content">
|
||||||
|
<p>{item.model_number}</p>
|
||||||
|
<p>
|
||||||
|
{(item.price / item.quantity).toFixed(2)}{" "}
|
||||||
|
{shipment.currency}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`mt-2 flex flex-wrap gap-2 p-2 border rounded-lg transition-colors duration-300 ${
|
||||||
|
animationPhase === "bounce"
|
||||||
|
? "bg-primary text-primary-content"
|
||||||
|
: "bg-base-200 text-base-content"
|
||||||
|
} border-base-300`}
|
||||||
|
>
|
||||||
|
{Object.entries(item.stockData).map(
|
||||||
|
([stockName, stockArray]) =>
|
||||||
|
stockArray.map((stock, idx) => (
|
||||||
|
<div key={`${stockName}-${idx}`} className="text-sm">
|
||||||
|
<p className="font-semibold">{stockName}</p>
|
||||||
|
<p>Sklad: {stock.location}</p>
|
||||||
|
<p>{stock.count} ks</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{animationPhase === "bounce" ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSquareCheck}
|
||||||
|
className="w-8 h-8 text-primary animate-bounce"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={handleProcess}
|
||||||
|
className="btn btn-primary btn-sm self-end"
|
||||||
|
>
|
||||||
|
Process
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ------------------- ProcessedRow (for marking as unprocessed) -------------------
|
||||||
|
interface ProcessedRowProps {
|
||||||
|
shipment: ShipmentRequest;
|
||||||
|
onUnprocess: (shipment: ShipmentRequest) => void;
|
||||||
|
onOpenDialog: (shipment: ShipmentRequest, type: "processed") => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProcessedRow = forwardRef<RowHandle, ProcessedRowProps>(
|
||||||
|
({ shipment, onUnprocess, onOpenDialog }, ref) => {
|
||||||
|
const [animationPhase, setAnimationPhase] = useState<
|
||||||
|
"none" | "bounce" | "fade"
|
||||||
|
>("none");
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
triggerAnimation: () => {
|
||||||
|
setAnimationPhase("bounce");
|
||||||
|
setTimeout(() => {
|
||||||
|
setAnimationPhase("fade");
|
||||||
|
}, 1000);
|
||||||
|
setTimeout(() => {
|
||||||
|
onUnprocess(shipment);
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleRowClick = () => {
|
||||||
|
onOpenDialog(shipment, "processed");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnprocess = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setAnimationPhase("bounce");
|
||||||
|
setTimeout(() => {
|
||||||
|
setAnimationPhase("fade");
|
||||||
|
}, 1000);
|
||||||
|
setTimeout(() => {
|
||||||
|
onUnprocess(shipment);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Determine classes based on the animation phase:
|
||||||
|
- "none": border-base-300, bg-base-100, text-base-content
|
||||||
|
- "bounce": border-error, bg-error, text-error-content
|
||||||
|
- "fade": border-error, bg-error, text-error-content, opacity-0
|
||||||
|
*/
|
||||||
|
const containerClasses = `
|
||||||
|
p-4 border rounded-lg shadow flex flex-col space-y-3 transition-all duration-1000 cursor-pointer
|
||||||
|
${
|
||||||
|
animationPhase === "none"
|
||||||
|
? "border-base-300 bg-base-100 text-base-content opacity-100"
|
||||||
|
: animationPhase === "bounce"
|
||||||
|
? "border-error bg-error text-error-content opacity-100"
|
||||||
|
: "border-error bg-error text-error-content opacity-0"
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onClick={handleRowClick} className={containerClasses}>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold">{shipment.shipment_reference}</p>
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
|
{shipment.items.map((item) => (
|
||||||
|
<div key={item.id} className="border-t-2 border-warning pt-2 space-y-1">
|
||||||
|
<p className="text-base-content">
|
||||||
|
{item.quantity}× {item.name}
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-between text-sm text-base-content">
|
||||||
|
<p>{item.model_number}</p>
|
||||||
|
<p>
|
||||||
|
{(item.price / item.quantity).toFixed(2)}{" "}
|
||||||
|
{shipment.currency}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`mt-2 flex flex-wrap gap-2 p-2 border rounded-lg transition-colors duration-300 ${
|
||||||
|
animationPhase === "bounce"
|
||||||
|
? "bg-error text-error-content"
|
||||||
|
: "bg-base-200 text-base-content"
|
||||||
|
} border-base-300`}
|
||||||
|
>
|
||||||
|
{Object.entries(item.stockData).map(
|
||||||
|
([stockName, stockArray]) =>
|
||||||
|
stockArray.map((stock, idx) => (
|
||||||
|
<div key={`${stockName}-${idx}`} className="text-sm">
|
||||||
|
<p className="font-semibold">{stockName}</p>
|
||||||
|
<p>Sklad: {stock.location}</p>
|
||||||
|
<p>{stock.count} ks</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{animationPhase === "bounce" ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faXmark}
|
||||||
|
className="w-8 h-8 text-error animate-bounce"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={handleUnprocess}
|
||||||
|
className="btn btn-error btn-sm self-end"
|
||||||
|
>
|
||||||
|
Unprocess
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type InertiaProps = {
|
||||||
|
auth: any; // adjust per your auth type
|
||||||
|
selectedBatchID: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ------------------- Main Component -------------------
|
||||||
|
export default function WarehouseExpedicePage() {
|
||||||
|
const { selectedBatchID } = usePage<InertiaProps>().props;
|
||||||
|
|
||||||
|
// States for shipments
|
||||||
|
const [parcels, setParcels] = useState<ShipmentRequest[]>([]);
|
||||||
|
const [parcelsOther, setParcelsOther] = useState<ShipmentRequest[]>([]);
|
||||||
|
const [processed, setProcessed] = useState<ShipmentRequest[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Refs to control individual row animations
|
||||||
|
const parcelRowRefs = useRef<{ [key: number]: RowHandle | null }>({});
|
||||||
|
const processedRowRefs = useRef<{ [key: number]: RowHandle | null }>({});
|
||||||
|
|
||||||
|
// Dialog state
|
||||||
|
const [selectedShipment, setSelectedShipment] =
|
||||||
|
useState<ShipmentRequest | null>(null);
|
||||||
|
const [selectedType, setSelectedType] = useState<
|
||||||
|
"parcels" | "processed" | null
|
||||||
|
>(null);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleCopyToClipboard = () => {
|
||||||
|
const copyString = parcels.map((p) => p.shipment_reference).join(",");
|
||||||
|
const copyStringOther = parcelsOther.map((p) => p.shipment_reference).join(
|
||||||
|
","
|
||||||
|
);
|
||||||
|
const copyStringProcessed = processed
|
||||||
|
.map((p) => p.shipment_reference)
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText([copyString, copyStringOther, copyStringProcessed].filter(Boolean).join(","))
|
||||||
|
.then(() => {
|
||||||
|
alert("Parcels copied to clipboard!");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to copy:", err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios
|
||||||
|
.post("/api/expediceListWMS", { batch_id: selectedBatchID })
|
||||||
|
.then((response) => {
|
||||||
|
setParcels(response.data.batch_items.shipments);
|
||||||
|
setParcelsOther(response.data.batch_items.shipments_other);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [selectedBatchID]);
|
||||||
|
|
||||||
|
// Move a shipment from Parcels to Processed
|
||||||
|
const markAsProcessed = (shipment: ShipmentRequest) => {
|
||||||
|
setParcels((prev) => prev.filter((s) => s.id !== shipment.id));
|
||||||
|
setProcessed((prev) => [...prev, shipment]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move a shipment from Processed back to Parcels
|
||||||
|
const markAsUnprocessed = (shipment: ShipmentRequest) => {
|
||||||
|
setProcessed((prev) => prev.filter((s) => s.id !== shipment.id));
|
||||||
|
setParcels((prev) => [...prev, shipment]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the dialog for a shipment
|
||||||
|
const openDialog = (
|
||||||
|
shipment: ShipmentRequest,
|
||||||
|
type: "parcels" | "processed"
|
||||||
|
) => {
|
||||||
|
setSelectedShipment(shipment);
|
||||||
|
setSelectedType(type);
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close the dialog
|
||||||
|
const closeDialog = () => {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setSelectedShipment(null);
|
||||||
|
setSelectedType(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pdaDialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
const openPdaModal = () => {
|
||||||
|
pdaDialogRef.current?.showModal();
|
||||||
|
};
|
||||||
|
const closePdaModal = () => {
|
||||||
|
pdaDialogRef.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [buttonStates, setButtonStates] = useState<{ [itemId: number]: any }>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle processing/unprocessing via the dialog action button
|
||||||
|
const handleDialogProcess = () => {
|
||||||
|
if (selectedShipment && selectedType === "parcels") {
|
||||||
|
const filteredButtonStates = selectedShipment.items.reduce(
|
||||||
|
(acc: any, item: Item) => {
|
||||||
|
if (buttonStates[item.id]) {
|
||||||
|
acc[item.id] = buttonStates[item.id];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post("/api/warehouseExpedice/passOptions", {
|
||||||
|
shipment_request_id: selectedShipment.id,
|
||||||
|
buttonStates: filteredButtonStates,
|
||||||
|
shipment_items: selectedShipment.items,
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error sending button states:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parcelRowRefs.current[selectedShipment.id]) {
|
||||||
|
parcelRowRefs.current[selectedShipment.id]?.triggerAnimation();
|
||||||
|
} else {
|
||||||
|
markAsProcessed(selectedShipment);
|
||||||
|
}
|
||||||
|
} else if (selectedShipment && selectedType === "processed") {
|
||||||
|
if (processedRowRefs.current[selectedShipment.id]) {
|
||||||
|
processedRowRefs.current[selectedShipment.id]?.triggerAnimation();
|
||||||
|
} else {
|
||||||
|
markAsUnprocessed(selectedShipment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-4 text-base-content">Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Top bar */}
|
||||||
|
<div className="flex justify-between items-center bg-base-100 p-4 shadow">
|
||||||
|
|
||||||
|
<a className="link" href={route('dashboard')}><FontAwesomeIcon icon={faArrowLeft} /> Back</a>
|
||||||
|
<button className="btn btn-ghost">
|
||||||
|
<FontAwesomeIcon icon={faQuestionCircle} /> Help
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Open the modal using document.getElementById('ID').showModal() method */}
|
||||||
|
<button className="btn" onClick={()=>openPdaModal()}>open modal</button>
|
||||||
|
{/* PDA VIEW DIALOG */}
|
||||||
|
<dialog id="pdaViewModal" className="modal" ref={pdaDialogRef}>
|
||||||
|
<div className="modal-box max-w-4xl max-h-[90vh] overflow-hidden p-0">
|
||||||
|
<PdaView closeParent={closePdaModal} />
|
||||||
|
</div>
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button className="btn">Close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="tabs tabs-box w-full">
|
||||||
|
{/* Parcels Tab */}
|
||||||
|
<label className="tab flex-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="expedice_tabs"
|
||||||
|
className="tab"
|
||||||
|
aria-label="Parcels"
|
||||||
|
defaultChecked
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-center gap-2 w-full">
|
||||||
|
<FontAwesomeIcon icon={faTruck} className="w-5 h-5" />
|
||||||
|
<span className="text-base-content">Parcels</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div className="tab-content bg-base-100 border-base-300 p-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{parcels.length === 0 ? (
|
||||||
|
<p className="text-base-content">No parcels available.</p>
|
||||||
|
) : (
|
||||||
|
parcels.map((shipment) => (
|
||||||
|
<ParcelRow
|
||||||
|
ref={(el) => {
|
||||||
|
parcelRowRefs.current[shipment.id] = el;
|
||||||
|
}}
|
||||||
|
key={shipment.id}
|
||||||
|
shipment={shipment}
|
||||||
|
onProcess={markAsProcessed}
|
||||||
|
onOpenDialog={openDialog}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nahravacky Tab */}
|
||||||
|
<label className="tab flex-1">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="expedice_tabs"
|
||||||
|
className="tab"
|
||||||
|
aria-label="Nahravacky"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-center gap-2 w-full">
|
||||||
|
<FontAwesomeIcon icon={faBarcode} className="w-5 h-5" />
|
||||||
|
<span className="text-base-content">Nahravacky</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div className="tab-content bg-base-100 border-base-300 p-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{parcelsOther.length === 0 ? (
|
||||||
|
<p className="text-base-content">No parcels available.</p>
|
||||||
|
) : (
|
||||||
|
parcelsOther.map((shipment) => (
|
||||||
|
<ParcelRow
|
||||||
|
ref={(el) => {
|
||||||
|
parcelRowRefs.current[shipment.id] = el;
|
||||||
|
}}
|
||||||
|
key={shipment.id}
|
||||||
|
shipment={shipment}
|
||||||
|
onProcess={markAsProcessed}
|
||||||
|
onOpenDialog={openDialog}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Processed Tab (15% width) */}
|
||||||
|
<label className="tab w-[15%] flex-none">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="expedice_tabs"
|
||||||
|
className="tab"
|
||||||
|
aria-label="Processed"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-center w-full">
|
||||||
|
<FontAwesomeIcon icon={faSquareCheck} className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div className="tab-content bg-base-100 border-base-300 p-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{processed.length === 0 ? (
|
||||||
|
<p className="text-base-content">No processed shipments.</p>
|
||||||
|
) : (
|
||||||
|
processed.map((shipment) => (
|
||||||
|
<ProcessedRow
|
||||||
|
ref={(el) => {
|
||||||
|
processedRowRefs.current[shipment.id] = el;
|
||||||
|
}}
|
||||||
|
key={shipment.id}
|
||||||
|
shipment={shipment}
|
||||||
|
onUnprocess={markAsUnprocessed}
|
||||||
|
onOpenDialog={openDialog}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shipment Details Dialog */}
|
||||||
|
{selectedShipment && (
|
||||||
|
<WarehouseExpediceDialog
|
||||||
|
selectedShipment={selectedShipment}
|
||||||
|
isDialogOpen={isDialogOpen}
|
||||||
|
closeDialog={closeDialog}
|
||||||
|
handleDialogProcess={handleDialogProcess}
|
||||||
|
selectedType={selectedType}
|
||||||
|
buttonStates={buttonStates}
|
||||||
|
setButtonStates={setButtonStates}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,16 +1,16 @@
|
|||||||
import React from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Head, usePage } from '@inertiajs/react'
|
import {Head, usePage} from '@inertiajs/react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
|
||||||
import {
|
import {
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faQuestionCircle,
|
faQuestionCircle,
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
faClipboardList,
|
faClipboardList,
|
||||||
faCubes,
|
faCubes,
|
||||||
faPlus,
|
faPlus, faBarcode,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { toast, Toaster } from 'react-hot-toast'
|
import {toast, Toaster} from 'react-hot-toast'
|
||||||
|
|
||||||
import Tile from '../Components/Tile'
|
import Tile from '../Components/Tile'
|
||||||
import TileLayout from '../Components/TileLayout'
|
import TileLayout from '../Components/TileLayout'
|
||||||
@ -20,6 +20,16 @@ import ModalPDA from '../Components/ModalPDA'
|
|||||||
import SetStockModal from '../Components/modals/SetStockModal'
|
import SetStockModal from '../Components/modals/SetStockModal'
|
||||||
import OtherReplacementModal from '../Components/modals/OtherReplacementModal'
|
import OtherReplacementModal from '../Components/modals/OtherReplacementModal'
|
||||||
import CountStockModal from '../Components/modals/CountStockModal'
|
import CountStockModal from '../Components/modals/CountStockModal'
|
||||||
|
import {Batch} from "@/interfaces/interfaces";
|
||||||
|
import {StockBatch, StockPosition, StockSection} from "@/types";
|
||||||
|
import BatchInfoModal from "@/Components/modals/BatchInfoModal";
|
||||||
|
import {downloadBatchBarcode} from "@/functions/functions"
|
||||||
|
import EditStockSections from "@/Components/modals/EditStockSections";
|
||||||
|
import MoveSectionModal from "@/Components/modals/MoveSectionModal";
|
||||||
|
import ChangeCountModal from "@/Components/modals/ChangeCountModal";
|
||||||
|
|
||||||
|
import error_scanner_sound from "@/sounds/error_scanner.mp3";
|
||||||
|
import SetStockModalFromTemp from "@/Components/modals/SetStockModalFromTemp";
|
||||||
|
|
||||||
type Role = 'Expedice' | 'Skladnik'
|
type Role = 'Expedice' | 'Skladnik'
|
||||||
|
|
||||||
@ -38,62 +48,103 @@ type TileConfig = {
|
|||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const tilesConfig: Record<Role, Record<string, TileConfig[]>> = {
|
|
||||||
Expedice: {
|
type ModalKey =
|
||||||
stockSectionScanned: [
|
'setStock'
|
||||||
{ title: 'Not Present', icon: faBoxOpen, onClick: () => toast('Not Present clicked') },
|
| 'setStockFromTemp'
|
||||||
{ title: 'Na pozici je jiny ovladac', icon: faBoxOpen, onClick: () => toast('Not Present clicked') },
|
| 'otherReplacement'
|
||||||
{
|
| 'countStock'
|
||||||
title: 'Present but Shouldn’t',
|
| 'batchInfo'
|
||||||
icon: faClipboardList,
|
| 'editStock'
|
||||||
onClick: async () => {
|
| 'moveStock'
|
||||||
// example direct axios call
|
| 'changeCount'
|
||||||
try {
|
| null
|
||||||
await axios.post('/api/presence-error', {}, { withCredentials: true })
|
|
||||||
toast.success('Reported!')
|
type PdaViewProps = { closeParent: () => void };
|
||||||
} catch {
|
|
||||||
toast.error('Failed to report')
|
export default function PdaView({closeParent}: PdaViewProps) {
|
||||||
}
|
|
||||||
|
const tilesConfig: Record<Role, Record<string, TileConfig[]>> = {
|
||||||
|
Expedice: {
|
||||||
|
stockSectionScanned: [
|
||||||
|
{title: 'Not Present', icon: faBoxOpen, onClick: () => toast('Not Present clicked')},
|
||||||
|
{title: 'Na pozici je jiny ovladac', icon: faBoxOpen, onClick: () => toast('Not Present clicked')},
|
||||||
|
{
|
||||||
|
title: 'Present but Shouldn’t',
|
||||||
|
icon: faClipboardList,
|
||||||
|
onClick: async () => {
|
||||||
|
// example direct axios call
|
||||||
|
try {
|
||||||
|
await axios.post('/api/presence-error', {}, {withCredentials: true})
|
||||||
|
toast.success('Reported!')
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to report')
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{title: 'Other Replacement (na test/one time)', icon: faPlus, modalKey: 'otherReplacement'},
|
||||||
{ title: 'Other Replacement (na test/one time)', icon: faPlus, modalKey: 'otherReplacement' },
|
{title: 'Pridej vazbu na jiny ovladac', icon: faPlus, onClick: () => toast('Batch Info clicked')},
|
||||||
{ title: 'Pridej vazbu na jiny ovladac', icon: faPlus, onClick: () => toast('Batch Info clicked') },
|
{
|
||||||
{ title: 'Pridej docasnou vazbu (nez prijde jine zbozi, i casove omezene, nejnizsi priorita)', icon: faPlus, onClick: () => toast('Batch Info clicked') },
|
title: 'Pridej docasnou vazbu (nez prijde jine zbozi, i casove omezene, nejnizsi priorita)',
|
||||||
{ title: 'Doplnit zbozi / dej vic kusu', icon: faPlus, onClick: () => toast('Batch Info clicked') },
|
icon: faPlus,
|
||||||
{ title: 'Report - chci zmenit pozici(bliz / dal od expedice)', icon: faPlus, onClick: () => toast('Batch Info clicked') },
|
onClick: () => toast('Batch Info clicked')
|
||||||
{ title: 'Pultovy prodej', icon: faPlus, onClick: () => toast('- naskenuju na webu / naskenuju na skladovem miste, obj. se udela automaticky, zakaznik si muze v rohu na PC vyplnit osobni udaje na special formulari - pak customera prida obsluha do obj') },
|
},
|
||||||
],
|
{title: 'Doplnit zbozi / dej vic kusu', icon: faPlus, onClick: () => toast('Batch Info clicked')},
|
||||||
labelScanned: [
|
{
|
||||||
{ title: 'Info', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
title: 'Report - chci zmenit pozici(bliz / dal od expedice)',
|
||||||
]
|
icon: faPlus,
|
||||||
},
|
onClick: () => toast('Batch Info clicked')
|
||||||
Skladnik: {
|
},
|
||||||
batchScan: [
|
{
|
||||||
{ title: 'Batch Info', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
title: 'Pultovy prodej',
|
||||||
{ title: 'Set Stock', icon: faCubes, modalKey: 'setStock' },
|
icon: faPlus,
|
||||||
{ title: 'Count Stock', icon: faPlus, modalKey: 'countStock' },
|
onClick: () => toast('- naskenuju na webu / naskenuju na skladovem miste, obj. se udela automaticky, zakaznik si muze v rohu na PC vyplnit osobni udaje na special formulari - pak customera prida obsluha do obj')
|
||||||
{ title: 'Stitkovani (male stitky)', icon: faClipboardList, onClick: () => toast('Stitkovani (male stitky)') },
|
},
|
||||||
{ title: 'Batch Info', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
],
|
||||||
{ title: 'Tisk QR kod na krabice', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
labelScanned: [
|
||||||
],
|
{title: 'Info', icon: faClipboardList, onClick: () => toast('Batch Info clicked')},
|
||||||
stockScan: [
|
]
|
||||||
{ title: 'Info', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
},
|
||||||
{ title: 'Zmena skladoveho mista (i presun jen casti kusu)', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
Skladnik: {
|
||||||
{ title: 'Uprava skladoveho mista (mene sekci, apod)', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
batchScan: [
|
||||||
{ title: 'Zmena poctu', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
{title: 'Batch Info', icon: faClipboardList, modalKey: 'batchInfo'},
|
||||||
{ title: 'Discard (odebrat ze skladoveho mista / posilame zpet)', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
{title: 'Set Stock', icon: faCubes, modalKey: 'setStock'},
|
||||||
],
|
{title: 'Count Stock', icon: faPlus, modalKey: 'countStock'},
|
||||||
others: [
|
{
|
||||||
{ title: 'Nove skladove misto', icon: faClipboardList, onClick: () => toast('Batch Info clicked') },
|
title: 'Stitkovani (male stitky)',
|
||||||
]
|
icon: faClipboardList,
|
||||||
},
|
onClick: () => toast('Stitkovani (male stitky)')
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
title: 'Tisk QR kod na krabice',
|
||||||
|
icon: faClipboardList,
|
||||||
|
onClick: () => downloadBatchBarcode(selectedBatch?.id)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stockScan: [
|
||||||
|
{title: 'Info', icon: faClipboardList, onClick: () => toast('Batch Info clicked')},
|
||||||
|
{title: 'Set Stock', icon: faCubes, modalKey: 'setStockFromTemp'},
|
||||||
|
{
|
||||||
|
title: 'Zmena skladoveho mista (i presun jen casti kusu)',
|
||||||
|
icon: faClipboardList,
|
||||||
|
modalKey: 'moveStock'
|
||||||
|
},
|
||||||
|
{title: 'Uprava skladoveho mista (mene sekci, apod)', icon: faClipboardList, modalKey: 'editStock'},
|
||||||
|
{title: 'Zmena poctu', icon: faClipboardList, modalKey: 'changeCount'},
|
||||||
|
{
|
||||||
|
title: 'Discard (odebrat ze skladoveho mista / posilame zpet)',
|
||||||
|
icon: faClipboardList,
|
||||||
|
onClick: () => toast('Batch Info clicked')
|
||||||
|
},
|
||||||
|
],
|
||||||
|
others: [
|
||||||
|
{title: 'Nove skladove misto', icon: faClipboardList, onClick: () => toast('Batch Info clicked')},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
type ModalKey = 'setStock' | 'otherReplacement' | 'countStock' | null
|
|
||||||
|
|
||||||
export default function PdaView() {
|
|
||||||
const {
|
const {
|
||||||
auth: { user },
|
auth: {user},
|
||||||
} = usePage<{ auth: { user: { role: string } } }>().props
|
} = usePage<{ auth: { user: { role: string } } }>().props
|
||||||
|
|
||||||
const [role, setRole] = React.useState<Role>(
|
const [role, setRole] = React.useState<Role>(
|
||||||
@ -106,19 +157,260 @@ export default function PdaView() {
|
|||||||
// const isAdmin = user.role === 'admin'
|
// const isAdmin = user.role === 'admin'
|
||||||
const tabs: Role[] = ['Expedice', 'Skladnik']
|
const tabs: Role[] = ['Expedice', 'Skladnik']
|
||||||
|
|
||||||
|
const [selectedSerial, setSelectedSerial] = useState(null);
|
||||||
|
|
||||||
|
const selectedSectionRef = React.useRef<StockSection | null>(null)
|
||||||
|
const selectedPositionRef = React.useRef<StockPosition | null>(null)
|
||||||
|
|
||||||
const closeModal = () => setActiveModal(null)
|
const closeModal = () => setActiveModal(null)
|
||||||
|
|
||||||
|
// const closeModal = () => {
|
||||||
|
// setActiveModal(null)
|
||||||
|
// setPrevPosition(null)
|
||||||
|
// setPrevSection(null)
|
||||||
|
// setSelectedPosition(null)
|
||||||
|
// setSelectedSection(null)
|
||||||
|
// }
|
||||||
|
|
||||||
|
const wrapOnClick = (orig?: () => void) => {
|
||||||
|
return () => {
|
||||||
|
if (orig) orig();
|
||||||
|
// after the action, close the PDA modal:
|
||||||
|
closeParent();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedBatch, setSelectedBatch] = React.useState<StockBatch>();
|
||||||
|
const [selectedPosition, setSelectedPosition] = React.useState<StockPosition>();
|
||||||
|
const [selectedSection, setSelectedSection] = React.useState<StockSection>();
|
||||||
|
|
||||||
|
const [prevPosition, setPrevPosition] = useState<StockPosition | null>(null)
|
||||||
|
const [prevSection, setPrevSection] = useState<StockSection | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// fetchBatches();
|
||||||
|
// fetchPosition();
|
||||||
|
initScanner();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initScanner = () => {
|
||||||
|
// run once on mount
|
||||||
|
(async () => {
|
||||||
|
// see if we already have permission for any ports
|
||||||
|
const ports = await navigator.serial.getPorts();
|
||||||
|
console.log("ports", ports);
|
||||||
|
if (ports.length > 0) {
|
||||||
|
// pick the first (or filter by vendorId/productId)
|
||||||
|
const port = ports[0];
|
||||||
|
setSelectedSerial(port);
|
||||||
|
// now you can immediately open it:
|
||||||
|
connectToScanner(port);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePortRequest = async () => {
|
||||||
|
try {
|
||||||
|
const port = await navigator.serial.requestPort(); // <-- user gesture!
|
||||||
|
setSelectedSerial(port);
|
||||||
|
await connectToScanner(port);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("User cancelled or error:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const connectToScanner = async (port) => {
|
||||||
|
try {
|
||||||
|
// Request a port and open a connection
|
||||||
|
console.log(selectedSerial);
|
||||||
|
await port.open({baudRate: 9600});
|
||||||
|
|
||||||
|
// setIsConnected(true);
|
||||||
|
|
||||||
|
const reader = port.readable.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = ""; // Buffer to accumulate chunks
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const {value, done} = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
// Close the stream
|
||||||
|
reader.releaseLock();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
// Decode and append to the buffer
|
||||||
|
buffer += decoder.decode(value, {stream: true});
|
||||||
|
// Process messages split by newline or other delimiter
|
||||||
|
let lines = buffer.split(/[\n\t]/);
|
||||||
|
buffer = lines.pop(); // Keep the last incomplete chunk
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
sendToBackend(trimmedLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error connecting to scanner:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const sendToBackend = async (scannedData: string) => {
|
||||||
|
const audioError = document.getElementById("audio_error") as HTMLAudioElement;
|
||||||
|
audioError.volume = 1.0;
|
||||||
|
|
||||||
|
console.log("scannedData", scannedData);
|
||||||
|
|
||||||
|
// 1) Parse JSON
|
||||||
|
let payload: { barcode_type: string; payload: { id: number } };
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(scannedData);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Invalid barcode data");
|
||||||
|
audioError.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {barcode_type, payload: inner} = payload;
|
||||||
|
const {id} = inner;
|
||||||
|
|
||||||
|
// 2) Call your API
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/api/pda/barcodeScan", {
|
||||||
|
barcode_type,
|
||||||
|
payload: {id},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {success, data, message, error} = response.data;
|
||||||
|
|
||||||
|
console.log("response", data);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
// Laravel sends 200+ success=false for business errors, or 4xx/5xx for validation/not-found/etc
|
||||||
|
toast.error(
|
||||||
|
// Validation errors come back as an object of messages
|
||||||
|
Array.isArray(message)
|
||||||
|
? Object.values(message).flat().join("; ")
|
||||||
|
: message || "Scan failed"
|
||||||
|
);
|
||||||
|
audioError.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) On success, data will be the model (batch/section/position/label) or undefined for carrier_scanned
|
||||||
|
switch (barcode_type) {
|
||||||
|
case "stock_batch":
|
||||||
|
setAction("batchScan");
|
||||||
|
setSelectedBatch(data);
|
||||||
|
// you now have the full batch record in `data`
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stock_section":
|
||||||
|
setAction("stockScan");
|
||||||
|
|
||||||
|
// the “from” section is whatever was last in the ref
|
||||||
|
const oldSection = selectedSectionRef.current
|
||||||
|
// const oldPosition = selectedPositionRef.current
|
||||||
|
|
||||||
|
// the “to” section is the data you just fetched
|
||||||
|
const newSection = data
|
||||||
|
|
||||||
|
setPrevSection(oldSection)
|
||||||
|
// setPrevPosition(oldPosition)
|
||||||
|
|
||||||
|
setSelectedSection(newSection)
|
||||||
|
// setSelectedPosition(newSection.position)
|
||||||
|
|
||||||
|
console.log("FROM (prevSection):", oldSection)
|
||||||
|
console.log(" TO (selectedSection):", newSection)
|
||||||
|
break
|
||||||
|
|
||||||
|
case "stock_position":
|
||||||
|
|
||||||
|
setPrevPosition(selectedPosition || null);
|
||||||
|
|
||||||
|
setAction("stockScan");
|
||||||
|
setSelectedPosition(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "label_scanned":
|
||||||
|
setAction("labelScan");
|
||||||
|
// perhaps `data.product` or `data.batch` are in here if you need them
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "carrier_scanned":
|
||||||
|
setAction("carrierScan");
|
||||||
|
// no data field returned, but success=true means it worked
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// validator already prevents this, but just in case
|
||||||
|
toast.error(`Unknown barcode type: ${barcode_type}`);
|
||||||
|
audioError.play();
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
// 4xx / 5xx HTTP errors, or network failures
|
||||||
|
if (err.response) {
|
||||||
|
// Laravel ValidationException → 422
|
||||||
|
if (err.response.status === 422) {
|
||||||
|
const errors = err.response.data.message;
|
||||||
|
toast.error(
|
||||||
|
Array.isArray(errors)
|
||||||
|
? errors.join("; ")
|
||||||
|
: typeof errors === "object"
|
||||||
|
? Object.values(errors).flat().join("; ")
|
||||||
|
: errors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// ModelNotFound → 404
|
||||||
|
else if (err.response.status === 404) {
|
||||||
|
toast.error(err.response.data.message || "Record not found");
|
||||||
|
}
|
||||||
|
// Any other server error
|
||||||
|
else {
|
||||||
|
toast.error(err.response.data.message || "Server error");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// network / CORS / timeout
|
||||||
|
toast.error("Network error");
|
||||||
|
}
|
||||||
|
console.error("sendToBackend error:", err);
|
||||||
|
audioError.play();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedSectionRef.current = selectedSection ?? null
|
||||||
|
}, [selectedSection])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedPositionRef.current = selectedPosition ?? null
|
||||||
|
}, [selectedPosition])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title="PDA View" />
|
<Head title="PDA View"/>
|
||||||
|
<audio id="audio_error" src={error_scanner_sound}>
|
||||||
|
|
||||||
|
</audio>
|
||||||
|
|
||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
<div className="flex justify-between items-center bg-base-100 p-4 shadow">
|
<div className="flex justify-between items-center bg-base-100 p-4 shadow">
|
||||||
|
|
||||||
<a className="link" href={route('dashboard')}><FontAwesomeIcon icon={faArrowLeft} /> Back</a>
|
<a className="link" href={route('dashboard')}><FontAwesomeIcon icon={faArrowLeft}/> Back</a>
|
||||||
|
{!selectedSerial && (
|
||||||
|
<button className="btn btn-ghost" onClick={handlePortRequest}>
|
||||||
|
<FontAwesomeIcon icon={faBarcode}/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button className="btn btn-ghost">
|
<button className="btn btn-ghost">
|
||||||
<FontAwesomeIcon icon={faQuestionCircle} /> Help
|
<FontAwesomeIcon icon={faQuestionCircle}/> Help
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin tabs */}
|
{/* Admin tabs */}
|
||||||
@ -158,8 +450,17 @@ export default function PdaView() {
|
|||||||
|
|
||||||
{/* Tiles */}
|
{/* Tiles */}
|
||||||
<TileLayout>
|
<TileLayout>
|
||||||
{action &&
|
{action && (() => {
|
||||||
(tilesConfig[role][action] || []).map(({ title, icon, onClick, modalKey }) => (
|
// grab the base array
|
||||||
|
let tiles = tilesConfig[role][action] || []
|
||||||
|
|
||||||
|
// if we're stock-scanning but the position isn’t temporary, hide “Set Stock”
|
||||||
|
if (action === 'stockScan' && !selectedPosition?.temporary) {
|
||||||
|
tiles = tiles.filter(tile => tile.modalKey !== 'setStockFromTemp')
|
||||||
|
}
|
||||||
|
|
||||||
|
// render the filtered list
|
||||||
|
return tiles.map(({ title, icon, onClick, modalKey }) => (
|
||||||
<Tile
|
<Tile
|
||||||
key={title}
|
key={title}
|
||||||
title={title}
|
title={title}
|
||||||
@ -169,17 +470,35 @@ export default function PdaView() {
|
|||||||
else if (onClick) onClick()
|
else if (onClick) onClick()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
})()}
|
||||||
</TileLayout>
|
</TileLayout>
|
||||||
|
|
||||||
{/* Single Modal */}
|
{/* Single Modal */}
|
||||||
<ModalPDA isOpen={activeModal !== null} onClose={closeModal}>
|
<ModalPDA isOpen={activeModal !== null} onClose={closeModal}>
|
||||||
{activeModal === 'setStock' && <SetStockModal onClose={closeModal} />}
|
{activeModal === 'batchInfo' && <BatchInfoModal onClose={closeModal} selectedBatch={selectedBatch}/>}
|
||||||
{activeModal === 'otherReplacement' && <OtherReplacementModal onClose={closeModal} />}
|
{activeModal === 'setStock' &&
|
||||||
{activeModal === 'countStock' && <CountStockModal onClose={closeModal} />}
|
<SetStockModal onClose={closeModal} selectedBatch={selectedBatch} selectedSection={selectedSection}
|
||||||
|
selectedPosition={selectedPosition}/>}
|
||||||
|
{activeModal === 'setStockFromTemp' &&
|
||||||
|
<SetStockModalFromTemp onClose={closeModal} selectedSection={selectedSection}
|
||||||
|
selectedPosition={selectedPosition}/>}
|
||||||
|
{activeModal === 'otherReplacement' && <OtherReplacementModal onClose={closeModal}/>}
|
||||||
|
{activeModal === 'countStock' && <CountStockModal onClose={closeModal} selectedBatch={selectedBatch}/>}
|
||||||
|
{activeModal === 'editStock' &&
|
||||||
|
<EditStockSections onClose={closeModal} selectedPosition={selectedPosition}/>}
|
||||||
|
{activeModal === 'moveStock' &&
|
||||||
|
<MoveSectionModal onClose={closeModal}
|
||||||
|
fromPosition={prevPosition}
|
||||||
|
fromSection={prevSection}
|
||||||
|
toPosition={selectedPosition}
|
||||||
|
toSection={selectedSection}/>
|
||||||
|
}
|
||||||
|
{activeModal === 'changeCount' &&
|
||||||
|
<ChangeCountModal onClose={closeModal} selectedPosition={selectedPosition}/>}
|
||||||
</ModalPDA>
|
</ModalPDA>
|
||||||
|
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right"/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
94
resources/js/Pages/ShippingBatchList.tsx
Normal file
94
resources/js/Pages/ShippingBatchList.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import {Head} from "@inertiajs/react";
|
||||||
|
import {Toaster} from "react-hot-toast";
|
||||||
|
import AppLayout from "@/Layouts/AppLayout";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faCircleXmark, faSquareCheck} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
export default function ShippingBatchList() {
|
||||||
|
const [batches, setBatches] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios
|
||||||
|
.get("/api/batchListWMS")
|
||||||
|
.then((response) => {
|
||||||
|
console.log(response.data.batches);
|
||||||
|
setBatches(response.data.batches);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching batches:", error);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
title="Expedice batches"
|
||||||
|
renderHeader={() => (
|
||||||
|
<h2 className="font-semibold text-xl">Expedice batches</h2>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Head title="Expedice batches"/>
|
||||||
|
<Toaster position="top-center"/>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-4">Loading...</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Carrier</th>
|
||||||
|
<th>Results</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{batches.filter(b => b.item_count > 0).map((batch) => (
|
||||||
|
<tr key={batch.id} onClick={() => window.location.href = `/expedice/${batch.id}`}>
|
||||||
|
<td>{batch.id}</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<div className="avatar">
|
||||||
|
<div className="w-8 rounded-2">
|
||||||
|
<img
|
||||||
|
src={`https://www.dalkove-ovladace.cz/includes/templates/vat_responsive/images/shipping/small_icons/${batch.carrier_master.img}`}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{batch.carrier_master.shortname}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex gap-4 flex-col">
|
||||||
|
<div className="flex gap-3 items-center text-green-500">
|
||||||
|
<FontAwesomeIcon icon={faSquareCheck} />
|
||||||
|
{batch.item_count}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 items-center text-red-400">
|
||||||
|
<FontAwesomeIcon icon={faCircleXmark} />
|
||||||
|
{batch.error_count}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{batch.created_at}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
80
resources/js/Pages/StorageSetup.tsx
Normal file
80
resources/js/Pages/StorageSetup.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
|
import {Inertia} from '@inertiajs/inertia';
|
||||||
|
import {Head, router, usePage} from '@inertiajs/react';
|
||||||
|
import NewRoomModal from '@/Components/modals/NewRoomModal';
|
||||||
|
import EditRoomModal from '@/Components/modals/EditRoomModal';
|
||||||
|
import axios from "axios";
|
||||||
|
import {Toaster} from "react-hot-toast";
|
||||||
|
import AppLayout from "@/Layouts/AppLayout";
|
||||||
|
import BatchInfoWindow from "@/Components/BatchInfoWindow";
|
||||||
|
|
||||||
|
export default function StorageSetup() {
|
||||||
|
const {rooms} = usePage<{ rooms: any[] }>().props;
|
||||||
|
const [showNew, setShowNew] = useState(false);
|
||||||
|
const [editingRoom, setEditingRoom] = useState<any>(null);
|
||||||
|
|
||||||
|
|
||||||
|
// Modal ref
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||||
|
const openModal = () => dialogRef.current?.showModal();
|
||||||
|
const closeModal = () => dialogRef.current?.close();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout title="Storage setup" renderHeader={() => <h2 className="font-semibold text-xl">Storage setup</h2>}>
|
||||||
|
<Head title="Storage setup"/>
|
||||||
|
<Toaster position="top-center"/>
|
||||||
|
<div className="py-12">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h1 className="text-2xl font-bold">Rooms</h1>
|
||||||
|
<button onClick={() => {setShowNew(true); openModal();}} className="btn btn-primary">
|
||||||
|
+ New Room
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{rooms.map(room => (
|
||||||
|
<div
|
||||||
|
key={room.room_id}
|
||||||
|
className="card p-4 cursor-pointer hover:shadow"
|
||||||
|
onClick={() => {setEditingRoom(room); openModal();}}
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-medium">{room.room_name}</h2>
|
||||||
|
<p className="text-sm text-gray-500">{room.room_symbol}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{rooms.length === 0 && (
|
||||||
|
<p className="italic text-gray-500">No rooms defined yet.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<dialog ref={dialogRef} className="modal">
|
||||||
|
|
||||||
|
{showNew && (
|
||||||
|
<NewRoomModal
|
||||||
|
onClose={() => {setShowNew(false); closeModal();}}
|
||||||
|
onSaved={() => {
|
||||||
|
Inertia.reload();
|
||||||
|
setShowNew(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editingRoom && (
|
||||||
|
<EditRoomModal
|
||||||
|
room={editingRoom}
|
||||||
|
onClose={() => {setEditingRoom(null); closeModal();}}
|
||||||
|
onUpdated={() => {
|
||||||
|
Inertia.reload();
|
||||||
|
setEditingRoom(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
33
resources/js/functions/functions.tsx
Normal file
33
resources/js/functions/functions.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import {toast} from "react-hot-toast";
|
||||||
|
|
||||||
|
export function downloadBatchBarcode(batchId: number) {
|
||||||
|
try {
|
||||||
|
axios
|
||||||
|
.post('/api/pda/batchBarcodePrint', {id: batchId})
|
||||||
|
.then((response) => {
|
||||||
|
const dataUri = response.data.data
|
||||||
|
if (!dataUri) {
|
||||||
|
console.error('No image data found in JSON response')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary <a> element and trigger download
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = dataUri
|
||||||
|
link.download = `barcode_${batchId}.png`
|
||||||
|
// Append to DOM, click it, then remove it
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
toast.error('Failed to fetch or download barcode')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
toast.error('Failed to fetch or download barcode')
|
||||||
|
}
|
||||||
|
}
|
313
resources/js/interfaces/interfaces.tsx
Normal file
313
resources/js/interfaces/interfaces.tsx
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
import {colors} from "@material-tailwind/react/types/generic";
|
||||||
|
|
||||||
|
export interface ShipmentRequest {
|
||||||
|
id: number;
|
||||||
|
shipment_reference: string;
|
||||||
|
note: string;
|
||||||
|
user_id: number,
|
||||||
|
delivery_address_name: string;
|
||||||
|
delivery_address_company_name: string | null;
|
||||||
|
delivery_address_street_name: string;
|
||||||
|
delivery_address_street_number: string;
|
||||||
|
delivery_address_city: string;
|
||||||
|
delivery_address_zip: string;
|
||||||
|
delivery_address_state_iso: string | null;
|
||||||
|
delivery_address_country_iso: string;
|
||||||
|
pickup_point_code: string | null;
|
||||||
|
postnummer: string | null;
|
||||||
|
contact_email: string;
|
||||||
|
contact_telephone: string;
|
||||||
|
cod_value: string | null;
|
||||||
|
cod_variable_symbol: string | null;
|
||||||
|
shipment_value: string;
|
||||||
|
currency: string;
|
||||||
|
weight: string;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
length: number;
|
||||||
|
carrier: Carrier;
|
||||||
|
package_type: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
delivery_address: DeliveryAddress;
|
||||||
|
shipment: Shipment;
|
||||||
|
current_shipment_status: ShipmentStatusHistory;
|
||||||
|
shipment_statuses: ShipmentStatusHistory[],
|
||||||
|
user: User;
|
||||||
|
batch: Batch;
|
||||||
|
items: Item[];
|
||||||
|
shipment_price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
id: number;
|
||||||
|
shipment_request_id: number;
|
||||||
|
name: string;
|
||||||
|
item_note: string | null;
|
||||||
|
model_number: string;
|
||||||
|
HSCode: string;
|
||||||
|
price: string;
|
||||||
|
quantity: number;
|
||||||
|
originCountry: string;
|
||||||
|
weight: string;
|
||||||
|
item_id_internal_warehouse: number;
|
||||||
|
item_id_external: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
imageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Shipment {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
shipment_request_id: number;
|
||||||
|
internal_shipment_id: string;
|
||||||
|
shipment_id: string;
|
||||||
|
tracking_number: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipmentStatusHistory {
|
||||||
|
id: number;
|
||||||
|
shipment_status_id: number;
|
||||||
|
shipment_id: number;
|
||||||
|
status_note: string;
|
||||||
|
shipment_status: ShipmentStatus;
|
||||||
|
}
|
||||||
|
export interface ShipmentStatus {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeliveryAddress {
|
||||||
|
name: string;
|
||||||
|
companyName: string | null;
|
||||||
|
streetName: string;
|
||||||
|
streetNumber: string;
|
||||||
|
city: string;
|
||||||
|
zip: string;
|
||||||
|
stateISO: string | null;
|
||||||
|
countryISO: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Batch {
|
||||||
|
carrier_master: CarrierMaster;
|
||||||
|
id: number;
|
||||||
|
user: User;
|
||||||
|
item_count: number;
|
||||||
|
error_count: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
processed: boolean;
|
||||||
|
printed: boolean;
|
||||||
|
shipment_request: ShipmentRequest;
|
||||||
|
latest_generated_file: File;
|
||||||
|
}
|
||||||
|
export interface Error {
|
||||||
|
id: number;
|
||||||
|
shipment_request_id: number;
|
||||||
|
fetched: boolean;
|
||||||
|
error_message: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorObject {
|
||||||
|
error: Error;
|
||||||
|
shipment_request: ShipmentRequest;
|
||||||
|
batch_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarrierMaster {
|
||||||
|
id: number;
|
||||||
|
display_name: string;
|
||||||
|
shortname: string;
|
||||||
|
img: string;
|
||||||
|
api_url: string;
|
||||||
|
api_url_sandbox: string;
|
||||||
|
label_reprint: boolean;
|
||||||
|
carrier_enabled: boolean;
|
||||||
|
carriers: Carrier[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Carrier {
|
||||||
|
id: number;
|
||||||
|
internal: string;
|
||||||
|
ext_id: string;
|
||||||
|
carrier_name: string;
|
||||||
|
carrier_shortname: string;
|
||||||
|
pickup_points: boolean;
|
||||||
|
carrier_contract: string; // Adjust the type if necessary
|
||||||
|
customs_declarations: boolean; // Adjust the type if necessary
|
||||||
|
cod_allowed: boolean;
|
||||||
|
cod_enabled: boolean;
|
||||||
|
cod_price: string; // Adjust the type if necessary
|
||||||
|
country: string;
|
||||||
|
zone_id: number;
|
||||||
|
currency: string;
|
||||||
|
img: string;
|
||||||
|
enabled_store: boolean;
|
||||||
|
shipping_price: string; // Adjust the type if necessary
|
||||||
|
free_shipping_enabled: boolean;
|
||||||
|
delivery_time: string; // Adjust the type if necessary
|
||||||
|
display_order: number;
|
||||||
|
api_allowed: boolean;
|
||||||
|
carrier_master: CarrierMaster;
|
||||||
|
base_pricing: CarrierBasePricing;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertState {
|
||||||
|
show: boolean;
|
||||||
|
message: React.ReactNode;
|
||||||
|
color: colors;
|
||||||
|
pdfUrl: string;
|
||||||
|
file_id: number;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDetails {
|
||||||
|
user_id: number;
|
||||||
|
apiCallbackURL: string | null;
|
||||||
|
imageURL: string | null;
|
||||||
|
callbacks_enabled: boolean;
|
||||||
|
carrier_pricings?: UserCarrierPricing[]; // Adjusted to match model and JSON data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
details?: UserDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserCarrierPricing {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
carrier_id: number;
|
||||||
|
cod_price?: number;
|
||||||
|
carrier?: Carrier; // Relationship to Carrier
|
||||||
|
pricing_weights?: UserCarrierPricing2Weight[]; // Adjusted to match model and JSON data
|
||||||
|
extra_fees?: UserCarrierPricingExtraFees[]; // Adjusted to match model and JSON data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserCarrierPricing2Weight {
|
||||||
|
id: number;
|
||||||
|
user_carrier_pricing_id: number;
|
||||||
|
weight_max: number;
|
||||||
|
shipping_price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserCarrierPricingExtraFees {
|
||||||
|
id: number;
|
||||||
|
user_carrier_pricing_id: number;
|
||||||
|
carrier_extra_fee_type_id: number;
|
||||||
|
carrier_extra_fee_value: number;
|
||||||
|
carrier_extra_fee_value_type: string; // Could be "monetary" or other types
|
||||||
|
extra_fee_type?: CarrierExtraFeeTypes; // Relationship to CarrierExtraFeeTypes
|
||||||
|
}
|
||||||
|
export interface CarrierBasePricing {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
carrier_id: number;
|
||||||
|
cod_price?: number;
|
||||||
|
pricing_weights?: CarrierBasePricing2Weight[]; // Adjusted to match model and JSON data
|
||||||
|
extra_fees?: CarrierBasePricingExtraFees[]; // Adjusted to match model and JSON data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarrierBasePricing2Weight {
|
||||||
|
id: number;
|
||||||
|
carrier_base_pricing_id: number;
|
||||||
|
weight_max: number;
|
||||||
|
shipping_price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarrierBasePricingExtraFees {
|
||||||
|
id: number;
|
||||||
|
carrier_base_pricing_id: number;
|
||||||
|
carrier_extra_fee_type_id: number;
|
||||||
|
carrier_extra_fee_value: number;
|
||||||
|
carrier_extra_fee_value_type: string; // Could be "monetary" or other types
|
||||||
|
extra_fee_type?: CarrierExtraFeeTypes; // Relationship to CarrierExtraFeeTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CarrierExtraFeeTypes {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface PickupPoint {
|
||||||
|
id: number;
|
||||||
|
ext_id: string;
|
||||||
|
carrier_id: number;
|
||||||
|
carrier: Carrier;
|
||||||
|
location: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
directions?: string;
|
||||||
|
place_name?: string;
|
||||||
|
address: string;
|
||||||
|
zip: string;
|
||||||
|
city: string;
|
||||||
|
country_iso: string;
|
||||||
|
status_id?: number;
|
||||||
|
card?: boolean;
|
||||||
|
cod?: boolean;
|
||||||
|
wheelchair?: boolean;
|
||||||
|
photo_url?: string;
|
||||||
|
thumbnail_url?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
distance: string;
|
||||||
|
shipment_price: number;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Files {
|
||||||
|
files: File[]
|
||||||
|
}
|
||||||
|
export interface File {
|
||||||
|
|
||||||
|
id: number;
|
||||||
|
filename: string;
|
||||||
|
user: User;
|
||||||
|
// file_data: string;
|
||||||
|
lifetime: string;
|
||||||
|
associated_id: number;
|
||||||
|
type: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface StockList {
|
||||||
|
to_be_withdrawn: number,
|
||||||
|
name: string,
|
||||||
|
model: string,
|
||||||
|
physicalItemId: string,
|
||||||
|
mapping_breakdown: MappingProduct[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MappingProduct {
|
||||||
|
to_be_withdrawn: number,
|
||||||
|
name: string,
|
||||||
|
model: string,
|
||||||
|
mapping_id: string,
|
||||||
|
shipment_request_item_ids: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
id: number;
|
||||||
|
user: User;
|
||||||
|
task_title: string;
|
||||||
|
task_desc: string;
|
||||||
|
related_id: number;
|
||||||
|
related_id_type: string;
|
||||||
|
comment: string;
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
|
|
BIN
resources/js/sounds/error_scanner.mp3
Normal file
BIN
resources/js/sounds/error_scanner.mp3
Normal file
Binary file not shown.
@ -287,10 +287,26 @@ export interface PhysicalItem {
|
|||||||
updatedBy: number | null;
|
updatedBy: number | null;
|
||||||
|
|
||||||
// relations
|
// relations
|
||||||
// physicalItemType?: PhysicalItemType;
|
type?: PhysicalItemType;
|
||||||
// manufacturer?: Supplier;
|
// manufacturer?: Supplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PhysicalItemType {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
code_suffix: string;
|
||||||
|
_name: string;
|
||||||
|
priority: number;
|
||||||
|
weight: number | null;
|
||||||
|
volume: number | null;
|
||||||
|
title_template_id: number;
|
||||||
|
description_template_id: number;
|
||||||
|
created_at: string;
|
||||||
|
created_by: number;
|
||||||
|
updated_at: string;
|
||||||
|
updated_by: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -395,6 +411,7 @@ export interface StockPosition {
|
|||||||
shelf_id: number;
|
shelf_id: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
|
capacity: number;
|
||||||
|
|
||||||
// relations
|
// relations
|
||||||
shelf?: StockShelf;
|
shelf?: StockShelf;
|
||||||
@ -432,3 +449,10 @@ export interface StockEntries2Section {
|
|||||||
entry?: StockEntry;
|
entry?: StockEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
|
||||||
|
auth: {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="dracula">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
@ -1,18 +1,31 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\ExpediceController;
|
||||||
use App\Http\Controllers\Api\StockBatchController;
|
use App\Http\Controllers\Api\StockBatchController;
|
||||||
|
use App\Http\Controllers\Api\StockSectionController;
|
||||||
|
use App\Http\Controllers\Api\ScannerController;
|
||||||
use App\Http\Controllers\Api\StockEntryController;
|
use App\Http\Controllers\Api\StockEntryController;
|
||||||
use App\Http\Controllers\Api\StockHandleExpediceController;
|
use App\Http\Controllers\Api\StockHandleExpediceController;
|
||||||
use App\Http\Controllers\Api\SupplierController;
|
use App\Http\Controllers\Api\SupplierController;
|
||||||
use App\Http\Controllers\Api\StockEntryStatusController;
|
use App\Http\Controllers\Api\StockEntryStatusController;
|
||||||
|
use App\Http\Controllers\Api\StockPositionController;
|
||||||
|
use App\Http\Controllers\Api\StockRoomController;
|
||||||
|
use App\Http\Controllers\Api\StockLineController;
|
||||||
|
use App\Http\Controllers\Api\StockRackController;
|
||||||
|
use App\Http\Controllers\Api\StockShelfController;
|
||||||
|
use App\Http\Controllers\StorageController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
|
||||||
|
|
||||||
Route::get('/user', function (Request $request) {
|
Route::get('/user', function (Request $request) {
|
||||||
return $request->user();
|
return $request->user();
|
||||||
})->middleware('auth:sanctum');
|
})->middleware('auth:sanctum');
|
||||||
|
|
||||||
|
|
||||||
|
Route::middleware([EnsureFrontendRequestsAreStateful::class, 'auth:sanctum'])->group(function () {
|
||||||
|
|
||||||
|
// Route::group(['middleware' => ['role:admin']], function () {
|
||||||
// Stock Entry endpoints
|
// Stock Entry endpoints
|
||||||
Route::get('/stockData/options', [StockEntryController::class, 'getOptions']);
|
Route::get('/stockData/options', [StockEntryController::class, 'getOptions']);
|
||||||
Route::get('/stockData/options/items', [StockEntryController::class, 'getItems']);
|
Route::get('/stockData/options/items', [StockEntryController::class, 'getItems']);
|
||||||
@ -46,16 +59,15 @@ Route::get('/stockStatusList', [StockEntryController::class, 'getStatusList']);
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// barcode scanner methods
|
// barcode scanner methods
|
||||||
|
|
||||||
Route::post('stockActions/itemPickup', [StockEntryController::class, 'itemPickup']);
|
Route::post('stockActions/itemPickup', [StockEntryController::class, 'itemPickup']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Route::post('expediceActions/itemPickup', [StockHandleExpediceController::class, 'updateSectionCount']);
|
Route::post('expediceActions/itemPickup', [StockHandleExpediceController::class, 'updateSectionCount']);
|
||||||
|
|
||||||
|
|
||||||
// expedice akce
|
// expedice akce
|
||||||
@ -71,8 +83,6 @@ Route::post('expediceActions/itemPickup', [StockHandleExpediceController::class,
|
|||||||
// vybrat kolik se vejde do obalky, rating / volume
|
// vybrat kolik se vejde do obalky, rating / volume
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// skladnik akce
|
// skladnik akce
|
||||||
// prijde zbozi, vytiskne si X stitku s QR - nalepi na kazdou krabici + pripadne jeden "batch" stitek na palete
|
// prijde zbozi, vytiskne si X stitku s QR - nalepi na kazdou krabici + pripadne jeden "batch" stitek na palete
|
||||||
// vytvori batch u PC + zada vsechny produkty, ceny, ocekavane pocty
|
// vytvori batch u PC + zada vsechny produkty, ceny, ocekavane pocty
|
||||||
@ -90,8 +100,77 @@ Route::post('expediceActions/itemPickup', [StockHandleExpediceController::class,
|
|||||||
|
|
||||||
// rozbaleno produkty - prozkoumat
|
// rozbaleno produkty - prozkoumat
|
||||||
|
|
||||||
Route::post('/stockActions/{stockEntry}/status', [StockEntryStatusController::class, 'store']);
|
Route::post('/stockActions/{stockEntry}/status', [StockEntryStatusController::class, 'store']);
|
||||||
|
|
||||||
|
|
||||||
|
Route::get('/batchListWMS', [ExpediceController::class, 'batchListWMS']);
|
||||||
|
Route::post('/expediceListWMS', [ExpediceController::class, 'expediceListWMS']);
|
||||||
|
Route::get('/expediceListWMS/getImage', [ExpediceController::class, 'getProductImage']);
|
||||||
|
|
||||||
|
|
||||||
|
Route::post('/storage/setup', [StorageController::class, 'setup']);
|
||||||
|
|
||||||
|
Route::post('/pda/barcodeScan', [ScannerController::class, 'barcodeScan']);
|
||||||
|
Route::post('/pda/batchBarcodePrint', [ScannerController::class, 'batchBarcodePrint']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Route::post('/pdaView/countStock', [StockEntryController::class, 'countStock']);
|
||||||
|
Route::post('/pdaView/setStockSection', [StockSectionController::class, 'setSection']);
|
||||||
|
Route::post('/pdaView/storeStock', [StockSectionController::class, 'storeStock']);
|
||||||
|
Route::post('/pdaView/setBatchSection', [StockSectionController::class, 'setSectionForBatch']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Route::get('/pdaView/getStockPosition', [StockPositionController::class, 'getPosition']);
|
||||||
|
Route::post('/pdaView/moveStockSection', [StockSectionController::class, 'movePosition']);
|
||||||
|
Route::post('/pdaView/changeCount', [StockSectionController::class, 'changeCount']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Route::put('/stockPositions/{id}', [StockPositionController::class, 'update']);
|
||||||
|
Route::get('/stockSections/{id}', [StockSectionController::class, 'getSection']);
|
||||||
|
Route::post('/stockSections', [StockSectionController::class, 'store']);
|
||||||
|
Route::put('/stockSections/{id}', [StockSectionController::class, 'update']);
|
||||||
|
Route::delete('/stockSections/{id}', [StockSectionController::class, 'destroy']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//stock rooms
|
||||||
|
|
||||||
|
Route::post('/stock-rooms', [StockRoomController::class, 'store']);
|
||||||
|
Route::get('/stock-rooms/{room}', [StockRoomController::class, 'show']);
|
||||||
|
Route::put('/stock-rooms/{room}', [StockRoomController::class, 'update']);
|
||||||
|
Route::delete('/stock-rooms/{room}', [StockRoomController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Lines
|
||||||
|
Route::post ('/stock-lines', [StockLineController::class, 'store']);
|
||||||
|
Route::put ('/stock-lines/{line}', [StockLineController::class, 'update']);
|
||||||
|
Route::delete ('/stock-lines/{line}', [StockLineController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Racks
|
||||||
|
Route::post ('/stock-racks', [StockRackController::class, 'store']);
|
||||||
|
Route::put ('/stock-racks/{rack}', [StockRackController::class, 'update']);
|
||||||
|
Route::delete ('/stock-racks/{rack}', [StockRackController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Shelves
|
||||||
|
Route::post ('/stock-shelves', [StockShelfController::class, 'store']);
|
||||||
|
Route::put ('/stock-shelves/{shelf}',[StockShelfController::class, 'update']);
|
||||||
|
Route::delete ('/stock-shelves/{shelf}',[StockShelfController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Positions
|
||||||
|
Route::post ('/stock-positions', [StockPositionController::class,'store']);
|
||||||
|
Route::put ('/stock-positions/{pos}',[StockPositionController::class,'updateBasic']);
|
||||||
|
Route::delete ('/stock-positions/{pos}',[StockPositionController::class,'destroy']);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\StockBatch;
|
use App\Models\StockBatch;
|
||||||
|
use App\Models\StockRoom;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
@ -51,9 +52,29 @@ Route::middleware([
|
|||||||
})->name('batchCounting');
|
})->name('batchCounting');
|
||||||
|
|
||||||
|
|
||||||
// Stock Entries routes
|
|
||||||
Route::get('/pdaView', function () {
|
Route::get('/pdaView', function () {
|
||||||
return Inertia::render('PdaView');
|
return Inertia::render('PdaView');
|
||||||
})->name('pdaView');
|
})->name('pdaView');
|
||||||
|
|
||||||
|
Route::get('/expediceBatches', function () {
|
||||||
|
return Inertia::render('ShippingBatchList');
|
||||||
|
})->name('expediceBatches');
|
||||||
|
|
||||||
|
Route::get('/expedice/{id}', function (Request $request, $id) {
|
||||||
|
return Inertia::render('Expedice', [
|
||||||
|
'selectedBatchID' => $id
|
||||||
|
]);
|
||||||
|
})->name('expedice');
|
||||||
|
|
||||||
|
Route::get('/storageSetup', function () {
|
||||||
|
$rooms = StockRoom::with('lines.racks.shelves.positions.sections')->get();
|
||||||
|
return Inertia::render('StorageSetup',
|
||||||
|
[
|
||||||
|
'rooms' => $rooms
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
})->name('storageSetup');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user