feat: add Projects settings with sub-locations and cascading dropdowns in purchase request modal
- Migration: add project_id FK to settings_locations - Models: ProjectSetting hasMany Location, Location belongsTo ProjectSetting - Settings: /settings/projects page — manage projects and their sub-locations (two-panel UI) - Sidebar: Projects nav item under Settings group - Routes: 7 new settings/projects routes (Admin only) - Modal: project_name and location fields now cascading dropdowns populated from settings_projects/settings_locations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
75e4890a08
commit
7f8ae898d5
@ -360,3 +360,12 @@ To trigger a toast from JavaScript (e.g. after an in-page action), call:
|
||||
showToast('Message text', 'success'); // types: success | error | info | warn
|
||||
```
|
||||
**Never** add inline `@if(session('success'))` banner divs to individual views — the layout handles all of them. The toast appears bottom-right, auto-dismisses after 4 s, has a shrinking progress bar, and can be clicked or ×-closed early.
|
||||
|
||||
### 10. Purchase Request creation — always use `<x-purchase.request-modal />`
|
||||
The create form lives in `resources/views/components/purchase/request-modal.blade.php` as a reusable Blade component. Wherever a "New Purchase Request" trigger is needed, drop in the component tag — it renders the button and the full modal itself:
|
||||
```blade
|
||||
<x-purchase.request-modal />
|
||||
```
|
||||
- **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.
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Settings\Location;
|
||||
use App\Models\Settings\ProjectSetting;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -10,7 +11,7 @@ class ProjectSettingController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$projects = ProjectSetting::orderBy('name')->get();
|
||||
$projects = ProjectSetting::with('locations')->orderBy('name')->get();
|
||||
return view('settings.projects.index', compact('projects'));
|
||||
}
|
||||
|
||||
@ -36,4 +37,27 @@ class ProjectSettingController extends Controller
|
||||
$project->delete();
|
||||
return redirect()->route('settings.projects.index')->with('success', 'Project deleted.');
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
public function updateLocation(Request $request, ProjectSetting $project, Location $location)
|
||||
{
|
||||
$request->validate(['name' => 'required|string|max:255']);
|
||||
$location->update([
|
||||
'name' => $request->name,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
return redirect()->route('settings.projects.index')->with('success', 'Location updated.');
|
||||
}
|
||||
|
||||
public function destroyLocation(ProjectSetting $project, Location $location)
|
||||
{
|
||||
$location->delete();
|
||||
return redirect()->route('settings.projects.index')->with('success', 'Location deleted.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ class Location extends Model
|
||||
{
|
||||
protected $table = 'settings_locations';
|
||||
|
||||
protected $fillable = ['name', 'is_active'];
|
||||
protected $fillable = ['name', 'project_id', 'is_active'];
|
||||
|
||||
protected $casts = ['is_active' => 'boolean'];
|
||||
|
||||
@ -16,4 +16,9 @@ class Location extends Model
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function project(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Settings\ProjectSetting::class, 'project_id');
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,4 +16,9 @@ class ProjectSetting extends Model
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function locations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Models\Settings\Location::class, 'project_id');
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('settings_locations', function (Blueprint $table) {
|
||||
$table->foreignId('project_id')
|
||||
->nullable()
|
||||
->after('name')
|
||||
->constrained('settings_projects')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->index('project_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('settings_locations', function (Blueprint $table) {
|
||||
$table->dropIndex(['project_id']);
|
||||
$table->dropForeign(['project_id']);
|
||||
$table->dropColumn('project_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
276
resources/views/components/purchase/request-modal.blade.php
Normal file
276
resources/views/components/purchase/request-modal.blade.php
Normal file
@ -0,0 +1,276 @@
|
||||
@php
|
||||
$hasErrors = $errors->any();
|
||||
$mprProjects = \App\Models\Settings\ProjectSetting::active()->with(['locations' => function($q){ $q->where('is_active', true)->orderBy('name'); }])->orderBy('name')->get();
|
||||
@endphp
|
||||
|
||||
{{-- Trigger button --}}
|
||||
<button type="button" onclick="mprModalOpen()" class="btn-primary">
|
||||
+ New Request
|
||||
</button>
|
||||
|
||||
{{-- ── Modal overlay ── --}}
|
||||
<div id="mpr-modal-overlay"
|
||||
onclick="if(event.target===this) mprModalClose()"
|
||||
style="display:none;position:fixed;inset:0;z-index:9999;align-items:center;justify-content:center;padding:1rem;background:rgba(15,23,42,0.55);backdrop-filter:blur(3px);">
|
||||
|
||||
<div style="width:100%;max-width:58rem;max-height:88vh;display:flex;flex-direction:column;background:white;border-radius:1.25rem;box-shadow:0 25px 60px -10px rgba(0,0,0,0.3),0 10px 20px -5px rgba(0,0,0,0.15);">
|
||||
|
||||
{{-- ── Header ── --}}
|
||||
<div style="flex-shrink:0;padding:1.25rem 1.5rem;border-radius:1.25rem 1.25rem 0 0;background:linear-gradient(135deg,#2563eb 0%,#4f46e5 100%);display:flex;align-items:center;justify-content:space-between;">
|
||||
<div style="display:flex;align-items:center;gap:0.875rem;">
|
||||
<div style="background:rgba(255,255,255,0.15);border-radius:0.625rem;padding:0.5rem;">
|
||||
<svg style="width:1.25rem;height:1.25rem;stroke:white;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="color:white;font-size:1rem;font-weight:700;line-height:1.2;">New Purchase Request</h2>
|
||||
<p style="color:#bfdbfe;font-size:0.7rem;margin-top:0.1rem;">Material Purchase Request (MPR)</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="mprModalClose()" type="button"
|
||||
style="color:white;background:rgba(255,255,255,0.15);border:none;border-radius:50%;width:2rem;height:2rem;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:1.25rem;line-height:1;transition:background 0.15s;"
|
||||
onmouseover="this.style.background='rgba(255,255,255,0.25)'"
|
||||
onmouseout="this.style.background='rgba(255,255,255,0.15)'">×</button>
|
||||
</div>
|
||||
|
||||
{{-- ── Scrollable body ── --}}
|
||||
<div style="flex:1;overflow-y:auto;padding:1.5rem;">
|
||||
|
||||
@if($errors->any())
|
||||
<div style="margin-bottom:1.25rem;padding:0.875rem 1rem;background:#fef2f2;border:1px solid #fecaca;border-radius:0.75rem;font-size:0.8rem;color:#b91c1c;">
|
||||
<p style="font-weight:600;margin-bottom:0.25rem;">Please fix the following:</p>
|
||||
<ul style="list-style:disc;padding-left:1.25rem;line-height:1.8;">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('purchase.requests.store') }}" method="POST" id="mpr-modal-form">
|
||||
@csrf
|
||||
|
||||
{{-- Section: Project / Department --}}
|
||||
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:0.875rem;padding:1.25rem;margin-bottom:1.25rem;">
|
||||
<h3 style="font-size:0.7rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:1rem;display:flex;align-items:center;gap:0.4rem;">
|
||||
<span style="display:inline-block;width:3px;height:12px;background:#6366f1;border-radius:2px;"></span>
|
||||
Project / Department Details
|
||||
</h3>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem;">
|
||||
<div>
|
||||
<label class="form-label">Date <span class="text-red-500">*</span></label>
|
||||
<input type="date" name="date" value="{{ old('date', date('Y-m-d')) }}" required class="form-input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Project / Site Name <span class="text-red-500">*</span></label>
|
||||
<select name="project_name" id="mpr-project-select" required class="form-input" onchange="mprFilterLocations(this.value)">
|
||||
<option value="">— Select Project —</option>
|
||||
@foreach($mprProjects as $proj)
|
||||
<option value="{{ $proj->name }}" {{ old('project_name') == $proj->name ? 'selected' : '' }} data-id="{{ $proj->id }}">{{ $proj->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Requested By <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="requested_by_name" value="{{ old('requested_by_name') }}" required class="form-input" placeholder="Person's name">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Required Date / Urgency</label>
|
||||
<input type="text" name="required_date_text" value="{{ old('required_date_text') }}" class="form-input" placeholder="e.g. Urgent, or 2026-06-01">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Location / Site</label>
|
||||
<select name="location" id="mpr-location-select" class="form-input">
|
||||
<option value="">— Select Location —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Department</label>
|
||||
<input type="text" name="department" value="{{ old('department') }}" class="form-input" placeholder="e.g. Operations">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Section: Material Items --}}
|
||||
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:0.875rem;padding:1.25rem;margin-bottom:1.25rem;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;">
|
||||
<h3 style="font-size:0.7rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:0.08em;display:flex;align-items:center;gap:0.4rem;">
|
||||
<span style="display:inline-block;width:3px;height:12px;background:#6366f1;border-radius:2px;"></span>
|
||||
Material Details
|
||||
</h3>
|
||||
<button type="button" id="mpr-add-row" class="btn-primary btn-sm">+ Add Item</button>
|
||||
</div>
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:0.8rem;">
|
||||
<thead>
|
||||
<tr style="background:white;">
|
||||
<th style="border:1px solid #e2e8f0;padding:0.5rem 0.625rem;text-align:left;width:2.5rem;font-size:0.65rem;font-weight:600;color:#94a3b8;text-transform:uppercase;">#</th>
|
||||
<th style="border:1px solid #e2e8f0;padding:0.5rem 0.625rem;text-align:left;font-size:0.65rem;font-weight:600;color:#94a3b8;text-transform:uppercase;">Description <span style="color:#f87171;">*</span></th>
|
||||
<th style="border:1px solid #e2e8f0;padding:0.5rem 0.625rem;text-align:left;width:5rem;font-size:0.65rem;font-weight:600;color:#94a3b8;text-transform:uppercase;">Unit</th>
|
||||
<th style="border:1px solid #e2e8f0;padding:0.5rem 0.625rem;text-align:left;width:6rem;font-size:0.65rem;font-weight:600;color:#94a3b8;text-transform:uppercase;">Qty <span style="color:#f87171;">*</span></th>
|
||||
<th style="border:1px solid #e2e8f0;padding:0.5rem 0.625rem;text-align:left;width:9rem;font-size:0.65rem;font-weight:600;color:#94a3b8;text-transform:uppercase;">Purpose</th>
|
||||
<th style="border:1px solid #e2e8f0;padding:0.5rem 0.625rem;text-align:left;width:8rem;font-size:0.65rem;font-weight:600;color:#94a3b8;text-transform:uppercase;">Req. Date</th>
|
||||
<th style="border:1px solid #e2e8f0;padding:0.5rem 0.4rem;width:2rem;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="mpr-items-body">
|
||||
@php $oldItems = old('items', [[]]); @endphp
|
||||
@foreach($oldItems as $idx => $oldItem)
|
||||
<tr class="mpr-item-row" style="background:white;">
|
||||
<td style="border:1px solid #e2e8f0;padding:0.375rem 0.625rem;text-align:center;color:#cbd5e1;font-size:0.75rem;" class="mpr-row-num">{{ $idx + 1 }}</td>
|
||||
<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;">
|
||||
<input type="text" name="items[{{ $idx }}][description]" value="{{ $oldItem['description'] ?? '' }}"
|
||||
style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;" placeholder="Material description" required>
|
||||
</td>
|
||||
<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;">
|
||||
<input type="text" name="items[{{ $idx }}][unit]" value="{{ $oldItem['unit'] ?? '' }}"
|
||||
style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;" placeholder="PCS…">
|
||||
</td>
|
||||
<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;">
|
||||
<input type="number" name="items[{{ $idx }}][quantity_required]" value="{{ $oldItem['quantity_required'] ?? '' }}"
|
||||
min="0.01" step="0.01" style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;" placeholder="0" required>
|
||||
</td>
|
||||
<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;">
|
||||
<input type="text" name="items[{{ $idx }}][purpose_use]" value="{{ $oldItem['purpose_use'] ?? '' }}"
|
||||
style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;" placeholder="Purpose…">
|
||||
</td>
|
||||
<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;">
|
||||
<input type="date" name="items[{{ $idx }}][required_date]" value="{{ $oldItem['required_date'] ?? '' }}"
|
||||
style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;">
|
||||
</td>
|
||||
<td style="border:1px solid #e2e8f0;padding:0.25rem 0.4rem;text-align:center;">
|
||||
<button type="button" class="mpr-remove-row"
|
||||
style="color:#f87171;background:none;border:none;cursor:pointer;font-size:1.1rem;font-weight:700;line-height:1;padding:0.125rem 0.25rem;border-radius:0.25rem;"
|
||||
onmouseover="this.style.color='#dc2626'" onmouseout="this.style.color='#f87171'">×</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Section: Remarks --}}
|
||||
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:0.875rem;padding:1.25rem;">
|
||||
<h3 style="font-size:0.7rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:0.75rem;display:flex;align-items:center;gap:0.4rem;">
|
||||
<span style="display:inline-block;width:3px;height:12px;background:#6366f1;border-radius:2px;"></span>
|
||||
Remarks / Notes
|
||||
</h3>
|
||||
<textarea name="remarks" rows="2" class="form-textarea">{{ old('remarks') }}</textarea>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ── Footer ── --}}
|
||||
<div style="flex-shrink:0;padding:1rem 1.5rem;border-top:1px solid #f1f5f9;border-radius:0 0 1.25rem 1.25rem;background:#f8fafc;display:flex;align-items:center;gap:0.75rem;">
|
||||
<button type="submit" form="mpr-modal-form" class="btn-primary">Submit Request</button>
|
||||
<button type="button" onclick="mprModalClose()" class="btn-secondary">Cancel</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function mprModalOpen() {
|
||||
var el = document.getElementById('mpr-modal-overlay');
|
||||
el.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
function mprModalClose() {
|
||||
var el = document.getElementById('mpr-modal-overlay');
|
||||
el.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') mprModalClose();
|
||||
});
|
||||
|
||||
// Auto-open if there are validation errors
|
||||
@if($errors->any())
|
||||
document.addEventListener('DOMContentLoaded', function () { mprModalOpen(); });
|
||||
@endif
|
||||
|
||||
// Dynamic rows
|
||||
(function () {
|
||||
var mprRowIndex = {{ count($oldItems ?? [[]]) }};
|
||||
|
||||
function mprRenumber() {
|
||||
document.querySelectorAll('#mpr-items-body .mpr-row-num').forEach(function (el, i) {
|
||||
el.textContent = i + 1;
|
||||
});
|
||||
}
|
||||
|
||||
function mprNewRow() {
|
||||
var idx = mprRowIndex++;
|
||||
var tr = document.createElement('tr');
|
||||
tr.className = 'mpr-item-row';
|
||||
tr.style.background = 'white';
|
||||
tr.innerHTML =
|
||||
'<td style="border:1px solid #e2e8f0;padding:0.375rem 0.625rem;text-align:center;color:#cbd5e1;font-size:0.75rem;" class="mpr-row-num"></td>' +
|
||||
'<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;"><input type="text" name="items[' + idx + '][description]" style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;" placeholder="Material description" required></td>' +
|
||||
'<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;"><input type="text" name="items[' + idx + '][unit]" style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;" placeholder="PCS…"></td>' +
|
||||
'<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;"><input type="number" name="items[' + idx + '][quantity_required]" min="0.01" step="0.01" style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;" placeholder="0" required></td>' +
|
||||
'<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;"><input type="text" name="items[' + idx + '][purpose_use]" style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;" placeholder="Purpose…"></td>' +
|
||||
'<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;"><input type="date" name="items[' + idx + '][required_date]" style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;"></td>' +
|
||||
'<td style="border:1px solid #e2e8f0;padding:0.25rem 0.4rem;text-align:center;"><button type="button" class="mpr-remove-row" style="color:#f87171;background:none;border:none;cursor:pointer;font-size:1.1rem;font-weight:700;line-height:1;" onmouseover="this.style.color=\'#dc2626\'" onmouseout="this.style.color=\'#f87171\'">×</button></td>';
|
||||
return tr;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var addBtn = document.getElementById('mpr-add-row');
|
||||
var tbody = document.getElementById('mpr-items-body');
|
||||
if (!addBtn || !tbody) return;
|
||||
|
||||
addBtn.addEventListener('click', function () {
|
||||
tbody.appendChild(mprNewRow());
|
||||
mprRenumber();
|
||||
});
|
||||
|
||||
tbody.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('mpr-remove-row')) {
|
||||
if (tbody.querySelectorAll('.mpr-item-row').length > 1) {
|
||||
e.target.closest('tr').remove();
|
||||
mprRenumber();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mprRenumber();
|
||||
});
|
||||
})();
|
||||
|
||||
// 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 mprOldLocation = "{{ old('location') }}";
|
||||
var mprOldProjectName = "{{ old('project_name') }}";
|
||||
|
||||
function mprFilterLocations(projectName) {
|
||||
var sel = document.getElementById('mpr-location-select');
|
||||
sel.innerHTML = '<option value="">— Select Location —</option>';
|
||||
if (!projectName) { sel.disabled = true; return; }
|
||||
var proj = mprProjectsData.find(function(p){ return p.name === projectName; });
|
||||
if (proj && proj.locations.length) {
|
||||
proj.locations.forEach(function(loc){
|
||||
var opt = document.createElement('option');
|
||||
opt.value = loc.name;
|
||||
opt.textContent = loc.name;
|
||||
if (loc.name === mprOldLocation) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.disabled = false;
|
||||
} else {
|
||||
sel.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// On load: if old project value exists (validation error repopulation), trigger filter
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
if (mprOldProjectName) {
|
||||
mprFilterLocations(mprOldProjectName);
|
||||
} else {
|
||||
document.getElementById('mpr-location-select').disabled = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -175,11 +175,22 @@
|
||||
System
|
||||
</span>
|
||||
</div>
|
||||
<a href="{{ route('settings.projects.index') }}" style="
|
||||
display:flex; align-items:center; gap:8px;
|
||||
padding:7px 12px 7px 24px; border-radius:7px; margin-bottom:1px;
|
||||
font-size:13px; text-decoration:none;
|
||||
{{ request()->routeIs('settings.projects.*') ? 'background:#1e293b;color:#fff;font-weight:500;' : 'color:#94a3b8;' }}
|
||||
" onmouseover="if(!this.style.color.includes('fff'))this.style.color='#e2e8f0'" onmouseout="if(!this.style.background.includes('1e293b'))this.style.color='#94a3b8'">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="flex-shrink:0;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
Projects
|
||||
</a>
|
||||
<a href="{{ route('settings.integrations') }}" style="
|
||||
display:flex; align-items:center; gap:8px;
|
||||
padding:7px 12px 7px 24px; border-radius:7px; margin-bottom:1px;
|
||||
font-size:13px; text-decoration:none;
|
||||
{{ request()->routeIs('settings.*') ? 'background:#1e293b;color:#fff;font-weight:500;' : 'color:#94a3b8;' }}
|
||||
{{ request()->routeIs('settings.integrations*') ? 'background:#1e293b;color:#fff;font-weight:500;' : 'color:#94a3b8;' }}
|
||||
" onmouseover="if(!this.style.color.includes('fff'))this.style.color='#e2e8f0'" onmouseout="if(!this.style.background.includes('1e293b'))this.style.color='#94a3b8'">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="flex-shrink:0;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
@ -412,8 +423,9 @@ function confirmDelete(form, title, body) {
|
||||
var modal = document.getElementById('global-delete-modal');
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('gdm-confirm-btn').onclick = function() {
|
||||
var form = _gdmForm;
|
||||
closeGlobalDeleteModal();
|
||||
_gdmForm.submit();
|
||||
form.submit();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
320
resources/views/settings/projects/index.blade.php
Normal file
320
resources/views/settings/projects/index.blade.php
Normal file
@ -0,0 +1,320 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Settings — Projects')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
.proj-row { cursor:pointer; transition:background .15s; }
|
||||
.proj-row:hover { background:#f8fafc; }
|
||||
.proj-row.selected { background:#eff6ff; }
|
||||
.loc-row { transition:background .15s; }
|
||||
.loc-row:hover { background:#f8fafc; }
|
||||
.edit-form { display:none; }
|
||||
.edit-form.open { display:flex; }
|
||||
.badge-active { display:inline-block; padding:2px 8px; border-radius:9px; font-size:11px; font-weight:600; background:#dcfce7; color:#16a34a; }
|
||||
.badge-inactive { display:inline-block; padding:2px 8px; border-radius:9px; font-size:11px; font-weight:600; background:#fee2e2; color:#dc2626; }
|
||||
</style>
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="page-title">Settings — Projects</h1>
|
||||
<p class="page-subtitle">Manage projects and their sub-locations.</p>
|
||||
</div>
|
||||
|
||||
{{-- Two-panel layout --}}
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:24px; align-items:start;">
|
||||
|
||||
{{-- ── LEFT PANEL: Projects ── --}}
|
||||
<div class="card">
|
||||
<div style="padding:16px 20px 14px; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; justify-content:space-between;">
|
||||
<h3 style="font-size:15px; font-weight:600; color:#111827; margin:0;">
|
||||
Projects
|
||||
<span style="font-size:12px; font-weight:400; color:#6b7280; margin-left:6px;">{{ $projects->count() }}</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{{-- Add Project form --}}
|
||||
<div style="padding:16px 20px; border-bottom:1px solid #f3f4f6;">
|
||||
<form method="POST" action="{{ route('settings.projects.store') }}" style="display:flex; gap:8px; align-items:flex-start;">
|
||||
@csrf
|
||||
<div style="flex:1;">
|
||||
<input type="text" name="name" value="{{ old('name') }}"
|
||||
placeholder="New project name…"
|
||||
class="form-input @error('name') border-red-400 @enderror"
|
||||
style="width:100%;">
|
||||
@error('name')
|
||||
<p style="color:#dc2626; font-size:12px; margin-top:4px;">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="white-space:nowrap; flex-shrink:0;">Add Project</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Projects list --}}
|
||||
<div style="padding:8px 0;">
|
||||
@forelse($projects as $project)
|
||||
<div class="proj-row" id="proj-row-{{ $project->id }}" data-id="{{ $project->id }}" onclick="selectProject({{ $project->id }})">
|
||||
|
||||
{{-- Display row --}}
|
||||
<div class="proj-display-{{ $project->id }}" style="display:flex; align-items:center; justify-content:space-between; padding:10px 16px;">
|
||||
<div style="display:flex; align-items:center; gap:10px; flex:1; min-width:0;">
|
||||
<svg width="14" height="14" fill="none" stroke="{{ $project->is_active ? '#22c55e' : '#9ca3af' }}" viewBox="0 0 24 24" style="flex-shrink:0;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
<span style="font-size:13.5px; color:#1e293b; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">
|
||||
{{ $project->name }}
|
||||
</span>
|
||||
@if(!$project->is_active)
|
||||
<span class="badge-inactive">Inactive</span>
|
||||
@endif
|
||||
<span style="font-size:11px; color:#94a3b8; flex-shrink:0;">{{ $project->locations->count() }} loc.</span>
|
||||
</div>
|
||||
<div style="display:flex; gap:4px; flex-shrink:0;" onclick="event.stopPropagation()">
|
||||
<button type="button"
|
||||
onclick="openEditProject({{ $project->id }}, '{{ addslashes($project->name) }}', {{ $project->is_active ? 'true' : 'false' }})"
|
||||
class="btn-secondary btn-sm" style="padding:3px 8px; font-size:12px;">Edit</button>
|
||||
<form method="POST" action="{{ route('settings.projects.destroy', $project) }}" id="del-proj-{{ $project->id }}">
|
||||
@csrf @method('DELETE')
|
||||
<button type="button"
|
||||
onclick="confirmDelete(document.getElementById('del-proj-{{ $project->id }}'), 'Delete Project', 'Delete "{{ addslashes($project->name) }}" and all its locations?')"
|
||||
class="btn-danger btn-sm" style="padding:3px 8px; font-size:12px;">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Edit inline form --}}
|
||||
<div class="edit-form proj-edit-{{ $project->id }}" style="padding:10px 16px; background:#f8fafc; border-top:1px solid #e5e7eb; gap:8px; align-items:flex-start;" onclick="event.stopPropagation()">
|
||||
<form method="POST" action="{{ route('settings.projects.update', $project) }}" style="display:flex; gap:8px; align-items:center; flex:1;">
|
||||
@csrf @method('PATCH')
|
||||
<input type="text" name="name" id="edit-proj-name-{{ $project->id }}"
|
||||
class="form-input" style="flex:1; font-size:13px;">
|
||||
<label style="display:flex; align-items:center; gap:4px; font-size:12px; color:#374151; white-space:nowrap; cursor:pointer;">
|
||||
<input type="hidden" name="is_active" value="0">
|
||||
<input type="checkbox" name="is_active" value="1" id="edit-proj-active-{{ $project->id }}"
|
||||
style="width:14px; height:14px; cursor:pointer;">
|
||||
Active
|
||||
</label>
|
||||
<button type="submit" class="btn-primary" style="padding:5px 12px; font-size:12px; white-space:nowrap;">Save</button>
|
||||
<button type="button" onclick="closeEditProject({{ $project->id }})"
|
||||
class="btn-secondary btn-sm" style="padding:5px 10px; font-size:12px;">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style="height:1px; background:#f3f4f6; margin:0 16px;"></div>
|
||||
@empty
|
||||
<div style="padding:32px 20px; text-align:center; color:#9ca3af; font-size:13.5px;">
|
||||
<svg width="32" height="32" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="margin:0 auto 8px; display:block; opacity:.4;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
No projects yet. Add one above.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── RIGHT PANEL: Locations ── --}}
|
||||
<div class="card" id="locations-panel">
|
||||
{{-- Placeholder when nothing selected --}}
|
||||
<div id="loc-placeholder" style="padding:48px 24px; text-align:center; color:#9ca3af;">
|
||||
<svg width="36" height="36" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="margin:0 auto 10px; display:block; opacity:.35;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<p style="font-size:13.5px; margin:0;">Select a project to see its locations</p>
|
||||
</div>
|
||||
|
||||
{{-- Content shown after a project is selected --}}
|
||||
<div id="loc-content" style="display:none;">
|
||||
<div style="padding:16px 20px 14px; border-bottom:1px solid #e5e7eb; display:flex; align-items:center; justify-content:space-between;">
|
||||
<h3 style="font-size:15px; font-weight:600; color:#111827; margin:0;">
|
||||
Locations — <span id="loc-project-name" style="color:#2563eb;"></span>
|
||||
<span id="loc-count" style="font-size:12px; font-weight:400; color:#6b7280; margin-left:6px;"></span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{{-- Add Location form --}}
|
||||
<div style="padding:16px 20px; border-bottom:1px solid #f3f4f6;">
|
||||
<form id="add-loc-form" method="POST" action="" style="display:flex; gap:8px; align-items:flex-start;">
|
||||
@csrf
|
||||
<div style="flex:1;">
|
||||
<input type="text" name="name" id="add-loc-name"
|
||||
placeholder="New location name…"
|
||||
class="form-input" style="width:100%;">
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="white-space:nowrap; flex-shrink:0;">Add Location</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Locations list --}}
|
||||
<div id="loc-list" style="padding:8px 0;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Pre-load all project+location data as JSON for client-side rendering --}}
|
||||
<script>
|
||||
var projectsData = @json($projects->map(function($p) {
|
||||
return [
|
||||
'id' => $p->id,
|
||||
'name' => $p->name,
|
||||
'is_active' => $p->is_active,
|
||||
'locations' => $p->locations->map(fn($l) => [
|
||||
'id' => $l->id,
|
||||
'name' => $l->name,
|
||||
'is_active' => $l->is_active,
|
||||
])->values(),
|
||||
];
|
||||
})->values());
|
||||
|
||||
// Route templates (server-rendered, safe)
|
||||
var routeStoreLocation = '{{ url("settings/projects") }}/__ID__/locations';
|
||||
var routeUpdateLocation = '{{ url("settings/projects") }}/__ID__/locations/__LOC__';
|
||||
var routeDeleteLocation = '{{ url("settings/projects") }}/__ID__/locations/__LOC__';
|
||||
var csrfToken = document.querySelector('meta[name="csrf-token"]').content;
|
||||
|
||||
var selectedProjectId = null;
|
||||
|
||||
function selectProject(id) {
|
||||
// Highlight selected row
|
||||
document.querySelectorAll('.proj-row').forEach(function(r) {
|
||||
r.classList.remove('selected');
|
||||
});
|
||||
var row = document.getElementById('proj-row-' + id);
|
||||
if (row) row.classList.add('selected');
|
||||
|
||||
selectedProjectId = id;
|
||||
var proj = projectsData.find(function(p) { return p.id === id; });
|
||||
if (!proj) return;
|
||||
|
||||
// Show content panel
|
||||
document.getElementById('loc-placeholder').style.display = 'none';
|
||||
document.getElementById('loc-content').style.display = '';
|
||||
|
||||
// Update header
|
||||
document.getElementById('loc-project-name').textContent = proj.name;
|
||||
document.getElementById('loc-count').textContent = proj.locations.length;
|
||||
|
||||
// Set add-form action
|
||||
document.getElementById('add-loc-form').action = routeStoreLocation.replace('__ID__', id);
|
||||
|
||||
// Render locations
|
||||
renderLocations(proj);
|
||||
}
|
||||
|
||||
function renderLocations(proj) {
|
||||
var list = document.getElementById('loc-list');
|
||||
var locs = proj.locations;
|
||||
|
||||
if (locs.length === 0) {
|
||||
list.innerHTML = '<div style="padding:28px 20px; text-align:center; color:#9ca3af; font-size:13.5px;">No locations yet. Add one above.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
locs.forEach(function(loc) {
|
||||
var updateUrl = routeUpdateLocation.replace('__ID__', proj.id).replace('__LOC__', loc.id);
|
||||
var deleteUrl = routeDeleteLocation.replace('__ID__', proj.id).replace('__LOC__', loc.id);
|
||||
var activeChecked = loc.is_active ? 'checked' : '';
|
||||
var badgeHtml = loc.is_active ? '' : '<span class="badge-inactive" style="margin-left:6px;">Inactive</span>';
|
||||
|
||||
html += '<div class="loc-row" id="loc-row-' + loc.id + '">';
|
||||
// Display row
|
||||
html += '<div class="loc-display-' + loc.id + '" style="display:flex; align-items:center; justify-content:space-between; padding:9px 16px;">';
|
||||
html += '<div style="display:flex; align-items:center; gap:8px; flex:1; min-width:0;">';
|
||||
html += '<svg width="13" height="13" fill="none" stroke="' + (loc.is_active ? '#22c55e' : '#9ca3af') + '" viewBox="0 0 24 24" style="flex-shrink:0;">';
|
||||
html += '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>';
|
||||
html += '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>';
|
||||
html += '</svg>';
|
||||
html += '<span style="font-size:13px; color:#374151;">' + escHtml(loc.name) + '</span>';
|
||||
html += badgeHtml;
|
||||
html += '</div>';
|
||||
html += '<div style="display:flex; gap:4px; flex-shrink:0;">';
|
||||
html += '<button type="button" onclick="openEditLoc(' + loc.id + ', \'' + escJs(loc.name) + '\', ' + (loc.is_active ? 'true' : 'false') + ')" class="btn-secondary btn-sm" style="padding:3px 8px; font-size:12px;">Edit</button>';
|
||||
html += '<form method="POST" action="' + deleteUrl + '" id="del-loc-' + loc.id + '">';
|
||||
html += '<input type="hidden" name="_token" value="' + csrfToken + '">';
|
||||
html += '<input type="hidden" name="_method" value="DELETE">';
|
||||
html += '<button type="button" onclick="confirmDelete(document.getElementById(\'del-loc-' + loc.id + '\'), \'Delete Location\', \'Delete "' + escJs(loc.name) + '"?\')" class="btn-danger btn-sm" style="padding:3px 8px; font-size:12px;">Delete</button>';
|
||||
html += '</form>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
// Edit form row
|
||||
html += '<div class="edit-form loc-edit-' + loc.id + '" style="padding:10px 16px; background:#f8fafc; border-top:1px solid #e5e7eb; gap:8px; align-items:center;">';
|
||||
html += '<form method="POST" action="' + updateUrl + '" style="display:flex; gap:8px; align-items:center; flex:1;">';
|
||||
html += '<input type="hidden" name="_token" value="' + csrfToken + '">';
|
||||
html += '<input type="hidden" name="_method" value="PATCH">';
|
||||
html += '<input type="text" name="name" id="edit-loc-name-' + loc.id + '" value="' + escHtml(loc.name) + '" class="form-input" style="flex:1; font-size:13px;">';
|
||||
html += '<label style="display:flex; align-items:center; gap:4px; font-size:12px; color:#374151; white-space:nowrap; cursor:pointer;">';
|
||||
html += '<input type="hidden" name="is_active" value="0">';
|
||||
html += '<input type="checkbox" name="is_active" value="1" id="edit-loc-active-' + loc.id + '" ' + activeChecked + ' style="width:14px; height:14px; cursor:pointer;">';
|
||||
html += ' Active';
|
||||
html += '</label>';
|
||||
html += '<button type="submit" class="btn-primary" style="padding:5px 12px; font-size:12px; white-space:nowrap;">Save</button>';
|
||||
html += '<button type="button" onclick="closeEditLoc(' + loc.id + ')" class="btn-secondary btn-sm" style="padding:5px 10px; font-size:12px;">Cancel</button>';
|
||||
html += '</form>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div style="height:1px; background:#f3f4f6; margin:0 16px;"></div>';
|
||||
});
|
||||
|
||||
list.innerHTML = html;
|
||||
document.getElementById('loc-count').textContent = locs.length;
|
||||
}
|
||||
|
||||
// ── Project edit helpers ──
|
||||
function openEditProject(id, name, isActive) {
|
||||
// Hide display row, show edit form
|
||||
document.querySelector('.proj-display-' + id).style.display = 'none';
|
||||
var editEl = document.querySelector('.proj-edit-' + id);
|
||||
editEl.classList.add('open');
|
||||
document.getElementById('edit-proj-name-' + id).value = name;
|
||||
document.getElementById('edit-proj-active-' + id).checked = isActive;
|
||||
}
|
||||
function closeEditProject(id) {
|
||||
document.querySelector('.proj-display-' + id).style.display = '';
|
||||
document.querySelector('.proj-edit-' + id).classList.remove('open');
|
||||
}
|
||||
|
||||
// ── Location edit helpers ──
|
||||
function openEditLoc(id, name, isActive) {
|
||||
document.querySelector('.loc-display-' + id).style.display = 'none';
|
||||
var editEl = document.querySelector('.loc-edit-' + id);
|
||||
editEl.classList.add('open');
|
||||
document.getElementById('edit-loc-name-' + id).value = name;
|
||||
document.getElementById('edit-loc-active-' + id).checked = isActive;
|
||||
}
|
||||
function closeEditLoc(id) {
|
||||
document.querySelector('.loc-display-' + id).style.display = '';
|
||||
document.querySelector('.loc-edit-' + id).classList.remove('open');
|
||||
}
|
||||
|
||||
// ── Utilities ──
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
function escJs(s) {
|
||||
return String(s).replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||
}
|
||||
|
||||
// Auto-select project after redirect (restore selection from URL hash or first project)
|
||||
(function() {
|
||||
var hash = window.location.hash;
|
||||
var autoId = null;
|
||||
if (hash && hash.startsWith('#project-')) {
|
||||
autoId = parseInt(hash.replace('#project-', ''), 10);
|
||||
} else if (projectsData.length > 0) {
|
||||
autoId = projectsData[0].id;
|
||||
}
|
||||
if (autoId) {
|
||||
// Wait for DOM
|
||||
setTimeout(function() { selectProject(autoId); }, 0);
|
||||
}
|
||||
})();
|
||||
|
||||
// When project form submitted, attach hash to redirect
|
||||
document.querySelectorAll('.proj-row').forEach(function(row) {
|
||||
var id = parseInt(row.getAttribute('data-id'), 10);
|
||||
// Update add-location form to set hash on submit for restoring selection
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@ -27,6 +27,8 @@ use App\Http\Controllers\Sales\PaymentReceiptController;
|
||||
use App\Http\Controllers\Sales\SalesInvoiceController;
|
||||
use App\Http\Controllers\Sales\SalesOrderController;
|
||||
use App\Http\Controllers\SettingsController;
|
||||
use App\Http\Controllers\Settings\ProjectSettingController;
|
||||
use App\Models\Settings\Location;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
@ -120,6 +122,15 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('settings/integrations', [SettingsController::class, 'integrations'])->name('settings.integrations');
|
||||
Route::post('settings/integrations/whatsapp', [SettingsController::class, 'updateWhatsapp'])->name('settings.integrations.whatsapp');
|
||||
Route::post('settings/integrations/test-whatsapp', [SettingsController::class, 'testWhatsappConnection'])->name('settings.integrations.test-whatsapp');
|
||||
|
||||
// Projects settings
|
||||
Route::get('settings/projects', [ProjectSettingController::class, 'index'])->name('settings.projects.index');
|
||||
Route::post('settings/projects', [ProjectSettingController::class, 'store'])->name('settings.projects.store');
|
||||
Route::patch('settings/projects/{project}', [ProjectSettingController::class, 'update'])->name('settings.projects.update');
|
||||
Route::delete('settings/projects/{project}', [ProjectSettingController::class, 'destroy'])->name('settings.projects.destroy');
|
||||
Route::post('settings/projects/{project}/locations', [ProjectSettingController::class, 'storeLocation'])->name('settings.projects.locations.store');
|
||||
Route::patch('settings/projects/{project}/locations/{location}', [ProjectSettingController::class, 'updateLocation'])->name('settings.projects.locations.update');
|
||||
Route::delete('settings/projects/{project}/locations/{location}', [ProjectSettingController::class, 'destroyLocation'])->name('settings.projects.locations.destroy');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user