fix orderable datatable on mutations and products index

This commit is contained in:
2025-06-16 19:01:11 +07:00
parent aa233eb793
commit b803068d0e
12 changed files with 383 additions and 178 deletions

View File

@@ -10,6 +10,7 @@ use App\Enums\MutationStatus;
use App\Models\Menu;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Yajra\DataTables\DataTables;
class MutationsController extends Controller
@@ -19,13 +20,18 @@ class MutationsController extends Controller
$menu = Menu::where('link','mutations.index')->first();
if ($request->ajax()) {
Log::info('Mutations DataTables request', [
'order' => $request->get('order'),
'columns' => $request->get('columns'),
'user_id' => auth()->id()
]);
// Use a more specific query to avoid join conflicts
$data = Mutation::query()
->with(['fromDealer', 'toDealer', 'requestedBy.role', 'approvedBy.role', 'receivedBy.role'])
->select([
'mutations.*'
])
->orderBy('mutations.id', 'desc'); // Default order by ID desc
]); // Remove default ordering to let DataTables handle it
// Filter berdasarkan dealer jika user bukan admin
if (auth()->user()->dealer_id) {
@@ -60,7 +66,7 @@ class MutationsController extends Controller
return number_format($row->total_items, 0);
})
->addColumn('created_at', function($row) {
return $row->created_at->format('d/m/Y H:i');
return $row->created_at->format('d M Y, H:i');
})
->addColumn('action', function($row) {
return view('warehouse_management.mutations._action', compact('row'))->render();
@@ -426,7 +432,7 @@ class MutationsController extends Controller
return number_format($row->total_items, 0);
})
->addColumn('created_at', function($row) {
return $row->created_at->format('d/m/Y H:i');
return $row->created_at->format('d M Y, H:i');
})
->addColumn('action', function($row) use ($dealerId) {
$buttons = '';
@@ -478,7 +484,7 @@ class MutationsController extends Controller
]);
// Format created_at
$mutation->created_at_formatted = $mutation->created_at->format('d/m/Y H:i');
$mutation->created_at_formatted = $mutation->created_at->format('d M Y, H:i');
// Add status color and label
$mutation->status_color = $mutation->status_color;

View File

@@ -12,6 +12,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\DB;
use Yajra\DataTables\Facades\DataTables;
use Illuminate\Validation\Rule;
@@ -26,17 +27,41 @@ class ProductsController extends Controller
{
$menu = Menu::where('link','products.index')->first();
if($request->ajax()){
$data = Product::with(['category'])
->select('products.*')
->leftJoin('product_categories', 'products.product_category_id', '=', 'product_categories.id');
Log::info('Products DataTables request received');
Log::info('Request parameters:', $request->all());
try {
// Check if products exist
$productCount = Product::count();
Log::info('Total products in database: ' . $productCount);
$data = Product::with(['category', 'stocks'])
->select(['id', 'code', 'name', 'product_category_id', 'unit', 'active']);
Log::info('Query built, executing DataTables...');
return DataTables::of($data)
->addIndexColumn()
->addColumn('code', function ($row) {
return $row->code;
})
->addColumn('name', function ($row) {
return $row->name;
})
->addColumn('category_name', function ($row) {
return $row->category ? $row->category->name : '-';
})
->addColumn('unit', function ($row) {
return $row->unit ?? '-';
})
->addColumn('total_stock', function ($row){
return number_format($row->current_total_stock, 2);
try {
$totalStock = $row->stocks()->sum('quantity');
return number_format($totalStock, 2);
} catch (\Exception $e) {
Log::error('Error calculating total stock for product ' . $row->id . ': ' . $e->getMessage());
return '0.00';
}
})
->addColumn('action', function ($row) use ($menu) {
$btn = '<div class="d-flex">';
@@ -58,13 +83,33 @@ class ProductsController extends Controller
return $btn;
})
->filterColumn('category_name', function($query, $keyword) {
$query->where('product_categories.name', 'like', "%{$keyword}%");
$query->whereHas('category', function($q) use ($keyword) {
$q->where('name', 'like', "%{$keyword}%");
});
})
->orderColumn('code', function ($query, $order) {
$query->orderBy('products.code', $order);
})
->orderColumn('name', function ($query, $order) {
$query->orderBy('products.name', $order);
})
->orderColumn('category_name', function ($query, $order) {
$query->orderBy('product_categories.name', $order);
$query->orderBy(
DB::raw('(SELECT name FROM product_categories WHERE product_categories.id = products.product_category_id)'),
$order
);
})
->orderColumn('unit', function ($query, $order) {
$query->orderBy('products.unit', $order);
})
->rawColumns(['action'])
->make(true);
} catch (\Exception $e) {
Log::error('Products DataTables error: ' . $e->getMessage());
Log::error('Stack trace: ' . $e->getTraceAsString());
return response()->json(['error' => 'Failed to load data'], 500);
}
}
return view('warehouse_management.products.index');
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,15 @@ $(document).ready(function () {
return;
}
// Wait for DOM to be fully ready
setTimeout(function () {
initializeDataTable();
}, 100);
});
function initializeDataTable() {
console.log("Initializing DataTable...");
// Destroy existing table if any
if ($.fn.DataTable.isDataTable("#mutations-table")) {
$("#mutations-table").DataTable().destroy();
@@ -20,26 +29,24 @@ $(document).ready(function () {
ajax: {
url: $("#mutations-table").data("url"),
type: "GET",
data: function (d) {
console.log("DataTables request data:", d);
console.log("Order info:", d.order);
console.log("Columns info:", d.columns);
return d;
},
error: function (xhr, error, code) {
console.error("DataTables AJAX error:", error, code);
console.error("Response:", xhr.responseText);
},
},
columnDefs: [
{
targets: 0, // No. column
orderable: false,
searchable: false,
width: "5%",
},
{
targets: [1, 2, 3, 4, 5, 6, 7], // All sortable columns
orderable: true,
searchable: true,
},
{
targets: 8, // Action column
orderable: false,
searchable: false,
width: "20%",
className: "text-center",
},
@@ -52,43 +59,95 @@ $(document).ready(function () {
{
data: "DT_RowIndex",
name: "DT_RowIndex",
orderable: false,
searchable: false,
},
{
data: "mutation_number",
name: "mutation_number",
orderable: true,
},
{
data: "created_at",
name: "created_at",
orderable: true,
},
{
data: "from_dealer",
name: "fromDealer.name",
name: "from_dealer",
orderable: true,
},
{
data: "to_dealer",
name: "toDealer.name",
name: "to_dealer",
orderable: true,
},
{
data: "requested_by",
name: "requestedBy.name",
name: "requested_by",
orderable: true,
},
{
data: "total_items",
name: "total_items",
orderable: true,
},
{
data: "status",
name: "status",
orderable: true,
},
{
data: "action",
name: "action",
orderable: false,
searchable: false,
},
],
order: [[1, "desc"]], // Order by mutation_number desc (which follows ID order)
pageLength: 10,
responsive: true,
ordering: true, // Enable column ordering
orderMulti: false, // Single column ordering only
});
// Debug ordering events
table.on("order.dt", function () {
console.log("Order changed:", table.order());
});
// Add loading indicator for ordering
table.on("processing.dt", function (e, settings, processing) {
if (processing) {
console.log("DataTable processing started (ordering/filtering)");
} else {
console.log("DataTable processing finished");
}
});
// Manual click handler for column headers (fallback)
$("#mutations-table thead th").on("click", function () {
var columnIndex = $(this).index();
console.log("Column header clicked:", columnIndex, $(this).text());
// Skip if it's the first (No.) or last (Action) column
if (columnIndex === 0 || columnIndex === 8) {
console.log("Non-sortable column clicked, ignoring");
return;
}
// Check if DataTables is handling the click
if (
$(this).hasClass("sorting") ||
$(this).hasClass("sorting_asc") ||
$(this).hasClass("sorting_desc")
) {
console.log("DataTables should handle this click");
} else {
console.log("DataTables not handling click, manual trigger needed");
// Force DataTables to handle the ordering
table.order([columnIndex, "asc"]).draw();
}
});
// Handle Cancel Button Click with SweetAlert
@@ -117,43 +176,6 @@ $(document).ready(function () {
}
});
function cancelMutation(mutationId) {
$.ajax({
url: "/warehouse/mutations/" + mutationId + "/cancel",
type: "POST",
data: {
_token: $('meta[name="csrf-token"]').attr("content"),
},
success: function (response) {
if (typeof Swal !== "undefined") {
Swal.fire({
title: "Berhasil!",
text: "Mutasi berhasil dibatalkan",
icon: "success",
timer: 2000,
showConfirmButton: false,
});
} else {
alert("Mutasi berhasil dibatalkan");
}
table.ajax.reload();
},
error: function (xhr) {
var errorMsg =
xhr.responseJSON?.message || "Gagal membatalkan mutasi";
if (typeof Swal !== "undefined") {
Swal.fire({
title: "Error!",
text: errorMsg,
icon: "error",
});
} else {
alert("Error: " + errorMsg);
}
},
});
}
// Handle form submissions with loading state
$(document).on("submit", ".approve-form", function () {
$(this)
@@ -180,4 +202,43 @@ $(document).ready(function () {
$(this).siblings(".invalid-feedback").remove();
}
});
}
function cancelMutation(mutationId) {
$.ajax({
url: "/warehouse/mutations/" + mutationId + "/cancel",
type: "POST",
data: {
_token: $('meta[name="csrf-token"]').attr("content"),
},
success: function (response) {
if (typeof Swal !== "undefined") {
Swal.fire({
title: "Berhasil!",
text: "Mutasi berhasil dibatalkan",
icon: "success",
timer: 2000,
showConfirmButton: false,
});
} else {
alert("Mutasi berhasil dibatalkan");
}
// Get table instance
var table = $("#mutations-table").DataTable();
table.ajax.reload();
},
error: function (xhr) {
var errorMsg =
xhr.responseJSON?.message || "Gagal membatalkan mutasi";
if (typeof Swal !== "undefined") {
Swal.fire({
title: "Error!",
text: errorMsg,
icon: "error",
});
} else {
alert("Error: " + errorMsg);
}
},
});
}

View File

@@ -1,68 +1,74 @@
$.ajaxSetup({
headers: {
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
},
});
// Wait for DataTables to be available
function initializeDataTable() {
// Debug: Check if DataTables is loaded
console.log("DataTables available:", typeof $.fn.DataTable !== "undefined");
console.log("jQuery version:", $.fn.jquery);
$(document).ready(function () {
console.log("Products index.js loaded");
// Check if DataTables is available
if (typeof $.fn.DataTable === "undefined") {
console.error("DataTables is not loaded! Retrying in 1 second...");
setTimeout(initializeDataTable, 1000);
console.error("DataTables not available!");
return;
}
let tableContainer = $("#products-table");
let url = tableContainer.data("url");
// Wait for DOM to be fully ready
setTimeout(function () {
initializeDataTable();
}, 100);
});
console.log("Table URL:", url);
function initializeDataTable() {
console.log("Initializing DataTable...");
let table = $("#products-table").DataTable({
// Destroy existing table if any
if ($.fn.DataTable.isDataTable("#products-table")) {
$("#products-table").DataTable().destroy();
}
// Initialize DataTable
var table = $("#products-table").DataTable({
processing: true,
serverSide: true,
destroy: true,
ajax: {
url: url,
error: function (xhr, error, thrown) {
console.error("DataTables Ajax Error:", error, thrown);
url: $("#products-table").data("url"),
type: "GET",
data: function (d) {
console.log("DataTables request data:", d);
return d;
},
error: function (xhr, error, code) {
console.error("DataTables AJAX error:", error, code);
console.error("Response:", xhr.responseText);
},
},
order: [[0, "asc"]], // Order by first column (code) ascending
columns: [
{
data: "DT_RowIndex",
name: "DT_RowIndex",
orderable: false,
searchable: false,
},
{
data: "code",
name: "code",
orderable: true,
searchable: true,
},
{
data: "name",
name: "name",
orderable: true,
searchable: true,
},
{
data: "category_name",
name: "category.name",
name: "category_name",
orderable: true,
searchable: true,
},
{
data: "unit",
name: "unit",
orderable: true,
searchable: true,
},
{
data: "total_stock",
name: "total_stock",
orderable: false,
searchable: false,
},
{
data: "action",
@@ -71,52 +77,13 @@ function initializeDataTable() {
searchable: false,
},
],
columnDefs: [
{
targets: [4, 5], // total_stock and action columns
orderable: false,
},
],
initComplete: function (settings, json) {
console.log("DataTables initialized successfully");
console.log("Settings:", settings);
console.log(
"Column ordering enabled for:",
settings.aoColumns.map((col, index) => ({
index: index,
orderable: col.bSortable,
name: col.sName || col.mData,
}))
);
},
drawCallback: function (settings) {
console.log("DataTables draw completed");
},
headerCallback: function (thead, data, start, end, display) {
console.log("Header callback - sorting icons should be visible");
},
order: [[1, "asc"]], // Order by code asc
pageLength: 10,
responsive: true,
ordering: true,
orderMulti: false,
});
// Debug: Log table instance
console.log("DataTable instance:", table);
// Test column ordering programmatically
setTimeout(function () {
console.log("Testing column ordering...");
try {
table.order([1, "desc"]).draw();
console.log("Column ordering test successful");
} catch (e) {
console.error("Column ordering test failed:", e);
}
}, 2000);
}
// Initialize when document is ready
$(document).ready(function () {
console.log("Document ready, checking for DataTables...");
initializeDataTable();
});
$(document).on("click", ".btn-destroy-product", function () {
Swal.fire({
@@ -142,7 +109,14 @@ $(document).on("click", ".btn-destroy-product", function () {
"Produk berhasil dihapus.",
"success"
);
try {
if ($.fn.DataTable.isDataTable("#products-table")) {
$("#products-table").DataTable().ajax.reload();
}
} catch (e) {
console.error("Error reloading table:", e);
location.reload(); // Fallback to page reload
}
},
error: function (xhr) {
Swal.fire("Error!", "Gagal menghapus produk.", "error");
@@ -174,9 +148,16 @@ $(document).on("click", ".btn-toggle-active", function () {
},
success: function (response) {
if (response.success) {
try {
if ($.fn.DataTable.isDataTable("#products-table")) {
$("#products-table")
.DataTable()
.ajax.reload(null, false);
}
} catch (e) {
console.error("Error reloading table:", e);
location.reload(); // Fallback to page reload
}
Swal.fire("Berhasil!", response.message, "success");
}
},
@@ -197,9 +178,25 @@ $(document).on("click", ".btn-product-stock-dealers", function () {
const productName = $(this).data("name");
const ajaxUrl = $(this).data("url");
// Check if modal elements exist
if ($("#product-name-title").length === 0) {
console.error("Modal title element not found");
return;
}
if ($("#dealer-stock-table").length === 0) {
console.error("Dealer stock table element not found");
return;
}
// Set product name in modal title
$("#product-name-title").text(productName);
// Destroy existing DataTable if any
if ($.fn.DataTable.isDataTable("#dealer-stock-table")) {
$("#dealer-stock-table").DataTable().destroy();
}
// Initialize or reload DataTable inside modal
$("#dealer-stock-table").DataTable({
destroy: true,
@@ -210,6 +207,14 @@ $(document).on("click", ".btn-product-stock-dealers", function () {
data: {
product_id: productId,
},
error: function (xhr, error, thrown) {
console.error(
"Dealer stock DataTables Ajax Error:",
error,
thrown
);
console.error("Response:", xhr.responseText);
},
},
columns: [
{
@@ -226,7 +231,15 @@ $(document).on("click", ".btn-product-stock-dealers", function () {
},
],
initComplete: function () {
try {
if ($("#dealerStockModal").length > 0) {
$("#dealerStockModal").modal("show");
} else {
console.error("Modal #dealerStockModal not found");
}
} catch (e) {
console.error("Error showing modal:", e);
}
},
});
});

View File

@@ -50,6 +50,24 @@
@section('styles')
<style>
/* Override any conflicting styles */
#mutations-table thead th {
position: relative !important;
cursor: pointer !important;
user-select: none !important;
}
#mutations-table thead th:not(.sorting_disabled) {
cursor: pointer !important;
}
/* Ensure DataTables classes are applied */
#mutations-table.dataTable thead th.sorting,
#mutations-table.dataTable thead th.sorting_asc,
#mutations-table.dataTable thead th.sorting_desc {
cursor: pointer !important;
background-image: none !important;
}
/* DataTables Sorting Icons */
table.dataTable thead .sorting:before,
table.dataTable thead .sorting:after,
@@ -92,7 +110,25 @@ table.dataTable thead th {
table.dataTable thead th.sorting,
table.dataTable thead th.sorting_asc,
table.dataTable thead th.sorting_desc {
cursor: pointer;
cursor: pointer !important;
pointer-events: auto !important;
}
/* Force clickable area */
table.dataTable thead th.sorting:hover,
table.dataTable thead th.sorting_asc:hover,
table.dataTable thead th.sorting_desc:hover {
background-color: #f8f9fa;
}
/* Ensure sorting icons are visible */
table.dataTable thead .sorting:before,
table.dataTable thead .sorting_asc:before,
table.dataTable thead .sorting_desc:before {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
}
</style>
@endsection

View File

@@ -2,28 +2,69 @@
@section('styles')
<style>
/* Ensure DataTables sorting icons are visible */
/* DataTables Sorting Icons - Make them visible and clear */
table.dataTable thead .sorting:before,
table.dataTable thead .sorting:after,
table.dataTable thead .sorting_asc:before,
table.dataTable thead .sorting_asc:after,
table.dataTable thead .sorting_desc:before,
table.dataTable thead .sorting_desc:after {
opacity: 0.5 !important;
font-family: 'Font Awesome 5 Free';
font-weight: 900;
opacity: 0.6;
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
}
table.dataTable thead .sorting_asc:before,
/* Sorting icons */
table.dataTable thead .sorting:before {
content: "\f0dc"; /* fa-sort */
opacity: 0.4;
}
table.dataTable thead .sorting_asc:before {
content: "\f0de"; /* fa-sort-up */
opacity: 1;
color: #007bff;
}
table.dataTable thead .sorting_desc:before {
content: "\f0dd"; /* fa-sort-down */
opacity: 1;
color: #007bff;
}
/* Hide default after pseudo elements */
table.dataTable thead .sorting:after,
table.dataTable thead .sorting_asc:after,
table.dataTable thead .sorting_desc:after {
opacity: 1 !important;
display: none;
}
/* Make sure table headers are clickable */
table.dataTable thead th {
cursor: pointer;
position: relative;
padding-right: 30px !important; /* Space for sorting icon */
}
table.dataTable thead th.sorting_disabled {
cursor: default;
padding-right: 8px !important;
}
/* Hover effect for sortable columns */
table.dataTable thead th.sorting:hover,
table.dataTable thead th.sorting_asc:hover,
table.dataTable thead th.sorting_desc:hover {
background-color: #f8f9fa;
}
/* Ensure sorting icons are visible on hover */
table.dataTable thead th.sorting:hover:before {
opacity: 0.8;
}
</style>
@@ -57,6 +98,7 @@ table.dataTable thead th.sorting_disabled {
<table class="table table-striped table-bordered table-hover" id="products-table" data-url="{{ route("products.index") }}">
<thead>
<tr>
<th>No.</th>
<th>Kode</th>
<th>Nama</th>
<th>Kategori</th>
@@ -65,6 +107,8 @@ table.dataTable thead th.sorting_disabled {
<th>Aksi</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<!--end: Datatable -->
</div>