partial update stock opname feature
This commit is contained in:
@@ -4,6 +4,8 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Dealer;
|
use App\Models\Dealer;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Stock;
|
||||||
use App\Models\Transaction;
|
use App\Models\Transaction;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Work;
|
use App\Models\Work;
|
||||||
@@ -26,7 +28,15 @@ class TransactionController extends Controller
|
|||||||
->select('d.name as dealer_name', 'd.id as dealer_id', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
->select('d.name as dealer_name', 'd.id as dealer_id', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
||||||
->where('users.id', Auth::user()->id)->first();
|
->where('users.id', Auth::user()->id)->first();
|
||||||
$now = Carbon::now()->translatedFormat('d F Y');
|
$now = Carbon::now()->translatedFormat('d F Y');
|
||||||
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic'));
|
|
||||||
|
// Get products with stock based on user role
|
||||||
|
$products = Product::with(['stocks' => function($query) {
|
||||||
|
$query->where('dealer_id', Auth::user()->dealer_id);
|
||||||
|
}, 'stocks.dealer'])
|
||||||
|
->where('active', true)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function workcategory($category_id)
|
public function workcategory($category_id)
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ use App\Models\Opname;
|
|||||||
use App\Models\OpnameDetail;
|
use App\Models\OpnameDetail;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\Stock;
|
use App\Models\Stock;
|
||||||
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use Yajra\DataTables\Facades\DataTables;
|
use Yajra\DataTables\Facades\DataTables;
|
||||||
|
|
||||||
class OpnamesController extends Controller
|
class OpnamesController extends Controller
|
||||||
@@ -89,8 +91,56 @@ class OpnamesController extends Controller
|
|||||||
try {
|
try {
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
|
|
||||||
// 1. Validasi input
|
// Check if this is from transaction form or regular opname form
|
||||||
$validated = $request->validate([
|
$isTransactionForm = $request->has('form') && $request->form === 'opname';
|
||||||
|
|
||||||
|
if ($isTransactionForm) {
|
||||||
|
// Custom validation for transaction form
|
||||||
|
$request->validate([
|
||||||
|
'dealer_id' => 'required|exists:dealers,id',
|
||||||
|
'user_id' => 'required|exists:users,id',
|
||||||
|
'opname_date' => [
|
||||||
|
'nullable',
|
||||||
|
'string',
|
||||||
|
'date_format:Y-m-d',
|
||||||
|
'before_or_equal:today'
|
||||||
|
],
|
||||||
|
'description' => 'nullable|string|max:1000',
|
||||||
|
'product_id' => 'required|array|min:1',
|
||||||
|
'product_id.*' => 'required|exists:products,id',
|
||||||
|
'system_stock' => 'required|array',
|
||||||
|
'system_stock.*' => 'required|numeric|min:0',
|
||||||
|
'physical_stock' => 'required|array',
|
||||||
|
'physical_stock.*' => 'required|numeric|min:0'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Process transaction form data with proper date parsing
|
||||||
|
$dealerId = $request->dealer_id;
|
||||||
|
$userId = $request->user_id;
|
||||||
|
|
||||||
|
// Parse opname date (YYYY-MM-DD format) or use today if empty
|
||||||
|
$inputDate = $request->opname_date ?: now()->format('Y-m-d');
|
||||||
|
Log::info('Parsing opname date', ['input' => $request->opname_date, 'using' => $inputDate]);
|
||||||
|
$opnameDate = Carbon::createFromFormat('Y-m-d', $inputDate);
|
||||||
|
Log::info('Successfully parsed opname date', ['parsed' => $opnameDate->format('Y-m-d')]);
|
||||||
|
|
||||||
|
$note = $request->description;
|
||||||
|
$productIds = $request->product_id;
|
||||||
|
$systemStocks = $request->system_stock;
|
||||||
|
$physicalStocks = $request->physical_stock;
|
||||||
|
|
||||||
|
// Log input data untuk debugging
|
||||||
|
Log::info('Transaction form input data', [
|
||||||
|
'product_ids' => $productIds,
|
||||||
|
'system_stocks' => $systemStocks,
|
||||||
|
'physical_stocks' => $physicalStocks,
|
||||||
|
'dealer_id' => $dealerId,
|
||||||
|
'user_id' => $userId
|
||||||
|
]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Original validation for regular opname form
|
||||||
|
$request->validate([
|
||||||
'dealer' => 'required|exists:dealers,id',
|
'dealer' => 'required|exists:dealers,id',
|
||||||
'product' => 'required|array|min:1',
|
'product' => 'required|array|min:1',
|
||||||
'product.*' => 'required|exists:products,id',
|
'product.*' => 'required|exists:products,id',
|
||||||
@@ -98,25 +148,60 @@ class OpnamesController extends Controller
|
|||||||
'system_quantity.*' => 'required|numeric|min:0',
|
'system_quantity.*' => 'required|numeric|min:0',
|
||||||
'physical_quantity' => 'required|array',
|
'physical_quantity' => 'required|array',
|
||||||
'physical_quantity.*' => 'required|numeric|min:0',
|
'physical_quantity.*' => 'required|numeric|min:0',
|
||||||
'note' => 'nullable|string|max:1000', // note utama
|
'note' => 'nullable|string|max:1000',
|
||||||
'item_notes' => 'nullable|array', // notes per item
|
'item_notes' => 'nullable|array',
|
||||||
'item_notes.*' => 'required_if:physical_quantity.*,!=,system_quantity.*|nullable|string|max:255'
|
'item_notes.*' => 'nullable|string|max:255',
|
||||||
|
'opname_date' => 'nullable|date' // Add opname_date validation for regular form
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. Validasi duplikasi produk
|
// Process regular form data
|
||||||
$productCounts = array_count_values(array_filter($request->product));
|
$dealerId = $request->dealer;
|
||||||
|
$userId = auth()->id();
|
||||||
|
|
||||||
|
// Use provided date or current date
|
||||||
|
$inputDate = $request->opname_date ?: now()->format('Y-m-d');
|
||||||
|
$opnameDate = $request->opname_date ?
|
||||||
|
Carbon::createFromFormat('Y-m-d', $inputDate) :
|
||||||
|
now();
|
||||||
|
|
||||||
|
$note = $request->note;
|
||||||
|
$productIds = $request->product;
|
||||||
|
$systemStocks = $request->system_quantity;
|
||||||
|
$physicalStocks = $request->physical_quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validasi minimal ada produk yang diisi (termasuk nilai 0)
|
||||||
|
$validProductIds = array_filter($productIds);
|
||||||
|
$validSystemStocks = array_filter($systemStocks, function($value) { return $value !== null && $value !== ''; });
|
||||||
|
$validPhysicalStocks = array_filter($physicalStocks, function($value) {
|
||||||
|
return $value !== null && $value !== '' && is_numeric($value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (empty($validProductIds) || count($validProductIds) === 0) {
|
||||||
|
throw new \Exception('Minimal harus ada satu produk yang diisi untuk opname.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($validPhysicalStocks) === 0) {
|
||||||
|
throw new \Exception('Minimal harus ada satu stock fisik yang diisi (termasuk nilai 0).');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validasi duplikasi produk
|
||||||
|
$productCounts = array_count_values($validProductIds);
|
||||||
foreach ($productCounts as $productId => $count) {
|
foreach ($productCounts as $productId => $count) {
|
||||||
if ($count > 1) {
|
if ($count > 1) {
|
||||||
throw new \Exception('Product tidak boleh duplikat.');
|
throw new \Exception('Produk tidak boleh duplikat dalam satu opname.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Validasi dealer
|
// 4. Validasi dealer
|
||||||
$dealer = Dealer::findOrFail($request->dealer);
|
$dealer = Dealer::findOrFail($dealerId);
|
||||||
|
|
||||||
// 4. Validasi produk aktif
|
// 5. Validasi user exists
|
||||||
$productIds = array_filter($request->product);
|
$user = User::findOrFail($userId);
|
||||||
$inactiveProducts = Product::whereIn('id', $productIds)
|
|
||||||
|
// 6. Validasi produk aktif
|
||||||
|
$filteredProductIds = array_filter($productIds);
|
||||||
|
$inactiveProducts = Product::whereIn('id', $filteredProductIds)
|
||||||
->where('active', false)
|
->where('active', false)
|
||||||
->pluck('name')
|
->pluck('name')
|
||||||
->toArray();
|
->toArray();
|
||||||
@@ -125,13 +210,15 @@ class OpnamesController extends Controller
|
|||||||
throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts));
|
throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Validasi stock dan note
|
// 7. Validasi stock difference (for transaction form, we'll allow any difference without note requirement)
|
||||||
$stockDifferences = [];
|
$stockDifferences = [];
|
||||||
foreach ($request->product as $index => $productId) {
|
if (!$isTransactionForm) {
|
||||||
|
// Only validate notes for regular opname form
|
||||||
|
foreach ($productIds as $index => $productId) {
|
||||||
if (!$productId) continue;
|
if (!$productId) continue;
|
||||||
|
|
||||||
$systemStock = floatval($request->system_quantity[$index] ?? 0);
|
$systemStock = floatval($systemStocks[$index] ?? 0);
|
||||||
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
|
$physicalStock = floatval($physicalStocks[$index] ?? 0);
|
||||||
$itemNote = $request->input("item_notes.{$index}");
|
$itemNote = $request->input("item_notes.{$index}");
|
||||||
|
|
||||||
// Jika ada perbedaan stock dan note kosong
|
// Jika ada perbedaan stock dan note kosong
|
||||||
@@ -147,27 +234,49 @@ class OpnamesController extends Controller
|
|||||||
implode(', ', $stockDifferences)
|
implode(', ', $stockDifferences)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Create Opname master record with approved status
|
// 8. Create Opname master record with approved status
|
||||||
$opname = Opname::create([
|
$opname = Opname::create([
|
||||||
'dealer_id' => $request->dealer,
|
'dealer_id' => $dealerId,
|
||||||
'opname_date' => now(),
|
'opname_date' => $opnameDate,
|
||||||
'user_id' => auth()->id(),
|
'user_id' => $userId,
|
||||||
'note' => $request->note,
|
'note' => $note,
|
||||||
'status' => 'approved', // Set status langsung approved
|
'status' => 'approved', // Set status langsung approved
|
||||||
'approved_by' => auth()->id(), // Set current user sebagai approver
|
'approved_by' => $userId, // Set current user sebagai approver
|
||||||
'approved_at' => now() // Set waktu approval
|
'approved_at' => now() // Set waktu approval
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 7. Create OpnameDetails and update stock
|
// 9. Create OpnameDetails and update stock - only for valid entries
|
||||||
$details = [];
|
$details = [];
|
||||||
foreach ($request->product as $index => $productId) {
|
$processedCount = 0;
|
||||||
|
|
||||||
|
foreach ($productIds as $index => $productId) {
|
||||||
if (!$productId) continue;
|
if (!$productId) continue;
|
||||||
|
|
||||||
$systemStock = floatval($request->system_quantity[$index] ?? 0);
|
// Skip only if physical stock is truly not provided (empty string or null)
|
||||||
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
|
// Accept 0 as valid input
|
||||||
|
if (!isset($physicalStocks[$index]) || $physicalStocks[$index] === '' || $physicalStocks[$index] === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that physical stock is numeric (including 0)
|
||||||
|
if (!is_numeric($physicalStocks[$index])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$systemStock = floatval($systemStocks[$index] ?? 0);
|
||||||
|
$physicalStock = floatval($physicalStocks[$index]);
|
||||||
$difference = $physicalStock - $systemStock;
|
$difference = $physicalStock - $systemStock;
|
||||||
|
|
||||||
|
$processedCount++;
|
||||||
|
|
||||||
|
// Get item note (only for regular opname form)
|
||||||
|
$itemNote = null;
|
||||||
|
if (!$isTransactionForm) {
|
||||||
|
$itemNote = $request->input("item_notes.{$index}");
|
||||||
|
}
|
||||||
|
|
||||||
// Create opname detail
|
// Create opname detail
|
||||||
$details[] = [
|
$details[] = [
|
||||||
'opname_id' => $opname->id,
|
'opname_id' => $opname->id,
|
||||||
@@ -175,7 +284,7 @@ class OpnamesController extends Controller
|
|||||||
'system_stock' => $systemStock,
|
'system_stock' => $systemStock,
|
||||||
'physical_stock' => $physicalStock,
|
'physical_stock' => $physicalStock,
|
||||||
'difference' => $difference,
|
'difference' => $difference,
|
||||||
'note' => $request->input("item_notes.{$index}"),
|
'note' => $itemNote,
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now()
|
'updated_at' => now()
|
||||||
];
|
];
|
||||||
@@ -184,7 +293,7 @@ class OpnamesController extends Controller
|
|||||||
$stock = Stock::firstOrCreate(
|
$stock = Stock::firstOrCreate(
|
||||||
[
|
[
|
||||||
'product_id' => $productId,
|
'product_id' => $productId,
|
||||||
'dealer_id' => $request->dealer
|
'dealer_id' => $dealerId
|
||||||
],
|
],
|
||||||
['quantity' => 0]
|
['quantity' => 0]
|
||||||
);
|
);
|
||||||
@@ -197,35 +306,84 @@ class OpnamesController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate we have at least one detail to insert
|
||||||
|
if (empty($details)) {
|
||||||
|
throw new \Exception('Tidak ada data stock fisik yang valid untuk diproses.');
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk insert untuk performa lebih baik
|
// Bulk insert untuk performa lebih baik
|
||||||
OpnameDetail::insert($details);
|
OpnameDetail::insert($details);
|
||||||
|
|
||||||
// 8. Log aktivitas
|
// 10. Log aktivitas dengan detail produk yang diproses
|
||||||
|
$processedProducts = collect($details)->map(function($detail) {
|
||||||
|
return [
|
||||||
|
'product_id' => $detail['product_id'],
|
||||||
|
'system_stock' => $detail['system_stock'],
|
||||||
|
'physical_stock' => $detail['physical_stock'],
|
||||||
|
'difference' => $detail['difference']
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
Log::info('Opname created and auto-approved', [
|
Log::info('Opname created and auto-approved', [
|
||||||
'opname_id' => $opname->id,
|
'opname_id' => $opname->id,
|
||||||
'dealer_id' => $opname->dealer_id,
|
'dealer_id' => $opname->dealer_id,
|
||||||
'user_id' => auth()->id(),
|
'user_id' => $userId,
|
||||||
'approver_id' => auth()->id(),
|
'approver_id' => $userId,
|
||||||
'product_count' => count($details)
|
'product_count' => count($details),
|
||||||
|
'processed_count' => $processedCount,
|
||||||
|
'form_type' => $isTransactionForm ? 'transaction' : 'regular',
|
||||||
|
'opname_date' => $opnameDate->format('Y-m-d'),
|
||||||
|
'processed_products' => $processedProducts->toArray()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
|
|
||||||
|
if ($isTransactionForm) {
|
||||||
|
// Redirect back to transaction page with success message
|
||||||
|
return redirect()
|
||||||
|
->route('transaction')
|
||||||
|
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.");
|
||||||
|
} else {
|
||||||
|
// Redirect to opname index for regular form
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('opnames.index')
|
->route('opnames.index')
|
||||||
->with('success', 'Opname berhasil disimpan dan disetujui.');
|
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.");
|
||||||
|
}
|
||||||
|
|
||||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
|
Log::error('Validation error in OpnamesController@store', [
|
||||||
|
'errors' => $e->errors(),
|
||||||
|
'input' => $request->all()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($isTransactionForm) {
|
||||||
|
return redirect()
|
||||||
|
->route('transaction')
|
||||||
|
->withErrors($e->validator)
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Terjadi kesalahan validasi. Periksa kembali data yang dimasukkan.');
|
||||||
|
} else {
|
||||||
return back()->withErrors($e->validator)->withInput();
|
return back()->withErrors($e->validator)->withInput();
|
||||||
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
Log::error('Error in OpnamesController@store: ' . $e->getMessage());
|
Log::error('Error in OpnamesController@store: ' . $e->getMessage());
|
||||||
Log::error($e->getTraceAsString());
|
Log::error($e->getTraceAsString());
|
||||||
|
Log::error('Request data:', $request->all());
|
||||||
|
|
||||||
return back()
|
$errorMessage = $e->getMessage();
|
||||||
->with('error', $e->getMessage())
|
|
||||||
|
if ($isTransactionForm) {
|
||||||
|
return redirect()
|
||||||
|
->route('transaction')
|
||||||
|
->with('error', $errorMessage)
|
||||||
->withInput();
|
->withInput();
|
||||||
|
} else {
|
||||||
|
return back()
|
||||||
|
->with('error', $errorMessage)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,4 +447,6 @@ class OpnamesController extends Controller
|
|||||||
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500);
|
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
100
dev-restart.sh
Executable file
100
dev-restart.sh
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Development Restart Script
|
||||||
|
# Usage: ./dev-restart.sh [cache|config|routes|all|container]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
ACTION=${1:-cache}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
case $ACTION in
|
||||||
|
cache)
|
||||||
|
print_info "Clearing Laravel cache..."
|
||||||
|
docker-compose exec app php artisan cache:clear
|
||||||
|
print_success "Cache cleared!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
config)
|
||||||
|
print_info "Clearing configuration cache..."
|
||||||
|
docker-compose exec app php artisan config:clear
|
||||||
|
print_success "Config cache cleared!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
routes)
|
||||||
|
print_info "Clearing route cache..."
|
||||||
|
docker-compose exec app php artisan route:clear
|
||||||
|
print_success "Route cache cleared!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
all)
|
||||||
|
print_info "Clearing all Laravel caches..."
|
||||||
|
docker-compose exec app php artisan optimize:clear
|
||||||
|
print_success "All caches cleared!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
container)
|
||||||
|
print_info "Restarting app container..."
|
||||||
|
docker-compose restart app
|
||||||
|
print_success "Container restarted!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
build)
|
||||||
|
print_info "Rebuilding app container..."
|
||||||
|
docker-compose up -d --build app
|
||||||
|
print_success "Container rebuilt!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
migrate)
|
||||||
|
print_info "Running database migrations..."
|
||||||
|
docker-compose exec app php artisan migrate
|
||||||
|
print_success "Migrations completed!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
composer)
|
||||||
|
print_info "Installing/updating composer dependencies..."
|
||||||
|
docker-compose exec app composer install --optimize-autoloader
|
||||||
|
print_success "Composer dependencies updated!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
npm)
|
||||||
|
print_info "Installing npm dependencies and building assets..."
|
||||||
|
docker-compose exec app npm install
|
||||||
|
docker-compose exec app npm run dev
|
||||||
|
print_success "Frontend assets built!"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Development Restart Script"
|
||||||
|
echo "Usage: $0 [cache|config|routes|all|container|build|migrate|composer|npm]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " cache - Clear application cache only"
|
||||||
|
echo " config - Clear configuration cache"
|
||||||
|
echo " routes - Clear route cache"
|
||||||
|
echo " all - Clear all Laravel caches"
|
||||||
|
echo " container - Restart app container"
|
||||||
|
echo " build - Rebuild app container"
|
||||||
|
echo " migrate - Run database migrations"
|
||||||
|
echo " composer - Update composer dependencies"
|
||||||
|
echo " npm - Update npm and build assets"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
@extends('layouts.frontapp')
|
@extends('layouts.frontapp')
|
||||||
|
|
||||||
|
@php
|
||||||
|
use App\Models\Dealer;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
@endphp
|
||||||
|
|
||||||
{{-- @section('contentHead')
|
{{-- @section('contentHead')
|
||||||
<div class="kt-subheader kt-grid__item" id="kt_subheader">
|
<div class="kt-subheader kt-grid__item" id="kt_subheader">
|
||||||
<div class="kt-container kt-container--fluid ">
|
<div class="kt-container kt-container--fluid ">
|
||||||
@@ -19,6 +24,79 @@
|
|||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
.table-responsive {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.text-success {
|
||||||
|
color: #28a745 !important;
|
||||||
|
}
|
||||||
|
.text-danger {
|
||||||
|
color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
.text-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.system-stock {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
.table-bordered th,
|
||||||
|
.table-bordered td {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
.table thead th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: rgba(0,0,0,.02);
|
||||||
|
}
|
||||||
|
.physical-stock {
|
||||||
|
font-weight: 500;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
transition: border-color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
.physical-stock:focus {
|
||||||
|
border-color: #ffc107;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25);
|
||||||
|
}
|
||||||
|
.difference {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
.btn-lg {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.btn-lg:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
.physical-stock.is-invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
background-color: #fff5f5;
|
||||||
|
}
|
||||||
|
.physical-stock.is-invalid:focus {
|
||||||
|
border-color: #dc3545;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@@ -92,22 +170,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if (session('error'))
|
||||||
|
<div class="row mt-2 mb-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-danger fade show" role="alert">
|
||||||
|
<div class="alert-text">{{ session('error') }}</div>
|
||||||
|
<div class="alert-close">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true"><i class="la la-close"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<!--begin::Portlet-->
|
<!--begin::Portlet-->
|
||||||
<div class="kt-portlet">
|
<div class="kt-portlet">
|
||||||
<div class="kt-portlet__body">
|
<div class="kt-portlet__body">
|
||||||
|
<!-- Main Tabs -->
|
||||||
<ul class="nav nav-tabs nav-tabs-line nav-tabs-line-primary" role="tablist">
|
<ul class="nav nav-tabs nav-tabs-line nav-tabs-line-primary" role="tablist">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link @if(old('form') == 'wash') @else active @endif" data-toggle="tab" href="#work">Form Kerja</a>
|
<a class="nav-link active" data-toggle="tab" href="#transaksi">Transaksi</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link @if(old('form') == 'wash') active @endif" data-toggle="tab" href="#wash">Form Cuci</a>
|
<a class="nav-link" data-toggle="tab" href="#stock">Stock</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane @if(old('form') == 'wash') @else active @endif" id="work" role="tabpanel">
|
<!-- Tab Transaksi -->
|
||||||
|
<div class="tab-pane active" id="transaksi" role="tabpanel">
|
||||||
|
<!-- Sub Tabs untuk Transaksi -->
|
||||||
|
<ul class="nav nav-tabs nav-tabs-line nav-tabs-line-success mt-3" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link @if(old('form') == 'wash') @else active @endif" data-toggle="tab" href="#form-kerja">Form Kerja</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link @if(old('form') == 'wash') active @endif" data-toggle="tab" href="#form-cuci">Form Cuci</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content mt-3">
|
||||||
|
<!-- Form Kerja -->
|
||||||
|
<div class="tab-pane @if(old('form') == 'wash') @else active @endif" id="form-kerja" role="tabpanel">
|
||||||
<form action="{{ route('transaction.store') }}" method="POST" id="workForm">
|
<form action="{{ route('transaction.store') }}" method="POST" id="workForm">
|
||||||
@csrf
|
@csrf
|
||||||
<input type="hidden" name="form" value="work">
|
<input type="hidden" name="form" value="work">
|
||||||
@@ -304,7 +412,9 @@
|
|||||||
<button class="btn btn-brand button-save" style="display: block; width: 100%;">Simpan</button>
|
<button class="btn btn-brand button-save" style="display: block; width: 100%;">Simpan</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane @if(old('form') == 'wash') active @endif" id="wash" role="tabpanel">
|
|
||||||
|
<!-- Form Cuci -->
|
||||||
|
<div class="tab-pane @if(old('form') == 'wash') active @endif" id="form-cuci" role="tabpanel">
|
||||||
<form action="{{ route('transaction.store') }}" method="POST" id="washForm">
|
<form action="{{ route('transaction.store') }}" method="POST" id="washForm">
|
||||||
@csrf
|
@csrf
|
||||||
<input type="hidden" name="form" value="wash">
|
<input type="hidden" name="form" value="wash">
|
||||||
@@ -383,6 +493,178 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Stock -->
|
||||||
|
<div class="tab-pane" id="stock" role="tabpanel">
|
||||||
|
<!-- Sub Tabs untuk Stock -->
|
||||||
|
<ul class="nav nav-tabs nav-tabs-line nav-tabs-line-warning mt-3" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" data-toggle="tab" href="#opname">Opname</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-toggle="tab" href="#mutasi">Mutasi</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content mt-3">
|
||||||
|
<!-- Form Opname -->
|
||||||
|
<div class="tab-pane active" id="opname" role="tabpanel">
|
||||||
|
<form action="{{ route('opnames.store') }}" method="POST" id="opnameForm">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="form" value="opname">
|
||||||
|
<input type="hidden" name="user_id" value="{{ $mechanic->id }}">
|
||||||
|
<input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tanggal Opname <small class="text-muted">(Default: Hari ini)</small></label>
|
||||||
|
<input type="text" name="opname_date" id="date-opname" class="form-control @error('opname_date') is-invalid @enderror" value="{{ old('opname_date', date('Y-m-d')) }}" placeholder="YYYY-MM-DD">
|
||||||
|
@error('opname_date')
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $message }}
|
||||||
|
</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Keterangan</label>
|
||||||
|
<textarea name="description" class="form-control @error('description') is-invalid @enderror" rows="3" placeholder="Keterangan opname">{{ old('description') }}</textarea>
|
||||||
|
@error('description')
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $message }}
|
||||||
|
</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List Produk dengan Stock -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th class="text-center">Produk</th>
|
||||||
|
<th class="text-center">Dealer</th>
|
||||||
|
<th class="text-center">Stock Sistem</th>
|
||||||
|
<th class="text-center">Stock Fisik</th>
|
||||||
|
<th class="text-center">Selisih</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{-- Dealer/Mechanic - Show products for current dealer only --}}
|
||||||
|
@foreach($products as $product)
|
||||||
|
@php
|
||||||
|
$stock = $product->stocks->first();
|
||||||
|
$currentStock = $stock ? $stock->quantity : 0;
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">{{ $product->name }}</td>
|
||||||
|
<td class="text-center">{{ $mechanic->dealer_name }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="system-stock">{{ number_format($currentStock, 2) }}</span>
|
||||||
|
<input type="hidden" name="product_id[]" value="{{ $product->id }}">
|
||||||
|
<input type="hidden" name="dealer_id_stock[]" value="{{ $mechanic->dealer_id }}">
|
||||||
|
<input type="hidden" name="system_stock[]" value="{{ $currentStock }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control physical-stock @error('physical_stock.'.$loop->index) is-invalid @enderror" name="physical_stock[]" step="0.01" placeholder="0.00" data-system="{{ $currentStock }}" value="{{ old('physical_stock.'.$loop->index) }}">
|
||||||
|
@error('physical_stock.'.$loop->index)
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{ $message }}
|
||||||
|
</div>
|
||||||
|
@enderror
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="difference text-bold">0.00</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 mb-3">
|
||||||
|
<button class="btn btn-warning btn-lg button-save d-block w-100 mt-2">
|
||||||
|
Simpan Opname
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Mutasi -->
|
||||||
|
<div class="tab-pane" id="mutasi" role="tabpanel">
|
||||||
|
<form action="#" method="POST" id="mutasiForm">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="form" value="mutasi">
|
||||||
|
<input type="hidden" name="mechanic_id" value="{{ $mechanic->id }}">
|
||||||
|
<input type="hidden" name="dealer_id" value="{{ $mechanic->dealer_id }}">
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-6">
|
||||||
|
<label>Tanggal Mutasi</label>
|
||||||
|
<input type="text" name="mutasi_date" id="date-mutasi" class="form-control" placeholder="Tanggal Mutasi" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label>Jenis Mutasi</label>
|
||||||
|
<select name="mutasi_type" class="form-control" required>
|
||||||
|
<option value="">Pilih Jenis</option>
|
||||||
|
<option value="masuk">Barang Masuk</option>
|
||||||
|
<option value="keluar">Barang Keluar</option>
|
||||||
|
<option value="transfer">Transfer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Keterangan</label>
|
||||||
|
<textarea name="description" class="form-control" rows="3" placeholder="Keterangan mutasi"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List Produk untuk Mutasi -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="5%">
|
||||||
|
<input type="checkbox" id="select-all-mutasi">
|
||||||
|
</th>
|
||||||
|
<th>Produk</th>
|
||||||
|
<th>Stock Saat Ini</th>
|
||||||
|
<th>Jumlah Mutasi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{-- Dealer/Mechanic - Show products for current dealer only --}}
|
||||||
|
@foreach($products as $product)
|
||||||
|
@php
|
||||||
|
$stock = $product->stocks->first();
|
||||||
|
$currentStock = $stock ? $stock->quantity : 0;
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="mutasi-product-checkbox" name="selected_products[]" value="{{ $product->id }}_{{ $mechanic->dealer_id }}">
|
||||||
|
</td>
|
||||||
|
<td>{{ $product->name }}</td>
|
||||||
|
<td class="text-right">{{ number_format($currentStock, 2) }}</td>
|
||||||
|
<td>
|
||||||
|
<input type="number" class="form-control mutasi-quantity" name="mutasi_quantity[{{ $product->id }}_{{ $mechanic->dealer_id }}]" step="0.01" placeholder="0.00" disabled>
|
||||||
|
<input type="hidden" name="product_id_mutasi[]" value="{{ $product->id }}">
|
||||||
|
<input type="hidden" name="dealer_id_mutasi[]" value="{{ $mechanic->dealer_id }}">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 mb-3">
|
||||||
|
<button class="btn btn-warning btn-lg button-save d-block w-100 mt-2">
|
||||||
|
Simpan Mutasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--end::Portlet-->
|
<!--end::Portlet-->
|
||||||
</div>
|
</div>
|
||||||
@@ -498,8 +780,307 @@
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
|
|
||||||
$("#date-work").datepicker()
|
$("#opnameForm").submit(function(e) {
|
||||||
$("#date-wash").datepicker()
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validate form
|
||||||
|
var hasValidStock = false;
|
||||||
|
var invalidRows = [];
|
||||||
|
|
||||||
|
$('.physical-stock').each(function(index) {
|
||||||
|
var value = $(this).val();
|
||||||
|
var row = $(this).closest('tr');
|
||||||
|
var productName = row.find('td:first').text().trim();
|
||||||
|
|
||||||
|
// Check if value is valid (including 0)
|
||||||
|
if (value !== '' && value !== null && value !== undefined) {
|
||||||
|
var numValue = parseFloat(value);
|
||||||
|
if (!isNaN(numValue) && numValue >= 0) {
|
||||||
|
hasValidStock = true;
|
||||||
|
// Ensure the value is properly formatted
|
||||||
|
$(this).val(numValue.toFixed(2));
|
||||||
|
} else {
|
||||||
|
invalidRows.push(productName + ' (nilai tidak valid)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Don't remove elements here - let them stay for re-editing
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show error if no valid stock entries
|
||||||
|
if (!hasValidStock) {
|
||||||
|
resetSubmitButton();
|
||||||
|
highlightInvalidFields();
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Peringatan',
|
||||||
|
text: 'Minimal harus ada satu produk dengan stock fisik yang diisi dengan benar!'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error if there are invalid entries
|
||||||
|
if (invalidRows.length > 0) {
|
||||||
|
resetSubmitButton();
|
||||||
|
highlightInvalidFields();
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Data Tidak Valid',
|
||||||
|
text: 'Perbaiki data berikut: ' + invalidRows.join(', ')
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get opname date or use today as default
|
||||||
|
var opnameDate = $('#date-opname').val();
|
||||||
|
if (!opnameDate) {
|
||||||
|
// Set default to today if empty
|
||||||
|
var today = new Date().toISOString().split('T')[0];
|
||||||
|
$('#date-opname').val(today);
|
||||||
|
opnameDate = today;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date format (YYYY-MM-DD)
|
||||||
|
var datePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||||
|
if (!datePattern.test(opnameDate)) {
|
||||||
|
resetSubmitButton();
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Format Tanggal Salah',
|
||||||
|
text: 'Format tanggal harus YYYY-MM-DD (contoh: 2023-12-25)'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if date is valid
|
||||||
|
var dateParts = opnameDate.match(datePattern);
|
||||||
|
var year = parseInt(dateParts[1], 10);
|
||||||
|
var month = parseInt(dateParts[2], 10);
|
||||||
|
var day = parseInt(dateParts[3], 10);
|
||||||
|
|
||||||
|
var testDate = new Date(year, month - 1, day);
|
||||||
|
if (testDate.getDate() !== day || testDate.getMonth() !== (month - 1) || testDate.getFullYear() !== year) {
|
||||||
|
resetSubmitButton();
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Tanggal Tidak Valid',
|
||||||
|
text: 'Tanggal yang dimasukkan tidak valid!'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if date is not in the future
|
||||||
|
var today = new Date();
|
||||||
|
today.setHours(23, 59, 59, 999); // Set to end of today
|
||||||
|
if (testDate > today) {
|
||||||
|
resetSubmitButton();
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'warning',
|
||||||
|
title: 'Tanggal Tidak Valid',
|
||||||
|
text: 'Tanggal opname tidak boleh lebih dari hari ini!'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".button-save").attr("disabled", true);
|
||||||
|
$(".button-save").addClass("disabled");
|
||||||
|
$(".button-save").html('<i class="fa fa-spinner fa-spin"></i> Menyimpan...');
|
||||||
|
|
||||||
|
// Date format is already YYYY-MM-DD, no conversion needed
|
||||||
|
|
||||||
|
// Clean up empty rows before submit (remove hidden inputs for truly empty fields only)
|
||||||
|
$('.physical-stock').each(function() {
|
||||||
|
var value = $(this).val();
|
||||||
|
var row = $(this).closest('tr');
|
||||||
|
|
||||||
|
// Only remove hidden inputs if physical stock is truly empty (not 0)
|
||||||
|
// Keep 0 values as they are valid input
|
||||||
|
if (value === '' || value === null || value === undefined) {
|
||||||
|
row.find('input[name="product_id[]"]').remove();
|
||||||
|
row.find('input[name="dealer_id_stock[]"]').remove();
|
||||||
|
row.find('input[name="system_stock[]"]').remove();
|
||||||
|
// But keep the physical_stock input so it can be re-edited if form fails
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit form
|
||||||
|
this.submit();
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#mutasiForm").submit(function(e) {
|
||||||
|
$(".button-save").attr("disabled");
|
||||||
|
$(".button-save").addClass("disabled");
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#date-work").datepicker({
|
||||||
|
format: 'yyyy-mm-dd',
|
||||||
|
autoclose: true,
|
||||||
|
todayHighlight: true
|
||||||
|
})
|
||||||
|
$("#date-wash").datepicker({
|
||||||
|
format: 'yyyy-mm-dd',
|
||||||
|
autoclose: true,
|
||||||
|
todayHighlight: true
|
||||||
|
})
|
||||||
|
$("#date-opname").datepicker({
|
||||||
|
format: 'yyyy-mm-dd',
|
||||||
|
autoclose: true,
|
||||||
|
todayHighlight: true,
|
||||||
|
startDate: '-30d',
|
||||||
|
endDate: '+0d'
|
||||||
|
})
|
||||||
|
$("#date-mutasi").datepicker({
|
||||||
|
format: 'yyyy-mm-dd',
|
||||||
|
autoclose: true,
|
||||||
|
todayHighlight: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate difference for opname
|
||||||
|
$(document).on('input change keyup', '.physical-stock', function() {
|
||||||
|
var systemStock = parseFloat($(this).data('system')) || 0;
|
||||||
|
var inputValue = $(this).val();
|
||||||
|
|
||||||
|
// Handle empty input - set to 0 for calculation
|
||||||
|
var physicalStock = 0;
|
||||||
|
if (inputValue !== '' && inputValue !== null && inputValue !== undefined) {
|
||||||
|
physicalStock = parseFloat(inputValue) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var difference = physicalStock - systemStock;
|
||||||
|
|
||||||
|
var differenceCell = $(this).closest('tr').find('.difference');
|
||||||
|
differenceCell.text(difference.toFixed(2));
|
||||||
|
|
||||||
|
// Add color coding for difference
|
||||||
|
if (difference > 0) {
|
||||||
|
differenceCell.removeClass('text-danger').addClass('text-success');
|
||||||
|
} else if (difference < 0) {
|
||||||
|
differenceCell.removeClass('text-success').addClass('text-danger');
|
||||||
|
} else {
|
||||||
|
differenceCell.removeClass('text-success text-danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update product counter
|
||||||
|
updateProductCounter();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to update product counter
|
||||||
|
function updateProductCounter() {
|
||||||
|
var filledProducts = 0;
|
||||||
|
var totalProducts = $('.physical-stock').length;
|
||||||
|
|
||||||
|
$('.physical-stock').each(function() {
|
||||||
|
var value = $(this).val();
|
||||||
|
// Count as filled if it's a valid number (including 0)
|
||||||
|
if (value !== '' && value !== null && value !== undefined && !isNaN(parseFloat(value)) && parseFloat(value) >= 0) {
|
||||||
|
filledProducts++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button text to show progress
|
||||||
|
var buttonText = 'Simpan Opname';
|
||||||
|
if (filledProducts > 0) {
|
||||||
|
buttonText += ` (${filledProducts}/${totalProducts} produk)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$('.button-save').hasClass('disabled')) {
|
||||||
|
$('.button-save').html(buttonText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle when input loses focus - don't auto-fill, let user decide
|
||||||
|
$(document).on('blur', '.physical-stock', function() {
|
||||||
|
// Trigger calculation even for empty values
|
||||||
|
$(this).trigger('input');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to reset button state if validation fails
|
||||||
|
function resetSubmitButton() {
|
||||||
|
$(".button-save").attr("disabled", false);
|
||||||
|
$(".button-save").removeClass("disabled");
|
||||||
|
updateProductCounter(); // Update with current counter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to show field error highlighting
|
||||||
|
function highlightInvalidFields() {
|
||||||
|
$('.physical-stock').each(function() {
|
||||||
|
var value = $(this).val();
|
||||||
|
var $input = $(this);
|
||||||
|
|
||||||
|
if (value !== '' && value !== null && value !== undefined) {
|
||||||
|
var numValue = parseFloat(value);
|
||||||
|
if (isNaN(numValue) || numValue < 0) {
|
||||||
|
$input.addClass('is-invalid');
|
||||||
|
} else {
|
||||||
|
$input.removeClass('is-invalid');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$input.removeClass('is-invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove error styling when user starts typing
|
||||||
|
$(document).on('input', '.physical-stock', function() {
|
||||||
|
$(this).removeClass('is-invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle server-side errors - scroll to first error and highlight
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Set default date for opname if empty
|
||||||
|
if ($('#date-opname').val() === '') {
|
||||||
|
var today = new Date().toISOString().split('T')[0];
|
||||||
|
$('#date-opname').val(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are validation errors
|
||||||
|
if ($('.is-invalid').length > 0) {
|
||||||
|
// Scroll to first error
|
||||||
|
$('html, body').animate({
|
||||||
|
scrollTop: $('.is-invalid:first').offset().top - 100
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Show alert for validation errors
|
||||||
|
@if(session('error'))
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'Terjadi Kesalahan',
|
||||||
|
text: '{{ session("error") }}',
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
@endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-dismiss alerts after 5 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
$('.alert').fadeOut('slow');
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle opname date field - set default if becomes empty
|
||||||
|
$('#date-opname').on('blur', function() {
|
||||||
|
if ($(this).val() === '') {
|
||||||
|
var today = new Date().toISOString().split('T')[0];
|
||||||
|
$(this).val(today);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle mutasi product selection
|
||||||
|
$(document).on('change', '.mutasi-product-checkbox', function() {
|
||||||
|
var quantityInput = $(this).closest('tr').find('.mutasi-quantity');
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
quantityInput.prop('disabled', false);
|
||||||
|
} else {
|
||||||
|
quantityInput.prop('disabled', true).val('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select all mutasi products
|
||||||
|
$('#select-all-mutasi').change(function() {
|
||||||
|
$('.mutasi-product-checkbox').prop('checked', this.checked).trigger('change');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function createTransaction(form) {
|
function createTransaction(form) {
|
||||||
let work_ids;
|
let work_ids;
|
||||||
|
|||||||
Reference in New Issue
Block a user