feat: supplier modal wizard, pipeline delete, sidebar cleanup
- Replace two-tab supplier selector with two-step wizard (method select → suppliers → summary) - Add per-item channel picker (Email / WhatsApp / Both) in By Item mode - Add confirmation summary step before submitting By Item supplier assignments - Add type-to-confirm delete on pipeline list rows - Redirect purchase.requests.index to pipeline (same data, single entry point) - Remove Purchase Requests from sidebar nav - Add edit-request-modal, supplier-invite-list components - Add address coordinates migration for settings_locations Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
dd924904c5
commit
d8cab94bcb
18 files changed
+1943
-840
No files matched your search
@@ -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 ── --}}
|
||||
<button type="button" onclick="{{ $prefix }}Open()" class="btn-secondary btn-sm">
|
||||
Edit
|
||||
</button>
|
||||
|
||||
{{-- ── Modal overlay ── --}}
|
||||
<div id="{{ $prefix }}Overlay"
|
||||
onclick="if(event.target===this){{ $prefix }}Close()"
|
||||
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,#0f766e 0%,#0284c7 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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="color:white;font-size:1rem;font-weight:700;line-height:1.2;">Edit Purchase Request</h2>
|
||||
<p style="color:#bae6fd;font-size:0.7rem;margin-top:0.1rem;">{{ $purchaseRequest->request_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="{{ $prefix }}Close()" 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($repop)
|
||||
<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.update', $purchaseRequest) }}" method="POST" id="{{ $prefix }}Form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="hidden" name="_edit_request_id" value="{{ $prId }}">
|
||||
|
||||
{{-- 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:#0ea5e9;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="{{ $curDate }}" 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="{{ $prefix }}Project" required class="form-input"
|
||||
onchange="{{ $prefix }}FilterLoc(this.value)">
|
||||
<option value="">— Select Project —</option>
|
||||
@if($curProject && !$curProjectInList)
|
||||
<option value="{{ $curProject }}" selected>{{ $curProject }}</option>
|
||||
@endif
|
||||
@foreach($editProjects as $proj)
|
||||
<option value="{{ $proj->name }}" {{ $curProject === $proj->name ? 'selected' : '' }}>{{ $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="{{ $curReqBy }}" 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="{{ $curReqDate }}" class="form-input" placeholder="e.g. Urgent, or 2026-06-01">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Location / Site</label>
|
||||
<select name="location" id="{{ $prefix }}Location" class="form-input">
|
||||
<option value="">— Select Location —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Department</label>
|
||||
<input type="text" name="department" value="{{ $curDept }}" class="form-input" placeholder="e.g. Operations">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 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:#0ea5e9;border-radius:2px;"></span>
|
||||
Material Details
|
||||
</h3>
|
||||
<button type="button" id="{{ $prefix }}AddRow" 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="{{ $prefix }}Items">
|
||||
@foreach($existingItems as $idx => $item)
|
||||
@php
|
||||
$iArr = is_array($item) ? $item : ($item instanceof \Illuminate\Database\Eloquent\Model ? $item->toArray() : (array)$item);
|
||||
$iDate = !empty($iArr['required_date']) ? \Carbon\Carbon::parse($iArr['required_date'])->format('Y-m-d') : '';
|
||||
@endphp
|
||||
<tr class="{{ $prefix }}-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="{{ $prefix }}-row-num">{{ $idx + 1 }}</td>
|
||||
<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;">
|
||||
<input type="text" name="items[{{ $idx }}][description]" value="{{ $iArr['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="{{ $iArr['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="{{ $iArr['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="{{ $iArr['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="{{ $iDate }}"
|
||||
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="{{ $prefix }}-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>
|
||||
|
||||
{{-- 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:#0ea5e9;border-radius:2px;"></span>
|
||||
Remarks / Notes
|
||||
</h3>
|
||||
<textarea name="remarks" rows="2" class="form-textarea">{{ $curRemarks }}</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="{{ $prefix }}Form" class="btn-primary">Save Changes</button>
|
||||
<button type="button" onclick="{{ $prefix }}Close()" class="btn-secondary">Cancel</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function {{ $prefix }}Open() {
|
||||
var el = document.getElementById('{{ $prefix }}Overlay');
|
||||
el.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
function {{ $prefix }}Close() {
|
||||
var el = document.getElementById('{{ $prefix }}Overlay');
|
||||
el.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
window.{{ $prefix }}Open = {{ $prefix }}Open;
|
||||
window.{{ $prefix }}Close = {{ $prefix }}Close;
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && document.getElementById('{{ $prefix }}Overlay').style.display === 'flex') {
|
||||
{{ $prefix }}Close();
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-open on validation error for this request
|
||||
@if($repop)
|
||||
document.addEventListener('DOMContentLoaded', function () { {{ $prefix }}Open(); });
|
||||
@endif
|
||||
|
||||
// ── Dynamic rows ──────────────────────────────────────────────────────────
|
||||
var rowIdx = {{ $existingItems->count() }};
|
||||
|
||||
function renumber() {
|
||||
document.querySelectorAll('#{{ $prefix }}Items .{{ $prefix }}-row-num').forEach(function (el, i) {
|
||||
el.textContent = i + 1;
|
||||
});
|
||||
}
|
||||
|
||||
function newRow() {
|
||||
var idx = rowIdx++;
|
||||
var tr = document.createElement('tr');
|
||||
tr.className = '{{ $prefix }}-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="{{ $prefix }}-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="{{ $prefix }}-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('{{ $prefix }}AddRow');
|
||||
var tbody = document.getElementById('{{ $prefix }}Items');
|
||||
if (!addBtn || !tbody) return;
|
||||
|
||||
addBtn.addEventListener('click', function () {
|
||||
tbody.appendChild(newRow());
|
||||
renumber();
|
||||
});
|
||||
|
||||
tbody.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('{{ $prefix }}-remove-row')) {
|
||||
if (tbody.querySelectorAll('.{{ $prefix }}-item-row').length > 1) {
|
||||
e.target.closest('tr').remove();
|
||||
renumber();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
renumber();
|
||||
});
|
||||
|
||||
// ── Cascading project → location dropdown ─────────────────────────────────
|
||||
var _editProjects{{ $prId }} = {!! $editProjectsJson !!};
|
||||
var _curLoc{{ $prId }} = {!! json_encode($curLoc) !!};
|
||||
|
||||
function {{ $prefix }}FilterLoc(projectName) {
|
||||
var sel = document.getElementById('{{ $prefix }}Location');
|
||||
sel.innerHTML = '<option value="">— Select Location —</option>';
|
||||
if (!projectName) { sel.disabled = true; return; }
|
||||
var proj = _editProjects{{ $prId }}.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;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.disabled = false;
|
||||
} else {
|
||||
sel.disabled = true;
|
||||
}
|
||||
}
|
||||
window.{{ $prefix }}FilterLoc = {{ $prefix }}FilterLoc;
|
||||
|
||||
// On load: populate locations and pre-select the current ones
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var curProj = document.getElementById('{{ $prefix }}Project').value;
|
||||
if (curProj) {
|
||||
{{ $prefix }}FilterLoc(curProj);
|
||||
var locSel = document.getElementById('{{ $prefix }}Location');
|
||||
var matched = false;
|
||||
for (var i = 0; i < locSel.options.length; i++) {
|
||||
if (locSel.options[i].value === _curLoc{{ $prId }}) {
|
||||
locSel.selectedIndex = i;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If saved location not in project's list, add it as a selectable option
|
||||
if (!matched && _curLoc{{ $prId }}) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = _curLoc{{ $prId }};
|
||||
opt.textContent = _curLoc{{ $prId }};
|
||||
opt.selected = true;
|
||||
locSel.appendChild(opt);
|
||||
locSel.disabled = false;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('{{ $prefix }}Location').disabled = true;
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -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') }}";
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
@props([
|
||||
'suppliers',
|
||||
'alreadyIds' => [],
|
||||
'formAction',
|
||||
])
|
||||
|
||||
<div style="font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px;">
|
||||
{{ count($alreadyIds) ? 'Add More Suppliers' : 'Select Suppliers' }}
|
||||
</div>
|
||||
|
||||
<input type="text" id="supplier-search" placeholder="Search suppliers…" oninput="filterSuppliers(this.value)"
|
||||
style="width:100%;padding:9px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;margin-bottom:12px;box-sizing:border-box;">
|
||||
|
||||
<form method="POST" action="{{ $formAction }}" id="rfq-form">
|
||||
@csrf
|
||||
|
||||
<div id="supplier-list" style="display:flex;flex-direction:column;gap:6px;max-height:400px;overflow-y:auto;padding-right:2px;">
|
||||
@forelse($suppliers as $supplier)
|
||||
@php $already = in_array($supplier->id, $alreadyIds); @endphp
|
||||
<div class="sup-row" data-name="{{ strtolower($supplier->name) }}"
|
||||
style="border:1.5px solid #e2e8f0;border-radius:10px;padding:12px 14px;transition:border-color .15s;{{ $already ? 'opacity:.45;' : '' }}">
|
||||
<label style="display:flex;align-items:center;gap:12px;cursor:{{ $already ? 'default' : 'pointer' }};">
|
||||
<input type="checkbox" name="supplier_ids[]" value="{{ $supplier->id }}"
|
||||
data-sid="{{ $supplier->id }}" onchange="toggleRow(this)"
|
||||
{{ $already ? 'disabled' : '' }}
|
||||
style="width:17px;height:17px;cursor:pointer;flex-shrink:0;">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<div style="font-size:13px;font-weight:600;color:#0f172a;">{{ $supplier->name }}</div>
|
||||
<div style="font-size:11px;color:#64748b;margin-top:1px;">
|
||||
{{ $supplier->email ?: '—' }} · {{ $supplier->phone ?: '—' }}
|
||||
</div>
|
||||
</div>
|
||||
@if($already)
|
||||
<span style="font-size:11px;font-weight:600;color:#64748b;background:#f1f5f9;padding:3px 10px;border-radius:20px;">Already invited</span>
|
||||
@endif
|
||||
</label>
|
||||
<div id="ch-{{ $supplier->id }}" style="display:none;margin-top:10px;padding-top:10px;border-top:1px dashed #e2e8f0;">
|
||||
<div style="font-size:11px;font-weight:700;color:#64748b;margin-bottom:8px;">Send via:</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
@foreach(['whatsapp' => 'WhatsApp', 'email' => 'Email', 'both' => 'Both'] as $val => $lbl)
|
||||
<label style="flex:1;cursor:pointer;">
|
||||
<input type="radio" name="channel_{{ $supplier->id }}" value="{{ $val }}" {{ $val === 'both' ? 'checked' : '' }}
|
||||
style="display:none;" id="ch-{{ $supplier->id }}-{{ $val }}"
|
||||
onchange="styleChannelBtns({{ $supplier->id }})">
|
||||
<span id="chbtn-{{ $supplier->id }}-{{ $val }}"
|
||||
onclick="document.getElementById('ch-{{ $supplier->id }}-{{ $val }}').checked=true;styleChannelBtns({{ $supplier->id }});"
|
||||
style="display:block;text-align:center;padding:7px 6px;border-radius:7px;font-size:12px;font-weight:600;cursor:pointer;
|
||||
border:1.5px solid {{ $val === 'both' ? '#0ea5e9' : '#e2e8f0' }};
|
||||
background:{{ $val === 'both' ? '#eff6ff' : '#fff' }};
|
||||
color:{{ $val === 'both' ? '#0284c7' : '#64748b' }};">
|
||||
{{ $lbl }}
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p style="text-align:center;color:#94a3b8;padding:24px;">No active suppliers found. Add suppliers first.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
style="width:100%;margin-top:20px;padding:13px;background:linear-gradient(135deg,#0ea5e9,#0284c7);color:#fff;border:none;border-radius:9px;font-size:14px;font-weight:700;cursor:pointer;">
|
||||
Send Invitations →
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function filterSuppliers(q) {
|
||||
document.querySelectorAll('.sup-row').forEach(function(row) {
|
||||
row.style.display = row.dataset.name.includes(q.toLowerCase()) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleRow(cb) {
|
||||
var ch = document.getElementById('ch-' + cb.dataset.sid);
|
||||
if (ch) ch.style.display = cb.checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function styleChannelBtns(sid) {
|
||||
['whatsapp','email','both'].forEach(function(v) {
|
||||
var inp = document.getElementById('ch-' + sid + '-' + v);
|
||||
var btn = document.getElementById('chbtn-' + sid + '-' + v);
|
||||
if (!inp || !btn) return;
|
||||
if (inp.checked) {
|
||||
btn.style.borderColor = '#0ea5e9'; btn.style.background = '#eff6ff'; btn.style.color = '#0284c7';
|
||||
} else {
|
||||
btn.style.borderColor = '#e2e8f0'; btn.style.background = '#fff'; btn.style.color = '#64748b';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,588 @@
|
||||
@props([
|
||||
'pr',
|
||||
'suppliers',
|
||||
'selectedIds' => [],
|
||||
])
|
||||
|
||||
{{-- ============================================================
|
||||
SELECT SUPPLIERS MODAL
|
||||
============================================================ --}}
|
||||
<div id="supplier-modal" class="pipe-modal" role="dialog" aria-modal="true" aria-labelledby="sup-modal-title">
|
||||
<div style="background:#fff;border-radius:20px;width:100%;max-width:680px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 30px 60px rgba(0,0,0,.3);overflow:hidden;">
|
||||
|
||||
{{-- Header --}}
|
||||
<div style="padding:20px 24px 16px;border-bottom:1px solid #f1f5f9;flex-shrink:0;">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:4px;">
|
||||
<div>
|
||||
<div id="sup-modal-title" style="font-size:17px;font-weight:700;color:#0f172a;">Request for Quotation</div>
|
||||
<div id="sup-modal-subtitle" style="font-size:12px;color:#64748b;margin-top:3px;">How do you want to assign suppliers?</div>
|
||||
</div>
|
||||
<button onclick="closeSupplierModal()" aria-label="Close"
|
||||
style="width:32px;height:32px;border-radius:8px;border:none;background:#f1f5f9;cursor:pointer;font-size:18px;color:#64748b;display:flex;align-items:center;justify-content:center;flex-shrink:0;">×</button>
|
||||
</div>
|
||||
{{-- Mode badge row: hidden in Step 1, shown in Step 2 --}}
|
||||
<div id="sup-mode-badge-row" style="display:none;align-items:center;gap:8px;margin-top:10px;">
|
||||
<button type="button" onclick="goBack()"
|
||||
style="display:flex;align-items:center;gap:4px;font-size:12px;color:#2563eb;background:none;border:none;cursor:pointer;padding:0;font-weight:600;">
|
||||
<svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/></svg>
|
||||
Change method
|
||||
</button>
|
||||
<span style="color:#cbd5e1;">·</span>
|
||||
<span id="sup-mode-badge" style="font-size:11px;padding:2px 9px;border-radius:10px;font-weight:700;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Step 1: Method selection --}}
|
||||
<div id="sup-step1" style="flex:1;display:flex;flex-direction:column;">
|
||||
<div style="padding:24px;display:flex;flex-direction:column;gap:12px;flex:1;">
|
||||
|
||||
<button type="button" onclick="showStep('global')"
|
||||
style="width:100%;text-align:left;border:2px solid #e2e8f0;border-radius:12px;padding:18px 20px;cursor:pointer;display:flex;align-items:center;gap:16px;background:#fff;transition:border-color .15s,background .15s;"
|
||||
onmouseover="this.style.borderColor='#2563eb';this.style.background='#f8fbff'"
|
||||
onmouseout="this.style.borderColor='#e2e8f0';this.style.background='#fff'">
|
||||
<div style="width:44px;height:44px;background:#eff6ff;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">📦</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:14px;font-weight:700;color:#0f172a;">Full Order</div>
|
||||
<div style="font-size:12px;color:#64748b;margin-top:3px;">One set of suppliers handles the entire purchase request</div>
|
||||
</div>
|
||||
<svg width="16" height="16" fill="none" stroke="#cbd5e1" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 18l6-6-6-6"/></svg>
|
||||
</button>
|
||||
|
||||
<button type="button" onclick="showStep('item')"
|
||||
style="width:100%;text-align:left;border:2px solid #e2e8f0;border-radius:12px;padding:18px 20px;cursor:pointer;display:flex;align-items:center;gap:16px;background:#fff;transition:border-color .15s,background .15s;"
|
||||
onmouseover="this.style.borderColor='#2563eb';this.style.background='#f8fbff'"
|
||||
onmouseout="this.style.borderColor='#e2e8f0';this.style.background='#fff'">
|
||||
<div style="width:44px;height:44px;background:#f0fdf4;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🔀</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:14px;font-weight:700;color:#0f172a;">By Item</div>
|
||||
<div style="font-size:12px;color:#64748b;margin-top:3px;">Assign different suppliers to specific items in this request</div>
|
||||
</div>
|
||||
<svg width="16" height="16" fill="none" stroke="#cbd5e1" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 18l6-6-6-6"/></svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div style="padding:14px 24px;border-top:1px solid #f1f5f9;display:flex;justify-content:flex-end;flex-shrink:0;background:#fafafa;">
|
||||
<button type="button" onclick="closeSupplierModal()"
|
||||
style="padding:8px 18px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-weight:600;color:#64748b;background:#fff;cursor:pointer;">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Step 2: Supplier selection --}}
|
||||
<div id="sup-step2" style="flex:1;display:none;flex-direction:column;min-height:0;">
|
||||
|
||||
{{-- Single form wrapping both panes --}}
|
||||
<form id="sup-form" action="{{ route('purchase.requests.rfq.select', $pr) }}" method="POST"
|
||||
style="flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0;">
|
||||
@csrf
|
||||
<input type="hidden" name="mode" id="sup-mode" value="">
|
||||
|
||||
{{-- ── GLOBAL PANE ── --}}
|
||||
<div id="sup-global-pane" style="flex:1;overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;">
|
||||
{{-- Search --}}
|
||||
<div style="padding:14px 24px 10px;flex-shrink:0;">
|
||||
<div style="position:relative;">
|
||||
<svg style="position:absolute;left:11px;top:50%;transform:translateY(-50%);pointer-events:none;"
|
||||
width="14" height="14" fill="none" stroke="#94a3b8" stroke-width="2" viewBox="0 0 24 24">
|
||||
<circle cx="11" cy="11" r="8"/><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<input id="sup-search" type="text" placeholder="Search suppliers…" oninput="filterGlobalSups(this.value)"
|
||||
style="width:100%;box-sizing:border-box;padding:9px 12px 9px 34px;border:1px solid #e2e8f0;border-radius:9px;font-size:13px;outline:none;"
|
||||
onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'">
|
||||
</div>
|
||||
</div>
|
||||
<div id="sup-list" style="flex:1;">
|
||||
@forelse($suppliers as $sup)
|
||||
@php $alreadyAdded = in_array($sup->id, $selectedIds); @endphp
|
||||
<div class="g-sup-item" data-name="{{ strtolower($sup->name) }}"
|
||||
style="padding:11px 24px;display:flex;align-items:center;gap:14px;border-bottom:1px solid #f8fafc;transition:background .1s;
|
||||
{{ $alreadyAdded ? 'opacity:.45;pointer-events:none;' : '' }}">
|
||||
<input type="checkbox" name="supplier_ids[]" value="{{ $sup->id }}" id="gsup-{{ $sup->id }}"
|
||||
{{ $alreadyAdded ? 'disabled checked' : '' }}
|
||||
onchange="toggleGlobalChan({{ $sup->id }}, this.checked); updateGlobalCount();"
|
||||
style="width:17px;height:17px;accent-color:#2563eb;cursor:pointer;flex-shrink:0;">
|
||||
<label for="gsup-{{ $sup->id }}" style="flex:1;cursor:{{ $alreadyAdded ? 'default' : 'pointer' }};">
|
||||
<div style="font-size:13px;font-weight:600;color:#0f172a;">
|
||||
{{ $sup->name }}
|
||||
@if($alreadyAdded)
|
||||
<span style="font-size:10px;background:#dcfce7;color:#15803d;padding:1px 6px;border-radius:10px;margin-left:6px;font-weight:700;">Added</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($sup->email || $sup->phone)
|
||||
<div style="font-size:11px;color:#94a3b8;margin-top:1px;">
|
||||
{{ collect([$sup->email, $sup->phone])->filter()->implode(' · ') }}
|
||||
</div>
|
||||
@endif
|
||||
</label>
|
||||
@if(!$alreadyAdded)
|
||||
<div id="gchan-{{ $sup->id }}" style="display:none;flex-shrink:0;">
|
||||
<input type="hidden" name="channel_{{ $sup->id }}" id="gchan-val-{{ $sup->id }}" value="email">
|
||||
<div style="display:flex;border:1px solid #e2e8f0;border-radius:7px;overflow:hidden;">
|
||||
<span onclick="setGChan({{ $sup->id }},'email')" id="gce-{{ $sup->id }}"
|
||||
style="padding:5px 9px;font-size:10px;font-weight:700;cursor:pointer;background:#eff6ff;color:#2563eb;">Email</span>
|
||||
<span onclick="setGChan({{ $sup->id }},'whatsapp')" id="gcw-{{ $sup->id }}"
|
||||
style="padding:5px 9px;font-size:10px;font-weight:700;cursor:pointer;background:#fff;color:#94a3b8;border-left:1px solid #e2e8f0;">WA</span>
|
||||
<span onclick="setGChan({{ $sup->id }},'both')" id="gcb-{{ $sup->id }}"
|
||||
style="padding:5px 9px;font-size:10px;font-weight:700;cursor:pointer;background:#fff;color:#94a3b8;border-left:1px solid #e2e8f0;">Both</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div style="padding:40px;text-align:center;color:#94a3b8;font-size:13px;">No active suppliers found.</div>
|
||||
@endforelse
|
||||
<div id="no-sup-msg" style="display:none;padding:30px;text-align:center;color:#94a3b8;font-size:13px;">No suppliers match your search.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── BY ITEM PANE ── --}}
|
||||
<div id="sup-item-pane" style="flex:1;overflow-y:auto;overscroll-behavior:contain;display:none;flex-direction:column;">
|
||||
@if($pr->items->isEmpty())
|
||||
<div style="padding:40px;text-align:center;color:#94a3b8;font-size:13px;">This request has no items yet.</div>
|
||||
@else
|
||||
{{-- Column headers --}}
|
||||
<div style="display:grid;grid-template-columns:1fr 175px 120px;gap:12px;padding:9px 24px;background:#f8fafc;border-bottom:1px solid #e2e8f0;flex-shrink:0;">
|
||||
<div style="font-size:10px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;">Item</div>
|
||||
<div style="font-size:10px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;">Assign Suppliers</div>
|
||||
<div style="font-size:10px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;">Channel</div>
|
||||
</div>
|
||||
|
||||
{{-- Item rows --}}
|
||||
@foreach($pr->items as $item)
|
||||
<div style="display:grid;grid-template-columns:1fr 175px 120px;gap:12px;align-items:center;padding:13px 24px;border-bottom:1px solid #f8fafc;">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;color:#0f172a;line-height:1.3;">{{ $item->description }}</div>
|
||||
<div style="font-size:11px;color:#94a3b8;margin-top:2px;">
|
||||
Qty: {{ rtrim(rtrim(number_format($item->quantity_required,2),'0'),'.') }}{{ $item->unit ? ' '.$item->unit : '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" id="idd-btn-{{ $item->id }}"
|
||||
onclick="toggleItemDd(event,{{ $item->id }})"
|
||||
data-itemname="{{ $item->description }}"
|
||||
data-itemqty="{{ rtrim(rtrim(number_format($item->quantity_required,2),'0'),'.') }}{{ $item->unit ? ' '.$item->unit : '' }}"
|
||||
style="width:100%;padding:8px 11px;border:1.5px solid #e2e8f0;border-radius:8px;background:#fff;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:space-between;gap:6px;text-align:left;transition:border .15s;">
|
||||
<span id="idd-label-{{ $item->id }}" style="font-size:12px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">
|
||||
Select suppliers…
|
||||
</span>
|
||||
<svg width="11" height="11" fill="none" stroke="#94a3b8" stroke-width="2.5" viewBox="0 0 24 24" style="flex-shrink:0;">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="ichan-item-btn-{{ $item->id }}"
|
||||
style="display:flex;border:1.5px solid #e2e8f0;border-radius:8px;overflow:hidden;opacity:.35;pointer-events:none;">
|
||||
<span onclick="setItemChan({{ $item->id }},'email')" id="ichan-ie-{{ $item->id }}"
|
||||
style="flex:1;padding:7px 4px;font-size:10px;font-weight:700;cursor:pointer;text-align:center;background:#eff6ff;color:#2563eb;">Email</span>
|
||||
<span onclick="setItemChan({{ $item->id }},'whatsapp')" id="ichan-iw-{{ $item->id }}"
|
||||
style="flex:1;padding:7px 4px;font-size:10px;font-weight:700;cursor:pointer;text-align:center;background:#fff;color:#94a3b8;border-left:1.5px solid #e2e8f0;">WA</span>
|
||||
<span onclick="setItemChan({{ $item->id }},'both')" id="ichan-ib-{{ $item->id }}"
|
||||
style="flex:1;padding:7px 4px;font-size:10px;font-weight:700;cursor:pointer;text-align:center;background:#fff;color:#94a3b8;border-left:1.5px solid #e2e8f0;">Both</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
{{-- Hidden channel inputs — read by backend as channel_{sup_id} --}}
|
||||
<div style="display:none;">
|
||||
@foreach($suppliers as $sup)
|
||||
<input type="hidden" name="channel_{{ $sup->id }}" id="ichan-val-{{ $sup->id }}" value="email">
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Dropdown panels: fixed-positioned, one per item — must be inside form so checkboxes are submitted --}}
|
||||
@foreach($pr->items as $item)
|
||||
<div id="idd-{{ $item->id }}"
|
||||
style="display:none;position:fixed;z-index:99999;background:#fff;border:1.5px solid #e2e8f0;
|
||||
border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,.18);overflow:hidden;min-width:240px;">
|
||||
<div style="padding:10px 10px 6px;">
|
||||
<input type="text" placeholder="Search suppliers…"
|
||||
oninput="filterItemDd({{ $item->id }}, this.value)"
|
||||
style="width:100%;box-sizing:border-box;padding:7px 10px;border:1px solid #e2e8f0;border-radius:7px;font-size:12px;outline:none;"
|
||||
onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'">
|
||||
</div>
|
||||
<div style="max-height:210px;overflow-y:auto;padding-bottom:4px;">
|
||||
@forelse($suppliers as $sup)
|
||||
@php $alreadyAdded = in_array($sup->id, $selectedIds); @endphp
|
||||
<label class="idd-row-{{ $item->id }}" data-name="{{ strtolower($sup->name) }}"
|
||||
style="display:flex;align-items:center;gap:10px;padding:8px 12px;cursor:{{ $alreadyAdded ? 'default' : 'pointer' }};
|
||||
{{ $alreadyAdded ? 'opacity:.45;' : '' }}"
|
||||
onmouseover="{{ $alreadyAdded ? '' : "this.style.background='#f8fafc'" }}"
|
||||
onmouseout="this.style.background='transparent'">
|
||||
<input type="checkbox" name="item_suppliers[{{ $item->id }}][]" value="{{ $sup->id }}"
|
||||
data-supname="{{ $sup->name }}"
|
||||
id="isup-{{ $item->id }}-{{ $sup->id }}"
|
||||
{{ $alreadyAdded ? 'disabled checked' : '' }}
|
||||
onchange="onItemSupChange({{ $item->id }}, {{ $sup->id }})"
|
||||
style="width:15px;height:15px;accent-color:#2563eb;cursor:{{ $alreadyAdded ? 'default' : 'pointer' }};flex-shrink:0;">
|
||||
<div style="min-width:0;">
|
||||
<div style="font-size:12px;font-weight:600;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
||||
{{ $sup->name }}
|
||||
@if($alreadyAdded)
|
||||
<span style="font-size:9px;background:#dcfce7;color:#15803d;padding:1px 5px;border-radius:8px;margin-left:4px;">Added</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($sup->email)
|
||||
<div style="font-size:10px;color:#94a3b8;">{{ $sup->email }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</label>
|
||||
@empty
|
||||
<div style="padding:16px;text-align:center;color:#94a3b8;font-size:12px;">No suppliers found.</div>
|
||||
@endforelse
|
||||
<div id="idd-no-{{ $item->id }}" style="display:none;padding:14px;text-align:center;color:#94a3b8;font-size:12px;">No results</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</form>
|
||||
|
||||
{{-- Footer --}}
|
||||
<div style="padding:14px 24px;border-top:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;background:#fafafa;">
|
||||
<div style="font-size:12px;color:#64748b;" id="sup-footer-msg">0 selected</div>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<button type="button" onclick="closeSupplierModal()"
|
||||
style="padding:8px 18px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-weight:600;color:#64748b;background:#fff;cursor:pointer;">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" onclick="submitSuppliers()"
|
||||
style="padding:8px 22px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;">
|
||||
Save & Continue →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>{{-- /sup-step2 --}}
|
||||
|
||||
{{-- Step 3: Summary / confirmation (By Item only) --}}
|
||||
<div id="sup-step3" style="flex:1;display:none;flex-direction:column;min-height:0;">
|
||||
<div id="sup-summary-body" style="flex:1;overflow-y:auto;overscroll-behavior:contain;padding:20px 24px;">
|
||||
</div>
|
||||
<div style="padding:14px 24px;border-top:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;background:#fafafa;">
|
||||
<button type="button" onclick="backToEdit()"
|
||||
style="display:flex;align-items:center;gap:5px;font-size:13px;font-weight:600;color:#64748b;background:none;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;padding:8px 16px;">
|
||||
← Back to edit
|
||||
</button>
|
||||
<button type="button" onclick="document.getElementById('sup-form').submit()"
|
||||
style="padding:8px 22px;background:#16a34a;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;">
|
||||
Confirm & Send →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---- Supplier modal ----
|
||||
var _supTab = 'global';
|
||||
|
||||
function openSupplierModal() {
|
||||
document.getElementById('supplier-modal').classList.add('open');
|
||||
goBack();
|
||||
}
|
||||
function closeSupplierModal() {
|
||||
closeAllItemDd();
|
||||
document.getElementById('supplier-modal').classList.remove('open');
|
||||
}
|
||||
function showStep(method) {
|
||||
_supTab = method;
|
||||
document.getElementById('sup-mode').value = method === 'item' ? 'by_item' : 'global';
|
||||
|
||||
var badge = document.getElementById('sup-mode-badge');
|
||||
if (method === 'global') {
|
||||
badge.textContent = '📦 Full Order';
|
||||
badge.style.background = '#eff6ff';
|
||||
badge.style.color = '#2563eb';
|
||||
} else {
|
||||
badge.textContent = '🔀 By Item';
|
||||
badge.style.background = '#f0fdf4';
|
||||
badge.style.color = '#15803d';
|
||||
}
|
||||
|
||||
document.getElementById('sup-global-pane').style.display = method === 'global' ? 'flex' : 'none';
|
||||
document.getElementById('sup-item-pane').style.display = method === 'item' ? 'flex' : 'none';
|
||||
document.getElementById('sup-modal-title').textContent = 'Select Suppliers';
|
||||
document.getElementById('sup-modal-subtitle').textContent = 'Choose who receives the quote request';
|
||||
document.getElementById('sup-mode-badge-row').style.display = 'flex';
|
||||
document.getElementById('sup-step1').style.display = 'none';
|
||||
document.getElementById('sup-step2').style.display = 'flex';
|
||||
|
||||
if (method === 'global') {
|
||||
setTimeout(function(){ document.getElementById('sup-search').focus(); }, 50);
|
||||
}
|
||||
updateFooter();
|
||||
}
|
||||
function goBack() {
|
||||
document.querySelectorAll('#sup-form input[type="checkbox"]:not([disabled])').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
document.querySelectorAll('[id^="gchan-"]').forEach(function(el) { el.style.display = 'none'; });
|
||||
var searchEl = document.getElementById('sup-search');
|
||||
if (searchEl) { searchEl.value = ''; filterGlobalSups(''); }
|
||||
document.querySelectorAll('[id^="idd-label-"]').forEach(function(el) {
|
||||
el.textContent = 'Select suppliers…';
|
||||
el.style.color = '#94a3b8';
|
||||
});
|
||||
document.querySelectorAll('[id^="ichan-item-btn-"]').forEach(function(el) {
|
||||
el.style.opacity = '.35'; el.style.pointerEvents = 'none';
|
||||
});
|
||||
document.querySelectorAll('[id^="ichan-ie-"]').forEach(function(el) { el.style.background='#eff6ff'; el.style.color='#2563eb'; });
|
||||
document.querySelectorAll('[id^="ichan-iw-"],[id^="ichan-ib-"]').forEach(function(el) { el.style.background='#fff'; el.style.color='#94a3b8'; });
|
||||
_itemChan = {};
|
||||
|
||||
document.getElementById('sup-mode').value = '';
|
||||
document.getElementById('sup-modal-title').textContent = 'Request for Quotation';
|
||||
document.getElementById('sup-modal-subtitle').textContent = 'How do you want to assign suppliers?';
|
||||
document.getElementById('sup-mode-badge-row').style.display = 'none';
|
||||
document.getElementById('sup-step1').style.display = 'flex';
|
||||
document.getElementById('sup-step2').style.display = 'none';
|
||||
document.getElementById('sup-step3').style.display = 'none';
|
||||
closeAllItemDd();
|
||||
}
|
||||
document.getElementById('supplier-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeSupplierModal();
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
if (typeof closeSignModal === 'function') closeSignModal();
|
||||
closeSupplierModal();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Global tab helpers ──
|
||||
function filterGlobalSups(q) {
|
||||
q = q.toLowerCase().trim();
|
||||
var items = document.querySelectorAll('.g-sup-item');
|
||||
var visible = 0;
|
||||
items.forEach(function(item) {
|
||||
var match = !q || item.dataset.name.indexOf(q) !== -1;
|
||||
item.style.display = match ? 'flex' : 'none';
|
||||
if (match) visible++;
|
||||
});
|
||||
document.getElementById('no-sup-msg').style.display = (visible === 0 && q) ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function toggleGlobalChan(id, checked) {
|
||||
var el = document.getElementById('gchan-' + id);
|
||||
if (el) el.style.display = checked ? 'block' : 'none';
|
||||
updateFooter();
|
||||
}
|
||||
|
||||
var chanStyles = {
|
||||
email: { bg:'#eff6ff', fg:'#2563eb' },
|
||||
whatsapp: { bg:'#f0fdf4', fg:'#15803d' },
|
||||
both: { bg:'#fef3c7', fg:'#92400e' },
|
||||
};
|
||||
function applyBtnStyles(prefix, id, val) {
|
||||
[['email','e'],['whatsapp','w'],['both','b']].forEach(function(p) {
|
||||
var el = document.getElementById(prefix + p[1] + '-' + id);
|
||||
if (!el) return;
|
||||
if (p[0] === val) { el.style.background = chanStyles[val].bg; el.style.color = chanStyles[val].fg; }
|
||||
else { el.style.background = '#fff'; el.style.color = '#94a3b8'; }
|
||||
});
|
||||
}
|
||||
function setGChan(id, val) {
|
||||
document.getElementById('gchan-val-' + id).value = val;
|
||||
applyBtnStyles('gc', id, val);
|
||||
}
|
||||
|
||||
function updateGlobalCount() { updateFooter(); }
|
||||
|
||||
// ── By-item tab helpers ──
|
||||
var _openItemDd = null;
|
||||
var _itemChan = {};
|
||||
|
||||
function setItemChan(itemId, val) {
|
||||
_itemChan[itemId] = val;
|
||||
['e','w','b'].forEach(function(k) {
|
||||
var map = { e:'email', w:'whatsapp', b:'both' };
|
||||
var el = document.getElementById('ichan-i' + k + '-' + itemId);
|
||||
if (!el) return;
|
||||
if (map[k] === val) { el.style.background = chanStyles[val].bg; el.style.color = chanStyles[val].fg; }
|
||||
else { el.style.background = '#fff'; el.style.color = '#94a3b8'; }
|
||||
});
|
||||
document.querySelectorAll('input[name="item_suppliers[' + itemId + '][]"]:checked:not([disabled])').forEach(function(cb) {
|
||||
var inp = document.getElementById('ichan-val-' + cb.value);
|
||||
if (inp) inp.value = val;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleItemDd(event, itemId) {
|
||||
event.stopPropagation();
|
||||
var dd = document.getElementById('idd-' + itemId);
|
||||
if (!dd) return;
|
||||
|
||||
if (_openItemDd === itemId) {
|
||||
dd.style.display = 'none';
|
||||
_openItemDd = null;
|
||||
return;
|
||||
}
|
||||
closeAllItemDd();
|
||||
|
||||
var btn = document.getElementById('idd-btn-' + itemId);
|
||||
var rect = btn.getBoundingClientRect();
|
||||
var ddWidth = 260;
|
||||
var left = Math.min(rect.left, window.innerWidth - ddWidth - 8);
|
||||
dd.style.left = left + 'px';
|
||||
dd.style.top = (rect.bottom + 4) + 'px';
|
||||
dd.style.width = ddWidth + 'px';
|
||||
dd.style.display = 'block';
|
||||
_openItemDd = itemId;
|
||||
|
||||
var inp = dd.querySelector('input[type="text"]');
|
||||
if (inp) setTimeout(function(){ inp.focus(); }, 30);
|
||||
}
|
||||
|
||||
function closeAllItemDd() {
|
||||
if (_openItemDd !== null) {
|
||||
var dd = document.getElementById('idd-' + _openItemDd);
|
||||
if (dd) dd.style.display = 'none';
|
||||
_openItemDd = null;
|
||||
}
|
||||
}
|
||||
|
||||
function filterItemDd(itemId, q) {
|
||||
q = q.toLowerCase().trim();
|
||||
var rows = document.querySelectorAll('.idd-row-' + itemId);
|
||||
var visible = 0;
|
||||
rows.forEach(function(row) {
|
||||
var match = !q || (row.dataset.name || '').indexOf(q) !== -1;
|
||||
row.style.display = match ? 'flex' : 'none';
|
||||
if (match) visible++;
|
||||
});
|
||||
var noEl = document.getElementById('idd-no-' + itemId);
|
||||
if (noEl) noEl.style.display = (visible === 0 && q) ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function updateItemDdLabel(itemId) {
|
||||
var checked = document.querySelectorAll('[id^="isup-' + itemId + '-"]:checked:not([disabled])');
|
||||
var label = document.getElementById('idd-label-' + itemId);
|
||||
if (!label) return;
|
||||
if (checked.length === 0) {
|
||||
label.textContent = 'Select suppliers…';
|
||||
label.style.color = '#94a3b8';
|
||||
} else {
|
||||
var names = Array.from(checked).map(function(c) { return c.dataset.supname || c.value; });
|
||||
label.textContent = names.join(', ');
|
||||
label.style.color = '#0f172a';
|
||||
}
|
||||
}
|
||||
|
||||
function onItemSupChange(itemId, supId) {
|
||||
updateItemDdLabel(itemId);
|
||||
|
||||
// Activate/deactivate this item's channel picker
|
||||
var anyCheckedForItem = document.querySelectorAll('input[name="item_suppliers[' + itemId + '][]"]:checked:not([disabled])').length > 0;
|
||||
var picker = document.getElementById('ichan-item-btn-' + itemId);
|
||||
if (picker) {
|
||||
picker.style.opacity = anyCheckedForItem ? '1' : '.35';
|
||||
picker.style.pointerEvents = anyCheckedForItem ? 'auto' : 'none';
|
||||
}
|
||||
|
||||
// Apply this item's current channel to the newly checked supplier
|
||||
var cb = document.getElementById('isup-' + itemId + '-' + supId);
|
||||
if (cb && cb.checked) {
|
||||
var chan = _itemChan[itemId] || 'email';
|
||||
var inp = document.getElementById('ichan-val-' + supId);
|
||||
if (inp) inp.value = chan;
|
||||
}
|
||||
|
||||
updateFooter();
|
||||
}
|
||||
|
||||
function setIChan(id, val) {
|
||||
document.getElementById('ichan-val-' + id).value = val;
|
||||
applyBtnStyles('ic', id, val);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (_openItemDd === null) return;
|
||||
var dd = document.getElementById('idd-' + _openItemDd);
|
||||
var btn = document.getElementById('idd-btn-' + _openItemDd);
|
||||
if (dd && !dd.contains(e.target) && btn && !btn.contains(e.target)) {
|
||||
closeAllItemDd();
|
||||
}
|
||||
});
|
||||
|
||||
function updateFooter() {
|
||||
var msg = document.getElementById('sup-footer-msg');
|
||||
if (_supTab === 'global') {
|
||||
var n = document.querySelectorAll('#sup-list input[type="checkbox"]:checked:not([disabled])').length;
|
||||
msg.textContent = n + ' supplier' + (n === 1 ? '' : 's') + ' selected';
|
||||
} else {
|
||||
var checked = document.querySelectorAll('input[name^="item_suppliers["]:checked:not([disabled])');
|
||||
var supIds = new Set();
|
||||
checked.forEach(function(c) { supIds.add(c.value); });
|
||||
var n = supIds.size;
|
||||
msg.textContent = n + ' supplier' + (n === 1 ? '' : 's') + ' assigned';
|
||||
}
|
||||
}
|
||||
|
||||
function submitSuppliers() {
|
||||
if (_supTab === 'global') {
|
||||
var checked = document.querySelectorAll('#sup-list input[type="checkbox"]:checked:not([disabled])');
|
||||
if (checked.length === 0) { showToast('Please select at least one supplier.', 'warn'); return; }
|
||||
document.getElementById('sup-form').submit();
|
||||
} else {
|
||||
var checked = document.querySelectorAll('input[name^="item_suppliers["]:checked:not([disabled])');
|
||||
if (checked.length === 0) { showToast('Please assign at least one supplier to an item.', 'warn'); return; }
|
||||
showSummary();
|
||||
}
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function showSummary() {
|
||||
var chanLabel = {
|
||||
email: '<span style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:8px;font-size:12px;font-weight:700;">Email</span>',
|
||||
whatsapp: '<span style="background:#f0fdf4;color:#15803d;padding:3px 10px;border-radius:8px;font-size:12px;font-weight:700;">WhatsApp</span>',
|
||||
both: '<span style="background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:8px;font-size:12px;font-weight:700;">Email + WA</span>',
|
||||
};
|
||||
var html = '<div style="font-size:13px;font-weight:700;color:#0f172a;margin-bottom:16px;">Review assignments before sending</div>';
|
||||
|
||||
var btns = document.querySelectorAll('[id^="idd-btn-"]');
|
||||
btns.forEach(function(btn) {
|
||||
var itemId = btn.id.replace('idd-btn-', '');
|
||||
var checked = document.querySelectorAll('input[name="item_suppliers[' + itemId + '][]"]:checked:not([disabled])');
|
||||
if (checked.length === 0) return;
|
||||
|
||||
var name = btn.dataset.itemname || ('Item ' + itemId);
|
||||
var qty = btn.dataset.itemqty || '';
|
||||
|
||||
html += '<div style="margin-bottom:12px;border:1.5px solid #e2e8f0;border-radius:10px;overflow:hidden;">';
|
||||
html += '<div style="padding:10px 14px;background:#f8fafc;border-bottom:1px solid #e2e8f0;">';
|
||||
html += '<div style="font-size:13px;font-weight:700;color:#0f172a;">' + escHtml(name) + '</div>';
|
||||
if (qty) html += '<div style="font-size:11px;color:#94a3b8;margin-top:1px;">Qty: ' + escHtml(qty) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
checked.forEach(function(cb) {
|
||||
var supName = cb.dataset.supname || cb.value;
|
||||
var chan = (document.getElementById('ichan-val-' + cb.value) || {}).value || 'email';
|
||||
var cl = chanLabel[chan] || '<span style="background:#f1f5f9;color:#475569;padding:3px 10px;border-radius:8px;font-size:12px;font-weight:700;">' + escHtml(chan) + '</span>';
|
||||
html += '<div style="padding:9px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #f8fafc;">';
|
||||
html += '<span style="font-size:13px;font-weight:600;color:#0f172a;">' + escHtml(supName) + '</span>';
|
||||
html += cl;
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
document.getElementById('sup-summary-body').innerHTML = html;
|
||||
document.getElementById('sup-modal-title').textContent = 'Confirm Assignments';
|
||||
document.getElementById('sup-modal-subtitle').textContent = 'Review before sending to suppliers';
|
||||
document.getElementById('sup-step2').style.display = 'none';
|
||||
document.getElementById('sup-step3').style.display = 'flex';
|
||||
}
|
||||
|
||||
function backToEdit() {
|
||||
document.getElementById('sup-modal-title').textContent = 'Select Suppliers';
|
||||
document.getElementById('sup-modal-subtitle').textContent = 'Choose who receives the quote request';
|
||||
document.getElementById('sup-step3').style.display = 'none';
|
||||
document.getElementById('sup-step2').style.display = 'flex';
|
||||
}
|
||||
</script>
|
||||
Reference in new issue
Block a user