create stock and stock logs
This commit is contained in:
93
app/Console/Commands/ClearOpnameData.php
Normal file
93
app/Console/Commands/ClearOpnameData.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Models\Opname;
|
||||
use App\Models\OpnameDetail;
|
||||
use App\Models\Stock;
|
||||
use App\Models\StockLog;
|
||||
|
||||
class ClearOpnameData extends Command
|
||||
{
|
||||
protected $signature = 'opname:clear {--force : Force clear without confirmation}';
|
||||
protected $description = 'Clear all opname-related data including opnames, details, stocks, logs, and reset all IDs to 1';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (!$this->option('force')) {
|
||||
if (!$this->confirm('This will delete ALL opname data, stocks, stock logs, and reset ALL IDs to 1. This is irreversible! Are you sure?')) {
|
||||
$this->info('Operation cancelled.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Starting complete data cleanup...');
|
||||
|
||||
try {
|
||||
// Disable foreign key checks
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
|
||||
// 1. Clear and reset stock logs
|
||||
if (Schema::hasTable('stock_logs')) {
|
||||
DB::table('stock_logs')->truncate();
|
||||
DB::statement('ALTER TABLE stock_logs AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset stock_logs table');
|
||||
}
|
||||
|
||||
// 2. Clear and reset stocks
|
||||
if (Schema::hasTable('stocks')) {
|
||||
DB::table('stocks')->truncate();
|
||||
DB::statement('ALTER TABLE stocks AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset stocks table');
|
||||
}
|
||||
|
||||
// 3. Clear and reset opname details
|
||||
if (Schema::hasTable('opname_details')) {
|
||||
DB::table('opname_details')->truncate();
|
||||
DB::statement('ALTER TABLE opname_details AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset opname_details table');
|
||||
}
|
||||
|
||||
// 4. Clear and reset opnames
|
||||
if (Schema::hasTable('opnames')) {
|
||||
DB::table('opnames')->truncate();
|
||||
DB::statement('ALTER TABLE opnames AUTO_INCREMENT = 1;');
|
||||
$this->info('✓ Cleared and reset opnames table');
|
||||
}
|
||||
|
||||
// Re-enable foreign key checks
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$this->info('Successfully cleared all data and reset IDs to 1!');
|
||||
$this->info('Cleared tables:');
|
||||
$this->info('- stock_logs');
|
||||
$this->info('- stocks');
|
||||
$this->info('- opname_details');
|
||||
$this->info('- opnames');
|
||||
|
||||
Log::info('Complete data cleared and IDs reset by command', [
|
||||
'user' => auth()->user() ? auth()->user()->id : 'system',
|
||||
'timestamp' => now(),
|
||||
'tables_cleared' => ['stock_logs', 'stocks', 'opname_details', 'opnames']
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Re-enable foreign key checks if they were disabled
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
|
||||
$this->error('Error clearing data: ' . $e->getMessage());
|
||||
Log::error('Error in ClearOpnameData command: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return 1; // Return error code
|
||||
}
|
||||
|
||||
return 0; // Return success code
|
||||
}
|
||||
}
|
||||
@@ -28,5 +28,9 @@ class Kernel extends ConsoleKernel
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
|
||||
$this->commands = [
|
||||
Commands\ClearOpnameData::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
21
app/Enums/StockChangeType.php
Normal file
21
app/Enums/StockChangeType.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum StockChangeType: string
|
||||
{
|
||||
case INCREASE = 'increase';
|
||||
case DECREASE = 'decrease';
|
||||
case ADJUSTMENT = 'adjustment'; // Untuk kasus dimana quantity sama tapi perlu dicatat
|
||||
case NO_CHANGE = 'no_change'; // Untuk kasus dimana quantity sama dan tidak perlu dicatat
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::INCREASE => 'Penambahan',
|
||||
self::DECREASE => 'Pengurangan',
|
||||
self::ADJUSTMENT => 'Penyesuaian',
|
||||
self::NO_CHANGE => 'Tidak Ada Perubahan'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,10 @@ use App\Models\Menu;
|
||||
use App\Models\Opname;
|
||||
use App\Models\OpnameDetail;
|
||||
use App\Models\Product;
|
||||
use App\Models\Stock;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
|
||||
@@ -19,7 +20,7 @@ class OpnamesController extends Controller
|
||||
public function index(Request $request){
|
||||
$menu = Menu::where('link','opnames.index')->first();
|
||||
if($request->ajax()){
|
||||
$data = Opname::with('user','dealer');
|
||||
$data = Opname::with('user','dealer')->get();
|
||||
return DataTables::of($data)
|
||||
->addColumn('user_name', function ($row){
|
||||
return $row->user ? $row->user->name : '-';
|
||||
@@ -49,52 +50,167 @@ class OpnamesController extends Controller
|
||||
public function create(){
|
||||
try{
|
||||
$dealers = Dealer::all();
|
||||
$products = Product::all();
|
||||
return view('warehouse_management.opnames.create', compact('dealers','products'));
|
||||
}catch(\Exception $ex){
|
||||
$products = Product::where('active', true)->get();
|
||||
|
||||
// Get initial stock data for the first dealer (if any)
|
||||
$initialDealerId = $dealers->first()?->id;
|
||||
$stocks = [];
|
||||
if ($initialDealerId) {
|
||||
$stocks = Stock::where('dealer_id', $initialDealerId)
|
||||
->whereIn('product_id', $products->pluck('id'))
|
||||
->get()
|
||||
->keyBy('product_id');
|
||||
}
|
||||
|
||||
return view('warehouse_management.opnames.create', compact('dealers', 'products', 'stocks'));
|
||||
} catch(\Exception $ex) {
|
||||
Log::error($ex->getMessage());
|
||||
return back()->with('error', 'Terjadi kesalahan saat memuat data');
|
||||
}
|
||||
}
|
||||
|
||||
public function store(Request $request){
|
||||
try{
|
||||
$request->validate([
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// 1. Validasi input
|
||||
$validated = $request->validate([
|
||||
'dealer' => 'required|exists:dealers,id',
|
||||
'product' => 'required|array',
|
||||
'product.*' => 'nullable|exists:products,id',
|
||||
'product' => 'required|array|min:1',
|
||||
'product.*' => 'required|exists:products,id',
|
||||
'system_quantity' => 'required|array',
|
||||
'system_quantity.*' => 'required|numeric|min:0',
|
||||
'physical_quantity' => 'required|array',
|
||||
'physical_quantity.*' => 'required|numeric|min:0',
|
||||
'note' => 'nullable|string|max:1000', // note utama
|
||||
'item_notes' => 'nullable|array', // notes per item
|
||||
'item_notes.*' => 'required_if:physical_quantity.*,!=,system_quantity.*|nullable|string|max:255'
|
||||
]);
|
||||
|
||||
// 1. Create Opname master record
|
||||
$opname = Opname::create([
|
||||
'dealer_id' => $request->dealer,
|
||||
'opname_date' => now(), // or $request->opname_date if you provide it
|
||||
'user_id' => auth()->id(), // assuming the user is logged in
|
||||
'note' => null, // or $request->note if needed
|
||||
]);
|
||||
|
||||
// 2. Loop over products to create OpnameDetails
|
||||
foreach ($request->product as $index => $productId) {
|
||||
if (!$productId) continue; // Skip empty rows
|
||||
|
||||
$system = $request->system_quantity[$index] ?? 0;
|
||||
$physical = $request->physical_quantity[$index] ?? 0;
|
||||
|
||||
OpnameDetail::create([
|
||||
'opname_id' => $opname->id,
|
||||
'product_id' => $productId,
|
||||
'system_stock' => $system,
|
||||
'physical_stock' => $physical,
|
||||
'difference' => $physical - $system,
|
||||
'note' => null, // or include from input
|
||||
]);
|
||||
// 2. Validasi duplikasi produk
|
||||
$productCounts = array_count_values(array_filter($request->product));
|
||||
foreach ($productCounts as $productId => $count) {
|
||||
if ($count > 1) {
|
||||
throw new \Exception('Product tidak boleh duplikat.');
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('opnames.index')
|
||||
->with('success', 'Opname berhasil disimpan.');
|
||||
}catch(\Exception $ex){
|
||||
Log::error($ex->getMessage());
|
||||
// 3. Validasi dealer
|
||||
$dealer = Dealer::findOrFail($request->dealer);
|
||||
|
||||
// 4. Validasi produk aktif
|
||||
$productIds = array_filter($request->product);
|
||||
$inactiveProducts = Product::whereIn('id', $productIds)
|
||||
->where('active', false)
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
|
||||
if (!empty($inactiveProducts)) {
|
||||
throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts));
|
||||
}
|
||||
|
||||
// 5. Validasi stock dan note
|
||||
$stockDifferences = [];
|
||||
foreach ($request->product as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
|
||||
$systemStock = floatval($request->system_quantity[$index] ?? 0);
|
||||
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
|
||||
$itemNote = $request->input("item_notes.{$index}");
|
||||
|
||||
// Jika ada perbedaan stock dan note kosong
|
||||
if (abs($systemStock - $physicalStock) > 0.01 && empty($itemNote)) {
|
||||
$product = Product::find($productId);
|
||||
$stockDifferences[] = $product->name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($stockDifferences)) {
|
||||
throw new \Exception(
|
||||
'Catatan harus diisi untuk produk berikut karena ada perbedaan stock: ' .
|
||||
implode(', ', $stockDifferences)
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Create Opname master record with approved status
|
||||
$opname = Opname::create([
|
||||
'dealer_id' => $request->dealer,
|
||||
'opname_date' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
'note' => $request->note,
|
||||
'status' => 'approved', // Set status langsung approved
|
||||
'approved_by' => auth()->id(), // Set current user sebagai approver
|
||||
'approved_at' => now() // Set waktu approval
|
||||
]);
|
||||
|
||||
// 7. Create OpnameDetails and update stock
|
||||
$details = [];
|
||||
foreach ($request->product as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
|
||||
$systemStock = floatval($request->system_quantity[$index] ?? 0);
|
||||
$physicalStock = floatval($request->physical_quantity[$index] ?? 0);
|
||||
$difference = $physicalStock - $systemStock;
|
||||
|
||||
// Create opname detail
|
||||
$details[] = [
|
||||
'opname_id' => $opname->id,
|
||||
'product_id' => $productId,
|
||||
'system_stock' => $systemStock,
|
||||
'physical_stock' => $physicalStock,
|
||||
'difference' => $difference,
|
||||
'note' => $request->input("item_notes.{$index}"),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
];
|
||||
|
||||
// Update stock langsung karena auto approve
|
||||
$stock = Stock::firstOrCreate(
|
||||
[
|
||||
'product_id' => $productId,
|
||||
'dealer_id' => $request->dealer
|
||||
],
|
||||
['quantity' => 0]
|
||||
);
|
||||
|
||||
// Update stock dengan physical stock
|
||||
$stock->updateStock(
|
||||
$physicalStock,
|
||||
$opname,
|
||||
"Stock adjustment from auto-approved opname #{$opname->id}"
|
||||
);
|
||||
}
|
||||
|
||||
// Bulk insert untuk performa lebih baik
|
||||
OpnameDetail::insert($details);
|
||||
|
||||
// 8. Log aktivitas
|
||||
Log::info('Opname created and auto-approved', [
|
||||
'opname_id' => $opname->id,
|
||||
'dealer_id' => $opname->dealer_id,
|
||||
'user_id' => auth()->id(),
|
||||
'approver_id' => auth()->id(),
|
||||
'product_count' => count($details)
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()
|
||||
->route('opnames.index')
|
||||
->with('success', 'Opname berhasil disimpan dan disetujui.');
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
DB::rollBack();
|
||||
return back()->withErrors($e->validator)->withInput();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Error in OpnamesController@store: ' . $e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
|
||||
return back()
|
||||
->with('error', $e->getMessage())
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +223,7 @@ class OpnamesController extends Controller
|
||||
return DataTables::of($opname->details)
|
||||
->addIndexColumn()
|
||||
->addColumn('opname_date', function () use ($opname) {
|
||||
return $opname->opname_date->format('d M Y');
|
||||
return Carbon::parse($opname->opname_date)->format('d M Y');
|
||||
})
|
||||
->addColumn('user_name', function () use ($opname) {
|
||||
return $opname->user ? $opname->user->name : '-';
|
||||
@@ -134,4 +250,28 @@ class OpnamesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Add new method to get stock data via AJAX
|
||||
public function getStockData(Request $request)
|
||||
{
|
||||
try {
|
||||
$dealerId = $request->dealer_id;
|
||||
$productIds = $request->product_ids;
|
||||
|
||||
if (!$dealerId || !$productIds) {
|
||||
return response()->json(['error' => 'Dealer ID dan Product IDs diperlukan'], 400);
|
||||
}
|
||||
|
||||
$stocks = Stock::where('dealer_id', $dealerId)
|
||||
->whereIn('product_id', $productIds)
|
||||
->get()
|
||||
->mapWithKeys(function ($stock) {
|
||||
return [$stock->product_id => $stock->quantity];
|
||||
});
|
||||
|
||||
return response()->json(['stocks' => $stocks]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error getting stock data: ' . $e->getMessage());
|
||||
return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@@ -25,19 +26,19 @@ class ProductsController extends Controller
|
||||
{
|
||||
$menu = Menu::where('link','products.index')->first();
|
||||
if($request->ajax()){
|
||||
$data = Product::with(['category','opnameDetails']);
|
||||
$data = Product::with(['category', 'stocks']);
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('category_name', function ($row) {
|
||||
return $row->category ? $row->category->name : '-';
|
||||
})
|
||||
->addColumn('total_stock', function ($row){
|
||||
return $row->opnameDetails->sum('system_stock');
|
||||
return number_format($row->current_total_stock, 2);
|
||||
})
|
||||
->addColumn('action', function ($row) use ($menu) {
|
||||
$btn = '<div class="d-flex">';
|
||||
|
||||
if (Auth::user()->can('update', $menu)) {
|
||||
if (Gate::allows('update', $menu)) {
|
||||
$btn .= '<a href="' . route('products.edit', $row->id) . '" class="btn btn-warning btn-sm" style="margin-right: 8px;">Edit</a>';
|
||||
}
|
||||
|
||||
@@ -196,7 +197,7 @@ class ProductsController extends Controller
|
||||
|
||||
public function all_products(){
|
||||
try{
|
||||
$products = Product::select('id','name')->get();
|
||||
$products = Product::where('is_active', true)->select('id','name')->get();
|
||||
return response()->json($products);
|
||||
}catch(\Exception $ex){
|
||||
Log::error($ex->getMessage());
|
||||
@@ -206,17 +207,12 @@ class ProductsController extends Controller
|
||||
public function dealers_stock(Request $request){
|
||||
$productId = $request->get('product_id');
|
||||
|
||||
$product = Product::with(['opnameDetails.opname.dealer'])->findOrFail($productId);
|
||||
$product = Product::with(['stocks.dealer'])->findOrFail($productId);
|
||||
|
||||
$opnameDetails = $product->opnameDetails;
|
||||
|
||||
$data = $opnameDetails->map(function ($detail) {
|
||||
$data = $product->stocks->map(function ($stock) {
|
||||
return [
|
||||
'dealer_name' => $detail->opname->dealer->name ?? '-',
|
||||
'system_stock' => $detail->system_stock,
|
||||
'physical_stock' => $detail->physical_stock,
|
||||
'difference' => $detail->physical_stock - $detail->system_stock,
|
||||
'opname_date' => optional($detail->opname)->created_at->format('d M Y')
|
||||
'dealer_name' => $stock->dealer->name ?? '-',
|
||||
'quantity' => $stock->quantity
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
117
app/Http/Controllers/WarehouseManagement/StocksController.php
Normal file
117
app/Http/Controllers/WarehouseManagement/StocksController.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\WarehouseManagement;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Product;
|
||||
use App\Models\Stock;
|
||||
use App\Models\StockLog;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StocksController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$query = Stock::with(['dealer', 'product'])
|
||||
->when($request->dealer_id, function($q) use ($request) {
|
||||
return $q->where('dealer_id', $request->dealer_id);
|
||||
})
|
||||
->when($request->product_id, function($q) use ($request) {
|
||||
return $q->where('product_id', $request->product_id);
|
||||
});
|
||||
|
||||
return datatables()->of($query)
|
||||
->addColumn('dealer_name', function($stock) {
|
||||
return $stock->dealer->name;
|
||||
})
|
||||
->addColumn('product_name', function($stock) {
|
||||
return $stock->product->name;
|
||||
})
|
||||
->addColumn('action', function($stock) {
|
||||
return view('warehouse_management.stocks._action', compact('stock'));
|
||||
})
|
||||
->toJson();
|
||||
}
|
||||
|
||||
$dealers = Dealer::all();
|
||||
$products = Product::where('active', true)->get();
|
||||
|
||||
return view('warehouse_management.stocks.index', compact('dealers', 'products'));
|
||||
}
|
||||
|
||||
public function adjust(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'stock_id' => 'required|exists:stocks,id',
|
||||
'type' => 'required|in:add,reduce',
|
||||
'quantity' => 'required|numeric|min:0.01',
|
||||
'note' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$stock = Stock::findOrFail($request->stock_id);
|
||||
$oldQuantity = $stock->quantity;
|
||||
|
||||
// Calculate new quantity
|
||||
$change = $request->type === 'add' ? $request->quantity : -$request->quantity;
|
||||
$newQuantity = $oldQuantity + $change;
|
||||
|
||||
// Update stock
|
||||
$stock->update(['quantity' => $newQuantity]);
|
||||
|
||||
// Log the change
|
||||
StockLog::create([
|
||||
'stock_id' => $stock->id,
|
||||
'user_id' => Auth::id(),
|
||||
'old_quantity' => $oldQuantity,
|
||||
'new_quantity' => $newQuantity,
|
||||
'change' => $change,
|
||||
'note' => $request->note,
|
||||
'reference_type' => 'manual_adjustment'
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Stok berhasil diadjust'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal mengadjust stok: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function history(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'stock_id' => 'required|exists:stocks,id'
|
||||
]);
|
||||
|
||||
$logs = StockLog::with('user')
|
||||
->where('stock_id', $request->stock_id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->get()
|
||||
->map(function($log) {
|
||||
return [
|
||||
'date' => $log->created_at->format('d/m/Y H:i'),
|
||||
'user' => $log->user->name,
|
||||
'change' => $log->change > 0 ? '+' . $log->change : $log->change,
|
||||
'old_quantity' => $log->old_quantity,
|
||||
'new_quantity' => $log->new_quantity,
|
||||
'note' => $log->note
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json(['logs' => $logs]);
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,108 @@ class Opname extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = ['dealer_id','opname_date','user_id','note'];
|
||||
protected $fillable = [
|
||||
'dealer_id',
|
||||
'opname_date',
|
||||
'user_id',
|
||||
'note',
|
||||
'status',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'rejection_note'
|
||||
];
|
||||
|
||||
public function dealer(){
|
||||
protected $casts = [
|
||||
'approved_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::updated(function ($opname) {
|
||||
// Jika status berubah menjadi approved
|
||||
if ($opname->isDirty('status') && $opname->status === 'approved') {
|
||||
// Update stock untuk setiap detail opname
|
||||
foreach ($opname->details as $detail) {
|
||||
$stock = Stock::firstOrCreate(
|
||||
[
|
||||
'product_id' => $detail->product_id,
|
||||
'dealer_id' => $opname->dealer_id
|
||||
],
|
||||
['quantity' => 0]
|
||||
);
|
||||
|
||||
// Update stock dengan physical_stock dari opname
|
||||
$stock->updateStock(
|
||||
$detail->physical_stock,
|
||||
$opname,
|
||||
"Stock adjustment from approved opname #{$opname->id}"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
public function details(){
|
||||
public function details()
|
||||
{
|
||||
return $this->hasMany(OpnameDetail::class);
|
||||
}
|
||||
|
||||
public function user(){
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function approver()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
// Method untuk approve opname
|
||||
public function approve(User $approver)
|
||||
{
|
||||
if ($this->status !== 'pending') {
|
||||
throw new \Exception('Only pending opnames can be approved');
|
||||
}
|
||||
|
||||
$this->status = 'approved';
|
||||
$this->approved_by = $approver->id;
|
||||
$this->approved_at = now();
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Method untuk reject opname
|
||||
public function reject(User $rejector, string $note)
|
||||
{
|
||||
if ($this->status !== 'pending') {
|
||||
throw new \Exception('Only pending opnames can be rejected');
|
||||
}
|
||||
|
||||
$this->status = 'rejected';
|
||||
$this->approved_by = $rejector->id;
|
||||
$this->approved_at = now();
|
||||
$this->rejection_note = $note;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Method untuk submit opname untuk approval
|
||||
public function submit()
|
||||
{
|
||||
if ($this->status !== 'draft') {
|
||||
throw new \Exception('Only draft opnames can be submitted');
|
||||
}
|
||||
|
||||
$this->status = 'pending';
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,14 @@ class Product extends Model
|
||||
public function opnameDetails(){
|
||||
return $this->hasMany(OpnameDetail::class);
|
||||
}
|
||||
|
||||
public function stocks(){
|
||||
return $this->hasMany(Stock::class);
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan total stock saat ini
|
||||
public function getCurrentTotalStockAttribute()
|
||||
{
|
||||
return $this->stocks()->sum('quantity');
|
||||
}
|
||||
}
|
||||
|
||||
56
app/Models/Stock.php
Normal file
56
app/Models/Stock.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Stock extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'product_id',
|
||||
'dealer_id',
|
||||
'quantity'
|
||||
];
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
public function stockLogs()
|
||||
{
|
||||
return $this->hasMany(StockLog::class);
|
||||
}
|
||||
|
||||
// Method untuk mengupdate stock
|
||||
public function updateStock($newQuantity, $source, $description = null)
|
||||
{
|
||||
$previousQuantity = $this->quantity;
|
||||
$quantityChange = $newQuantity - $previousQuantity;
|
||||
|
||||
$this->quantity = $newQuantity;
|
||||
$this->save();
|
||||
|
||||
// Buat log perubahan
|
||||
StockLog::create([
|
||||
'stock_id' => $this->id,
|
||||
'source_type' => get_class($source),
|
||||
'source_id' => $source->id,
|
||||
'previous_quantity' => $previousQuantity,
|
||||
'new_quantity' => $newQuantity,
|
||||
'quantity_change' => $quantityChange,
|
||||
'description' => $description,
|
||||
'user_id' => auth()->id()
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
70
app/Models/StockLog.php
Normal file
70
app/Models/StockLog.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\StockChangeType;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class StockLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'stock_id',
|
||||
'source_type',
|
||||
'source_id',
|
||||
'previous_quantity',
|
||||
'new_quantity',
|
||||
'quantity_change',
|
||||
'change_type',
|
||||
'description',
|
||||
'user_id'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'change_type' => StockChangeType::class,
|
||||
'previous_quantity' => 'decimal:2',
|
||||
'new_quantity' => 'decimal:2',
|
||||
'quantity_change' => 'decimal:2'
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($stockLog) {
|
||||
// Hitung quantity_change
|
||||
$stockLog->quantity_change = $stockLog->new_quantity - $stockLog->previous_quantity;
|
||||
|
||||
// Tentukan change_type berdasarkan quantity_change
|
||||
if ($stockLog->quantity_change == 0) {
|
||||
// Jika quantity sama persis (tanpa toleransi)
|
||||
$stockLog->change_type = StockChangeType::NO_CHANGE;
|
||||
} else if ($stockLog->quantity_change > 0) {
|
||||
$stockLog->change_type = StockChangeType::INCREASE;
|
||||
} else {
|
||||
$stockLog->change_type = StockChangeType::DECREASE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function stock()
|
||||
{
|
||||
return $this->belongsTo(Stock::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function source()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
// Helper method untuk mendapatkan label change_type
|
||||
public function getChangeTypeLabelAttribute()
|
||||
{
|
||||
return $this->change_type->label();
|
||||
}
|
||||
}
|
||||
95
ckb.sql
95
ckb.sql
@@ -144,7 +144,7 @@ CREATE TABLE `migrations` (
|
||||
`migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`batch` int NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@@ -153,7 +153,7 @@ CREATE TABLE `migrations` (
|
||||
|
||||
LOCK TABLES `migrations` WRITE;
|
||||
/*!40000 ALTER TABLE `migrations` DISABLE KEYS */;
|
||||
INSERT INTO `migrations` VALUES (1,'2014_10_12_100000_create_password_resets_table',1),(2,'2019_08_19_000000_create_failed_jobs_table',1),(3,'2019_12_14_000001_create_personal_access_tokens_table',1),(4,'2022_05_20_100209_create_dealers_table',1),(5,'2022_05_20_100326_create_categories_table',1),(6,'2022_05_20_100335_create_works_table',1),(7,'2022_05_20_100340_create_users_table',1),(8,'2022_05_20_100410_create_transactions_table',1),(9,'2022_05_25_024641_update_works_table',2),(10,'2022_05_25_103839_update_categories_table',3),(13,'2022_05_25_144502_update_transaction_table',4),(18,'2022_05_29_211410_update_table_works_add_shortname_field',5),(20,'2022_05_29_211531_update_dealers_table_add_pic_field',6),(21,'2022_05_29_220642_update_transactions_table_add_status_column',7),(22,'2022_05_31_003725_update_transaction_table_add_dealer_id',8),(23,'2022_06_23_215115_add_deleted_at_at_users_table',9),(24,'2022_06_23_215243_add_deleted_at_at_dealers_table',9),(25,'2022_06_23_215321_add_deleted_at_at_categories_table',9),(26,'2022_06_23_215341_add_deleted_at_at_works_table',9),(27,'2022_06_23_215404_add_deleted_at_at_transactions_table',9),(28,'2023_08_11_140743_create_roles_table',10),(29,'2023_08_11_140957_create_privileges_table',10),(30,'2023_08_11_141029_add_role_id_into_users_table',10),(31,'2023_08_11_144823_create_menus_table',10),(32,'2023_08_11_144857_add_menu_id_in_privileges_table',10),(33,'2023_08_11_145537_remove_name_in_privileges_table',10),(55,'2025_05_28_113228_create_product_categories_table',11),(56,'2025_05_28_113324_create_products_table',11),(59,'2025_06_04_101915_create_opnames_table',12),(60,'2025_06_04_103359_create_opname_details_table',12);
|
||||
INSERT INTO `migrations` VALUES (1,'2014_10_12_100000_create_password_resets_table',1),(2,'2019_08_19_000000_create_failed_jobs_table',1),(3,'2019_12_14_000001_create_personal_access_tokens_table',1),(4,'2022_05_20_100209_create_dealers_table',1),(5,'2022_05_20_100326_create_categories_table',1),(6,'2022_05_20_100335_create_works_table',1),(7,'2022_05_20_100340_create_users_table',1),(8,'2022_05_20_100410_create_transactions_table',1),(9,'2022_05_25_024641_update_works_table',2),(10,'2022_05_25_103839_update_categories_table',3),(13,'2022_05_25_144502_update_transaction_table',4),(18,'2022_05_29_211410_update_table_works_add_shortname_field',5),(20,'2022_05_29_211531_update_dealers_table_add_pic_field',6),(21,'2022_05_29_220642_update_transactions_table_add_status_column',7),(22,'2022_05_31_003725_update_transaction_table_add_dealer_id',8),(23,'2022_06_23_215115_add_deleted_at_at_users_table',9),(24,'2022_06_23_215243_add_deleted_at_at_dealers_table',9),(25,'2022_06_23_215321_add_deleted_at_at_categories_table',9),(26,'2022_06_23_215341_add_deleted_at_at_works_table',9),(27,'2022_06_23_215404_add_deleted_at_at_transactions_table',9),(28,'2023_08_11_140743_create_roles_table',10),(29,'2023_08_11_140957_create_privileges_table',10),(30,'2023_08_11_141029_add_role_id_into_users_table',10),(31,'2023_08_11_144823_create_menus_table',10),(32,'2023_08_11_144857_add_menu_id_in_privileges_table',10),(33,'2023_08_11_145537_remove_name_in_privileges_table',10),(55,'2025_05_28_113228_create_product_categories_table',11),(56,'2025_05_28_113324_create_products_table',11),(59,'2025_06_04_101915_create_opnames_table',12),(60,'2025_06_04_103359_create_opname_details_table',12),(66,'2025_06_10_135321_create_stocks_table',13),(67,'2025_06_10_135341_create_stock_logs_table',14),(68,'2025_06_10_135417_add_approval_columns_to_opnames_table',14),(69,'2025_06_10_140540_change_stock_columns_to_decimal_in_opname_details',14);
|
||||
/*!40000 ALTER TABLE `migrations` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
@@ -168,9 +168,9 @@ CREATE TABLE `opname_details` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`opname_id` bigint unsigned NOT NULL,
|
||||
`product_id` bigint unsigned NOT NULL,
|
||||
`system_stock` int NOT NULL,
|
||||
`physical_stock` int NOT NULL,
|
||||
`difference` int NOT NULL DEFAULT '0',
|
||||
`system_stock` decimal(10,2) NOT NULL,
|
||||
`physical_stock` decimal(10,2) NOT NULL,
|
||||
`difference` decimal(10,2) NOT NULL DEFAULT '0.00',
|
||||
`note` text COLLATE utf8mb4_unicode_ci,
|
||||
`deleted_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
@@ -189,7 +189,7 @@ CREATE TABLE `opname_details` (
|
||||
|
||||
LOCK TABLES `opname_details` WRITE;
|
||||
/*!40000 ALTER TABLE `opname_details` DISABLE KEYS */;
|
||||
INSERT INTO `opname_details` VALUES (1,1,1,66,88,22,NULL,NULL,'2025-06-05 04:27:30','2025-06-05 04:27:30'),(2,1,3,77,88,11,NULL,NULL,'2025-06-05 04:27:30','2025-06-05 04:27:30');
|
||||
INSERT INTO `opname_details` VALUES (1,1,1,0.00,10.00,10.00,'tambah produk baru',NULL,'2025-06-10 10:26:24','2025-06-10 10:26:24'),(2,2,1,0.00,30.00,30.00,'tambah baru',NULL,'2025-06-10 10:27:30','2025-06-10 10:27:30');
|
||||
/*!40000 ALTER TABLE `opname_details` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
@@ -209,12 +209,18 @@ CREATE TABLE `opnames` (
|
||||
`deleted_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
`status` enum('draft','pending','approved','rejected') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'draft',
|
||||
`approved_by` bigint unsigned DEFAULT NULL,
|
||||
`approved_at` timestamp NULL DEFAULT NULL,
|
||||
`rejection_note` text COLLATE utf8mb4_unicode_ci,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `opnames_dealer_id_foreign` (`dealer_id`),
|
||||
KEY `opnames_user_id_foreign` (`user_id`),
|
||||
KEY `opnames_approved_by_foreign` (`approved_by`),
|
||||
CONSTRAINT `opnames_approved_by_foreign` FOREIGN KEY (`approved_by`) REFERENCES `users` (`id`),
|
||||
CONSTRAINT `opnames_dealer_id_foreign` FOREIGN KEY (`dealer_id`) REFERENCES `dealers` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `opnames_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@@ -223,7 +229,7 @@ CREATE TABLE `opnames` (
|
||||
|
||||
LOCK TABLES `opnames` WRITE;
|
||||
/*!40000 ALTER TABLE `opnames` DISABLE KEYS */;
|
||||
INSERT INTO `opnames` VALUES (1,20,'2025-06-05',8,NULL,NULL,'2025-06-05 04:27:30','2025-06-05 04:27:30');
|
||||
INSERT INTO `opnames` VALUES (1,20,'2025-06-10',8,'test tambah product by opname',NULL,'2025-06-10 10:26:24','2025-06-10 10:26:24','approved',8,'2025-06-10 10:26:24',NULL),(2,24,'2025-06-10',8,'test tambah produk by opname',NULL,'2025-06-10 10:27:30','2025-06-10 10:27:30','approved',8,'2025-06-10 10:27:30',NULL);
|
||||
/*!40000 ALTER TABLE `opnames` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
@@ -412,6 +418,77 @@ INSERT INTO `roles` VALUES (2,'admin',NULL,'2023-08-13 11:21:43','2023-08-13 11:
|
||||
/*!40000 ALTER TABLE `roles` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `stock_logs`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `stock_logs`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `stock_logs` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`stock_id` bigint unsigned NOT NULL,
|
||||
`source_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`source_id` bigint unsigned NOT NULL,
|
||||
`previous_quantity` decimal(10,2) NOT NULL,
|
||||
`new_quantity` decimal(10,2) NOT NULL,
|
||||
`quantity_change` decimal(10,2) NOT NULL,
|
||||
`change_type` enum('increase','decrease','adjustment','no_change') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'no_change',
|
||||
`description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`user_id` bigint unsigned NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `stock_logs_stock_id_foreign` (`stock_id`),
|
||||
KEY `stock_logs_user_id_foreign` (`user_id`),
|
||||
KEY `stock_logs_source_type_source_id_index` (`source_type`,`source_id`),
|
||||
CONSTRAINT `stock_logs_stock_id_foreign` FOREIGN KEY (`stock_id`) REFERENCES `stocks` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `stock_logs_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `stock_logs`
|
||||
--
|
||||
|
||||
LOCK TABLES `stock_logs` WRITE;
|
||||
/*!40000 ALTER TABLE `stock_logs` DISABLE KEYS */;
|
||||
INSERT INTO `stock_logs` VALUES (1,1,'App\\Models\\Opname',1,0.00,10.00,10.00,'increase','Stock adjustment from auto-approved opname #1',8,'2025-06-10 10:26:24','2025-06-10 10:26:24'),(2,2,'App\\Models\\Opname',2,0.00,30.00,30.00,'increase','Stock adjustment from auto-approved opname #2',8,'2025-06-10 10:27:30','2025-06-10 10:27:30');
|
||||
/*!40000 ALTER TABLE `stock_logs` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `stocks`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `stocks`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `stocks` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`product_id` bigint unsigned NOT NULL,
|
||||
`dealer_id` bigint unsigned NOT NULL,
|
||||
`quantity` decimal(10,2) NOT NULL DEFAULT '0.00',
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `stocks_product_id_foreign` (`product_id`),
|
||||
KEY `stocks_dealer_id_foreign` (`dealer_id`),
|
||||
CONSTRAINT `stocks_dealer_id_foreign` FOREIGN KEY (`dealer_id`) REFERENCES `dealers` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `stocks_product_id_foreign` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `stocks`
|
||||
--
|
||||
|
||||
LOCK TABLES `stocks` WRITE;
|
||||
/*!40000 ALTER TABLE `stocks` DISABLE KEYS */;
|
||||
INSERT INTO `stocks` VALUES (1,1,20,10.00,'2025-06-10 10:26:24','2025-06-10 10:26:24'),(2,1,24,30.00,'2025-06-10 10:27:30','2025-06-10 10:27:30');
|
||||
/*!40000 ALTER TABLE `stocks` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `transactions`
|
||||
--
|
||||
@@ -569,4 +646,4 @@ UNLOCK TABLES;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2025-06-05 19:06:52
|
||||
-- Dump completed on 2025-06-10 18:16:53
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateStocksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('stocks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('product_id')->constrained('products')->onDelete('cascade');
|
||||
$table->foreignId('dealer_id')->constrained('dealers')->onDelete('cascade');
|
||||
$table->decimal('quantity', 10, 2)->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('stocks');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CreateStockLogsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('stock_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('stock_id')->constrained('stocks')->onDelete('cascade');
|
||||
$table->string('source_type'); // 'opname' atau 'mutation'
|
||||
$table->unsignedBigInteger('source_id'); // ID dari opname atau mutation
|
||||
$table->decimal('previous_quantity', 10, 2);
|
||||
$table->decimal('new_quantity', 10, 2);
|
||||
$table->decimal('quantity_change', 10, 2); // bisa positif atau negatif
|
||||
$table->enum('change_type', ['increase', 'decrease', 'adjustment', 'no_change'])->default('no_change');
|
||||
$table->string('description')->nullable();
|
||||
$table->foreignId('user_id')->constrained('users');
|
||||
$table->timestamps();
|
||||
|
||||
// Index untuk pencarian berdasarkan source
|
||||
$table->index(['source_type', 'source_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('stock_logs');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddApprovalColumnsToOpnamesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('opnames', function (Blueprint $table) {
|
||||
$table->enum('status', ['draft', 'pending', 'approved', 'rejected'])->default('draft');
|
||||
$table->foreignId('approved_by')->nullable()->constrained('users');
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
$table->text('rejection_note')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('opnames', function (Blueprint $table) {
|
||||
$table->dropForeign(['approved_by']);
|
||||
$table->dropColumn(['status', 'approved_by', 'approved_at', 'rejection_note']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ChangeStockColumnsToDecimalInOpnameDetails extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('opname_details', function (Blueprint $table) {
|
||||
// Mengubah kolom system_stock dari integer ke decimal(10,2)
|
||||
$table->decimal('system_stock', 10, 2)->change();
|
||||
|
||||
// Mengubah kolom physical_stock dari integer ke decimal(10,2)
|
||||
$table->decimal('physical_stock', 10, 2)->change();
|
||||
|
||||
// Mengubah kolom difference dari integer ke decimal(10,2)
|
||||
$table->decimal('difference', 10, 2)->default(0)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('opname_details', function (Blueprint $table) {
|
||||
$table->integer('system_stock')->change();
|
||||
$table->integer('physical_stock')->change();
|
||||
$table->integer('difference')->default(0)->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ "./resources/js/warehouse_management/stock_mutations/index.js":
|
||||
/*!********************************************************************!*\
|
||||
!*** ./resources/js/warehouse_management/stock_mutations/index.js ***!
|
||||
\********************************************************************/
|
||||
/***/ (() => {
|
||||
|
||||
eval("$.ajaxSetup({\n headers: {\n \"X-CSRF-TOKEN\": $('meta[name=\"csrf-token\"]').attr(\"content\")\n }\n});\nvar tableContainer = $(\"#stock-mutations-table\");\nvar url = tableContainer.data(\"url\");\nvar table = $(\"#stock-mutations-table\").DataTable({\n processing: true,\n serverSide: true,\n ajax: url,\n columns: [{\n data: \"product_name\",\n name: \"product_name\"\n }, {\n data: \"dealer_name\",\n name: \"dealer_name\"\n }, {\n data: \"user_name\",\n name: \"user_name\"\n }, {\n data: \"mutation_type_label\",\n name: \"mutation_type_label\"\n }, {\n data: \"quantity\",\n name: \"quantity\"\n }, {\n data: \"created_at\",\n name: \"created_at\"\n }]\n});//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyIkIiwiYWpheFNldHVwIiwiaGVhZGVycyIsImF0dHIiLCJ0YWJsZUNvbnRhaW5lciIsInVybCIsImRhdGEiLCJ0YWJsZSIsIkRhdGFUYWJsZSIsInByb2Nlc3NpbmciLCJzZXJ2ZXJTaWRlIiwiYWpheCIsImNvbHVtbnMiLCJuYW1lIl0sInNvdXJjZXMiOlsid2VicGFjazovLy8uL3Jlc291cmNlcy9qcy93YXJlaG91c2VfbWFuYWdlbWVudC9zdG9ja19tdXRhdGlvbnMvaW5kZXguanM/OGNlZiJdLCJzb3VyY2VzQ29udGVudCI6WyIkLmFqYXhTZXR1cCh7XG4gICAgaGVhZGVyczoge1xuICAgICAgICBcIlgtQ1NSRi1UT0tFTlwiOiAkKCdtZXRhW25hbWU9XCJjc3JmLXRva2VuXCJdJykuYXR0cihcImNvbnRlbnRcIiksXG4gICAgfSxcbn0pO1xubGV0IHRhYmxlQ29udGFpbmVyID0gJChcIiNzdG9jay1tdXRhdGlvbnMtdGFibGVcIik7XG5sZXQgdXJsID0gdGFibGVDb250YWluZXIuZGF0YShcInVybFwiKTtcbmxldCB0YWJsZSA9ICQoXCIjc3RvY2stbXV0YXRpb25zLXRhYmxlXCIpLkRhdGFUYWJsZSh7XG4gICAgcHJvY2Vzc2luZzogdHJ1ZSxcbiAgICBzZXJ2ZXJTaWRlOiB0cnVlLFxuICAgIGFqYXg6IHVybCxcbiAgICBjb2x1bW5zOiBbXG4gICAgICAgIHsgZGF0YTogXCJwcm9kdWN0X25hbWVcIiwgbmFtZTogXCJwcm9kdWN0X25hbWVcIiB9LFxuICAgICAgICB7IGRhdGE6IFwiZGVhbGVyX25hbWVcIiwgbmFtZTogXCJkZWFsZXJfbmFtZVwiIH0sXG4gICAgICAgIHsgZGF0YTogXCJ1c2VyX25hbWVcIiwgbmFtZTogXCJ1c2VyX25hbWVcIiB9LFxuICAgICAgICB7IGRhdGE6IFwibXV0YXRpb25fdHlwZV9sYWJlbFwiLCBuYW1lOiBcIm11dGF0aW9uX3R5cGVfbGFiZWxcIiB9LFxuICAgICAgICB7IGRhdGE6IFwicXVhbnRpdHlcIiwgbmFtZTogXCJxdWFudGl0eVwiIH0sXG4gICAgICAgIHsgZGF0YTogXCJjcmVhdGVkX2F0XCIsIG5hbWU6IFwiY3JlYXRlZF9hdFwiIH0sXG4gICAgXSxcbn0pO1xuIl0sIm1hcHBpbmdzIjoiQUFBQUEsQ0FBQyxDQUFDQyxTQUFGLENBQVk7RUFDUkMsT0FBTyxFQUFFO0lBQ0wsZ0JBQWdCRixDQUFDLENBQUMseUJBQUQsQ0FBRCxDQUE2QkcsSUFBN0IsQ0FBa0MsU0FBbEM7RUFEWDtBQURELENBQVo7QUFLQSxJQUFJQyxjQUFjLEdBQUdKLENBQUMsQ0FBQyx3QkFBRCxDQUF0QjtBQUNBLElBQUlLLEdBQUcsR0FBR0QsY0FBYyxDQUFDRSxJQUFmLENBQW9CLEtBQXBCLENBQVY7QUFDQSxJQUFJQyxLQUFLLEdBQUdQLENBQUMsQ0FBQyx3QkFBRCxDQUFELENBQTRCUSxTQUE1QixDQUFzQztFQUM5Q0MsVUFBVSxFQUFFLElBRGtDO0VBRTlDQyxVQUFVLEVBQUUsSUFGa0M7RUFHOUNDLElBQUksRUFBRU4sR0FId0M7RUFJOUNPLE9BQU8sRUFBRSxDQUNMO0lBQUVOLElBQUksRUFBRSxjQUFSO0lBQXdCTyxJQUFJLEVBQUU7RUFBOUIsQ0FESyxFQUVMO0lBQUVQLElBQUksRUFBRSxhQUFSO0lBQXVCTyxJQUFJLEVBQUU7RUFBN0IsQ0FGSyxFQUdMO0lBQUVQLElBQUksRUFBRSxXQUFSO0lBQXFCTyxJQUFJLEVBQUU7RUFBM0IsQ0FISyxFQUlMO0lBQUVQLElBQUksRUFBRSxxQkFBUjtJQUErQk8sSUFBSSxFQUFFO0VBQXJDLENBSkssRUFLTDtJQUFFUCxJQUFJLEVBQUUsVUFBUjtJQUFvQk8sSUFBSSxFQUFFO0VBQTFCLENBTEssRUFNTDtJQUFFUCxJQUFJLEVBQUUsWUFBUjtJQUFzQk8sSUFBSSxFQUFFO0VBQTVCLENBTks7QUFKcUMsQ0FBdEMsQ0FBWiIsImZpbGUiOiIuL3Jlc291cmNlcy9qcy93YXJlaG91c2VfbWFuYWdlbWVudC9zdG9ja19tdXRhdGlvbnMvaW5kZXguanMiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./resources/js/warehouse_management/stock_mutations/index.js\n");
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/
|
||||
/******/ // startup
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ // This entry module can't be inlined because the eval-source-map devtool is used.
|
||||
/******/ var __webpack_exports__ = {};
|
||||
/******/ __webpack_modules__["./resources/js/warehouse_management/stock_mutations/index.js"]();
|
||||
/******/
|
||||
/******/ })()
|
||||
;
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ "./resources/js/warehouse_management/stock_opnames/create.js":
|
||||
/*!*******************************************************************!*\
|
||||
!*** ./resources/js/warehouse_management/stock_opnames/create.js ***!
|
||||
\*******************************************************************/
|
||||
/***/ (() => {
|
||||
|
||||
eval("//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6W10sInNvdXJjZXMiOlsid2VicGFjazovLy8uL3Jlc291cmNlcy9qcy93YXJlaG91c2VfbWFuYWdlbWVudC9zdG9ja19vcG5hbWVzL2NyZWF0ZS5qcz9kYzA1Il0sInNvdXJjZXNDb250ZW50IjpbIiJdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiIuL3Jlc291cmNlcy9qcy93YXJlaG91c2VfbWFuYWdlbWVudC9zdG9ja19vcG5hbWVzL2NyZWF0ZS5qcyIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./resources/js/warehouse_management/stock_opnames/create.js\n");
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/
|
||||
/******/ // startup
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ // This entry module can't be inlined because the eval-source-map devtool is used.
|
||||
/******/ var __webpack_exports__ = {};
|
||||
/******/ __webpack_modules__["./resources/js/warehouse_management/stock_opnames/create.js"]();
|
||||
/******/
|
||||
/******/ })()
|
||||
;
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* ATTENTION: An "eval-source-map" devtool has been used.
|
||||
* This devtool is neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ "./resources/js/warehouse_management/stock_opnames/index.js":
|
||||
/*!******************************************************************!*\
|
||||
!*** ./resources/js/warehouse_management/stock_opnames/index.js ***!
|
||||
\******************************************************************/
|
||||
/***/ (() => {
|
||||
|
||||
eval("$.ajaxSetup({\n headers: {\n \"X-CSRF-TOKEN\": $('meta[name=\"csrf-token\"]').attr(\"content\")\n }\n});\nvar tableContainer = $(\"#stock-opnames-table\");\nvar url = tableContainer.data(\"url\");\nvar table = $(\"#stock-opnames-table\").DataTable({\n processing: true,\n serverSide: true,\n ajax: url,\n columns: [{\n data: \"product_name\",\n name: \"product_name\"\n }, {\n data: \"dealer_name\",\n name: \"dealer_name\"\n }, {\n data: \"user_name\",\n name: \"user_name\"\n }, {\n data: \"system_quantity\",\n name: \"system_quantity\"\n }, {\n data: \"physical_quantity\",\n name: \"physical_quantity\"\n }, {\n data: \"difference\",\n name: \"difference\"\n }, {\n data: \"opname_date\",\n name: \"opname_date\"\n }]\n});//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyIkIiwiYWpheFNldHVwIiwiaGVhZGVycyIsImF0dHIiLCJ0YWJsZUNvbnRhaW5lciIsInVybCIsImRhdGEiLCJ0YWJsZSIsIkRhdGFUYWJsZSIsInByb2Nlc3NpbmciLCJzZXJ2ZXJTaWRlIiwiYWpheCIsImNvbHVtbnMiLCJuYW1lIl0sInNvdXJjZXMiOlsid2VicGFjazovLy8uL3Jlc291cmNlcy9qcy93YXJlaG91c2VfbWFuYWdlbWVudC9zdG9ja19vcG5hbWVzL2luZGV4LmpzPzI3YTQiXSwic291cmNlc0NvbnRlbnQiOlsiJC5hamF4U2V0dXAoe1xuICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJYLUNTUkYtVE9LRU5cIjogJCgnbWV0YVtuYW1lPVwiY3NyZi10b2tlblwiXScpLmF0dHIoXCJjb250ZW50XCIpLFxuICAgIH0sXG59KTtcbmxldCB0YWJsZUNvbnRhaW5lciA9ICQoXCIjc3RvY2stb3BuYW1lcy10YWJsZVwiKTtcbmxldCB1cmwgPSB0YWJsZUNvbnRhaW5lci5kYXRhKFwidXJsXCIpO1xubGV0IHRhYmxlID0gJChcIiNzdG9jay1vcG5hbWVzLXRhYmxlXCIpLkRhdGFUYWJsZSh7XG4gICAgcHJvY2Vzc2luZzogdHJ1ZSxcbiAgICBzZXJ2ZXJTaWRlOiB0cnVlLFxuICAgIGFqYXg6IHVybCxcbiAgICBjb2x1bW5zOiBbXG4gICAgICAgIHsgZGF0YTogXCJwcm9kdWN0X25hbWVcIiwgbmFtZTogXCJwcm9kdWN0X25hbWVcIiB9LFxuICAgICAgICB7IGRhdGE6IFwiZGVhbGVyX25hbWVcIiwgbmFtZTogXCJkZWFsZXJfbmFtZVwiIH0sXG4gICAgICAgIHsgZGF0YTogXCJ1c2VyX25hbWVcIiwgbmFtZTogXCJ1c2VyX25hbWVcIiB9LFxuICAgICAgICB7IGRhdGE6IFwic3lzdGVtX3F1YW50aXR5XCIsIG5hbWU6IFwic3lzdGVtX3F1YW50aXR5XCIgfSxcbiAgICAgICAgeyBkYXRhOiBcInBoeXNpY2FsX3F1YW50aXR5XCIsIG5hbWU6IFwicGh5c2ljYWxfcXVhbnRpdHlcIiB9LFxuICAgICAgICB7IGRhdGE6IFwiZGlmZmVyZW5jZVwiLCBuYW1lOiBcImRpZmZlcmVuY2VcIiB9LFxuICAgICAgICB7IGRhdGE6IFwib3BuYW1lX2RhdGVcIiwgbmFtZTogXCJvcG5hbWVfZGF0ZVwiIH0sXG4gICAgXSxcbn0pO1xuIl0sIm1hcHBpbmdzIjoiQUFBQUEsQ0FBQyxDQUFDQyxTQUFGLENBQVk7RUFDUkMsT0FBTyxFQUFFO0lBQ0wsZ0JBQWdCRixDQUFDLENBQUMseUJBQUQsQ0FBRCxDQUE2QkcsSUFBN0IsQ0FBa0MsU0FBbEM7RUFEWDtBQURELENBQVo7QUFLQSxJQUFJQyxjQUFjLEdBQUdKLENBQUMsQ0FBQyxzQkFBRCxDQUF0QjtBQUNBLElBQUlLLEdBQUcsR0FBR0QsY0FBYyxDQUFDRSxJQUFmLENBQW9CLEtBQXBCLENBQVY7QUFDQSxJQUFJQyxLQUFLLEdBQUdQLENBQUMsQ0FBQyxzQkFBRCxDQUFELENBQTBCUSxTQUExQixDQUFvQztFQUM1Q0MsVUFBVSxFQUFFLElBRGdDO0VBRTVDQyxVQUFVLEVBQUUsSUFGZ0M7RUFHNUNDLElBQUksRUFBRU4sR0FIc0M7RUFJNUNPLE9BQU8sRUFBRSxDQUNMO0lBQUVOLElBQUksRUFBRSxjQUFSO0lBQXdCTyxJQUFJLEVBQUU7RUFBOUIsQ0FESyxFQUVMO0lBQUVQLElBQUksRUFBRSxhQUFSO0lBQXVCTyxJQUFJLEVBQUU7RUFBN0IsQ0FGSyxFQUdMO0lBQUVQLElBQUksRUFBRSxXQUFSO0lBQXFCTyxJQUFJLEVBQUU7RUFBM0IsQ0FISyxFQUlMO0lBQUVQLElBQUksRUFBRSxpQkFBUjtJQUEyQk8sSUFBSSxFQUFFO0VBQWpDLENBSkssRUFLTDtJQUFFUCxJQUFJLEVBQUUsbUJBQVI7SUFBNkJPLElBQUksRUFBRTtFQUFuQyxDQUxLLEVBTUw7SUFBRVAsSUFBSSxFQUFFLFlBQVI7SUFBc0JPLElBQUksRUFBRTtFQUE1QixDQU5LLEVBT0w7SUFBRVAsSUFBSSxFQUFFLGFBQVI7SUFBdUJPLElBQUksRUFBRTtFQUE3QixDQVBLO0FBSm1DLENBQXBDLENBQVoiLCJmaWxlIjoiLi9yZXNvdXJjZXMvanMvd2FyZWhvdXNlX21hbmFnZW1lbnQvc3RvY2tfb3BuYW1lcy9pbmRleC5qcyIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///./resources/js/warehouse_management/stock_opnames/index.js\n");
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/
|
||||
/******/ // startup
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ // This entry module can't be inlined because the eval-source-map devtool is used.
|
||||
/******/ var __webpack_exports__ = {};
|
||||
/******/ __webpack_modules__["./resources/js/warehouse_management/stock_opnames/index.js"]();
|
||||
/******/
|
||||
/******/ })()
|
||||
;
|
||||
@@ -2,8 +2,6 @@
|
||||
"/js/app.js": "/js/app.js",
|
||||
"/js/warehouse_management/product_categories/index.js": "/js/warehouse_management/product_categories/index.js",
|
||||
"/js/warehouse_management/products/index.js": "/js/warehouse_management/products/index.js",
|
||||
"/js/warehouse_management/products/create.js": "/js/warehouse_management/products/create.js",
|
||||
"/js/warehouse_management/products/edit.js": "/js/warehouse_management/products/edit.js",
|
||||
"/js/warehouse_management/opnames/index.js": "/js/warehouse_management/opnames/index.js",
|
||||
"/js/warehouse_management/opnames/create.js": "/js/warehouse_management/opnames/create.js",
|
||||
"/js/warehouse_management/opnames/detail.js": "/js/warehouse_management/opnames/detail.js",
|
||||
|
||||
@@ -1,60 +1,253 @@
|
||||
const productUrl = $("#product-container").data("url");
|
||||
|
||||
function createProductSelectOptions(callback) {
|
||||
$.ajax({
|
||||
url: productUrl,
|
||||
method: "GET",
|
||||
success: function (data) {
|
||||
let options = '<option value="">Pilih Produk</option>';
|
||||
data.forEach((product) => {
|
||||
options += `<option value="${product.id}">${product.name}</option>`;
|
||||
});
|
||||
callback(options);
|
||||
},
|
||||
error: function () {
|
||||
alert("Gagal memuat produk.");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Initial load only for the first row
|
||||
createProductSelectOptions((options) => {
|
||||
$(".product-select").first().html(options);
|
||||
});
|
||||
// Fungsi untuk mengambil data stok
|
||||
function fetchStockData() {
|
||||
const dealerId = $("#dealer").val();
|
||||
if (!dealerId) return;
|
||||
|
||||
// When adding a new row
|
||||
$(document).on("click", ".btn-add-row", function () {
|
||||
const row = `
|
||||
<div class="form-row align-items-end product-row">
|
||||
<div class="form-group col-md-4">
|
||||
<select name="product[]" class="form-control product-select">
|
||||
<option>Loading...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" name="system_quantity[]" class="form-control" placeholder="Stok sistem">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="text" name="physical_quantity[]" class="form-control" placeholder="Stok fisik">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<button type="button" class="btn btn-danger btn-remove-row"><i class="flaticon2-delete"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const productIds = $(".product-select")
|
||||
.map(function () {
|
||||
return $(this).val();
|
||||
})
|
||||
.get()
|
||||
.filter((id) => id !== "");
|
||||
|
||||
const $newRow = $(row);
|
||||
$("#product-container").append($newRow);
|
||||
if (productIds.length === 0) return;
|
||||
|
||||
// Load options only for the new select
|
||||
createProductSelectOptions((options) => {
|
||||
$newRow.find(".product-select").html(options);
|
||||
$.ajax({
|
||||
url: "/warehouse/opnames/get-stock-data",
|
||||
method: "POST",
|
||||
data: {
|
||||
_token: $('meta[name="csrf-token"]').attr("content"),
|
||||
dealer_id: dealerId,
|
||||
product_ids: productIds,
|
||||
},
|
||||
success: function (response) {
|
||||
if (response.stocks) {
|
||||
$(".product-row").each(function () {
|
||||
const productId = $(this).find(".product-select").val();
|
||||
const systemQtyInput = $(this).find(".system-quantity");
|
||||
const physicalQtyInput = $(this).find(
|
||||
'input[name^="physical_quantity"]'
|
||||
);
|
||||
|
||||
// Simpan nilai physical quantity yang sudah ada
|
||||
const currentPhysicalQty = physicalQtyInput.val();
|
||||
|
||||
if (
|
||||
productId &&
|
||||
response.stocks[productId] !== undefined
|
||||
) {
|
||||
systemQtyInput.val(response.stocks[productId]);
|
||||
// Kembalikan nilai physical quantity jika ada
|
||||
if (currentPhysicalQty) {
|
||||
physicalQtyInput.val(currentPhysicalQty);
|
||||
}
|
||||
calculateDifference(systemQtyInput[0]);
|
||||
} else {
|
||||
systemQtyInput.val("0");
|
||||
calculateDifference(systemQtyInput[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
console.error("Error fetching stock data:", xhr.responseText);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Update stok saat dealer berubah
|
||||
$("#dealer").change(function () {
|
||||
fetchStockData();
|
||||
});
|
||||
|
||||
// Remove row
|
||||
$(document).on("click", ".btn-remove-row", function () {
|
||||
$(this).closest(".product-row").remove();
|
||||
// Update stok saat produk berubah
|
||||
$(document).on("change", ".product-select", function () {
|
||||
const row = $(this).closest("tr");
|
||||
const productId = $(this).val();
|
||||
const systemQtyInput = row.find(".system-quantity");
|
||||
const physicalQtyInput = row.find('input[name^="physical_quantity"]');
|
||||
|
||||
// Simpan nilai physical quantity yang sudah ada
|
||||
const currentPhysicalQty = physicalQtyInput.val();
|
||||
|
||||
if (productId) {
|
||||
fetchStockData();
|
||||
} else {
|
||||
systemQtyInput.val("0");
|
||||
// Kembalikan nilai physical quantity jika ada
|
||||
if (currentPhysicalQty) {
|
||||
physicalQtyInput.val(currentPhysicalQty);
|
||||
}
|
||||
calculateDifference(systemQtyInput[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Fungsi untuk menambah baris produk
|
||||
$("#btn-add-row").click(function () {
|
||||
const template = document.getElementById("product-row-template");
|
||||
const tbody = $("#product-table tbody");
|
||||
const newRow = template.content.cloneNode(true);
|
||||
const rowIndex = $(".product-row").length;
|
||||
|
||||
// Update name attributes with correct index
|
||||
$(newRow)
|
||||
.find('select[name="product[]"]')
|
||||
.attr("name", `product[${rowIndex}]`);
|
||||
$(newRow)
|
||||
.find('input[name="system_quantity[]"]')
|
||||
.attr("name", `system_quantity[${rowIndex}]`);
|
||||
$(newRow)
|
||||
.find('input[name="physical_quantity[]"]')
|
||||
.attr("name", `physical_quantity[${rowIndex}]`);
|
||||
$(newRow)
|
||||
.find('input[name="item_notes[]"]')
|
||||
.attr("name", `item_notes[${rowIndex}]`);
|
||||
|
||||
// Add system-quantity class dan pastikan readonly
|
||||
const systemQtyInput = $(newRow).find(
|
||||
'input[name="system_quantity[]"]'
|
||||
);
|
||||
systemQtyInput
|
||||
.addClass("system-quantity")
|
||||
.attr("readonly", true)
|
||||
.val("0");
|
||||
|
||||
// Reset semua nilai input di baris baru kecuali system quantity
|
||||
$(newRow).find("select").val("");
|
||||
$(newRow).find("input:not(.system-quantity)").val("");
|
||||
|
||||
tbody.append(newRow);
|
||||
updateRemoveButtons();
|
||||
});
|
||||
|
||||
// Fungsi untuk menghapus baris produk
|
||||
$(document).on("click", ".btn-remove-row", function () {
|
||||
$(this).closest("tr").remove();
|
||||
updateRemoveButtons();
|
||||
// Reindex semua baris setelah penghapusan
|
||||
reindexRows();
|
||||
});
|
||||
|
||||
// Fungsi untuk update status tombol hapus
|
||||
function updateRemoveButtons() {
|
||||
const rows = $(".product-row").length;
|
||||
$(".btn-remove-row").prop("disabled", rows <= 1);
|
||||
}
|
||||
|
||||
// Fungsi untuk reindex semua baris
|
||||
function reindexRows() {
|
||||
$(".product-row").each(function (index) {
|
||||
$(this)
|
||||
.find('select[name^="product"]')
|
||||
.attr("name", `product[${index}]`);
|
||||
$(this)
|
||||
.find('input[name^="system_quantity"]')
|
||||
.attr("name", `system_quantity[${index}]`);
|
||||
$(this)
|
||||
.find('input[name^="physical_quantity"]')
|
||||
.attr("name", `physical_quantity[${index}]`);
|
||||
$(this)
|
||||
.find('input[name^="item_notes"]')
|
||||
.attr("name", `item_notes[${index}]`);
|
||||
});
|
||||
}
|
||||
|
||||
// Update calculateDifference function
|
||||
function calculateDifference(input) {
|
||||
const row = $(input).closest("tr");
|
||||
const systemQty = parseFloat(row.find(".system-quantity").val()) || 0;
|
||||
const physicalQty =
|
||||
parseFloat(row.find('input[name^="physical_quantity"]').val()) || 0;
|
||||
const noteInput = row.find('input[name^="item_notes"]');
|
||||
|
||||
if (Math.abs(systemQty - physicalQty) > 0.01) {
|
||||
noteInput.addClass("is-invalid");
|
||||
noteInput.attr("required", true);
|
||||
noteInput.attr(
|
||||
"placeholder",
|
||||
"Catatan wajib diisi karena ada perbedaan stock"
|
||||
);
|
||||
row.addClass("table-warning");
|
||||
} else {
|
||||
noteInput.removeClass("is-invalid");
|
||||
noteInput.removeAttr("required");
|
||||
noteInput.attr("placeholder", "Catatan item");
|
||||
row.removeClass("table-warning");
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent manual editing of system quantity
|
||||
$(document).on("keydown", ".system-quantity", function (e) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
$(document).on("paste", ".system-quantity", function (e) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
// Validasi form sebelum submit
|
||||
$("#opname-form").submit(function (e) {
|
||||
const dealerId = $("#dealer").val();
|
||||
if (!dealerId) {
|
||||
e.preventDefault();
|
||||
alert("Silakan pilih dealer terlebih dahulu!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const products = $('select[name^="product"]')
|
||||
.map(function () {
|
||||
return $(this).val();
|
||||
})
|
||||
.get();
|
||||
|
||||
// Cek duplikasi produk
|
||||
const uniqueProducts = [...new Set(products)];
|
||||
if (products.length !== uniqueProducts.length) {
|
||||
e.preventDefault();
|
||||
alert("Produk tidak boleh duplikat!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cek produk kosong
|
||||
if (products.includes("")) {
|
||||
e.preventDefault();
|
||||
alert("Semua produk harus dipilih!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cek catatan untuk perbedaan stock
|
||||
let hasInvalidNotes = false;
|
||||
$(".product-row").each(function () {
|
||||
const systemQty =
|
||||
parseFloat(
|
||||
$(this).find('input[name^="system_quantity"]').val()
|
||||
) || 0;
|
||||
const physicalQty =
|
||||
parseFloat(
|
||||
$(this).find('input[name^="physical_quantity"]').val()
|
||||
) || 0;
|
||||
const note = $(this).find('input[name^="item_notes"]').val();
|
||||
|
||||
if (Math.abs(systemQty - physicalQty) > 0.01 && !note) {
|
||||
hasInvalidNotes = true;
|
||||
$(this).addClass("table-danger");
|
||||
}
|
||||
});
|
||||
|
||||
if (hasInvalidNotes) {
|
||||
e.preventDefault();
|
||||
alert(
|
||||
"Catatan wajib diisi untuk produk yang memiliki perbedaan stock!"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Initial stock data load if dealer is selected
|
||||
if ($("#dealer").val()) {
|
||||
fetchStockData();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {});
|
||||
@@ -1 +0,0 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {});
|
||||
@@ -109,10 +109,7 @@ $(document).on("click", ".btn-product-stock-dealers", function () {
|
||||
},
|
||||
columns: [
|
||||
{ data: "dealer_name", name: "dealer_name" },
|
||||
{ data: "system_stock", name: "system_stock" },
|
||||
{ data: "physical_stock", name: "physical_stock" },
|
||||
{ data: "difference", name: "difference" },
|
||||
{ data: "opname_date", name: "opname_date" },
|
||||
{ data: "quantity", name: "quantity" },
|
||||
],
|
||||
initComplete: function () {
|
||||
$("#dealerStockModal").modal("show");
|
||||
|
||||
@@ -2,58 +2,205 @@
|
||||
|
||||
@section('content')
|
||||
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<span class="kt-portlet__head-icon">
|
||||
<i class="kt-font-brand flaticon2-plus"></i>
|
||||
</span>
|
||||
<h3 class="kt-portlet__head-title">Tambah Opnames</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kt-portlet__body">
|
||||
<form id="opname-form" action="{{ route('opnames.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<label for="dealer">Dealer</label>
|
||||
<select name="dealer" id="dealer" class="form-control" required >
|
||||
<option value="">Pilih Dealer</option>
|
||||
@foreach($dealers as $dealer)
|
||||
<option value="{{ $dealer->id }}">{{ $dealer->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="product-container" data-url="{{ route('products.all') }}">
|
||||
<div class="form-row align-items-end product-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label for="product[]">Produk</label>
|
||||
<select name="product[]" class="form-control product-select" required >
|
||||
<option value="">Pilih Produk</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Stok Sistem</label>
|
||||
<input type="text" name="system_quantity[]" class="form-control" placeholder="Stok sistem" required >
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Stok Fisik</label>
|
||||
<input type="text" name="physical_quantity[]" class="form-control" placeholder="Stok fisik" required >
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<button type="button" class="btn btn-success btn-add-row"><i class="flaticon2-plus"></i></button>
|
||||
</div>
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<span class="kt-portlet__head-icon">
|
||||
<i class="kt-font-brand flaticon2-plus"></i>
|
||||
</span>
|
||||
<h3 class="kt-portlet__head-title">Tambah Opnames</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-4">
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="kt-portlet__body">
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form id="opname-form" action="{{ route('opnames.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="dealer">Dealer <span class="text-danger">*</span></label>
|
||||
@php
|
||||
$oldDealer = old('dealer');
|
||||
$dealerValue = is_array($oldDealer) ? '' : $oldDealer;
|
||||
@endphp
|
||||
<select name="dealer" id="dealer" class="form-control @error('dealer') is-invalid @enderror" required>
|
||||
<option value="">Pilih Dealer</option>
|
||||
@foreach($dealers as $dealer)
|
||||
<option value="{{ $dealer->id }}" {{ $dealerValue == $dealer->id ? 'selected' : '' }}>
|
||||
{{ $dealer->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('dealer')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="note">Catatan</label>
|
||||
@php
|
||||
$oldNote = old('note');
|
||||
$noteValue = is_array($oldNote) ? '' : $oldNote;
|
||||
@endphp
|
||||
<textarea name="note" id="note" class="form-control @error('note') is-invalid @enderror"
|
||||
rows="2" placeholder="Catatan opname">{{ $noteValue }}</textarea>
|
||||
@error('note')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kt-separator kt-separator--border-dashed kt-separator--space-lg kt-separator--portlet-fit"></div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<h4>Detail Produk</h4>
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<button type="button" class="btn btn-success btn-sm" id="btn-add-row">
|
||||
<i class="flaticon2-plus"></i> Tambah Produk
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover" id="product-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 35%">Produk <span class="text-danger">*</span></th>
|
||||
<th style="width: 15%">Stok Sistem <span class="text-danger">*</span></th>
|
||||
<th style="width: 15%">Stok Fisik <span class="text-danger">*</span></th>
|
||||
<th style="width: 30%">Catatan</th>
|
||||
<th style="width: 5%" class="text-center">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$oldProducts = old('product', []);
|
||||
$oldSystemQuantities = old('system_quantity', []);
|
||||
$oldPhysicalQuantities = old('physical_quantity', []);
|
||||
$oldItemNotes = old('item_notes', []);
|
||||
@endphp
|
||||
<tr class="product-row">
|
||||
<td>
|
||||
<select name="product[0]" class="form-control product-select @error('product.0') is-invalid @enderror" required>
|
||||
<option value="">Pilih Produk</option>
|
||||
@foreach($products as $product)
|
||||
<option value="{{ $product->id }}" {{ (isset($oldProducts[0]) && $oldProducts[0] == $product->id) ? 'selected' : '' }}>
|
||||
{{ $product->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('product.0')
|
||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||
@enderror
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input type="number" name="system_quantity[0]"
|
||||
class="form-control system-quantity @error('system_quantity.0') is-invalid @enderror"
|
||||
step="0.01" min="0"
|
||||
value="{{ $oldSystemQuantities[0] ?? '0' }}"
|
||||
readonly>
|
||||
</div>
|
||||
@error('system_quantity.0')
|
||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||
@enderror
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input type="number" name="physical_quantity[0]"
|
||||
class="form-control @error('physical_quantity.0') is-invalid @enderror"
|
||||
step="0.01" min="0"
|
||||
value="{{ $oldPhysicalQuantities[0] ?? '' }}"
|
||||
required
|
||||
onchange="calculateDifference(this)">
|
||||
</div>
|
||||
@error('physical_quantity.0')
|
||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||
@enderror
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="item_notes[0]"
|
||||
class="form-control @error('item_notes.0') is-invalid @enderror"
|
||||
value="{{ $oldItemNotes[0] ?? '' }}"
|
||||
placeholder="(wajib jika ada perbedaan stock)">
|
||||
@error('item_notes.0')
|
||||
<div class="invalid-feedback d-block">{{ $message }}</div>
|
||||
@enderror
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-danger btn-sm btn-remove-row" disabled>
|
||||
<i class="flaticon2-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="kt-separator kt-separator--border-dashed kt-separator--space-lg kt-separator--portlet-fit"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary btn-md">
|
||||
Simpan
|
||||
</button>
|
||||
<a href="{{ route('opnames.index') }}" class="btn btn-secondary btn-md">
|
||||
Batal
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template untuk baris produk baru -->
|
||||
<template id="product-row-template">
|
||||
<tr class="product-row">
|
||||
<td>
|
||||
<select name="product[]" class="form-control product-select" required>
|
||||
<option value="">Pilih Produk</option>
|
||||
@foreach($products as $product)
|
||||
<option value="{{ $product->id }}">{{ $product->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input type="number" name="system_quantity[]" class="form-control system-quantity"
|
||||
step="0.01" min="0" value="0" readonly>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input type="number" name="physical_quantity[]" class="form-control"
|
||||
step="0.01" min="0" value="" required
|
||||
onchange="calculateDifference(this)">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="item_notes[]" class="form-control"
|
||||
value="" placeholder="(wajib jika ada perbedaan stock)">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-danger btn-sm btn-remove-row">
|
||||
<i class="flaticon2-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{ mix('js/warehouse_management/opnames/create.js') }}"></script>
|
||||
<script src="{{ asset('js/warehouse_management/opnames/create.js') }}"></script>
|
||||
@endsection
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
|
||||
@section('content')
|
||||
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<span class="kt-portlet__head-icon">
|
||||
<i class="kt-font-brand flaticon2-list-1"></i>
|
||||
</span>
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Detail Opname
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<span class="kt-portlet__head-icon">
|
||||
<i class="kt-font-brand flaticon2-list-1"></i>
|
||||
</span>
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Opname {{ $opname->dealer->name }} Tanggal {{ Carbon\Carbon::parse($opname->opname_date)->format('d M Y') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<a href="{{ route('opnames.index') }}" class="btn btn-secondary">
|
||||
<i class="la la-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kt-portlet__body">
|
||||
<div class="table-responsive">
|
||||
@@ -35,5 +40,5 @@
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{ mix('js/warehouse_management/opnames/detail.js') }}"></script>
|
||||
<script src="{{ asset('js/warehouse_management/opnames/detail.js') }}"></script>
|
||||
@endsection
|
||||
@@ -30,7 +30,7 @@
|
||||
<tr>
|
||||
<th>Dealer</th>
|
||||
<th>Pengguna</th>
|
||||
<th>Tanggal Opname</th>
|
||||
<th>Tanggal</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -42,5 +42,5 @@
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{ mix('js/warehouse_management/opnames/index.js') }}"></script>
|
||||
<script src="{{ asset('js/warehouse_management/opnames/index.js') }}"></script>
|
||||
@endsection
|
||||
@@ -75,5 +75,5 @@
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{mix('js/warehouse_management/product_categories/index.js')}}"></script>
|
||||
<script src="{{ asset('js/warehouse_management/product_categories/index.js') }}"></script>
|
||||
@endsection
|
||||
@@ -69,7 +69,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{ mix('js/warehouse_management/products/create.js') }}"></script>
|
||||
@endsection
|
||||
@@ -72,7 +72,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{ mix('js/warehouse_management/products/edit.js') }}"></script>
|
||||
@endsection
|
||||
@@ -57,13 +57,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Dealer</th>
|
||||
<th>System Stock</th>
|
||||
<th>Physical Stock</th>
|
||||
<th>Difference</th>
|
||||
<th>Opname Date</th>
|
||||
<th>Stok</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,5 +68,5 @@
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{ mix('js/warehouse_management/products/index.js') }}"></script>
|
||||
<script src="{{ asset('js/warehouse_management/products/index.js') }}"></script>
|
||||
@endsection
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="d-flex">
|
||||
<button type="button" class="btn btn-success btn-sm mr-2"
|
||||
onclick="showAdjustStockModal({{ $stock->id }}, 'add')"
|
||||
title="Tambah Stok">
|
||||
<i class="la la-plus"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-warning btn-sm mr-2"
|
||||
onclick="showAdjustStockModal({{ $stock->id }}, 'reduce')"
|
||||
title="Kurangi Stok">
|
||||
<i class="la la-minus"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-info btn-sm"
|
||||
onclick="showStockHistory({{ $stock->id }})"
|
||||
title="Lihat Riwayat">
|
||||
<i class="la la-history"></i>
|
||||
</button>
|
||||
</div>
|
||||
234
resources/views/warehouse_management/stocks/index.blade.php
Normal file
234
resources/views/warehouse_management/stocks/index.blade.php
Normal file
@@ -0,0 +1,234 @@
|
||||
@extends('layouts.backapp')
|
||||
|
||||
@section('content')
|
||||
<div class="kt-portlet kt-portlet--mobile">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<span class="kt-portlet__head-icon">
|
||||
<i class="kt-font-brand flaticon2-box"></i>
|
||||
</span>
|
||||
<h3 class="kt-portlet__head-title">Manajemen Stok</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<a href="{{ route('opnames.create') }}" class="btn btn-primary">
|
||||
<i class="la la-plus"></i> Buat Opname
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kt-portlet__body">
|
||||
<!-- Filter -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="dealer_filter">Filter Dealer</label>
|
||||
<select class="form-control" id="dealer_filter">
|
||||
<option value="">Semua Dealer</option>
|
||||
@foreach($dealers as $dealer)
|
||||
<option value="{{ $dealer->id }}">{{ $dealer->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="product_filter">Filter Produk</label>
|
||||
<select class="form-control" id="product_filter">
|
||||
<option value="">Semua Produk</option>
|
||||
@foreach($products as $product)
|
||||
<option value="{{ $product->id }}">{{ $product->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel Stok -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover" id="stocks-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Dealer</th>
|
||||
<th>Produk</th>
|
||||
<th>Stok</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Adjust Stock -->
|
||||
<div class="modal fade" id="adjustStockModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Adjust Stok</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="adjustStockForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="stock_id" name="stock_id">
|
||||
<input type="hidden" id="adjust_type" name="type">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Jumlah</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" name="quantity"
|
||||
step="0.01" min="0.01" required>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">pcs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Catatan</label>
|
||||
<textarea class="form-control" name="note" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Stock History -->
|
||||
<div class="modal fade" id="stockHistoryModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Riwayat Stok</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="history-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>User</th>
|
||||
<th>Perubahan</th>
|
||||
<th>Stok Lama</th>
|
||||
<th>Stok Baru</th>
|
||||
<th>Catatan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Inisialisasi DataTable
|
||||
var table = $('#stocks-table').DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
ajax: {
|
||||
url: '{{ route("stocks.index") }}',
|
||||
data: function(d) {
|
||||
d.dealer_id = $('#dealer_filter').val();
|
||||
d.product_id = $('#product_filter').val();
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{data: 'dealer_name', name: 'dealer_name'},
|
||||
{data: 'product_name', name: 'product_name'},
|
||||
{data: 'quantity', name: 'quantity'},
|
||||
{data: 'action', name: 'action', orderable: false, searchable: false}
|
||||
]
|
||||
});
|
||||
|
||||
// Filter change handler
|
||||
$('#dealer_filter, #product_filter').change(function() {
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
// Show adjust stock modal
|
||||
window.showAdjustStockModal = function(stockId, type) {
|
||||
$('#stock_id').val(stockId);
|
||||
$('#adjust_type').val(type);
|
||||
$('#adjustStockModal').modal('show');
|
||||
};
|
||||
|
||||
// Handle adjust stock form submit
|
||||
$('#adjustStockForm').submit(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
$.ajax({
|
||||
url: '{{ route("stocks.adjust") }}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
_token: '{{ csrf_token() }}',
|
||||
stock_id: $('#stock_id').val(),
|
||||
type: $('#adjust_type').val(),
|
||||
quantity: $('input[name="quantity"]').val(),
|
||||
note: $('textarea[name="note"]').val()
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#adjustStockModal').modal('hide');
|
||||
table.ajax.reload();
|
||||
toastr.success(response.message);
|
||||
} else {
|
||||
toastr.error(response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
toastr.error(xhr.responseJSON?.message || 'Terjadi kesalahan');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Show stock history
|
||||
window.showStockHistory = function(stockId) {
|
||||
$.get('{{ route("stocks.history") }}', {
|
||||
stock_id: stockId
|
||||
})
|
||||
.done(function(response) {
|
||||
var tbody = $('#history-table tbody');
|
||||
tbody.empty();
|
||||
|
||||
response.logs.forEach(function(log) {
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td>${log.date}</td>
|
||||
<td>${log.user}</td>
|
||||
<td>${log.change}</td>
|
||||
<td>${log.old_quantity}</td>
|
||||
<td>${log.new_quantity}</td>
|
||||
<td>${log.note}</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
$('#stockHistoryModal').modal('show');
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
toastr.error('Gagal memuat riwayat stok');
|
||||
});
|
||||
};
|
||||
|
||||
// Reset form when modal is closed
|
||||
$('#adjustStockModal').on('hidden.bs.modal', function() {
|
||||
$('#adjustStockForm')[0].reset();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -11,6 +11,7 @@ use App\Http\Controllers\WarehouseManagement\OpnamesController;
|
||||
use App\Http\Controllers\WarehouseManagement\ProductCategoriesController;
|
||||
use App\Http\Controllers\WarehouseManagement\ProductsController;
|
||||
use App\Http\Controllers\WorkController;
|
||||
use App\Http\Controllers\WarehouseManagement\StocksController;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Privilege;
|
||||
use App\Models\Role;
|
||||
@@ -237,12 +238,16 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
Route::get('create','create')->name('opnames.create');
|
||||
Route::post('/','store')->name('opnames.store');
|
||||
Route::get('{opnames}','show')->name('opnames.show');
|
||||
Route::post('get-stock-data', 'getStockData')->name('opnames.get-stock-data');
|
||||
});
|
||||
|
||||
Route::prefix('stocks')->controller(StocksController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('stocks.index');
|
||||
Route::post('adjust', 'adjust')->name('stocks.adjust');
|
||||
Route::get('history', 'history')->name('stocks.history');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
Auth::routes();
|
||||
// Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
|
||||
|
||||
@@ -21,14 +21,6 @@ mix.js("resources/js/app.js", "public/js")
|
||||
"resources/js/warehouse_management/products/index.js",
|
||||
"public/js/warehouse_management/products"
|
||||
)
|
||||
.js(
|
||||
"resources/js/warehouse_management/products/create.js",
|
||||
"public/js/warehouse_management/products"
|
||||
)
|
||||
.js(
|
||||
"resources/js/warehouse_management/products/edit.js",
|
||||
"public/js/warehouse_management/products"
|
||||
)
|
||||
.js(
|
||||
"resources/js/warehouse_management/opnames/index.js",
|
||||
"public/js/warehouse_management/opnames"
|
||||
|
||||
Reference in New Issue
Block a user