diff --git a/CLAUDE.md b/CLAUDE.md index 87107c6..4fa1e83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -369,3 +369,30 @@ The create form lives in `resources/views/components/purchase/request-modal.blad - **Never** link to `route('purchase.requests.create')` for creating new requests — the component replaces that flow entirely. - The component is self-contained: it owns the trigger button, the Alpine.js open/close state, the full MPR form (POSTing to `purchase.requests.store`), dynamic item rows, and validation-error auto-reopen logic. - The `/purchase/requests/create` page and route remain as a fallback but should not be referenced in new UI. + +### 11. Data entry pages — AJAX only, no page refreshes +All settings and management pages where users create, edit, or delete records MUST use `fetch()` AJAX. No `
` submissions, no page reloads, no redirects after data entry. + +**Controller:** return `response()->json(...)` for all create/update/delete endpoints. Laravel auto-returns 422 JSON on validation failure when `Accept: application/json` is set. + +**Frontend fetch helper pattern:** +```javascript +var CSRF = document.querySelector('meta[name="csrf-token"]').content; +function api(url, method, data) { + var opts = { method: method, headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json' } }; + if (data) opts.body = JSON.stringify(data); + return fetch(url, opts).then(function(r) { + return r.json().then(function(body) { + if (!r.ok) return Promise.reject(body); + return body; + }); + }); +} +``` + +**After each operation:** +- On success: update the DOM in-place (append row, update text, remove element), then call `showToast('Done.', 'success')` +- On error: call `showToast(err.message || 'Error', 'error')` +- For deletes: use `confirmAction(title, body, onConfirm)` (not `confirm()`) before calling the API + +**Never** use `` for inline data entry on settings/management pages. `` submissions that navigate away from the page are banned for these flows. diff --git a/app/Http/Controllers/Purchase/PurchaseRequestController.php b/app/Http/Controllers/Purchase/PurchaseRequestController.php index f165c43..8018484 100644 --- a/app/Http/Controllers/Purchase/PurchaseRequestController.php +++ b/app/Http/Controllers/Purchase/PurchaseRequestController.php @@ -12,9 +12,7 @@ class PurchaseRequestController extends Controller { public function index() { - $requests = PurchaseRequest::with(['requestedBy', 'items'])->latest()->paginate(15); - - return view('purchase.requests.index', compact('requests')); + return redirect()->route('purchase.pipeline.index'); } public function create() diff --git a/app/Http/Controllers/Settings/ProjectSettingController.php b/app/Http/Controllers/Settings/ProjectSettingController.php index 131aa0b..57cd94c 100644 --- a/app/Http/Controllers/Settings/ProjectSettingController.php +++ b/app/Http/Controllers/Settings/ProjectSettingController.php @@ -11,53 +11,106 @@ class ProjectSettingController extends Controller { public function index() { - $projects = ProjectSetting::with('locations')->orderBy('name')->get(); - return view('settings.projects.index', compact('projects')); + $projects = ProjectSetting::with(['locations' => function ($q) { + $q->orderBy('name'); + }])->orderBy('name')->get(); + + $stats = [ + 'total_projects' => $projects->count(), + 'active_projects' => $projects->where('is_active', true)->count(), + 'total_locations' => $projects->sum(fn ($p) => $p->locations->count()), + 'active_locations' => $projects->sum(fn ($p) => $p->locations->where('is_active', true)->count()), + ]; + + return view('settings.projects.index', compact('projects', 'stats')); } public function store(Request $request) { - $request->validate(['name' => 'required|string|max:255|unique:settings_projects,name']); - ProjectSetting::create(['name' => $request->name, 'is_active' => true]); - return redirect()->route('settings.projects.index')->with('success', 'Project added.'); + $validated = $request->validate(['name' => 'required|string|max:255|unique:settings_projects,name']); + $project = ProjectSetting::create(['name' => $validated['name'], 'is_active' => true]); + return response()->json(['project' => [ + 'id' => $project->id, + 'name' => $project->name, + 'is_active' => $project->is_active, + ]]); } public function update(Request $request, ProjectSetting $project) { - $request->validate(['name' => 'required|string|max:255|unique:settings_projects,name,' . $project->id]); + $validated = $request->validate([ + 'name' => 'required|string|max:255|unique:settings_projects,name,' . $project->id, + ]); $project->update([ - 'name' => $request->name, + 'name' => $validated['name'], 'is_active' => $request->boolean('is_active', true), ]); - return redirect()->route('settings.projects.index')->with('success', 'Project updated.'); + return response()->json(['project' => [ + 'id' => $project->id, + 'name' => $project->name, + 'is_active' => $project->is_active, + ]]); } public function destroy(ProjectSetting $project) { $project->delete(); - return redirect()->route('settings.projects.index')->with('success', 'Project deleted.'); + return response()->json(['ok' => true]); } public function storeLocation(Request $request, ProjectSetting $project) { - $request->validate(['name' => 'required|string|max:255']); - $project->locations()->create(['name' => $request->name, 'is_active' => true]); - return redirect()->route('settings.projects.index')->with('success', 'Location added.'); + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'address' => 'nullable|string|max:500', + 'latitude' => 'nullable|numeric|between:-90,90', + 'longitude' => 'nullable|numeric|between:-180,180', + ]); + $location = $project->locations()->create([ + 'name' => $validated['name'], + 'address' => $validated['address'] ?? null, + 'latitude' => $validated['latitude'] ?? null, + 'longitude' => $validated['longitude'] ?? null, + 'is_active' => true, + ]); + return response()->json(['location' => [ + 'id' => $location->id, + 'name' => $location->name, + 'address' => $location->address, + 'latitude' => $location->latitude, + 'longitude' => $location->longitude, + 'is_active' => $location->is_active, + ]]); } public function updateLocation(Request $request, ProjectSetting $project, Location $location) { - $request->validate(['name' => 'required|string|max:255']); + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'address' => 'nullable|string|max:500', + 'latitude' => 'nullable|numeric|between:-90,90', + 'longitude' => 'nullable|numeric|between:-180,180', + ]); $location->update([ - 'name' => $request->name, + 'name' => $validated['name'], + 'address' => $validated['address'] ?? null, + 'latitude' => $validated['latitude'] ?? null, + 'longitude' => $validated['longitude'] ?? null, 'is_active' => $request->boolean('is_active', true), ]); - return redirect()->route('settings.projects.index')->with('success', 'Location updated.'); + return response()->json(['location' => [ + 'id' => $location->id, + 'name' => $location->name, + 'address' => $location->address, + 'latitude' => $location->latitude, + 'longitude' => $location->longitude, + 'is_active' => $location->is_active, + ]]); } public function destroyLocation(ProjectSetting $project, Location $location) { $location->delete(); - return redirect()->route('settings.projects.index')->with('success', 'Location deleted.'); + return response()->json(['ok' => true]); } } diff --git a/app/Models/Settings/Location.php b/app/Models/Settings/Location.php index 25778bf..731e635 100644 --- a/app/Models/Settings/Location.php +++ b/app/Models/Settings/Location.php @@ -8,9 +8,9 @@ class Location extends Model { protected $table = 'settings_locations'; - protected $fillable = ['name', 'project_id', 'is_active']; + protected $fillable = ['name', 'project_id', 'is_active', 'address', 'latitude', 'longitude']; - protected $casts = ['is_active' => 'boolean']; + protected $casts = ['is_active' => 'boolean', 'latitude' => 'float', 'longitude' => 'float']; public function scopeActive($query) { diff --git a/database/migrations/2026_05_25_120000_add_address_coordinates_to_settings_locations_table.php b/database/migrations/2026_05_25_120000_add_address_coordinates_to_settings_locations_table.php new file mode 100644 index 0000000..58a5c24 --- /dev/null +++ b/database/migrations/2026_05_25_120000_add_address_coordinates_to_settings_locations_table.php @@ -0,0 +1,24 @@ +string('address', 500)->nullable()->after('name'); + $table->decimal('latitude', 10, 7)->nullable()->after('address'); + $table->decimal('longitude', 11, 7)->nullable()->after('latitude'); + }); + } + + public function down(): void + { + Schema::table('settings_locations', function (Blueprint $table) { + $table->dropColumn(['address', 'latitude', 'longitude']); + }); + } +}; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 78b684f..a5966b1 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -44,4 +44,17 @@
+ + {{-- Dev credentials hint --}} + @if(app()->isLocal()) +
+
Dev Credentials
+
Email: admin@erp.com
+
Password: password
+ +
+ @endif diff --git a/resources/views/components/purchase/edit-request-modal.blade.php b/resources/views/components/purchase/edit-request-modal.blade.php new file mode 100644 index 0000000..e5719d9 --- /dev/null +++ b/resources/views/components/purchase/edit-request-modal.blade.php @@ -0,0 +1,346 @@ +@props(['purchaseRequest']) + +@php +$prId = $purchaseRequest->id; +$prefix = 'mprEdit' . $prId; + +// Re-open on validation error only when THIS request was being edited +$repop = $errors->any() && (string) old('_edit_request_id') === (string) $prId; + +// Load items if not already eager-loaded +$prItems = $purchaseRequest->relationLoaded('items') ? $purchaseRequest->items : $purchaseRequest->load('items')->items; +$existingItems = $repop && old('items') + ? collect(old('items'))->map(fn($i) => (object)$i) + : $prItems; + +// Current field values (old() values take priority when repopulating) +$curDate = $repop ? old('date', $purchaseRequest->date ? \Carbon\Carbon::parse($purchaseRequest->date)->format('Y-m-d') : '') : ($purchaseRequest->date ? \Carbon\Carbon::parse($purchaseRequest->date)->format('Y-m-d') : ''); +$curProject = $repop ? old('project_name', $purchaseRequest->project_name ?? '') : ($purchaseRequest->project_name ?? ''); +$curReqBy = $repop ? old('requested_by_name', $purchaseRequest->requested_by_name ?? '') : ($purchaseRequest->requested_by_name ?? ''); +$curReqDate = $repop ? old('required_date_text', $purchaseRequest->required_date_text ?? '') : ($purchaseRequest->required_date_text ?? ''); +$curLoc = $repop ? old('location', $purchaseRequest->location ?? '') : ($purchaseRequest->location ?? ''); +$curDept = $repop ? old('department', $purchaseRequest->department ?? '') : ($purchaseRequest->department ?? ''); +$curRemarks = $repop ? old('remarks', $purchaseRequest->remarks ?? '') : ($purchaseRequest->remarks ?? ''); + +// Projects & locations for cascading dropdowns +$editProjects = \App\Models\Settings\ProjectSetting::active() + ->with(['locations' => function ($q) { $q->where('is_active', true)->orderBy('name'); }]) + ->orderBy('name') + ->get(); + +$editProjectsData = $editProjects->map(function ($p) { + return [ + 'id' => $p->id, + 'name' => $p->name, + 'locations' => $p->locations->map(fn($l) => ['name' => $l->name])->values()->toArray(), + ]; +})->values()->toArray(); +$editProjectsJson = json_encode($editProjectsData); + +// Check if current project is in the list +$curProjectInList = $editProjects->contains('name', $curProject); +@endphp + +{{-- ── Trigger button ── --}} + + +{{-- ── Modal overlay ── --}} + + + diff --git a/resources/views/components/purchase/request-modal.blade.php b/resources/views/components/purchase/request-modal.blade.php index fb04d40..11bac9a 100644 --- a/resources/views/components/purchase/request-modal.blade.php +++ b/resources/views/components/purchase/request-modal.blade.php @@ -1,6 +1,18 @@ @php -$hasErrors = $errors->any(); -$mprProjects = \App\Models\Settings\ProjectSetting::active()->with(['locations' => function($q){ $q->where('is_active', true)->orderBy('name'); }])->orderBy('name')->get(); +$hasErrors = $errors->any(); +$mprProjects = \App\Models\Settings\ProjectSetting::active() + ->with(['locations' => function ($q) { $q->where('is_active', true)->orderBy('name'); }]) + ->orderBy('name') + ->get(); +$mprProjectsJson = $mprProjects->map(function ($p) { + return [ + 'id' => $p->id, + 'name' => $p->name, + 'locations' => $p->locations->map(function ($l) { + return ['name' => $l->name]; + })->values(), + ]; +})->values(); @endphp {{-- Trigger button --}} @@ -242,7 +254,7 @@ document.addEventListener('DOMContentLoaded', function () { mprModalOpen(); }); })(); // Cascading project → location dropdown -var mprProjectsData = @json($mprProjects->map(fn($p) => ['id' => $p->id, 'name' => $p->name, 'locations' => $p->locations->map(fn($l) => ['name' => $l->name])->values()])->values()); +var mprProjectsData = @json($mprProjectsJson); var mprOldLocation = "{{ old('location') }}"; var mprOldProjectName = "{{ old('project_name') }}"; diff --git a/resources/views/components/purchase/supplier-invite-list.blade.php b/resources/views/components/purchase/supplier-invite-list.blade.php new file mode 100644 index 0000000..7b58078 --- /dev/null +++ b/resources/views/components/purchase/supplier-invite-list.blade.php @@ -0,0 +1,93 @@ +@props([ + 'suppliers', + 'alreadyIds' => [], + 'formAction', +]) + +
+ {{ count($alreadyIds) ? 'Add More Suppliers' : 'Select Suppliers' }} +
+ + + +
+ @csrf + +
+ @forelse($suppliers as $supplier) + @php $already = in_array($supplier->id, $alreadyIds); @endphp +
+ + +
+ @empty +

No active suppliers found. Add suppliers first.

+ @endforelse +
+ + +
+ + diff --git a/resources/views/components/purchase/supplier-select-modal.blade.php b/resources/views/components/purchase/supplier-select-modal.blade.php new file mode 100644 index 0000000..c223bf7 --- /dev/null +++ b/resources/views/components/purchase/supplier-select-modal.blade.php @@ -0,0 +1,588 @@ +@props([ + 'pr', + 'suppliers', + 'selectedIds' => [], +]) + +{{-- ============================================================ + SELECT SUPPLIERS MODAL + ============================================================ --}} + + + diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 362755b..249f053 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -82,7 +82,6 @@ @foreach([ ['purchase.suppliers.index', 'Suppliers'], - ['purchase.requests.index', 'Purchase Requests'], ['purchase.orders.index', 'Purchase Orders'], ['purchase.grns.index', 'Goods Receipt (GRN)'], ['purchase.invoices.index', 'Supplier Invoices'], @@ -396,6 +395,13 @@ function dismissToast(el) {
This action cannot be undone.
+
+ + + +
@endforeach diff --git a/resources/views/purchase/pipeline/index.blade.php b/resources/views/purchase/pipeline/index.blade.php index a0ec50e..1bfca4b 100644 --- a/resources/views/purchase/pipeline/index.blade.php +++ b/resources/views/purchase/pipeline/index.blade.php @@ -14,10 +14,7 @@

Purchase Pipeline

Track every purchase from request to payment

- - + New Request - + {{-- Tabs --}} diff --git a/resources/views/purchase/pipeline/show.blade.php b/resources/views/purchase/pipeline/show.blade.php index a3dfc60..3da3233 100644 --- a/resources/views/purchase/pipeline/show.blade.php +++ b/resources/views/purchase/pipeline/show.blade.php @@ -52,10 +52,7 @@
- - ✏️ Edit Request - + View Full Request → @@ -402,225 +399,8 @@
-{{-- ============================================================ - SELECT SUPPLIERS MODAL - ============================================================ --}} - @endsection diff --git a/resources/views/purchase/requests/index.blade.php b/resources/views/purchase/requests/index.blade.php index 986bc04..1b17b6e 100644 --- a/resources/views/purchase/requests/index.blade.php +++ b/resources/views/purchase/requests/index.blade.php @@ -8,9 +8,7 @@

Purchase Requests

Manage internal purchase requests

-
- + New Request - +
@@ -20,8 +18,8 @@ Request # Date Department - Item - Quantity + First Item + Qty / Items Status Requested By Actions @@ -30,11 +28,13 @@ @forelse($requests as $request) - {{ $request->request_number ?? '#' . $request->id }} + + {{ $request->request_number ?? '#' . $request->id }} + {{ $request->date ? $request->date->format('d M Y') : '' }} {{ $request->department }} - {{ $request->item->item_name ?? $request->item_name }} - {{ $request->quantity }} {{ $request->unit_of_measure }} + {{ $request->items->first()->description ?? '—' }} + {{ $request->items->count() > 1 ? $request->items->count() . ' items' : ($request->items->first() ? number_format($request->items->first()->quantity_required, 2) . ' ' . $request->items->first()->unit : '—') }} @php $badgeClass = match($request->status) { @@ -62,7 +62,7 @@ @endif - Edit +
@csrf diff --git a/resources/views/purchase/requests/show.blade.php b/resources/views/purchase/requests/show.blade.php index 58d35ad..15d91e7 100644 --- a/resources/views/purchase/requests/show.blade.php +++ b/resources/views/purchase/requests/show.blade.php @@ -12,7 +12,7 @@ Print MPR Form @if($purchaseRequest->status === 'pending') - Edit + @csrf @method('PATCH') diff --git a/resources/views/purchase/rfq/show.blade.php b/resources/views/purchase/rfq/show.blade.php index 756400d..eedccac 100644 --- a/resources/views/purchase/rfq/show.blade.php +++ b/resources/views/purchase/rfq/show.blade.php @@ -59,98 +59,13 @@
@endif - {{-- Invite more suppliers --}} -
- {{ $invitations->count() ? 'Add More Suppliers' : 'Select Suppliers' }} -
- - - - - @csrf - - -
- @php $alreadyIds = $invitations->pluck('supplier_id')->toArray(); @endphp - @forelse($suppliers as $supplier) - @php $already = in_array($supplier->id, $alreadyIds); @endphp -
- - {{-- Channel selector (hidden until checked) --}} - -
- @empty -

No active suppliers found. Add suppliers first.

- @endforelse -
- - - + @php $alreadyIds = $invitations->pluck('supplier_id')->toArray(); @endphp + - - @endsection diff --git a/resources/views/settings/projects/index.blade.php b/resources/views/settings/projects/index.blade.php index c1a8418..5329e7f 100644 --- a/resources/views/settings/projects/index.blade.php +++ b/resources/views/settings/projects/index.blade.php @@ -3,318 +3,707 @@ @section('title', 'Settings — Projects') @section('content') + -
-

Settings — Projects

-

Manage projects and their sub-locations.

+{{-- Page header --}} +
+
+

Projects & Locations

+

Manage projects and their physical sub-locations with addresses and GPS coordinates.

+
-{{-- Two-panel layout --}} -
- - {{-- ── LEFT PANEL: Projects ── --}} -
-
-

- Projects - {{ $projects->count() }} -

-
- - {{-- Add Project form --}} -
-
- @csrf -
- - @error('name') -

{{ $message }}

- @enderror -
- -
-
- - {{-- Projects list --}} -
- @forelse($projects as $project) -
- - {{-- Display row --}} -
-
- - - - - {{ $project->name }} - - @if(!$project->is_active) - Inactive - @endif - {{ $project->locations->count() }} loc. -
-
- -
- @csrf @method('DELETE') - -
-
-
- - {{-- Edit inline form --}} -
-
- @csrf @method('PATCH') - - - - -
-
- +{{-- Stat boxes --}} +
+
+
+
+
-
- @empty -
- - - - No projects yet. Add one above. +
+
{{ $stats['total_projects'] }}
+
Total Projects
- @endforelse
- - {{-- ── RIGHT PANEL: Locations ── --}} -
- {{-- Placeholder when nothing selected --}} -
- - - - -

Select a project to see its locations

-
- - {{-- Content shown after a project is selected --}} -