diff --git a/app/Filament/Resources/ConfeccionResource.php b/app/Filament/Resources/ConfeccionResource.php index 32f2b26..47b29eb 100644 --- a/app/Filament/Resources/ConfeccionResource.php +++ b/app/Filament/Resources/ConfeccionResource.php @@ -138,6 +138,10 @@ class ConfeccionResource extends Resource TextColumn::make('cantidad_recibida'), + TextColumn::make('faltantes') + ->label('Faltan') + ->sortable(), + BadgeColumn::make('estado') ->colors([ 'warning' => 'pendiente', @@ -165,7 +169,7 @@ class ConfeccionResource extends Resource public static function getRelations(): array { return [ - // + RelationManagers\RecepcionesRelationManager::class, ]; } diff --git a/app/Filament/Resources/ConfeccionResource/RelationManagers/RecepcionesRelationManager.php b/app/Filament/Resources/ConfeccionResource/RelationManagers/RecepcionesRelationManager.php new file mode 100644 index 0000000..cc38950 --- /dev/null +++ b/app/Filament/Resources/ConfeccionResource/RelationManagers/RecepcionesRelationManager.php @@ -0,0 +1,49 @@ +schema([ + DateTimePicker::make('fecha_recepcion')->required(), + TextInput::make('cantidad')->numeric()->required()->minValue(1), + Textarea::make('notas'), + ]); + } + + public function table(Tables\Table $table): Tables\Table + { + return $table + ->columns([ + TextColumn::make('id')->label('#'), + TextColumn::make('fecha_recepcion')->dateTime()->label('Fecha'), + TextColumn::make('cantidad')->label('Cantidad'), + TextColumn::make('usuario.name')->label('Usuario'), + TextColumn::make('notas')->limit(50)->wrap(), + TextColumn::make('created_at')->dateTime()->label('Creado'), + ]) + ->headerActions([]) + ->actions([ + EditAction::make(), + DeleteAction::make(), + ]) + ->bulkActions([]); + } +} diff --git a/app/Filament/Resources/TintoreriaResource.php b/app/Filament/Resources/TintoreriaResource.php index 5c9f2d9..b28a41d 100644 --- a/app/Filament/Resources/TintoreriaResource.php +++ b/app/Filament/Resources/TintoreriaResource.php @@ -124,6 +124,10 @@ class TintoreriaResource extends Resource TextColumn::make('cantidad_recibida'), + TextColumn::make('faltantes') + ->label('Faltan') + ->sortable(), + TextColumn::make('perdidas') ->label('Pérdidas'), @@ -146,7 +150,7 @@ class TintoreriaResource extends Resource public static function getRelations(): array { return [ - // + RelationManagers\RecepcionesRelationManager::class, ]; } diff --git a/app/Filament/Resources/TintoreriaResource/RelationManagers/RecepcionesRelationManager.php b/app/Filament/Resources/TintoreriaResource/RelationManagers/RecepcionesRelationManager.php new file mode 100644 index 0000000..4dcd602 --- /dev/null +++ b/app/Filament/Resources/TintoreriaResource/RelationManagers/RecepcionesRelationManager.php @@ -0,0 +1,49 @@ +schema([ + DateTimePicker::make('fecha_recepcion')->required(), + TextInput::make('cantidad')->numeric()->required()->minValue(1), + Textarea::make('notas'), + ]); + } + + public function table(Tables\Table $table): Tables\Table + { + return $table + ->columns([ + TextColumn::make('id')->label('#'), + TextColumn::make('fecha_recepcion')->dateTime()->label('Fecha'), + TextColumn::make('cantidad')->label('Cantidad'), + TextColumn::make('usuario.name')->label('Usuario'), + TextColumn::make('notas')->limit(50)->wrap(), + TextColumn::make('created_at')->dateTime()->label('Creado'), + ]) + ->headerActions([]) + ->actions([ + EditAction::make(), + DeleteAction::make(), + ]) + ->bulkActions([]); + } +} diff --git a/app/Models/Confeccion.php b/app/Models/Confeccion.php index 5743f68..24ed475 100644 --- a/app/Models/Confeccion.php +++ b/app/Models/Confeccion.php @@ -64,35 +64,23 @@ class Confeccion extends Model } }); - // Crear traslado cuando la confección tenga recepción (en creación y en actualización) - $createTrasladoFn = function ($confeccion) { - if ($confeccion->cantidad_recibida !== null) { - // Evitar duplicados - $exists = \App\Models\TrasladoPrenda::where('referencia_type', self::class) - ->where('referencia_id', $confeccion->id) - ->exists(); + /* Recepciones polimórficas */ + public function recepciones() + { + return $this->morphMany(\App\Models\Recepcion::class, 'referencia'); + } - if (! $exists) { - $destino = 'tintoreria'; + // Cuantas faltan por recibir + public function getFaltantesAttribute() + { + return max(0, (int)($this->cantidad_enviada ?? 0) - (int)($this->cantidad_recibida ?? 0)); + } - \App\Models\TrasladoPrenda::crearDesdeReferencia( - $confeccion, - 'confeccion', - $destino - ); - - // Marcar el paso como terminado y avanzar la OP incluso si fue parcial - $confeccion->ordenProduccion?->avanzarEstado(); - } - } - - // Mantener el comportamiento anterior: si está completo, también avanzamos (por seguridad) + // Mantener avance de OP cuando la confección se completa + static::saved(function ($confeccion) { if ($confeccion->estado === 'completo') { $confeccion->ordenProduccion?->avanzarEstado(); } - }; - - static::updated($createTrasladoFn); - static::saved($createTrasladoFn); + }); } } diff --git a/app/Models/Recepcion.php b/app/Models/Recepcion.php new file mode 100644 index 0000000..2b6f4ed --- /dev/null +++ b/app/Models/Recepcion.php @@ -0,0 +1,119 @@ + 'datetime', + 'cantidad' => 'integer', + ]; + + public function referencia() + { + return $this->morphTo(); + } + + public function usuario() + { + return $this->belongsTo(User::class, 'user_id'); + } + + protected static function booted() + { + static::creating(function ($rec) { + // Cantidad debe ser entero >= 1 + $rec->cantidad = (int) $rec->cantidad; + if ($rec->cantidad <= 0) { + throw ValidationException::withMessages(['cantidad' => 'La cantidad debe ser un entero mayor que 0.']); + } + + // Fecha por defecto + if (! $rec->fecha_recepcion) { + $rec->fecha_recepcion = now(); + } + + // Validación: no permitir sobrepasar la cantidad enviada en la referencia + if ($rec->referencia) { + $referencia = $rec->referencia; + $enviada = (int) ($referencia->cantidad_enviada ?? 0); + $yaRecibido = (int) \\App\\Models\\Recepcion::where('referencia_type', get_class($referencia)) + ->where('referencia_id', $referencia->id) + ->sum('cantidad'); + + if (($yaRecibido + $rec->cantidad) > $enviada) { + throw ValidationException::withMessages(['cantidad' => "La recepción excede la cantidad pendiente (faltan: " . max(0, $enviada - $yaRecibido) . ")."]); + } + } + + // Set user if available + if (! $rec->user_id && auth()->check()) { + $rec->user_id = auth()->id(); + } + }); + + static::created(function ($rec) { + $rec->syncParentAggregates(); + + // Crear un traslado parcial desde la referencia usando esta recepción + if ($rec->referencia) { + $referencia = $rec->referencia; + $payload = [ + 'cantidad_enviada' => $rec->cantidad, + 'cantidad_recibida' => $rec->cantidad, + 'fecha_recepcion' => $rec->fecha_recepcion, + 'notas' => 'Creado desde recepción #' . $rec->id . ($rec->notas ? (" - " . $rec->notas) : ''), + ]; + + // Map destino según tipo de referencia + if ($referencia instanceof Confeccion) { + $origen = 'confeccion'; + $destino = 'tintoreria'; + } elseif ($referencia instanceof Tintoreria) { + $origen = 'tintoreria'; + $destino = 'acabados'; + } else { + $origen = 'referencia'; + $destino = 'bodega'; + } + + \App\Models\TrasladoPrenda::crearDesdeReferencia($referencia, $origen, $destino, $payload); + + // Avanzar OP por seguridad + $referencia->ordenProduccion?->avanzarEstado(); + } + }); + + static::updated(function ($rec) { + $rec->syncParentAggregates(); + }); + + static::deleted(function ($rec) { + $rec->syncParentAggregates(); + }); + } + + public function syncParentAggregates() + { + if (! $this->referencia) return; + $referencia = $this->referencia; + $sum = (int) static::where('referencia_type', get_class($referencia)) + ->where('referencia_id', $referencia->id) + ->sum('cantidad'); + + $referencia->cantidad_recibida = $sum; + $referencia->save(); + } +} diff --git a/app/Models/Tintoreria.php b/app/Models/Tintoreria.php index 71c9f36..862d1a9 100644 --- a/app/Models/Tintoreria.php +++ b/app/Models/Tintoreria.php @@ -55,42 +55,23 @@ class Tintoreria extends Model } }); - $createTrasladoFn = function ($tintoreria) { - if ($tintoreria->cantidad_recibida !== null) { - // Evitar duplicados - $exists = \App\Models\TrasladoPrenda::where('referencia_type', self::class) - ->where('referencia_id', $tintoreria->id) - ->exists(); + /* Recepciones polimórficas */ + public function recepciones() + { + return $this->morphMany(\App\Models\Recepcion::class, 'referencia'); + } - if (! $exists) { - // Mapear "perdidas" a prendas_defectuosas en el traslado - $payload = [ - 'prendas_defectuosas' => $tintoreria->perdidas ?? 0, - ]; + // Cuantas faltan por recibir + public function getFaltantesAttribute() + { + return max(0, (int)($this->cantidad_enviada ?? 0) - (int)($this->cantidad_recibida ?? 0)); + } - $destino = 'acabados'; - - \App\Models\TrasladoPrenda::crearDesdeReferencia( - $tintoreria, - 'tintoreria', - $destino, - $payload - ); - - // Avanzar la OP al cerrar el paso - $tintoreria->ordenProduccion?->avanzarEstado(); - } - } - - if ( - $tintoreria->fecha_recepcion && - $tintoreria->cantidad_recibida !== null - ) { + // Avanzar OP al cerrar el paso o cuando se complete + static::saved(function ($tintoreria) { + if ($tintoreria->fecha_recepcion && $tintoreria->cantidad_recibida !== null) { $tintoreria->ordenProduccion?->avanzarEstado(); } - }; - - static::updated($createTrasladoFn); - static::saved($createTrasladoFn); + }); } } diff --git a/database/migrations/2026_01_19_120000_create_recepciones_table.php b/database/migrations/2026_01_19_120000_create_recepciones_table.php new file mode 100644 index 0000000..fb59545 --- /dev/null +++ b/database/migrations/2026_01_19_120000_create_recepciones_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('referencia_type'); + $table->unsignedBigInteger('referencia_id'); + $table->unsignedBigInteger('user_id')->nullable(); + $table->integer('cantidad')->unsigned(); + $table->dateTime('fecha_recepcion')->nullable(); + $table->text('notas')->nullable(); + + $table->timestamps(); + + $table->index(['referencia_type', 'referencia_id']); + $table->foreign('user_id')->references('id')->on('users')->onDelete('set null'); + }); + } + + public function down() + { + Schema::dropIfExists('recepciones'); + } +}; diff --git a/resources/views/filament/orden_produccion/timeline.blade.php b/resources/views/filament/orden_produccion/timeline.blade.php index 97b4b53..aac8dd8 100644 --- a/resources/views/filament/orden_produccion/timeline.blade.php +++ b/resources/views/filament/orden_produccion/timeline.blade.php @@ -4,26 +4,51 @@ $op = \App\Models\OrdenProduccion::find($opId); $events = []; if ($op) { - $confecciones = $op->confeccions()->orderBy('fecha_recepcion')->get(); + $confecciones = $op->confeccions()->get(); foreach ($confecciones as $c) { + // Evento principal de la confección (creación/envío) $events[] = [ 'tipo' => 'Confección', 'id' => $c->id, - 'fecha' => $c->fecha_recepcion ?? $c->created_at, - 'detalle' => "Enviado: {$c->cantidad_enviada} - Recibido: {$c->cantidad_recibida} - Defectos: {$c->prendas_defectuosas}", + 'fecha' => $c->fecha_envio ?? $c->created_at, + 'detalle' => "Enviado: {$c->cantidad_enviada} - Recibido total: {$c->cantidad_recibida} - Defectos: {$c->prendas_defectuosas}", 'link' => route('filament.admin.resources.confeccions.edit', ['record' => $c->id]), ]; + + // Eventos por cada recepción + $recepciones = $c->recepciones()->orderBy('fecha_recepcion')->get(); + foreach ($recepciones as $r) { + $events[] = [ + 'tipo' => 'Recepción (Confección)', + 'id' => $r->id, + 'fecha' => $r->fecha_recepcion ?? $r->created_at, + 'detalle' => "Recibido: {$r->cantidad} - Notas: " . ($r->notas ?? '—'), + 'link' => route('filament.admin.resources.confeccions.edit', ['record' => $c->id]), + ]; + } } - $tints = $op->tintorerias()->orderBy('fecha_recepcion')->get(); + $tints = $op->tintorerias()->get(); foreach ($tints as $t) { $events[] = [ 'tipo' => 'Tintorería', 'id' => $t->id, - 'fecha' => $t->fecha_recepcion ?? $t->created_at, - 'detalle' => "Enviado: {$t->cantidad_enviada} - Recibido: {$t->cantidad_recibida} - Perdidas: {$t->perdidas}", + 'fecha' => $t->fecha_envio ?? $t->created_at, + 'detalle' => "Enviado: {$t->cantidad_enviada} - Recibido total: {$t->cantidad_recibida} - Perdidas: {$t->perdidas}", 'link' => route('filament.admin.resources.tintorerias.edit', ['record' => $t->id]), ]; + + // Eventos por cada recepción + $recepciones = $t->recepciones()->orderBy('fecha_recepcion')->get(); + foreach ($recepciones as $r) { + $events[] = [ + 'tipo' => 'Recepción (Tintorería)', + 'id' => $r->id, + 'fecha' => $r->fecha_recepcion ?? $r->created_at, + 'detalle' => "Recibido: {$r->cantidad} - Notas: " . ($r->notas ?? '—'), + 'link' => route('filament.admin.resources.tintorerias.edit', ['record' => $t->id]), + ]; + } } $acabados = $op->procesosAcabado()->orderBy('fecha_recepcion')->get(); @@ -89,6 +114,11 @@ if ($op) {
{{ $e['detalle'] }}
{{-- Acciones rápidas --}} + @if(in_array($e['tipo'], ['Confección','Tintorería'])) + + @endif @if($e['tipo'] === 'Traslado') @php $tr = \App\Models\TrasladoPrenda::find($e['id']); @endphp