feat: implement drop-off points & dumpsites edit/delete and status transition logic

This commit is contained in:
Super Admin
2026-06-29 23:15:55 +08:00
parent f2c1573f00
commit cfd7166a24
6 changed files with 355 additions and 33 deletions

View File

@@ -10,12 +10,15 @@ use App\Http\Resources\DropOffCapacityLogResource;
use App\Http\Resources\DropOffPointResource;
use App\Models\DropOffCapacityLog;
use App\Models\DropOffPoint;
use App\Services\DropOff\DropOffPointFinder;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use MatanYadaev\EloquentSpatial\Objects\Point;
class AdminDropOffPointController extends ApiController
{
public function __construct(private readonly DropOffPointFinder $dopFinder) {}
public function index(Request $request): JsonResponse
{
$request->validate([
@@ -89,12 +92,28 @@ class AdminDropOffPointController extends ApiController
public function update(UpdateDropOffPointRequest $request, DropOffPoint $dropOffPoint): JsonResponse
{
$data = $request->validated();
$oldStatus = $dropOffPoint->status;
$newStatus = $data['status'] ?? $oldStatus;
if (isset($data['lat'], $data['lng'])) {
$data['coordinates'] = $this->makePoint($data);
}
unset($data['lat'], $data['lng']);
$dropOffPoint->update($data);
DB::transaction(function () use ($dropOffPoint, $data, $oldStatus, $newStatus) {
$dropOffPoint->update($data);
if ($newStatus !== DropOffPoint::STATUS_ACTIVE && $oldStatus === DropOffPoint::STATUS_ACTIVE) {
$households = $dropOffPoint->households;
foreach ($households as $household) {
$nearest = $this->dopFinder->nearest(
$household->coordinates->latitude,
$household->coordinates->longitude
);
$household->update(['assigned_drop_off_point_id' => $nearest?->id]);
}
}
});
return $this->ok(
new DropOffPointResource($dropOffPoint->fresh()->load('barangay')),

View File

@@ -7,6 +7,7 @@ use App\Http\Requests\Dumpsite\StoreDumpsiteRequest;
use App\Http\Requests\Dumpsite\UpdateDumpsiteRequest;
use App\Http\Resources\DumpsiteResource;
use App\Models\Dumpsite;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use MatanYadaev\EloquentSpatial\Objects\LineString;
@@ -102,7 +103,25 @@ class AdminDumpsiteController extends ApiController
}
}
$dumpsite->update($payload);
$oldStatus = $dumpsite->status;
$newStatus = $payload['status'] ?? $oldStatus;
DB::transaction(function () use ($dumpsite, $payload, $oldStatus, $newStatus) {
$dumpsite->update($payload);
if ($newStatus !== Dumpsite::STATUS_ACTIVE && $oldStatus === Dumpsite::STATUS_ACTIVE) {
$trips = \App\Models\Trip::where('dumpsite_id', $dumpsite->id)
->whereIn('status', [\App\Models\Trip::STATUS_SCHEDULED, \App\Models\Trip::STATUS_IN_PROGRESS, \App\Models\Trip::STATUS_AT_DUMPSITE])
->get();
foreach ($trips as $trip) {
$trip->update([
'status' => \App\Models\Trip::STATUS_CANCELLED,
'notes' => trim(($trip->notes ?? '') . "\nSystem: Trip cancelled because the dumpsite was moved to " . $newStatus . " status."),
]);
}
}
});
return $this->ok(
new DumpsiteResource($dumpsite->fresh()->load('cityMunicipality')),

View File

@@ -33,10 +33,11 @@
<th>Address</th>
<th>Capacity</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="rows">
<tr><td colspan="5" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>
<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>
</tbody>
</table>
</div>
@@ -46,7 +47,7 @@
<div class="slide-over-mask" data-close></div>
<div class="slide-over-panel translate-x-0 p-6 overflow-y-auto">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-base font-semibold text-neutral-900">New drop-off point</h3>
<h3 id="modal-title" class="text-base font-semibold text-neutral-900">New drop-off point</h3>
<button data-close class="text-neutral-400 hover:text-neutral-600">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
@@ -58,7 +59,7 @@
</div>
<div>
<label class="form-label">Code</label>
<input name="code" required class="form-input" placeholder="DOP-...">
<input name="code" id="form-code" required class="form-input" placeholder="DOP-...">
</div>
<div>
<label class="form-label">Address</label>
@@ -93,6 +94,14 @@
<label class="form-label">Capacity (kg)</label>
<input name="capacity_kg" type="number" min="0" class="form-input" placeholder="1000">
</div>
<div>
<label class="form-label">Contact Person</label>
<input name="contact_person" class="form-input" placeholder="Juan Dela Cruz">
</div>
<div>
<label class="form-label">Contact Phone</label>
<input name="contact_phone" class="form-input" placeholder="+639...">
</div>
<div>
<label class="form-label">Status</label>
<select name="status" class="form-select">
@@ -103,7 +112,10 @@
</div>
<div class="flex justify-end gap-2 pt-2">
<button type="button" data-close class="btn-ghost">Cancel</button>
<button type="submit" class="btn-primary">Create</button>
<button type="submit" id="submit-btn" class="btn-primary">Create</button>
</div>
<div class="pt-2 border-t border-neutral-100 mt-4" id="delete-container">
<button type="button" id="delete-btn" class="btn-danger w-full hidden">Delete Drop-off Point</button>
</div>
</form>
</div>
@@ -112,14 +124,54 @@
<script type="module">
const rows = document.getElementById('rows');
const modal = document.getElementById('form-modal');
const createForm = document.getElementById('create-form');
const deleteBtn = document.getElementById('delete-btn');
const submitBtn = document.getElementById('submit-btn');
const modalTitle = document.getElementById('modal-title');
const formCode = document.getElementById('form-code');
let editingDopId = null;
function badge(s) {
const m = { active: 'badge-active', maintenance: 'badge-pending', closed: 'badge-rejected' };
return `<span class="badge ${m[s] || 'badge-inactive'}">${s}</span>`;
}
async function editDop(id) {
editingDopId = id;
modalTitle.textContent = 'Edit drop-off point';
submitBtn.textContent = 'Save Changes';
deleteBtn.classList.remove('hidden');
formCode.readOnly = true;
formCode.classList.add('bg-neutral-100');
const res = await window.Verde.apiFetch(`/api/v1/admin/drop-off-points/${id}`);
if (!res.ok) {
window.Verde.toast('Failed to load drop-off point details', 'error');
return;
}
const d = res.body.data.drop_off_point || res.body.data;
createForm.querySelector('[name="name"]').value = d.name;
createForm.querySelector('[name="code"]').value = d.code;
createForm.querySelector('[name="address_line"]').value = d.address_line;
createForm.querySelector('[name="capacity_kg"]').value = d.capacity_kg ?? '';
createForm.querySelector('[name="contact_person"]').value = d.contact_person ?? '';
createForm.querySelector('[name="contact_phone"]').value = d.contact_phone ?? '';
createForm.querySelector('[name="status"]').value = d.status;
const lat = d.coordinates ? parseFloat(d.coordinates.lat) : 14.6539;
const lng = d.coordinates ? parseFloat(d.coordinates.lng) : 121.0685;
latInput.value = lat.toFixed(6);
lngInput.value = lng.toFixed(6);
modal.classList.remove('hidden');
await initMap(lat, lng);
}
async function load() {
rows.innerHTML = `<tr><td colspan="5" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>`;
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>`;
const params = new URLSearchParams();
const status = document.getElementById('filter-status').value;
const q = document.getElementById('filter-q').value.trim();
@@ -129,12 +181,12 @@
const res = await window.Verde.apiFetch(`/api/v1/admin/drop-off-points?${params}`);
if (!res.ok) {
rows.innerHTML = `<tr><td colspan="5" class="py-10 text-center text-sm text-red-500">Failed to load.</td></tr>`;
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-red-500">Failed to load.</td></tr>`;
return;
}
const items = res.body.data ?? [];
if (items.length === 0) {
rows.innerHTML = `<tr><td colspan="5" class="py-10 text-center text-sm text-neutral-400">No drop-off points yet.</td></tr>`;
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">No drop-off points yet.</td></tr>`;
return;
}
rows.innerHTML = items.map(d => `
@@ -144,8 +196,15 @@
<td class="max-w-xs truncate text-neutral-600">${window.Verde.escapeHtml(d.address_line)}</td>
<td>${d.capacity_kg ?? '—'} kg</td>
<td>${badge(d.status)}</td>
<td class="text-right">
<button data-id="${d.id}" data-action="edit" class="btn-ghost px-3 py-1 text-xs">Edit</button>
</td>
</tr>
`).join('');
rows.querySelectorAll('button[data-action="edit"]').forEach(btn => {
btn.addEventListener('click', () => editDop(btn.dataset.id));
});
}
function awaitLeaflet() {
@@ -191,8 +250,6 @@
lngInput.value = e.latlng.lng.toFixed(6);
});
// Setup geocoding search
const searchInput = document.getElementById('dop-search');
const searchBtn = document.getElementById('dop-search-btn');
@@ -280,6 +337,15 @@
}
document.getElementById('new-btn').addEventListener('click', () => {
editingDopId = null;
createForm.reset();
modalTitle.textContent = 'New drop-off point';
submitBtn.textContent = 'Create';
deleteBtn.classList.add('hidden');
formCode.readOnly = false;
formCode.classList.remove('bg-neutral-100');
latInput.value = '14.653900';
lngInput.value = '121.068500';
modal.classList.remove('hidden');
@@ -290,7 +356,7 @@
document.getElementById('filter-apply').addEventListener('click', load);
document.getElementById('filter-status').addEventListener('change', load);
document.getElementById('create-form').addEventListener('submit', async (e) => {
createForm.addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const payload = Object.fromEntries(fd.entries());
@@ -298,18 +364,41 @@
payload.lng = parseFloat(payload.lng);
if (payload.capacity_kg) payload.capacity_kg = parseInt(payload.capacity_kg, 10);
else delete payload.capacity_kg;
if (!payload.contact_person) payload.contact_person = null;
if (!payload.contact_phone) payload.contact_phone = null;
const res = await window.Verde.apiFetch('/api/v1/admin/drop-off-points', {
method: 'POST',
const url = editingDopId ? `/api/v1/admin/drop-off-points/${editingDopId}` : '/api/v1/admin/drop-off-points';
const method = editingDopId ? 'PATCH' : 'POST';
const res = await window.Verde.apiFetch(url, {
method: method,
body: JSON.stringify(payload),
});
if (res.ok) {
window.Verde.toast('Drop-off point created', 'success');
window.Verde.toast(editingDopId ? 'Drop-off point updated' : 'Drop-off point created', 'success');
modal.classList.add('hidden');
e.target.reset();
createForm.reset();
load();
} else {
window.Verde.toast(res.body?.message ?? 'Create failed', 'error');
window.Verde.toast(res.body?.message ?? 'Save failed', 'error');
}
});
deleteBtn.addEventListener('click', async () => {
if (!editingDopId) return;
if (!confirm('Are you sure you want to delete this drop-off point? This action cannot be undone.')) return;
const res = await window.Verde.apiFetch(`/api/v1/admin/drop-off-points/${editingDopId}`, {
method: 'DELETE'
});
if (res.ok) {
window.Verde.toast('Drop-off point deleted successfully', 'success');
modal.classList.add('hidden');
createForm.reset();
load();
} else {
window.Verde.toast(res.body?.message ?? 'Delete failed', 'error');
}
});

View File

@@ -23,10 +23,11 @@
<th>Permit #</th>
<th>Boundary</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody id="rows">
<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>
<tr><td colspan="7" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>
</tbody>
</table>
</div>
@@ -36,14 +37,14 @@
<div class="slide-over-mask" data-close></div>
<div class="slide-over-panel translate-x-0 p-6 overflow-y-auto">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-base font-semibold text-neutral-900">New dumpsite</h3>
<h3 id="modal-title" class="text-base font-semibold text-neutral-900">New dumpsite</h3>
<button data-close aria-label="Close" class="text-neutral-400 hover:text-neutral-600">
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<form id="create-form" class="space-y-4" autocomplete="off">
<div><label class="form-label">Name</label><input name="name" required class="form-input"></div>
<div><label class="form-label">Code</label><input name="code" required class="form-input" placeholder="DS-..."></div>
<div><label class="form-label">Code</label><input name="code" id="form-code" required class="form-input" placeholder="DS-..."></div>
<div><label class="form-label">Address</label><input name="address_line" required class="form-input"></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="form-label">Latitude</label><input name="lat" id="ds-lat" type="number" step="0.000001" required class="form-input bg-neutral-100 cursor-not-allowed" readonly></div>
@@ -91,6 +92,9 @@
</svg>
</button>
</div>
<div class="pt-2 border-t border-neutral-100 mt-4" id="delete-container">
<button type="button" id="delete-btn" class="btn-danger w-full hidden">Delete Dumpsite</button>
</div>
</form>
</div>
</div>
@@ -99,18 +103,73 @@
const rows = document.getElementById('rows');
const modal = document.getElementById('form-modal');
const pointsEl = document.getElementById('boundary-points');
const createForm = document.getElementById('create-form');
const deleteBtn = document.getElementById('delete-btn');
const submitBtn = document.getElementById('submit-btn');
const modalTitle = document.getElementById('modal-title');
const formCode = document.getElementById('form-code');
let editingDumpsiteId = null;
function badge(s) {
const m = { active: 'badge-active', maintenance: 'badge-pending', closed: 'badge-rejected' };
return `<span class="badge ${m[s] || 'badge-inactive'}">${s}</span>`;
}
async function editDumpsite(id) {
editingDumpsiteId = id;
modalTitle.textContent = 'Edit dumpsite';
submitBtn.querySelector('.btn-label').textContent = 'Save Changes';
deleteBtn.classList.remove('hidden');
formCode.readOnly = true;
formCode.classList.add('bg-neutral-100');
const res = await window.Verde.apiFetch(`/api/v1/admin/dumpsites/${id}`);
if (!res.ok) {
window.Verde.toast('Failed to load dumpsite details', 'error');
return;
}
const d = res.body.data;
createForm.querySelector('[name="name"]').value = d.name;
createForm.querySelector('[name="code"]').value = d.code;
createForm.querySelector('[name="address_line"]').value = d.address_line;
createForm.querySelector('[name="capacity_tons"]').value = d.capacity_tons ?? '';
createForm.querySelector('[name="permit_number"]').value = d.permit_number ?? '';
createForm.querySelector('[name="status"]').value = d.status;
// Populate boundary geofence points
pointsEl.innerHTML = '';
if (d.boundary_polygon && d.boundary_polygon.length > 0) {
const pts = d.boundary_polygon.slice();
if (pts.length > 1) {
const first = pts[0];
const last = pts[pts.length - 1];
if (first.lat === last.lat && first.lng === last.lng) {
pts.pop();
}
}
pts.forEach(p => addPointRow(p.lat, p.lng));
} else {
addPointRow(); addPointRow(); addPointRow();
}
const lat = d.coordinates ? parseFloat(d.coordinates.lat) : 14.6539;
const lng = d.coordinates ? parseFloat(d.coordinates.lng) : 121.0685;
latInput.value = lat.toFixed(6);
lngInput.value = lng.toFixed(6);
modal.classList.remove('hidden');
await initMap(lat, lng);
}
async function load() {
rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>`;
rows.innerHTML = `<tr><td colspan="7" class="py-10 text-center text-sm text-neutral-400">Loading…</td></tr>`;
const res = await window.Verde.apiFetch('/api/v1/admin/dumpsites?per_page=50');
if (!res.ok) { rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-red-500">Failed to load.</td></tr>`; return; }
if (!res.ok) { rows.innerHTML = `<tr><td colspan="7" class="py-10 text-center text-sm text-red-500">Failed to load.</td></tr>`; return; }
const items = res.body.data ?? [];
if (items.length === 0) { rows.innerHTML = `<tr><td colspan="6" class="py-10 text-center text-sm text-neutral-400">No dumpsites configured.</td></tr>`; return; }
if (items.length === 0) { rows.innerHTML = `<tr><td colspan="7" class="py-10 text-center text-sm text-neutral-400">No dumpsites configured.</td></tr>`; return; }
rows.innerHTML = items.map(d => `
<tr>
<td class="font-medium text-neutral-900">${window.Verde.escapeHtml(d.name)}</td>
@@ -121,8 +180,15 @@
? `<span class="badge badge-active">${d.boundary_polygon.length - 1} pts</span>`
: '<span class="badge badge-inactive">None</span>'}</td>
<td>${badge(d.status)}</td>
<td class="text-right">
<button data-id="${d.id}" data-action="edit" class="btn-ghost px-3 py-1 text-xs">Edit</button>
</td>
</tr>
`).join('');
rows.querySelectorAll('button[data-action="edit"]').forEach(btn => {
btn.addEventListener('click', () => editDumpsite(btn.dataset.id));
});
}
function addPointRow(lat = '', lng = '') {
@@ -190,8 +256,6 @@
lngInput.value = e.latlng.lng.toFixed(6);
});
// Setup geocoding search
const searchInput = document.getElementById('ds-search');
const searchBtn = document.getElementById('ds-search-btn');
@@ -279,6 +343,15 @@
}
document.getElementById('new-btn').addEventListener('click', () => {
editingDumpsiteId = null;
createForm.reset();
modalTitle.textContent = 'New dumpsite';
submitBtn.querySelector('.btn-label').textContent = 'Create';
deleteBtn.classList.add('hidden');
formCode.readOnly = false;
formCode.classList.remove('bg-neutral-100');
pointsEl.innerHTML = '';
addPointRow(); addPointRow(); addPointRow();
latInput.value = '14.653900';
@@ -286,18 +359,18 @@
modal.classList.remove('hidden');
initMap(14.6539, 121.0685);
});
modal.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', () => modal.classList.add('hidden')));
document.getElementById('add-point').addEventListener('click', () => addPointRow());
document.getElementById('create-form').addEventListener('submit', async (e) => {
createForm.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = document.getElementById('submit-btn');
const btnLabel = submitBtn.querySelector('.btn-label');
const btnSpinner = submitBtn.querySelector('.btn-spinner');
submitBtn.disabled = true;
btnLabel.textContent = 'Creating…';
btnLabel.textContent = editingDumpsiteId ? 'Saving…' : 'Creating…';
btnSpinner.classList.remove('hidden');
const fd = new FormData(e.target);
@@ -310,17 +383,44 @@
const boundary = getBoundary();
if (boundary.length >= 3) payload.boundary_polygon = boundary;
const res = await window.Verde.apiFetch('/api/v1/admin/dumpsites', {
method: 'POST',
const url = editingDumpsiteId ? `/api/v1/admin/dumpsites/${editingDumpsiteId}` : '/api/v1/admin/dumpsites';
const method = editingDumpsiteId ? 'PATCH' : 'POST';
const res = await window.Verde.apiFetch(url, {
method: method,
body: JSON.stringify(payload),
});
submitBtn.disabled = false;
btnLabel.textContent = 'Create';
btnLabel.textContent = editingDumpsiteId ? 'Save Changes' : 'Create';
btnSpinner.classList.add('hidden');
if (res.ok) { window.Verde.toast('Dumpsite created', 'success'); modal.classList.add('hidden'); e.target.reset(); load(); }
else window.Verde.toast(res.body?.message ?? 'Create failed', 'error');
if (res.ok) {
window.Verde.toast(editingDumpsiteId ? 'Dumpsite updated' : 'Dumpsite created', 'success');
modal.classList.add('hidden');
createForm.reset();
load();
} else {
window.Verde.toast(res.body?.message ?? 'Save failed', 'error');
}
});
deleteBtn.addEventListener('click', async () => {
if (!editingDumpsiteId) return;
if (!confirm('Are you sure you want to delete this dumpsite? This action cannot be undone.')) return;
const res = await window.Verde.apiFetch(`/api/v1/admin/dumpsites/${editingDumpsiteId}`, {
method: 'DELETE'
});
if (res.ok) {
window.Verde.toast('Dumpsite deleted successfully', 'success');
modal.classList.add('hidden');
createForm.reset();
load();
} else {
window.Verde.toast(res.body?.message ?? 'Delete failed', 'error');
}
});
load();

View File

@@ -152,4 +152,32 @@ class DropOffPointTest extends TestCase
$this->deleteJson("/api/v1/admin/drop-off-points/{$dop->uuid}")->assertOk();
$this->assertSoftDeleted('drop_off_points', ['id' => $dop->id]);
}
public function test_dop_status_transition_reassigns_households(): void
{
$admin = User::factory()->create(['role' => User::ROLE_ADMIN, 'status' => User::STATUS_ACTIVE]);
Sanctum::actingAs($admin);
$dop1 = DropOffPoint::where('status', DropOffPoint::STATUS_ACTIVE)->first();
$household = \App\Models\Household::create([
'tenant_id' => $dop1->tenant_id,
'head_user_id' => User::factory()->create()->id,
'barangay_id' => $dop1->barangay_id,
'address_line' => '123 Test St',
'coordinates' => new \MatanYadaev\EloquentSpatial\Objects\Point(14.6539, 121.0685, 4326),
'household_size' => 3,
'assigned_drop_off_point_id' => $dop1->id,
'verification_status' => 'approved',
]);
$response = $this->patchJson("/api/v1/admin/drop-off-points/{$dop1->uuid}", [
'status' => DropOffPoint::STATUS_MAINTENANCE,
]);
$response->assertOk();
$household->refresh();
$this->assertNotEquals($dop1->id, $household->assigned_drop_off_point_id);
}
}

View File

@@ -162,4 +162,71 @@ class DumpsiteCrudTest extends TestCase
$response->assertStatus(422)
->assertJsonValidationErrors(['code']);
}
public function test_dumpsite_status_transition_cancels_trips(): void
{
Sanctum::actingAs($this->admin);
$tenantId = $this->admin->tenant_id ?? \App\Models\Tenant::value('id');
$dumpsite = Dumpsite::factory()->create([
'status' => Dumpsite::STATUS_ACTIVE,
]);
$route = \App\Models\Route::create([
'tenant_id' => $tenantId,
'name' => 'Route A',
'code' => 'R-A',
'status' => 'active',
]);
$team = \App\Models\CollectionTeam::create([
'tenant_id' => $tenantId,
'name' => 'Team A',
'status' => 'active',
]);
$truck = \App\Models\Truck::create([
'tenant_id' => $tenantId,
'plate_number' => 'XYZ-123',
'brand' => 'Isz',
'model' => 'FTR',
'capacity_tons' => 10,
'default_dumpsite_id' => $dumpsite->id,
'status' => 'active',
]);
$trip1 = \App\Models\Trip::create([
'trip_number' => 'TRIP-DS-1',
'route_id' => $route->id,
'team_id' => $team->id,
'truck_id' => $truck->id,
'dumpsite_id' => $dumpsite->id,
'scheduled_date' => now()->toDateString(),
'status' => \App\Models\Trip::STATUS_IN_PROGRESS,
]);
$trip2 = \App\Models\Trip::create([
'trip_number' => 'TRIP-DS-2',
'route_id' => $route->id,
'team_id' => $team->id,
'truck_id' => $truck->id,
'dumpsite_id' => $dumpsite->id,
'scheduled_date' => now()->toDateString(),
'status' => \App\Models\Trip::STATUS_COMPLETED,
]);
$response = $this->patchJson("/api/v1/admin/dumpsites/{$dumpsite->uuid}", [
'status' => Dumpsite::STATUS_MAINTENANCE,
]);
$response->assertOk();
$trip1->refresh();
$trip2->refresh();
$this->assertEquals(\App\Models\Trip::STATUS_CANCELLED, $trip1->status);
$this->assertStringContainsString('System: Trip cancelled because the dumpsite was moved to maintenance status', $trip1->notes);
$this->assertEquals(\App\Models\Trip::STATUS_COMPLETED, $trip2->status);
}
}