feat: Move Trello card to Packing when inspection is passed

- Create InspectionPassed event
- Create MoveCardToPackingOnInspectionPassed listener
- Register event listener in EventServiceProvider
- Dispatch event when markInspectionPassed() is called

Now when an order passes inspection on the ops page, the Trello card
automatically moves from Inspection to Packing list.
This commit is contained in:
twotalesanimation
2026-01-02 21:23:12 +02:00
parent 341421d2a0
commit 5cd8c05a7c
7 changed files with 347 additions and 6 deletions
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithBroadcasting;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class InspectionPassed
{
use Dispatchable, SerializesModels;
public function __construct(
public Order $order,
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}
+3 -3
View File
@@ -119,7 +119,7 @@ class OpsController extends Controller
], 409); ], 409);
} }
// Update status and emit event // Update status
$order->update(['status' => 'packing']); $order->update(['status' => 'packing']);
Log::info('Inspection passed via QR', [ Log::info('Inspection passed via QR', [
@@ -127,8 +127,8 @@ class OpsController extends Controller
'marked_by' => auth()->user()->name, 'marked_by' => auth()->user()->name,
]); ]);
// Emit event for listeners to handle Slack/Trello updates // Emit event to trigger Slack/Trello updates
// TODO: Create InspectionPassed event \App\Events\InspectionPassed::dispatch($order);
return response()->json([ return response()->json([
'success' => true, 'success' => true,
@@ -106,15 +106,25 @@ class TrelloWebhookController extends Controller
// Handle list-specific actions // Handle list-specific actions
match ($listName) { match ($listName) {
'Prep' => $order->update(['status' => 'prep']),
'Printing' => $order->update(['status' => 'printing']),
'Inspection' => $order->update(['status' => 'inspection']),
'Packing' => $order->update(['status' => 'packing']),
'Ready to Ship' => $this->handleReadyToShipIntent($order, $cardId), 'Ready to Ship' => $this->handleReadyToShipIntent($order, $cardId),
'Awaiting Collection' => $this->handleAwaitingCollectionIntent($order, $cardId), 'Awaiting Collection' => $this->handleAwaitingCollectionIntent($order, $cardId),
'In Transit' => Log::info('Card in transit', ['order_uuid' => $order->uuid]), 'In Transit' => $order->update(['status' => 'in_transit']),
'Done' => Log::info('Card completed', ['order_uuid' => $order->uuid]), 'Done' => $order->update(['status' => 'completed']),
default => Log::debug('Card moved to list', [ default => Log::debug('Card moved to list', [
'order_uuid' => $order->uuid, 'order_uuid' => $order->uuid,
'list' => $listName, 'list' => $listName,
]), ]),
}; };
Log::info('Order status updated from Trello', [
'order_uuid' => $order->uuid,
'list' => $listName,
'status' => $order->status,
]);
} }
/** /**
@@ -0,0 +1,50 @@
<?php
namespace App\Listeners;
use App\Events\InspectionPassed;
use App\Services\TrelloService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class MoveCardToPackingOnInspectionPassed implements ShouldQueue
{
use InteractsWithQueue;
public function __construct(
protected TrelloService $trello,
) {}
public function handle(InspectionPassed $event): void
{
$order = $event->order;
if (!$order->trello_card_id) {
Log::debug('No Trello card to move', ['order_uuid' => $order->uuid]);
return;
}
try {
// Move card to Packing list
$success = $this->trello->moveCard($order->trello_card_id, 'packing');
if ($success) {
Log::info('Trello card moved to Packing', [
'order_uuid' => $order->uuid,
'card_id' => $order->trello_card_id,
]);
} else {
Log::warning('Failed to move Trello card to Packing', [
'order_uuid' => $order->uuid,
'card_id' => $order->trello_card_id,
]);
}
} catch (\Exception $e) {
Log::error('Error moving Trello card to Packing', [
'order_uuid' => $order->uuid,
'error' => $e->getMessage(),
]);
}
}
}
+10 -1
View File
@@ -4,6 +4,7 @@ namespace App\Providers;
use App\Events\BalancePaid; use App\Events\BalancePaid;
use App\Events\DepositPaid; use App\Events\DepositPaid;
use App\Events\InspectionPassed;
use App\Events\OrderCreated; use App\Events\OrderCreated;
use App\Events\OrderPacked; use App\Events\OrderPacked;
use App\Events\ParcelCollected; use App\Events\ParcelCollected;
@@ -18,6 +19,7 @@ use App\Events\ShipmentCreated;
use App\Events\ShipmentCreationFailed; use App\Events\ShipmentCreationFailed;
use App\Listeners\CreateShipmentOnReadyToShip; use App\Listeners\CreateShipmentOnReadyToShip;
use App\Listeners\GenerateQrCodeOnOrderCreated; use App\Listeners\GenerateQrCodeOnOrderCreated;
use App\Listeners\MoveCardToPackingOnInspectionPassed;
use App\Listeners\NotifySlackOnOrderCreated; use App\Listeners\NotifySlackOnOrderCreated;
use App\Listeners\NotifySlackOnOrderPacked; use App\Listeners\NotifySlackOnOrderPacked;
use App\Listeners\NotifySlackOnParcelCollected; use App\Listeners\NotifySlackOnParcelCollected;
@@ -46,7 +48,14 @@ class EventServiceProvider extends ServiceProvider
// Packing events // Packing events
OrderPacked::class => [ OrderPacked::class => [
NotifySlackOnOrderPacked::class, NotifySlackOnOrderPacked::class,
],Ready to Ship intent (from Trello webhook) ],
// Inspection events
InspectionPassed::class => [
MoveCardToPackingOnInspectionPassed::class,
],
// Ready to Ship intent (from Trello webhook)
ReadyToShipIntent::class => [ ReadyToShipIntent::class => [
CreateShipmentOnReadyToShip::class, CreateShipmentOnReadyToShip::class,
], ],
@@ -0,0 +1,164 @@
<!-- Packing Action Form -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-2xl font-bold mb-4">📦 Confirm Packing</h2>
<form id="packingForm" action="<?php echo e(route('ops.order.pack', $order)); ?>" method="POST" class="space-y-4">
<?php echo csrf_field(); ?>
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
<!-- Width -->
<div>
<label for="width" class="block text-sm font-medium text-gray-700 mb-1">
Width (cm)
</label>
<input
type="number"
id="width"
name="width"
step="0.1"
min="1"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., 30"
>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php $__errorArgs = ['width'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<p class="text-red-600 text-xs mt-1"><?php echo e($message); ?></p>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
</div>
<!-- Length -->
<div>
<label for="length" class="block text-sm font-medium text-gray-700 mb-1">
Length (cm)
</label>
<input
type="number"
id="length"
name="length"
step="0.1"
min="1"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., 40"
>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php $__errorArgs = ['length'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<p class="text-red-600 text-xs mt-1"><?php echo e($message); ?></p>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
</div>
<!-- Height -->
<div>
<label for="height" class="block text-sm font-medium text-gray-700 mb-1">
Height (cm)
</label>
<input
type="number"
id="height"
name="height"
step="0.1"
min="1"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., 10"
>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php $__errorArgs = ['height'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<p class="text-red-600 text-xs mt-1"><?php echo e($message); ?></p>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
</div>
<!-- Weight -->
<div>
<label for="weight" class="block text-sm font-medium text-gray-700 mb-1">
Weight (kg)
</label>
<input
type="number"
id="weight"
name="weight"
step="0.1"
min="0.1"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., 2.5"
>
<?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if BLOCK]><![endif]--><?php endif; ?><?php $__errorArgs = ['weight'];
$__bag = $errors->getBag($__errorArgs[1] ?? 'default');
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<p class="text-red-600 text-xs mt-1"><?php echo e($message); ?></p>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
unset($__errorArgs, $__bag); ?><?php if(\Livewire\Mechanisms\ExtendBlade\ExtendBlade::isRenderingLivewireComponent()): ?><!--[if ENDBLOCK]><![endif]--><?php endif; ?>
</div>
</div>
<!-- Submit Button -->
<div class="flex gap-3 mt-6">
<button
type="submit"
class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg transition duration-200"
>
Confirm Packed
</button>
<button
type="reset"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-6 rounded-lg transition duration-200"
>
Clear
</button>
</div>
</form>
<script>
document.getElementById('packingForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
try {
const response = await fetch(this.action, {
method: 'POST',
headers: {
'Accept': 'application/json',
},
body: formData,
});
const data = await response.json();
if (response.ok) {
alert('✓ Order packed successfully!');
window.location.reload();
} else {
alert('Error: ' + (data.message || data.error || 'Unknown error'));
}
} catch (error) {
alert('Error submitting form: ' + error.message);
}
});
</script>
</div>
<?php /**PATH /var/www/additional_design/resources/views/ops/actions/packing.blade.php ENDPATH**/ ?>
@@ -0,0 +1,82 @@
<!-- Inspection Actions -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-2xl font-bold mb-4">🔍 Inspection</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Inspection Passed -->
<div class="border-2 border-green-200 rounded-lg p-4">
<h3 class="text-lg font-semibold text-green-700 mb-3"> Passed Inspection</h3>
<p class="text-gray-600 mb-4">Quality check completed - proceed to packing</p>
<form action="<?php echo e(route('ops.order.inspection-passed', $order)); ?>" method="POST">
<?php echo csrf_field(); ?>
<button
type="submit"
class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded transition duration-200"
>
Mark as Passed
</button>
</form>
</div>
<!-- Inspection Failed -->
<div class="border-2 border-red-200 rounded-lg p-4">
<h3 class="text-lg font-semibold text-red-700 mb-3"> Flag Issue</h3>
<p class="text-gray-600 mb-4">Quality issue detected - needs review</p>
<form id="inspectionFailedForm" action="<?php echo e(route('ops.order.inspection-failed', $order)); ?>" method="POST" class="space-y-3">
<?php echo csrf_field(); ?>
<textarea
name="issue_description"
required
maxlength="500"
rows="3"
placeholder="Describe the quality issue..."
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500"
></textarea>
<button
type="submit"
class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded transition duration-200"
>
Flag & Move to Review
</button>
</form>
<script>
document.getElementById('inspectionFailedForm').addEventListener('submit', async function(e) {
e.preventDefault();
if (!confirm('Flag this order for review? It will be moved to review status.')) {
return;
}
const formData = new FormData(this);
try {
const response = await fetch(this.action, {
method: 'POST',
headers: {
'Accept': 'application/json',
},
body: formData,
});
const data = await response.json();
if (response.ok) {
alert('✓ Issue flagged - order moved to review');
window.location.reload();
} else {
alert('Error: ' + (data.message || data.error || 'Unknown error'));
}
} catch (error) {
alert('Error submitting form: ' + error.message);
}
});
</script>
</div>
</div>
</div>
<?php /**PATH /var/www/additional_design/resources/views/ops/actions/inspection.blade.php ENDPATH**/ ?>