up
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\OrdenProduccion;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class PipelineOrdersWidget extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.widgets.pipeline-orders-widget';
|
||||
|
||||
public $states = [
|
||||
'en_corte' => 'En corte',
|
||||
'en_confeccion' => 'En confección',
|
||||
'en_tintoreria' => 'En tintorería',
|
||||
'en_acabados' => 'En acabados',
|
||||
'finalizada' => 'Finalizada',
|
||||
];
|
||||
|
||||
public $ordersByState = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->loadOrders();
|
||||
}
|
||||
|
||||
public function loadOrders()
|
||||
{
|
||||
$this->ordersByState = [];
|
||||
foreach (array_keys($this->states) as $s) {
|
||||
$this->ordersByState[$s] = OrdenProduccion::where('estado', $s)
|
||||
->orderBy('fecha_entrega_estimada')
|
||||
->limit(12)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
// Allow external refresh
|
||||
protected function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'pipelineUpdated' => 'loadOrders',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Filament;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\OrdenProduccion;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class OrdenProduccionController extends Controller
|
||||
{
|
||||
public function changeState(Request $request, OrdenProduccion $orden)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'state' => ['required', 'in:en_corte,en_confeccion,en_tintoreria,en_acabados,finalizada'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$orden->update(['estado' => $data['state']]);
|
||||
return response()->json(['ok' => true, 'state' => $data['state']]);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function advance(Request $request, OrdenProduccion $orden)
|
||||
{
|
||||
try {
|
||||
$orden->avanzarEstado();
|
||||
return response()->json(['ok' => true, 'state' => $orden->estado]);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['ok' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,25 +24,8 @@ $recentTrans = TrasladoPrenda::latest()->limit(8)->get();
|
||||
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
{{-- Kanban column (span 2) --}}
|
||||
<div class="col-span-2 bg-white rounded shadow p-4">
|
||||
<h2 class="text-lg font-semibold mb-3">Pipeline de Órdenes</h2>
|
||||
<div class="grid grid-cols-5 gap-4">
|
||||
@foreach($ordersByState as $state => $orders)
|
||||
<div class="bg-gray-50 rounded p-3">
|
||||
<h3 class="text-sm font-semibold mb-2">{{ ucfirst(str_replace('_',' ', $state)) }} ({{ $orders->count() }})</h3>
|
||||
<div class="space-y-2">
|
||||
@forelse($orders as $op)
|
||||
<a href="{{ route('filament.admin.resources.orden-produccions.edit', ['record' => $op->id]) }}" class="block p-2 bg-white rounded border hover:shadow">
|
||||
<div class="text-sm font-semibold">#{{ $op->numero_orden }} — {{ $op->prenda_modelo }}</div>
|
||||
<div class="text-xs text-gray-500">Cant: {{ $op->cantidad_total }} · Metros: {{ $op->metros_requeridos }} · Entrega: {{ $op->fecha_entrega_estimada }}</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="text-sm text-gray-400">Sin órdenes</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
@livewire('filament.widgets.pipeline-orders-widget')
|
||||
</div>
|
||||
|
||||
{{-- Timeline of shipments --}}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<div class="bg-white rounded shadow p-4">
|
||||
<h2 class="text-lg font-semibold mb-3">Pipeline interactivo</h2>
|
||||
<div class="grid grid-cols-5 gap-4" id="kanban">
|
||||
@foreach($states as $stateKey => $label)
|
||||
<div class="bg-gray-50 rounded p-3" data-state="{{ $stateKey }}">
|
||||
<h3 class="text-sm font-semibold mb-2">{{ $label }} ({{ count($ordersByState[$stateKey]) }})</h3>
|
||||
<div class="space-y-2 min-h-[100px] dropzone" ondragover="event.preventDefault()" ondrop="dropHandler(event, '{{ $stateKey }}')">
|
||||
@foreach($ordersByState[$stateKey] as $op)
|
||||
<div class="op-card bg-white rounded border p-2 shadow-sm" draggable="true" ondragstart="dragStart(event, {{ $op->id }})">
|
||||
<div class="text-sm font-semibold">#{{ $op->numero_orden }} — {{ $op->prenda_modelo }}</div>
|
||||
<div class="text-xs text-gray-500">Cant: {{ $op->cantidad_total }} · Metros: {{ $op->metros_requeridos }} · Entrega: {{ $op->fecha_entrega_estimada }}</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<a href="{{ route('filament.admin.resources.orden-produccions.edit', ['record' => $op->id]) }}" class="text-blue-600 text-sm">Ver</a>
|
||||
<button class="text-sm px-2 py-1 bg-gray-100 rounded" onclick="quickAdvance({{ $op->id }})">Avanzar</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let draggedId = null;
|
||||
function dragStart(e, id) {
|
||||
draggedId = id;
|
||||
e.dataTransfer.setData('text/plain', id);
|
||||
}
|
||||
|
||||
async function dropHandler(e, targetState) {
|
||||
e.preventDefault();
|
||||
const id = draggedId || e.dataTransfer.getData('text/plain');
|
||||
if (!id) return;
|
||||
|
||||
// POST to backend to change state
|
||||
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
try {
|
||||
const res = await fetch("{{ url('/filament/ordenes') }}/" + id + '/change-state', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': token,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ state: targetState })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(err.message || 'Error cambiando estado');
|
||||
return;
|
||||
}
|
||||
|
||||
// reload widget area (simple full refresh)
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
alert('Error de red');
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function quickAdvance(id) {
|
||||
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
try {
|
||||
const res = await fetch("{{ url('/filament/ordenes') }}/" + id + '/advance', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': token,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(err.message || 'Error avanzando');
|
||||
return;
|
||||
}
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
alert('Error de red');
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -58,4 +58,8 @@ Route::get('/imprimir-recibo/{venta}', [VentaController::class, 'imprimirRecibo'
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::post('/filament/traslados/{traslado}/mark-received', [\App\Http\Controllers\Filament\TrasladoPrendaController::class, 'markReceived'])->name('filament.traslados.mark-received');
|
||||
|
||||
// Routes for Kanban actions
|
||||
Route::post('/filament/ordenes/{orden}/change-state', [\App\Http\Controllers\Filament\OrdenProduccionController::class, 'changeState']);
|
||||
Route::post('/filament/ordenes/{orden}/advance', [\App\Http\Controllers\Filament\OrdenProduccionController::class, 'advance']);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user