feat: RFQ portal, notifications, and project settings updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
72e6c3170e
commit
dca9cd5d99
@ -36,6 +36,9 @@ class RfqController extends Controller
|
||||
}
|
||||
|
||||
if (empty($supplierItems)) {
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => 'Please assign at least one supplier to an item.'], 422);
|
||||
}
|
||||
return redirect()->back()->with('error', 'Please assign at least one supplier to an item.');
|
||||
}
|
||||
|
||||
@ -67,6 +70,22 @@ class RfqController extends Controller
|
||||
|
||||
$stages->setStage($purchaseRequest, 'rfq');
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
$invitations = $purchaseRequest->rfqInvitations()->with('supplier')->get()->map(function ($inv) {
|
||||
return [
|
||||
'supplier_name' => $inv->supplier->name,
|
||||
'channel' => $inv->channel,
|
||||
'url' => route('rfq.show', $inv->token),
|
||||
'status' => $inv->status,
|
||||
];
|
||||
});
|
||||
return response()->json([
|
||||
'added' => $added,
|
||||
'invitations' => $invitations,
|
||||
'redirect' => route('purchase.pipeline.show', $purchaseRequest),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('purchase.pipeline.show', $purchaseRequest)
|
||||
->with('success', $added . ' supplier(s) added. Now send them the quote request links.');
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\RfqInvitation;
|
||||
use App\Models\SupplierQuote;
|
||||
use App\Models\SupplierQuoteItem;
|
||||
use App\Models\User;
|
||||
use App\Notifications\QuoteReceived;
|
||||
use App\Services\PurchaseStageService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -35,9 +37,16 @@ class RfqPortalController extends Controller
|
||||
}
|
||||
|
||||
$purchaseRequest = $invitation->purchaseRequest;
|
||||
$items = $purchaseRequest->items;
|
||||
$itemIds = $invitation->item_ids;
|
||||
$items = $itemIds
|
||||
? $purchaseRequest->items->whereIn('id', $itemIds)->values()
|
||||
: $purchaseRequest->items;
|
||||
|
||||
return view('rfq.show', compact('invitation', 'purchaseRequest', 'items'));
|
||||
// Generate a fresh confirmation code per page load and store in session
|
||||
$confirmCode = strtoupper(substr(bin2hex(random_bytes(3)), 0, 5));
|
||||
session(['rfq_confirm_' . $token => $confirmCode]);
|
||||
|
||||
return view('rfq.show', compact('invitation', 'purchaseRequest', 'items', 'confirmCode'));
|
||||
}
|
||||
|
||||
public function submit(Request $request, string $token)
|
||||
@ -49,6 +58,8 @@ class RfqPortalController extends Controller
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'terms' => ['accepted'],
|
||||
'confirm_code' => ['required', 'string'],
|
||||
'lead_time_days' => ['nullable', 'integer', 'min:0'],
|
||||
'payment_terms' => ['nullable', 'string', 'max:200'],
|
||||
'notes' => ['nullable', 'string', 'max:1000'],
|
||||
@ -56,7 +67,16 @@ class RfqPortalController extends Controller
|
||||
'items.*.unit_price' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
$purchaseItems = $invitation->purchaseRequest->items;
|
||||
$expectedCode = session('rfq_confirm_' . $token);
|
||||
if (!$expectedCode || strtoupper(trim($validated['confirm_code'])) !== $expectedCode) {
|
||||
return back()->withErrors(['confirm_code' => 'Incorrect confirmation code. Please copy the code exactly as shown.'])->withInput();
|
||||
}
|
||||
session()->forget('rfq_confirm_' . $token);
|
||||
|
||||
$itemIds = $invitation->item_ids;
|
||||
$purchaseItems = $itemIds
|
||||
? $invitation->purchaseRequest->items->whereIn('id', $itemIds)->values()
|
||||
: $invitation->purchaseRequest->items;
|
||||
|
||||
$quote = SupplierQuote::create([
|
||||
'rfq_invitation_id' => $invitation->id,
|
||||
@ -72,7 +92,7 @@ class RfqPortalController extends Controller
|
||||
$total = 0;
|
||||
foreach ($purchaseItems as $i => $item) {
|
||||
$unitPrice = (float)($validated['items'][$i]['unit_price'] ?? 0);
|
||||
$qty = (float)$item->quantity;
|
||||
$qty = (float)$item->quantity_required;
|
||||
$totalPrice = round($unitPrice * $qty, 3);
|
||||
$total += $totalPrice;
|
||||
|
||||
@ -95,6 +115,10 @@ class RfqPortalController extends Controller
|
||||
app(PurchaseStageService::class)->setStage($pr, 'comparison');
|
||||
}
|
||||
|
||||
// Notify all admin users
|
||||
$invitation->load('supplier', 'purchaseRequest');
|
||||
User::role('Admin')->each(fn($u) => $u->notify(new QuoteReceived($invitation)));
|
||||
|
||||
return view('rfq.submitted', compact('invitation'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +187,7 @@ class ProjectSettingController extends Controller
|
||||
|
||||
$parts = [];
|
||||
if ($stats['projects_created']) $parts[] = "{$stats['projects_created']} project(s)";
|
||||
if ($stats['locations_created']) $parts[] = "{$stats['locations_created']} location(s)";
|
||||
if ($stats['departments_created']) $parts[] = "{$stats['departments_created']} department(s)";
|
||||
if ($stats['companies_created']) $parts[] = "{$stats['companies_created']} new company(s)";
|
||||
|
||||
@ -220,29 +221,41 @@ class ProjectSettingController extends Controller
|
||||
'font' => ['italic' => true, 'color' => ['rgb' => '64748b'], 'size' => 10],
|
||||
];
|
||||
|
||||
// ── Sheet 1: Projects ──────────────────────────────────────────────
|
||||
// ── Sheet 1: Projects (with optional locations) ────────────────────
|
||||
$s1 = $spreadsheet->getActiveSheet()->setTitle('Projects');
|
||||
$s1->setCellValue('A1', 'Company Name')
|
||||
->setCellValue('B1', 'Project Name');
|
||||
$s1->getStyle('A1:B1')->applyFromArray($headerStyle);
|
||||
->setCellValue('B1', 'Project Name')
|
||||
->setCellValue('C1', 'Location Name')
|
||||
->setCellValue('D1', 'Address')
|
||||
->setCellValue('E1', 'Latitude')
|
||||
->setCellValue('F1', 'Longitude');
|
||||
$s1->getStyle('A1:F1')->applyFromArray($headerStyle);
|
||||
|
||||
// Sample rows
|
||||
$samples = [
|
||||
['Miknas Industrial', 'New Warehouse'],
|
||||
['Steel tech', 'Factory Extension'],
|
||||
['Steel tech', 'New Office Block'],
|
||||
['Miknas Industrial', 'New Warehouse', 'Main Gate', 'Industrial Area, Block 5', '24.7136', '46.6753'],
|
||||
['Miknas Industrial', 'New Warehouse', 'Storage Yard', '', '', ''],
|
||||
['Steel tech', 'Factory Extension','Site Office', '2nd Ring Road, Riyadh', '24.6877', '46.7219'],
|
||||
['Steel tech', 'New Office Block', '', '', '', ''],
|
||||
];
|
||||
foreach ($samples as $i => $row) {
|
||||
$s1->setCellValue('A' . ($i + 2), $row[0]);
|
||||
$s1->setCellValue('B' . ($i + 2), $row[1]);
|
||||
$s1->setCellValue('C' . ($i + 2), $row[2]);
|
||||
$s1->setCellValue('D' . ($i + 2), $row[3]);
|
||||
$s1->setCellValue('E' . ($i + 2), $row[4]);
|
||||
$s1->setCellValue('F' . ($i + 2), $row[5]);
|
||||
}
|
||||
|
||||
$s1->setCellValue('A6', '* Delete sample rows before importing. Company will be created if it does not exist.');
|
||||
$s1->getStyle('A6')->applyFromArray($noteStyle);
|
||||
$s1->mergeCells('A6:B6');
|
||||
$s1->setCellValue('A7', '* Leave Location Name blank for rows that are projects only. Company is created automatically. Duplicates are skipped.');
|
||||
$s1->getStyle('A7')->applyFromArray($noteStyle);
|
||||
$s1->mergeCells('A7:F7');
|
||||
|
||||
$s1->getColumnDimension('A')->setWidth(32);
|
||||
$s1->getColumnDimension('B')->setWidth(32);
|
||||
$s1->getColumnDimension('A')->setWidth(28);
|
||||
$s1->getColumnDimension('B')->setWidth(28);
|
||||
$s1->getColumnDimension('C')->setWidth(24);
|
||||
$s1->getColumnDimension('D')->setWidth(32);
|
||||
$s1->getColumnDimension('E')->setWidth(14);
|
||||
$s1->getColumnDimension('F')->setWidth(14);
|
||||
|
||||
// ── Sheet 2: Departments ───────────────────────────────────────────
|
||||
$s2 = new Worksheet($spreadsheet, 'Departments');
|
||||
|
||||
@ -41,8 +41,11 @@ class User extends Authenticatable
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function routeNotificationFor(string $channel, mixed $notification = null): ?string
|
||||
public function routeNotificationFor(string $channel, mixed $notification = null): mixed
|
||||
{
|
||||
if ($channel === 'database') {
|
||||
return $this->notifications();
|
||||
}
|
||||
return $this->whatsapp_number;
|
||||
}
|
||||
|
||||
|
||||
32
app/Notifications/QuoteReceived.php
Normal file
32
app/Notifications/QuoteReceived.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\RfqInvitation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class QuoteReceived extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(public RfqInvitation $invitation) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toDatabase(object $notifiable): array
|
||||
{
|
||||
$pr = $this->invitation->purchaseRequest;
|
||||
return [
|
||||
'type' => 'quote_received',
|
||||
'message' => $this->invitation->supplier->name . ' submitted a quote for ' . $pr->request_number,
|
||||
'supplier_name' => $this->invitation->supplier->name,
|
||||
'request_number' => $pr->request_number,
|
||||
'purchase_request_id' => $pr->id,
|
||||
'url' => route('purchase.pipeline.show', $pr),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ class ProjectImportService
|
||||
private array $stats = [
|
||||
'companies_created' => 0,
|
||||
'projects_created' => 0,
|
||||
'locations_created' => 0,
|
||||
'departments_created' => 0,
|
||||
'skipped' => 0,
|
||||
];
|
||||
@ -56,8 +57,12 @@ class ProjectImportService
|
||||
$rows = $sheet->toArray(null, true, true, false);
|
||||
$headers = $this->normalizeHeaders((array) array_shift($rows));
|
||||
|
||||
$coIdx = $this->findCol($headers, ['company', 'company name', 'company name', 'companyname']);
|
||||
$projIdx = $this->findCol($headers, ['project', 'project name', 'project name', 'projectname']);
|
||||
$coIdx = $this->findCol($headers, ['company', 'company name', 'companyname']);
|
||||
$projIdx = $this->findCol($headers, ['project', 'project name', 'projectname']);
|
||||
$locIdx = $this->findCol($headers, ['location', 'location name', 'locationname', 'loc', 'loc name']);
|
||||
$addrIdx = $this->findCol($headers, ['address', 'addr']);
|
||||
$latIdx = $this->findCol($headers, ['latitude', 'lat']);
|
||||
$lngIdx = $this->findCol($headers, ['longitude', 'lng', 'lon', 'long']);
|
||||
|
||||
if ($coIdx === null || $projIdx === null) {
|
||||
return;
|
||||
@ -73,16 +78,37 @@ class ProjectImportService
|
||||
}
|
||||
|
||||
$company = $this->findOrCreateCompany($coName);
|
||||
$existing = ProjectSetting::whereRaw('LOWER(name) = ?', [strtolower($projName)])
|
||||
$project = ProjectSetting::whereRaw('LOWER(name) = ?', [strtolower($projName)])
|
||||
->where('company_id', $company->id)->first();
|
||||
|
||||
if ($existing) {
|
||||
$this->stats['skipped']++;
|
||||
continue;
|
||||
if (!$project) {
|
||||
$project = ProjectSetting::create(['name' => $projName, 'company_id' => $company->id, 'is_active' => true]);
|
||||
$this->stats['projects_created']++;
|
||||
}
|
||||
|
||||
ProjectSetting::create(['name' => $projName, 'company_id' => $company->id, 'is_active' => true]);
|
||||
$this->stats['projects_created']++;
|
||||
// If a location name is present on this row, import it too
|
||||
$locName = $locIdx !== null ? $this->str($row[$locIdx] ?? null) : null;
|
||||
if ($locName) {
|
||||
$existingLoc = $project->locations()
|
||||
->whereRaw('LOWER(name) = ?', [strtolower($locName)])->first();
|
||||
|
||||
if ($existingLoc) {
|
||||
$this->stats['skipped']++;
|
||||
} else {
|
||||
$address = $addrIdx !== null ? $this->str($row[$addrIdx] ?? null) : null;
|
||||
$lat = $latIdx !== null ? $this->str($row[$latIdx] ?? null) : null;
|
||||
$lng = $lngIdx !== null ? $this->str($row[$lngIdx] ?? null) : null;
|
||||
|
||||
$project->locations()->create([
|
||||
'name' => $locName,
|
||||
'address' => $address,
|
||||
'latitude' => is_numeric($lat) ? (float) $lat : null,
|
||||
'longitude' => is_numeric($lng) ? (float) $lng : null,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$this->stats['locations_created']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('type');
|
||||
$table->morphs('notifiable');
|
||||
$table->text('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
@ -41,6 +41,10 @@ $editProjectsJson = json_encode($editProjectsData);
|
||||
$curProjectInList = $editProjects->contains('name', $curProject);
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
.{{ $prefix }}-urgency-opt:hover { background:#f8fafc; }
|
||||
</style>
|
||||
|
||||
{{-- ── Trigger button ── --}}
|
||||
<button type="button" onclick="{{ $prefix }}Open()" class="btn-secondary btn-sm">
|
||||
Edit
|
||||
@ -119,9 +123,67 @@ $curProjectInList = $editProjects->contains('name', $curProject);
|
||||
<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>
|
||||
<div style="position:relative;" id="{{ $prefix }}UrgencyWrapper">
|
||||
<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">
|
||||
<input type="hidden" name="required_date_text" id="{{ $prefix }}UrgencyValue" value="{{ $curReqDate }}">
|
||||
{{-- Trigger --}}
|
||||
<button type="button" id="{{ $prefix }}UrgencyTrigger" onclick="{{ $prefix }}UrgencyToggle()"
|
||||
style="width:100%;text-align:left;background:white;border:1.5px dashed #d1d5db;border-radius:0.5rem;padding:0.45rem 2rem 0.45rem 0.75rem;font-size:0.8rem;cursor:pointer;position:relative;min-height:2.35rem;display:flex;align-items:center;transition:border-color 0.15s;">
|
||||
<span id="{{ $prefix }}UrgencyDisplay" style="display:flex;align-items:center;gap:0.5rem;flex:1;">
|
||||
<span style="color:#9ca3af;font-size:0.8rem;">— Select urgency —</span>
|
||||
</span>
|
||||
<span style="position:absolute;right:0.6rem;top:50%;transform:translateY(-50%);color:#9ca3af;font-size:0.7rem;">▼</span>
|
||||
</button>
|
||||
{{-- Panel --}}
|
||||
<div id="{{ $prefix }}UrgencyPanel"
|
||||
style="display:none;position:absolute;z-index:10000;top:calc(100% + 4px);left:0;right:0;background:white;border:1px solid #e2e8f0;border-radius:0.75rem;box-shadow:0 12px 32px -4px rgba(0,0,0,0.18);overflow:hidden;">
|
||||
<ul style="list-style:none;margin:0;padding:0.375rem;">
|
||||
<li onclick="{{ $prefix }}UrgencySelect('Urgent','Urgent','#dc2626','#fef2f2')"
|
||||
class="{{ $prefix }}-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#fef2f2;display:flex;align-items:center;justify-content:center;"><span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#dc2626;"></span></span>
|
||||
<div><div style="font-size:0.8rem;font-weight:700;color:#dc2626;line-height:1.2;">Urgent</div><div style="font-size:0.68rem;color:#9ca3af;">Needed immediately</div></div>
|
||||
</li>
|
||||
<li onclick="{{ $prefix }}UrgencySelect('3 Days','3 Days','#ea580c','#fff7ed')"
|
||||
class="{{ $prefix }}-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#fff7ed;display:flex;align-items:center;justify-content:center;"><span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#ea580c;"></span></span>
|
||||
<div><div style="font-size:0.8rem;font-weight:700;color:#ea580c;line-height:1.2;">3 Days</div><div style="font-size:0.68rem;color:#9ca3af;">Within 3 days</div></div>
|
||||
</li>
|
||||
<li onclick="{{ $prefix }}UrgencySelect('1 Week','1 Week','#d97706','#fffbeb')"
|
||||
class="{{ $prefix }}-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#fffbeb;display:flex;align-items:center;justify-content:center;"><span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#d97706;"></span></span>
|
||||
<div><div style="font-size:0.8rem;font-weight:700;color:#d97706;line-height:1.2;">1 Week</div><div style="font-size:0.68rem;color:#9ca3af;">Within this week</div></div>
|
||||
</li>
|
||||
<li onclick="{{ $prefix }}UrgencySelect('2 Weeks','2 Weeks','#2563eb','#eff6ff')"
|
||||
class="{{ $prefix }}-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#eff6ff;display:flex;align-items:center;justify-content:center;"><span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#2563eb;"></span></span>
|
||||
<div><div style="font-size:0.8rem;font-weight:700;color:#2563eb;line-height:1.2;">2 Weeks</div><div style="font-size:0.68rem;color:#9ca3af;">Within 2 weeks</div></div>
|
||||
</li>
|
||||
<li onclick="{{ $prefix }}UrgencySelect('1 Month','1 Month','#16a34a','#f0fdf4')"
|
||||
class="{{ $prefix }}-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#f0fdf4;display:flex;align-items:center;justify-content:center;"><span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#16a34a;"></span></span>
|
||||
<div><div style="font-size:0.8rem;font-weight:700;color:#16a34a;line-height:1.2;">1 Month</div><div style="font-size:0.68rem;color:#9ca3af;">Within a month</div></div>
|
||||
</li>
|
||||
<li style="height:1px;background:#f1f5f9;margin:0.25rem 0;"></li>
|
||||
<li id="{{ $prefix }}UrgencyDateOpt" onclick="{{ $prefix }}UrgencyDateOptClick()"
|
||||
class="{{ $prefix }}-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#f5f3ff;display:flex;align-items:center;justify-content:center;">
|
||||
<svg style="width:0.875rem;height:0.875rem;stroke:#7c3aed;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
</span>
|
||||
<div style="flex:1;"><div style="font-size:0.8rem;font-weight:700;color:#7c3aed;line-height:1.2;">Specific Date</div><div style="font-size:0.68rem;color:#9ca3af;">Pick an exact date</div></div>
|
||||
</li>
|
||||
<li id="{{ $prefix }}UrgencyDatePickerRow" style="display:none;padding:0 0.75rem 0.5rem;">
|
||||
<input type="date" id="{{ $prefix }}UrgencyDate"
|
||||
style="width:100%;font-size:0.8rem;border:1.5px solid #7c3aed;border-radius:0.4rem;padding:0.35rem 0.5rem;outline:none;color:#7c3aed;box-sizing:border-box;"
|
||||
onchange="{{ $prefix }}UrgencyDateChange(this.value)">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Location / Site</label>
|
||||
@ -315,6 +377,87 @@ $curProjectInList = $editProjects->contains('name', $curProject);
|
||||
}
|
||||
window.{{ $prefix }}FilterLoc = {{ $prefix }}FilterLoc;
|
||||
|
||||
// ── Urgency colored dropdown ──────────────────────────────────────────────
|
||||
var _urgencyMap{{ $prId }} = {
|
||||
'Urgent': ['Urgent', '#dc2626','#fef2f2'],
|
||||
'3 Days': ['3 Days', '#ea580c','#fff7ed'],
|
||||
'1 Week': ['1 Week', '#d97706','#fffbeb'],
|
||||
'2 Weeks': ['2 Weeks', '#2563eb','#eff6ff'],
|
||||
'1 Month': ['1 Month', '#16a34a','#f0fdf4'],
|
||||
};
|
||||
|
||||
function _urgencyBadge{{ $prId }}(label, color, bgColor) {
|
||||
return '<span style="display:inline-flex;align-items:center;gap:0.4rem;background:' + bgColor + ';border:1px solid ' + color + '20;padding:0.15rem 0.6rem 0.15rem 0.4rem;border-radius:999px;">' +
|
||||
'<span style="width:0.45rem;height:0.45rem;border-radius:50%;background:' + color + ';flex-shrink:0;"></span>' +
|
||||
'<span style="font-size:0.78rem;font-weight:600;color:' + color + ';">' + label + '</span></span>';
|
||||
}
|
||||
|
||||
function _urgencyDateBadge{{ $prId }}(label) {
|
||||
return '<span style="display:inline-flex;align-items:center;gap:0.4rem;background:#f5f3ff;border:1px solid #7c3aed20;padding:0.15rem 0.6rem 0.15rem 0.4rem;border-radius:999px;">' +
|
||||
'<svg style="width:0.7rem;height:0.7rem;stroke:#7c3aed;flex-shrink:0;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>' +
|
||||
'<span style="font-size:0.78rem;font-weight:600;color:#7c3aed;">' + label + '</span></span>';
|
||||
}
|
||||
|
||||
window.{{ $prefix }}UrgencyToggle = function() {
|
||||
var p = document.getElementById('{{ $prefix }}UrgencyPanel');
|
||||
p.style.display = p.style.display === 'none' ? 'block' : 'none';
|
||||
};
|
||||
|
||||
window.{{ $prefix }}UrgencyClose = function() {
|
||||
document.getElementById('{{ $prefix }}UrgencyPanel').style.display = 'none';
|
||||
};
|
||||
|
||||
window.{{ $prefix }}UrgencySelect = function(value, label, color, bgColor) {
|
||||
document.getElementById('{{ $prefix }}UrgencyValue').value = value;
|
||||
document.getElementById('{{ $prefix }}UrgencyDisplay').innerHTML = _urgencyBadge{{ $prId }}(label, color, bgColor);
|
||||
var t = document.getElementById('{{ $prefix }}UrgencyTrigger');
|
||||
t.style.borderColor = color; t.style.borderStyle = 'solid';
|
||||
document.getElementById('{{ $prefix }}UrgencyDatePickerRow').style.display = 'none';
|
||||
{{ $prefix }}UrgencyClose();
|
||||
};
|
||||
|
||||
window.{{ $prefix }}UrgencyDateOptClick = function() {
|
||||
var row = document.getElementById('{{ $prefix }}UrgencyDatePickerRow');
|
||||
row.style.display = row.style.display === 'none' ? '' : 'none';
|
||||
if (row.style.display !== 'none') document.getElementById('{{ $prefix }}UrgencyDate').focus();
|
||||
};
|
||||
|
||||
window.{{ $prefix }}UrgencyDateChange = function(val) {
|
||||
if (!val) return;
|
||||
document.getElementById('{{ $prefix }}UrgencyValue').value = val;
|
||||
var d = new Date(val + 'T00:00:00');
|
||||
var label = d.toLocaleDateString('en-GB', {day:'2-digit', month:'short', year:'numeric'});
|
||||
document.getElementById('{{ $prefix }}UrgencyDisplay').innerHTML = _urgencyDateBadge{{ $prId }}(label);
|
||||
var t = document.getElementById('{{ $prefix }}UrgencyTrigger');
|
||||
t.style.borderColor = '#7c3aed'; t.style.borderStyle = 'solid';
|
||||
{{ $prefix }}UrgencyClose();
|
||||
};
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', function(e) {
|
||||
var w = document.getElementById('{{ $prefix }}UrgencyWrapper');
|
||||
if (w && !w.contains(e.target)) {{ $prefix }}UrgencyClose();
|
||||
});
|
||||
|
||||
// Restore saved value on load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var val = document.getElementById('{{ $prefix }}UrgencyValue').value;
|
||||
if (!val) return;
|
||||
if (_urgencyMap{{ $prId }}[val]) {
|
||||
var m = _urgencyMap{{ $prId }}[val];
|
||||
{{ $prefix }}UrgencySelect(val, m[0], m[1], m[2]);
|
||||
} else {
|
||||
document.getElementById('{{ $prefix }}UrgencyDatePickerRow').style.display = '';
|
||||
document.getElementById('{{ $prefix }}UrgencyDate').value = val;
|
||||
var d2 = new Date(val + 'T00:00:00');
|
||||
var lbl = d2.toLocaleDateString('en-GB', {day:'2-digit', month:'short', year:'numeric'});
|
||||
document.getElementById('{{ $prefix }}UrgencyDisplay').innerHTML = _urgencyDateBadge{{ $prId }}(lbl);
|
||||
var t2 = document.getElementById('{{ $prefix }}UrgencyTrigger');
|
||||
t2.style.borderColor = '#7c3aed'; t2.style.borderStyle = 'solid';
|
||||
}
|
||||
});
|
||||
|
||||
// ── Cascading project → location dropdown ─────────────────────────────────
|
||||
// On load: populate locations and pre-select the current ones
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var curProj = document.getElementById('{{ $prefix }}Project').value;
|
||||
|
||||
@ -1,20 +1,38 @@
|
||||
@php
|
||||
$hasErrors = $errors->any();
|
||||
$mprProjects = \App\Models\Settings\ProjectSetting::active()
|
||||
->with(['locations' => function ($q) { $q->where('is_active', true)->orderBy('name'); }])
|
||||
->whereNotNull('company_id')
|
||||
->with([
|
||||
'company',
|
||||
'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,
|
||||
'company_id' => $p->company_id,
|
||||
'company_name' => $p->company ? $p->company->name : '',
|
||||
'label' => ($p->company ? $p->company->name . ' — ' : '') . $p->name,
|
||||
'locations' => $p->locations->map(function ($l) {
|
||||
return ['name' => $l->name];
|
||||
})->values(),
|
||||
];
|
||||
})->values();
|
||||
|
||||
$mprDepartments = \App\Models\Settings\Department::where('is_active', true)->orderBy('name')->get();
|
||||
$mprDepartmentsJson = $mprDepartments->map(fn($d) => ['id' => $d->id, 'name' => $d->name, 'company_id' => $d->company_id])->values();
|
||||
|
||||
$mprUnits = ['PCS','NOS','KG','TON','MTR','SQM','LTR','BAG','BOX','ROLL','SET','EA','CANS','LOT'];
|
||||
$today = date('Y-m-d');
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
.mpr-proj-opt:hover { background:#eff6ff; border-left-color:#2563eb !important; }
|
||||
.mpr-urgency-opt:hover { background:#f8fafc; }
|
||||
</style>
|
||||
|
||||
{{-- Trigger button --}}
|
||||
<button type="button" onclick="mprModalOpen()" class="btn-primary">
|
||||
+ New Request
|
||||
@ -72,24 +90,139 @@ $mprProjectsJson = $mprProjects->map(function ($p) {
|
||||
<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">
|
||||
<input type="date" name="date" value="{{ old('date', $today) }}" required class="form-input">
|
||||
</div>
|
||||
<div>
|
||||
<div style="position:relative;" id="mpr-project-wrapper">
|
||||
<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>
|
||||
{{-- Hidden input carries the actual value for form submission --}}
|
||||
<input type="hidden" name="project_name" id="mpr-project-value" value="{{ old('project_name') }}">
|
||||
{{-- Trigger --}}
|
||||
<button type="button" id="mpr-project-trigger"
|
||||
onclick="mprProjectToggle()"
|
||||
style="width:100%;text-align:left;background:white;border:1px solid #d1d5db;border-radius:0.375rem;padding:0.4rem 2rem 0.4rem 0.625rem;font-size:0.875rem;color:#111827;cursor:pointer;position:relative;line-height:1.5;min-height:2.25rem;">
|
||||
<span id="mpr-project-label" style="color:#9ca3af;">— Select Project —</span>
|
||||
<span style="position:absolute;right:0.5rem;top:50%;transform:translateY(-50%);pointer-events:none;color:#6b7280;">▼</span>
|
||||
</button>
|
||||
{{-- Dropdown panel --}}
|
||||
<div id="mpr-project-panel"
|
||||
style="display:none;position:absolute;z-index:9999;top:calc(100% + 2px);left:0;right:0;background:white;border:1px solid #d1d5db;border-radius:0.5rem;box-shadow:0 10px 25px -5px rgba(0,0,0,0.15);overflow:hidden;">
|
||||
<div style="padding:0.5rem;">
|
||||
<input type="text" id="mpr-project-search" placeholder="Search project or company…"
|
||||
oninput="mprProjectSearch(this.value)"
|
||||
style="width:100%;border:1px solid #e2e8f0;border-radius:0.375rem;padding:0.375rem 0.625rem;font-size:0.8rem;outline:none;box-sizing:border-box;">
|
||||
</div>
|
||||
<ul id="mpr-project-list"
|
||||
style="max-height:13rem;overflow-y:auto;margin:0;padding:0 0 0.25rem 0;list-style:none;">
|
||||
@foreach($mprProjects as $proj)
|
||||
<option value="{{ $proj->name }}" {{ old('project_name') == $proj->name ? 'selected' : '' }} data-id="{{ $proj->id }}">{{ $proj->name }}</option>
|
||||
<li class="mpr-proj-opt"
|
||||
data-value="{{ $proj->name }}"
|
||||
data-label="{{ ($proj->company ? $proj->company->name . ' — ' : '') . $proj->name }}"
|
||||
data-search="{{ strtolower(($proj->company ? $proj->company->name . ' ' : '') . $proj->name) }}"
|
||||
onclick="mprProjectSelect('{{ $proj->name }}', '{{ addslashes(($proj->company ? $proj->company->name . ' — ' : '') . $proj->name) }}')"
|
||||
style="padding:0.45rem 0.875rem;cursor:pointer;font-size:0.8rem;color:#111827;line-height:1.4;border-left:3px solid transparent;">
|
||||
<span style="font-size:0.68rem;color:#6b7280;display:block;line-height:1.2;">{{ $proj->company ? $proj->company->name : '' }}</span>
|
||||
{{ $proj->name }}
|
||||
</li>
|
||||
@endforeach
|
||||
</select>
|
||||
<li id="mpr-proj-empty" style="display:none;padding:0.625rem 0.875rem;font-size:0.8rem;color:#9ca3af;">No projects found.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</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>
|
||||
<div style="position:relative;" id="mpr-urgency-wrapper">
|
||||
<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">
|
||||
<input type="hidden" name="required_date_text" id="mpr-urgency-value" value="{{ old('required_date_text') }}">
|
||||
{{-- Trigger --}}
|
||||
<button type="button" id="mpr-urgency-trigger" onclick="mprUrgencyToggle()"
|
||||
style="width:100%;text-align:left;background:white;border:1.5px dashed #d1d5db;border-radius:0.5rem;padding:0.45rem 2rem 0.45rem 0.75rem;font-size:0.8rem;cursor:pointer;position:relative;min-height:2.35rem;display:flex;align-items:center;transition:border-color 0.15s;">
|
||||
<span id="mpr-urgency-display" style="display:flex;align-items:center;gap:0.5rem;flex:1;">
|
||||
<span style="color:#9ca3af;font-size:0.8rem;">— Select urgency —</span>
|
||||
</span>
|
||||
<span style="position:absolute;right:0.6rem;top:50%;transform:translateY(-50%);color:#9ca3af;font-size:0.7rem;">▼</span>
|
||||
</button>
|
||||
{{-- Panel --}}
|
||||
<div id="mpr-urgency-panel"
|
||||
style="display:none;position:absolute;z-index:10000;top:calc(100% + 4px);left:0;right:0;background:white;border:1px solid #e2e8f0;border-radius:0.75rem;box-shadow:0 12px 32px -4px rgba(0,0,0,0.18);overflow:hidden;">
|
||||
<ul style="list-style:none;margin:0;padding:0.375rem;">
|
||||
<li onclick="mprUrgencySelect('Urgent','Urgent','#dc2626','#fef2f2')"
|
||||
class="mpr-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;transition:background 0.1s;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#fef2f2;display:flex;align-items:center;justify-content:center;">
|
||||
<span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#dc2626;"></span>
|
||||
</span>
|
||||
<div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:#dc2626;line-height:1.2;">Urgent</div>
|
||||
<div style="font-size:0.68rem;color:#9ca3af;">Needed immediately</div>
|
||||
</div>
|
||||
</li>
|
||||
<li onclick="mprUrgencySelect('3 Days','3 Days','#ea580c','#fff7ed')"
|
||||
class="mpr-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;transition:background 0.1s;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#fff7ed;display:flex;align-items:center;justify-content:center;">
|
||||
<span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#ea580c;"></span>
|
||||
</span>
|
||||
<div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:#ea580c;line-height:1.2;">3 Days</div>
|
||||
<div style="font-size:0.68rem;color:#9ca3af;">Within 3 days</div>
|
||||
</div>
|
||||
</li>
|
||||
<li onclick="mprUrgencySelect('1 Week','1 Week','#d97706','#fffbeb')"
|
||||
class="mpr-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;transition:background 0.1s;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#fffbeb;display:flex;align-items:center;justify-content:center;">
|
||||
<span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#d97706;"></span>
|
||||
</span>
|
||||
<div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:#d97706;line-height:1.2;">1 Week</div>
|
||||
<div style="font-size:0.68rem;color:#9ca3af;">Within this week</div>
|
||||
</div>
|
||||
</li>
|
||||
<li onclick="mprUrgencySelect('2 Weeks','2 Weeks','#2563eb','#eff6ff')"
|
||||
class="mpr-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;transition:background 0.1s;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#eff6ff;display:flex;align-items:center;justify-content:center;">
|
||||
<span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#2563eb;"></span>
|
||||
</span>
|
||||
<div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:#2563eb;line-height:1.2;">2 Weeks</div>
|
||||
<div style="font-size:0.68rem;color:#9ca3af;">Within 2 weeks</div>
|
||||
</div>
|
||||
</li>
|
||||
<li onclick="mprUrgencySelect('1 Month','1 Month','#16a34a','#f0fdf4')"
|
||||
class="mpr-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;margin-bottom:0.125rem;transition:background 0.1s;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#f0fdf4;display:flex;align-items:center;justify-content:center;">
|
||||
<span style="width:0.55rem;height:0.55rem;border-radius:50%;background:#16a34a;"></span>
|
||||
</span>
|
||||
<div>
|
||||
<div style="font-size:0.8rem;font-weight:700;color:#16a34a;line-height:1.2;">1 Month</div>
|
||||
<div style="font-size:0.68rem;color:#9ca3af;">Within a month</div>
|
||||
</div>
|
||||
</li>
|
||||
<li style="height:1px;background:#f1f5f9;margin:0.25rem 0;"></li>
|
||||
<li id="mpr-urgency-date-opt" onclick="mprUrgencyDateOptClick()"
|
||||
class="mpr-urgency-opt"
|
||||
style="display:flex;align-items:center;gap:0.625rem;padding:0.55rem 0.75rem;border-radius:0.5rem;cursor:pointer;transition:background 0.1s;">
|
||||
<span style="flex-shrink:0;width:2rem;height:2rem;border-radius:0.4rem;background:#f5f3ff;display:flex;align-items:center;justify-content:center;">
|
||||
<svg style="width:0.875rem;height:0.875rem;stroke:#7c3aed;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:0.8rem;font-weight:700;color:#7c3aed;line-height:1.2;">Specific Date</div>
|
||||
<div style="font-size:0.68rem;color:#9ca3af;">Pick an exact date</div>
|
||||
</div>
|
||||
</li>
|
||||
<li id="mpr-urgency-datepicker-row" style="display:none;padding:0 0.75rem 0.5rem;">
|
||||
<input type="date" id="mpr-urgency-date"
|
||||
style="width:100%;font-size:0.8rem;border:1.5px solid #7c3aed;border-radius:0.4rem;padding:0.35rem 0.5rem;outline:none;color:#7c3aed;box-sizing:border-box;"
|
||||
onchange="mprUrgencyDateChange(this.value)">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Location / Site</label>
|
||||
@ -99,7 +232,12 @@ $mprProjectsJson = $mprProjects->map(function ($p) {
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Department</label>
|
||||
<input type="text" name="department" value="{{ old('department') }}" class="form-input" placeholder="e.g. Operations">
|
||||
<select name="department" id="mpr-department-select" class="form-input">
|
||||
<option value="">— Select Department —</option>
|
||||
@foreach($mprDepartments as $dept)
|
||||
<option value="{{ $dept->name }}" data-company="{{ $dept->company_id }}" {{ old('department') == $dept->name ? 'selected' : '' }}>{{ $dept->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -119,7 +257,7 @@ $mprProjectsJson = $mprProjects->map(function ($p) {
|
||||
<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;">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>
|
||||
@ -136,8 +274,12 @@ $mprProjectsJson = $mprProjects->map(function ($p) {
|
||||
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…">
|
||||
<select name="items[{{ $idx }}][unit]" style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;cursor:pointer;">
|
||||
<option value="">—</option>
|
||||
@foreach($mprUnits as $u)
|
||||
<option value="{{ $u }}" {{ ($oldItem['unit'] ?? '') == $u ? 'selected' : '' }}>{{ $u }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td style="border:1px solid #e2e8f0;padding:0.25rem 0.5rem;">
|
||||
<input type="number" name="items[{{ $idx }}][quantity_required]" value="{{ $oldItem['quantity_required'] ?? '' }}"
|
||||
@ -148,7 +290,7 @@ $mprProjectsJson = $mprProjects->map(function ($p) {
|
||||
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'] ?? '' }}"
|
||||
<input type="date" name="items[{{ $idx }}][required_date]" value="{{ $oldItem['required_date'] ?? $today }}"
|
||||
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;">
|
||||
@ -204,6 +346,13 @@ document.addEventListener('keydown', function (e) {
|
||||
document.addEventListener('DOMContentLoaded', function () { mprModalOpen(); });
|
||||
@endif
|
||||
|
||||
// Unit options HTML (shared between template rows and JS-added rows)
|
||||
var mprUnitOptions = '<option value="">—</option>' +
|
||||
@foreach($mprUnits as $u)
|
||||
'<option value="{{ $u }}">{{ $u }}</option>' +
|
||||
@endforeach
|
||||
'';
|
||||
|
||||
// Dynamic rows
|
||||
(function () {
|
||||
var mprRowIndex = {{ count($oldItems ?? [[]]) }};
|
||||
@ -214,18 +363,26 @@ document.addEventListener('DOMContentLoaded', function () { mprModalOpen(); });
|
||||
});
|
||||
}
|
||||
|
||||
function mprLastRowDate() {
|
||||
var rows = document.querySelectorAll('#mpr-items-body .mpr-item-row');
|
||||
if (!rows.length) return '{{ $today }}';
|
||||
var lastDate = rows[rows.length - 1].querySelector('input[type="date"]');
|
||||
return (lastDate && lastDate.value) ? lastDate.value : '{{ $today }}';
|
||||
}
|
||||
|
||||
function mprNewRow() {
|
||||
var idx = mprRowIndex++;
|
||||
var date = mprLastRowDate();
|
||||
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;"><select name="items[' + idx + '][unit]" style="width:100%;border:0;outline:none;font-size:0.8rem;background:transparent;cursor:pointer;">' + mprUnitOptions + '</select></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.5rem;"><input type="date" name="items[' + idx + '][required_date]" value="' + 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;
|
||||
}
|
||||
@ -253,11 +410,61 @@ document.addEventListener('DOMContentLoaded', function () { mprModalOpen(); });
|
||||
});
|
||||
})();
|
||||
|
||||
// Cascading project → location dropdown
|
||||
// ── Cascading data ────────────────────────────────────────────────────────────
|
||||
var mprProjectsData = @json($mprProjectsJson);
|
||||
var mprDepartmentsData = @json($mprDepartmentsJson);
|
||||
var mprOldLocation = "{{ old('location') }}";
|
||||
var mprOldProjectName = "{{ old('project_name') }}";
|
||||
var mprOldDepartment = "{{ old('department') }}";
|
||||
|
||||
// ── Searchable project dropdown ───────────────────────────────────────────────
|
||||
function mprProjectToggle() {
|
||||
var panel = document.getElementById('mpr-project-panel');
|
||||
var open = panel.style.display !== 'none';
|
||||
if (open) {
|
||||
mprProjectClose();
|
||||
} else {
|
||||
panel.style.display = 'block';
|
||||
var search = document.getElementById('mpr-project-search');
|
||||
search.value = '';
|
||||
mprProjectSearch('');
|
||||
setTimeout(function(){ search.focus(); }, 30);
|
||||
}
|
||||
}
|
||||
|
||||
function mprProjectClose() {
|
||||
document.getElementById('mpr-project-panel').style.display = 'none';
|
||||
}
|
||||
|
||||
function mprProjectSearch(q) {
|
||||
var items = document.querySelectorAll('#mpr-project-list .mpr-proj-opt');
|
||||
var empty = document.getElementById('mpr-proj-empty');
|
||||
var term = q.trim().toLowerCase();
|
||||
var visible = 0;
|
||||
items.forEach(function(li) {
|
||||
var match = !term || li.dataset.search.indexOf(term) !== -1;
|
||||
li.style.display = match ? '' : 'none';
|
||||
if (match) visible++;
|
||||
});
|
||||
empty.style.display = visible === 0 ? '' : 'none';
|
||||
}
|
||||
|
||||
function mprProjectSelect(value, label) {
|
||||
document.getElementById('mpr-project-value').value = value;
|
||||
var lbl = document.getElementById('mpr-project-label');
|
||||
lbl.textContent = label;
|
||||
lbl.style.color = '#111827';
|
||||
mprProjectClose();
|
||||
mprOnProjectChange(value);
|
||||
}
|
||||
|
||||
// Close panel when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
var wrapper = document.getElementById('mpr-project-wrapper');
|
||||
if (wrapper && !wrapper.contains(e.target)) mprProjectClose();
|
||||
});
|
||||
|
||||
// ── Location cascade ──────────────────────────────────────────────────────────
|
||||
function mprFilterLocations(projectName) {
|
||||
var sel = document.getElementById('mpr-location-select');
|
||||
sel.innerHTML = '<option value="">— Select Location —</option>';
|
||||
@ -277,12 +484,138 @@ function mprFilterLocations(projectName) {
|
||||
}
|
||||
}
|
||||
|
||||
// On load: if old project value exists (validation error repopulation), trigger filter
|
||||
// ── Department cascade ────────────────────────────────────────────────────────
|
||||
function mprFilterDepartments(projectName) {
|
||||
var sel = document.getElementById('mpr-department-select');
|
||||
sel.innerHTML = '<option value="">— Select Department —</option>';
|
||||
var companyId = null;
|
||||
if (projectName) {
|
||||
var proj = mprProjectsData.find(function(p){ return p.name === projectName; });
|
||||
if (proj) companyId = proj.company_id;
|
||||
}
|
||||
var depts = companyId
|
||||
? mprDepartmentsData.filter(function(d){ return d.company_id === companyId; })
|
||||
: mprDepartmentsData;
|
||||
depts.forEach(function(d){
|
||||
var opt = document.createElement('option');
|
||||
opt.value = d.name;
|
||||
opt.textContent = d.name;
|
||||
if (d.name === mprOldDepartment) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.disabled = depts.length === 0;
|
||||
}
|
||||
|
||||
function mprOnProjectChange(projectName) {
|
||||
mprFilterLocations(projectName);
|
||||
mprFilterDepartments(projectName);
|
||||
}
|
||||
|
||||
// ── Restore old values on validation error repopulation ───────────────────────
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
if (mprOldProjectName) {
|
||||
mprFilterLocations(mprOldProjectName);
|
||||
var proj = mprProjectsData.find(function(p){ return p.name === mprOldProjectName; });
|
||||
if (proj) mprProjectSelect(proj.name, proj.label);
|
||||
} else {
|
||||
document.getElementById('mpr-location-select').disabled = true;
|
||||
mprFilterDepartments('');
|
||||
}
|
||||
// Restore urgency pill
|
||||
var oldUrgency = document.getElementById('mpr-urgency-value').value;
|
||||
if (oldUrgency) mprUrgencyRestore(oldUrgency);
|
||||
});
|
||||
|
||||
// ── Urgency colored dropdown ──────────────────────────────────────────────────
|
||||
var mprUrgencyPresets = ['Urgent','3 Days','1 Week','2 Weeks','1 Month'];
|
||||
|
||||
function mprUrgencyToggle() {
|
||||
var panel = document.getElementById('mpr-urgency-panel');
|
||||
if (panel.style.display === 'none') {
|
||||
panel.style.display = 'block';
|
||||
} else {
|
||||
mprUrgencyClose();
|
||||
}
|
||||
}
|
||||
|
||||
function mprUrgencyClose() {
|
||||
document.getElementById('mpr-urgency-panel').style.display = 'none';
|
||||
}
|
||||
|
||||
function mprUrgencySelect(value, label, color, bgColor) {
|
||||
document.getElementById('mpr-urgency-value').value = value;
|
||||
// Update trigger display
|
||||
var display = document.getElementById('mpr-urgency-display');
|
||||
display.innerHTML =
|
||||
'<span style="display:inline-flex;align-items:center;gap:0.4rem;background:' + bgColor + ';border:1px solid ' + color + '20;padding:0.15rem 0.6rem 0.15rem 0.4rem;border-radius:999px;">' +
|
||||
'<span style="width:0.45rem;height:0.45rem;border-radius:50%;background:' + color + ';flex-shrink:0;"></span>' +
|
||||
'<span style="font-size:0.78rem;font-weight:600;color:' + color + ';">' + label + '</span>' +
|
||||
'</span>';
|
||||
// Update trigger border color
|
||||
document.getElementById('mpr-urgency-trigger').style.borderColor = color;
|
||||
document.getElementById('mpr-urgency-trigger').style.borderStyle = 'solid';
|
||||
// Hide date picker row
|
||||
document.getElementById('mpr-urgency-datepicker-row').style.display = 'none';
|
||||
mprUrgencyClose();
|
||||
}
|
||||
|
||||
function mprUrgencyDateOptClick() {
|
||||
var row = document.getElementById('mpr-urgency-datepicker-row');
|
||||
row.style.display = row.style.display === 'none' ? '' : 'none';
|
||||
if (row.style.display !== 'none') {
|
||||
document.getElementById('mpr-urgency-date').focus();
|
||||
}
|
||||
}
|
||||
|
||||
function mprUrgencyDateChange(val) {
|
||||
if (!val) return;
|
||||
document.getElementById('mpr-urgency-value').value = val;
|
||||
// Format nicely for display
|
||||
var d = new Date(val + 'T00:00:00');
|
||||
var label = d.toLocaleDateString('en-GB', {day:'2-digit', month:'short', year:'numeric'});
|
||||
var display = document.getElementById('mpr-urgency-display');
|
||||
display.innerHTML =
|
||||
'<span style="display:inline-flex;align-items:center;gap:0.4rem;background:#f5f3ff;border:1px solid #7c3aed20;padding:0.15rem 0.6rem 0.15rem 0.4rem;border-radius:999px;">' +
|
||||
'<svg style="width:0.7rem;height:0.7rem;stroke:#7c3aed;flex-shrink:0;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>' +
|
||||
'<span style="font-size:0.78rem;font-weight:600;color:#7c3aed;">' + label + '</span>' +
|
||||
'</span>';
|
||||
document.getElementById('mpr-urgency-trigger').style.borderColor = '#7c3aed';
|
||||
document.getElementById('mpr-urgency-trigger').style.borderStyle = 'solid';
|
||||
mprUrgencyClose();
|
||||
}
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', function(e) {
|
||||
var w = document.getElementById('mpr-urgency-wrapper');
|
||||
if (w && !w.contains(e.target)) mprUrgencyClose();
|
||||
});
|
||||
|
||||
// Restore on validation error repopulation
|
||||
function mprUrgencyRestore(val) {
|
||||
if (!val) return;
|
||||
var presets = {
|
||||
'Urgent': ['Urgent', '#dc2626','#fef2f2'],
|
||||
'3 Days': ['3 Days', '#ea580c','#fff7ed'],
|
||||
'1 Week': ['1 Week', '#d97706','#fffbeb'],
|
||||
'2 Weeks': ['2 Weeks', '#2563eb','#eff6ff'],
|
||||
'1 Month': ['1 Month', '#16a34a','#f0fdf4'],
|
||||
};
|
||||
if (presets[val]) {
|
||||
mprUrgencySelect(val, presets[val][0], presets[val][1], presets[val][2]);
|
||||
} else {
|
||||
// It's a specific date string
|
||||
document.getElementById('mpr-urgency-value').value = val;
|
||||
document.getElementById('mpr-urgency-datepicker-row').style.display = '';
|
||||
document.getElementById('mpr-urgency-date').value = val;
|
||||
var d = new Date(val + 'T00:00:00');
|
||||
var label = d.toLocaleDateString('en-GB', {day:'2-digit', month:'short', year:'numeric'});
|
||||
var display = document.getElementById('mpr-urgency-display');
|
||||
display.innerHTML =
|
||||
'<span style="display:inline-flex;align-items:center;gap:0.4rem;background:#f5f3ff;border:1px solid #7c3aed20;padding:0.15rem 0.6rem 0.15rem 0.4rem;border-radius:999px;">' +
|
||||
'<svg style="width:0.7rem;height:0.7rem;stroke:#7c3aed;flex-shrink:0;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>' +
|
||||
'<span style="font-size:0.78rem;font-weight:600;color:#7c3aed;">' + label + '</span>' +
|
||||
'</span>';
|
||||
document.getElementById('mpr-urgency-trigger').style.borderColor = '#7c3aed';
|
||||
document.getElementById('mpr-urgency-trigger').style.borderStyle = 'solid';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -256,18 +256,15 @@
|
||||
|
||||
</div>{{-- /sup-step2 --}}
|
||||
|
||||
{{-- Step 3: Summary / confirmation (By Item only) --}}
|
||||
{{-- Step 3: Links --}}
|
||||
<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()"
|
||||
<div style="font-size:12px;color:#64748b;" id="sup-link-count"></div>
|
||||
<button type="button" onclick="doneWithLinks()"
|
||||
style="padding:8px 22px;background:#16a34a;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;">
|
||||
Confirm & Send →
|
||||
Done ✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -520,69 +517,117 @@ function updateFooter() {
|
||||
}
|
||||
}
|
||||
|
||||
var _rfqRedirect = null;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// Show step 3 with loading state immediately
|
||||
document.getElementById('sup-summary-body').innerHTML =
|
||||
'<div style="text-align:center;padding:40px;color:#64748b;font-size:13px;">Generating links…</div>';
|
||||
document.getElementById('sup-modal-title').textContent = 'Quote Links';
|
||||
document.getElementById('sup-modal-subtitle').textContent = 'One-time-use links for each supplier';
|
||||
document.getElementById('sup-step2').style.display = 'none';
|
||||
document.getElementById('sup-step3').style.display = 'flex';
|
||||
|
||||
var form = document.getElementById('sup-form');
|
||||
var CSRF = document.querySelector('meta[name="csrf-token"]').content;
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF },
|
||||
body: new FormData(form),
|
||||
}).then(function(r) {
|
||||
return r.json().then(function(body) {
|
||||
if (!r.ok) return Promise.reject(body);
|
||||
return body;
|
||||
});
|
||||
}).then(function(data) {
|
||||
_rfqRedirect = data.redirect || null;
|
||||
showLinks(data.invitations || []);
|
||||
}).catch(function(err) {
|
||||
document.getElementById('sup-step3').style.display = 'none';
|
||||
document.getElementById('sup-step2').style.display = 'flex';
|
||||
document.getElementById('sup-modal-title').textContent = 'Select Suppliers';
|
||||
document.getElementById('sup-modal-subtitle').textContent = 'Choose who receives the quote request';
|
||||
showToast((err && err.message) || 'Something went wrong.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
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 _chanBadge = {
|
||||
email: '<span style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:8px;font-size:11px;font-weight:700;flex-shrink:0;">Email</span>',
|
||||
whatsapp: '<span style="background:#f0fdf4;color:#15803d;padding:3px 10px;border-radius:8px;font-size:11px;font-weight:700;flex-shrink:0;">WhatsApp</span>',
|
||||
both: '<span style="background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:8px;font-size:11px;font-weight:700;flex-shrink:0;">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>';
|
||||
function showLinks(invitations) {
|
||||
var html = '';
|
||||
if (!invitations || invitations.length === 0) {
|
||||
html = '<div style="padding:30px;text-align:center;color:#94a3b8;font-size:13px;">No new invitations were created.</div>';
|
||||
} else {
|
||||
html += '<div style="font-size:12px;color:#64748b;margin-bottom:16px;">Each link is private to the supplier and valid until submitted or expired.</div>';
|
||||
invitations.forEach(function(inv, i) {
|
||||
var badge = _chanBadge[inv.channel] || '';
|
||||
html += '<div style="margin-bottom:10px;border:1.5px solid #e2e8f0;border-radius:10px;overflow:hidden;">';
|
||||
html += '<div style="padding:9px 14px;background:#f8fafc;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;gap:8px;">';
|
||||
html += '<span style="font-size:13px;font-weight:700;color:#0f172a;">' + escHtml(inv.supplier_name) + '</span>';
|
||||
html += badge;
|
||||
html += '</div>';
|
||||
html += '<div style="padding:10px 14px;display:flex;align-items:center;gap:8px;">';
|
||||
html += '<a href="' + escHtml(inv.url) + '" target="_blank"'
|
||||
+ ' style="flex:1;display:flex;align-items:center;gap:8px;padding:9px 14px;'
|
||||
+ 'background:#eff6ff;border:1.5px solid #bfdbfe;border-radius:8px;text-decoration:none;'
|
||||
+ 'color:#1d4ed8;font-size:12px;font-weight:600;transition:background .15s;overflow:hidden;"'
|
||||
+ ' onmouseover="this.style.background=\'#dbeafe\'" onmouseout="this.style.background=\'#eff6ff\'">'
|
||||
+ '<svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" style="flex-shrink:0;">'
|
||||
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>'
|
||||
+ '</svg>'
|
||||
+ '<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">Open Quote Link</span>'
|
||||
+ '</a>';
|
||||
html += '<button type="button" data-idx="' + i + '" data-url="' + escHtml(inv.url) + '" onclick="copyLink(this)"'
|
||||
+ ' style="flex-shrink:0;padding:9px 14px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;">Copy Link</button>';
|
||||
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';
|
||||
document.getElementById('sup-modal-title').textContent = 'Quote Links Ready';
|
||||
document.getElementById('sup-modal-subtitle').textContent = 'Share with suppliers via their preferred channel';
|
||||
var countEl = document.getElementById('sup-link-count');
|
||||
if (countEl) countEl.textContent = invitations.length + ' link' + (invitations.length === 1 ? '' : 's') + ' generated';
|
||||
}
|
||||
|
||||
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';
|
||||
function copyLink(btn) {
|
||||
var url = btn.dataset.url;
|
||||
if (!url) return;
|
||||
navigator.clipboard.writeText(url).then(function() {
|
||||
btn.textContent = 'Copied!';
|
||||
btn.style.background = '#16a34a';
|
||||
setTimeout(function() { btn.textContent = 'Copy'; btn.style.background = '#2563eb'; }, 2000);
|
||||
}).catch(function() {
|
||||
// Fallback for older browsers
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = url; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
try { document.execCommand('copy'); btn.textContent = 'Copied!'; btn.style.background = '#16a34a';
|
||||
setTimeout(function() { btn.textContent = 'Copy'; btn.style.background = '#2563eb'; }, 2000);
|
||||
} catch(e) { showToast('Could not copy — please copy manually.', 'warn'); }
|
||||
document.body.removeChild(ta);
|
||||
});
|
||||
}
|
||||
|
||||
function doneWithLinks() {
|
||||
if (_rfqRedirect) { window.location.href = _rfqRedirect; }
|
||||
else { window.location.reload(); }
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -249,6 +249,38 @@
|
||||
|
||||
<div style="display:flex;align-items:center;gap:16px;">
|
||||
<span style="font-size:12px;color:#94a3b8;">{{ now()->format('l, d M Y') }}</span>
|
||||
<div style="width:1px;height:20px;background:#e2e8f0;"></div>
|
||||
|
||||
{{-- Bell notification --}}
|
||||
<div style="position:relative;" id="bell-wrap">
|
||||
<button id="bell-btn" onclick="toggleBellDropdown()" title="Notifications"
|
||||
style="position:relative;width:34px;height:34px;border-radius:9px;border:1px solid #e2e8f0;
|
||||
background:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#64748b;
|
||||
transition:background .15s,color .15s;"
|
||||
onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='#fff'">
|
||||
<svg width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 10-12 0v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
||||
</svg>
|
||||
<span id="bell-badge" style="display:none;position:absolute;top:-4px;right:-4px;
|
||||
background:#ef4444;color:#fff;font-size:10px;font-weight:700;
|
||||
min-width:17px;height:17px;border-radius:9px;padding:0 4px;
|
||||
display:none;align-items:center;justify-content:center;border:2px solid #fff;"></span>
|
||||
</button>
|
||||
|
||||
{{-- Dropdown --}}
|
||||
<div id="bell-dropdown" style="display:none;position:absolute;top:42px;right:0;
|
||||
background:#fff;border:1.5px solid #e2e8f0;border-radius:14px;
|
||||
box-shadow:0 12px 32px rgba(0,0,0,.12);width:320px;z-index:9000;overflow:hidden;">
|
||||
<div style="padding:12px 16px;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between;">
|
||||
<span style="font-size:13px;font-weight:700;color:#0f172a;">Notifications</span>
|
||||
<button onclick="markAllRead()" style="font-size:11px;color:#2563eb;background:none;border:none;cursor:pointer;font-weight:600;">Mark all read</button>
|
||||
</div>
|
||||
<div id="bell-list" style="max-height:320px;overflow-y:auto;">
|
||||
<div style="padding:24px;text-align:center;color:#94a3b8;font-size:13px;" id="bell-empty">No new notifications</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="width:1px;height:20px;background:#e2e8f0;"></div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div style="width:32px;height:32px;border-radius:50%;background:#2563eb;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:#fff;">
|
||||
@ -493,6 +525,77 @@ document.addEventListener('keydown', function(e) {
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// ── Bell notifications ────────────────────────────────────
|
||||
(function() {
|
||||
var bellOpen = false;
|
||||
|
||||
function fetchNotifications() {
|
||||
fetch('{{ route('notifications.unread') }}', { headers: { 'Accept': 'application/json' } })
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(data) {
|
||||
var badge = document.getElementById('bell-badge');
|
||||
var list = document.getElementById('bell-list');
|
||||
var empty = document.getElementById('bell-empty');
|
||||
if (!badge) return;
|
||||
|
||||
if (data.count > 0) {
|
||||
badge.textContent = data.count > 9 ? '9+' : data.count;
|
||||
badge.style.display = 'flex';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
}
|
||||
|
||||
if (data.items && data.items.length > 0) {
|
||||
empty.style.display = 'none';
|
||||
var html = '';
|
||||
data.items.forEach(function(n) {
|
||||
html += '<a href="' + (n.url || '#') + '" onclick="markAllRead()" style="display:block;padding:12px 16px;border-bottom:1px solid #f8fafc;text-decoration:none;transition:background .1s;" onmouseover="this.style.background=\'#f8fafc\'" onmouseout="this.style.background=\'transparent\'">';
|
||||
html += '<div style="font-size:12px;font-weight:600;color:#0f172a;line-height:1.4;">' + n.message + '</div>';
|
||||
html += '<div style="font-size:11px;color:#94a3b8;margin-top:2px;">' + n.ago + '</div>';
|
||||
html += '</a>';
|
||||
});
|
||||
list.innerHTML = html + '<div id="bell-empty" style="display:none;"></div>';
|
||||
} else {
|
||||
list.innerHTML = '<div style="padding:24px;text-align:center;color:#94a3b8;font-size:13px;" id="bell-empty">No new notifications</div>';
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
window.toggleBellDropdown = function() {
|
||||
var dd = document.getElementById('bell-dropdown');
|
||||
bellOpen = !bellOpen;
|
||||
dd.style.display = bellOpen ? 'block' : 'none';
|
||||
if (bellOpen) fetchNotifications();
|
||||
};
|
||||
|
||||
window.markAllRead = function() {
|
||||
var CSRF = document.querySelector('meta[name="csrf-token"]').content;
|
||||
fetch('{{ route('notifications.read-all') }}', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' }
|
||||
}).then(function() {
|
||||
document.getElementById('bell-badge').style.display = 'none';
|
||||
document.getElementById('bell-dropdown').style.display = 'none';
|
||||
bellOpen = false;
|
||||
});
|
||||
};
|
||||
|
||||
// Close dropdown on outside click
|
||||
document.addEventListener('click', function(e) {
|
||||
var wrap = document.getElementById('bell-wrap');
|
||||
if (wrap && !wrap.contains(e.target)) {
|
||||
document.getElementById('bell-dropdown').style.display = 'none';
|
||||
bellOpen = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Poll every 30 seconds
|
||||
fetchNotifications();
|
||||
setInterval(fetchNotifications, 30000);
|
||||
}());
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function toggleSidebar() {
|
||||
var s = document.getElementById('sidebar');
|
||||
|
||||
@ -6,31 +6,50 @@
|
||||
<title>Quote Request — {{ $purchaseRequest->request_number }}</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0;}
|
||||
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;padding:20px 16px;}
|
||||
.card{background:#fff;border-radius:16px;box-shadow:0 4px 24px rgba(0,0,0,.08);max-width:740px;margin:0 auto;overflow:hidden;}
|
||||
.hd{background:linear-gradient(135deg,#2563eb,#1d4ed8);padding:28px 32px;}
|
||||
.body{padding:32px;}
|
||||
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;}
|
||||
label.fl{display:block;font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;margin-bottom:5px;}
|
||||
input[type=text],input[type=number],textarea{width:100%;padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:13px;outline:none;}
|
||||
input[type=text],input[type=number],textarea{width:100%;padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:13px;outline:none;background:#fff;}
|
||||
input:focus,textarea:focus{border-color:#2563eb;}
|
||||
.btn{width:100%;padding:14px;background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;margin-top:24px;}
|
||||
.btn{width:100%;padding:15px;background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;}
|
||||
table{width:100%;border-collapse:collapse;font-size:13px;}
|
||||
thead th{background:#f8fafc;padding:10px 12px;text-align:left;font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;}
|
||||
tbody td{padding:10px 12px;border-bottom:1px solid #f1f5f9;vertical-align:middle;}
|
||||
tfoot td{padding:12px;font-weight:700;}
|
||||
input[type=number].price{width:130px;text-align:right;}
|
||||
@media(max-width:600px){
|
||||
.body{padding:20px;}
|
||||
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px;}
|
||||
|
||||
/* ── Desktop: centered card ── */
|
||||
@media(min-width:641px){
|
||||
body{padding:28px 16px;}
|
||||
.card{background:#fff;border-radius:16px;box-shadow:0 4px 24px rgba(0,0,0,.08);max-width:740px;margin:0 auto;overflow:hidden;}
|
||||
.hd{background:linear-gradient(135deg,#2563eb,#1d4ed8);padding:28px 32px;}
|
||||
.body{padding:32px;}
|
||||
}
|
||||
|
||||
/* ── Mobile: full page ── */
|
||||
@media(max-width:640px){
|
||||
body{background:#fff;padding:0;}
|
||||
.card{background:#fff;border-radius:0;box-shadow:none;max-width:100%;}
|
||||
.hd{background:linear-gradient(135deg,#2563eb,#1d4ed8);padding:22px 18px;}
|
||||
.body{padding:18px 16px 32px;}
|
||||
.two-col{grid-template-columns:1fr;}
|
||||
/* Items table → stacked cards */
|
||||
table,thead,tbody,tfoot,tr,th,td{display:block;}
|
||||
thead{display:none;}
|
||||
tbody td{border:none;padding:4px 0;}
|
||||
tbody tr{border:1px solid #e2e8f0;border-radius:10px;padding:12px;margin-bottom:10px;}
|
||||
tbody td{border:none;padding:3px 0;}
|
||||
tbody tr{border:1px solid #e2e8f0;border-radius:10px;padding:12px 14px;margin-bottom:10px;background:#fff;}
|
||||
tfoot tr{border-top:2px solid #e2e8f0;padding:12px 0;}
|
||||
input[type=number].price{width:100%;}
|
||||
/* Confirm code stacks */
|
||||
.code-row{flex-direction:column !important;align-items:stretch !important;}
|
||||
.code-display{display:block;text-align:center;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="hd">
|
||||
<div style="font-size:11px;font-weight:600;color:rgba(255,255,255,.7);text-transform:uppercase;letter-spacing:.06em;">Request for Quotation</div>
|
||||
<div style="font-size:22px;font-weight:700;color:#fff;margin-top:4px;">{{ $purchaseRequest->request_number }}</div>
|
||||
@ -68,12 +87,12 @@
|
||||
<tr>
|
||||
<td style="color:#94a3b8;font-size:12px;">{{ $i + 1 }}</td>
|
||||
<td style="font-weight:500;">{{ $item->description }}</td>
|
||||
<td>{{ rtrim(rtrim(number_format((float)$item->quantity, 3), '0'), '.') }}</td>
|
||||
<td>{{ rtrim(rtrim(number_format((float)$item->quantity_required, 3), '0'), '.') }}</td>
|
||||
<td style="color:#64748b;">{{ $item->unit ?: '—' }}</td>
|
||||
<td style="text-align:right;">
|
||||
<input type="number" class="price" name="items[{{ $i }}][unit_price]"
|
||||
min="0" step="0.001" required placeholder="0.000"
|
||||
oninput="calcRow({{ $i }}, {{ (float)$item->quantity }})">
|
||||
oninput="calcRow({{ $i }}, {{ (float)$item->quantity_required }})">
|
||||
</td>
|
||||
<td style="text-align:right;font-weight:600;" id="tot-{{ $i }}">—</td>
|
||||
</tr>
|
||||
@ -88,10 +107,10 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Terms --}}
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px;">
|
||||
{{-- Delivery / Payment --}}
|
||||
<div class="two-col">
|
||||
<div>
|
||||
<label class="fl">Lead Time (days)</label>
|
||||
<label class="fl">Delivery Time (days)</label>
|
||||
<input type="number" name="lead_time_days" min="0" placeholder="e.g. 14">
|
||||
</div>
|
||||
<div>
|
||||
@ -99,15 +118,86 @@
|
||||
<input type="text" name="payment_terms" placeholder="e.g. 30 days net">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="margin-bottom:20px;">
|
||||
<label class="fl">Notes / Remarks</label>
|
||||
<textarea name="notes" rows="3" placeholder="Any additional notes, conditions, or remarks…"></textarea>
|
||||
</div>
|
||||
|
||||
<button class="btn" type="submit">Submit My Quote →</button>
|
||||
{{-- T&C --}}
|
||||
<div style="background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:10px;padding:16px 18px;margin-bottom:16px;">
|
||||
<div style="font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;">Terms & Conditions</div>
|
||||
<ul style="list-style:none;padding:0;margin:0 0 14px;display:flex;flex-direction:column;gap:7px;">
|
||||
<li style="display:flex;align-items:flex-start;gap:9px;font-size:12px;color:#475569;line-height:1.5;">
|
||||
<span style="flex-shrink:0;width:18px;height:18px;border-radius:50%;background:#dbeafe;display:flex;align-items:center;justify-content:center;margin-top:1px;">
|
||||
<svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M2 5.5L4 7.5L8 3" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
Prices stated in this quote are valid for <strong>30 days</strong> from the submission date.
|
||||
</li>
|
||||
<li style="display:flex;align-items:flex-start;gap:9px;font-size:12px;color:#475569;line-height:1.5;">
|
||||
<span style="flex-shrink:0;width:18px;height:18px;border-radius:50%;background:#dbeafe;display:flex;align-items:center;justify-content:center;margin-top:1px;">
|
||||
<svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M2 5.5L4 7.5L8 3" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
Delivery will be made within the specified delivery time stated above.
|
||||
</li>
|
||||
<li style="display:flex;align-items:flex-start;gap:9px;font-size:12px;color:#475569;line-height:1.5;">
|
||||
<span style="flex-shrink:0;width:18px;height:18px;border-radius:50%;background:#dbeafe;display:flex;align-items:center;justify-content:center;margin-top:1px;">
|
||||
<svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M2 5.5L4 7.5L8 3" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
All items supplied will meet the required specifications and quality standards.
|
||||
</li>
|
||||
<li style="display:flex;align-items:flex-start;gap:9px;font-size:12px;color:#475569;line-height:1.5;">
|
||||
<span style="flex-shrink:0;width:18px;height:18px;border-radius:50%;background:#dbeafe;display:flex;align-items:center;justify-content:center;margin-top:1px;">
|
||||
<svg width="9" height="9" viewBox="0 0 10 10" fill="none"><path d="M2 5.5L4 7.5L8 3" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
This quote is <strong>binding upon acceptance</strong> and constitutes a formal offer.
|
||||
</li>
|
||||
</ul>
|
||||
<label style="display:flex;align-items:flex-start;gap:10px;cursor:pointer;padding:10px 12px;background:#fff;border:1.5px solid #e2e8f0;border-radius:8px;">
|
||||
<input type="checkbox" name="terms" id="terms-cb" value="1" required
|
||||
onchange="checkReady()"
|
||||
style="width:16px;height:16px;margin-top:1px;accent-color:#2563eb;flex-shrink:0;cursor:pointer;">
|
||||
<span style="font-size:13px;font-weight:600;color:#0f172a;">I have read and agree to the terms and conditions above</span>
|
||||
</label>
|
||||
@error('terms')
|
||||
<div style="font-size:12px;color:#dc2626;margin-top:6px;">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Confirmation code --}}
|
||||
<div style="background:#fffbeb;border:1.5px solid #fde68a;border-radius:10px;padding:16px 18px;margin-bottom:20px;">
|
||||
<div style="font-size:11px;font-weight:700;color:#92400e;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px;">Confirmation Required</div>
|
||||
<div class="code-row" style="display:flex;align-items:center;gap:14px;">
|
||||
<div style="flex-shrink:0;">
|
||||
<div style="font-size:11px;color:#92400e;margin-bottom:6px;">Copy this code:</div>
|
||||
<div class="code-display"
|
||||
style="font-size:24px;font-weight:800;letter-spacing:.2em;color:#92400e;
|
||||
background:#fef3c7;border:2px dashed #f59e0b;border-radius:8px;
|
||||
padding:10px 20px;font-family:monospace;user-select:all;white-space:nowrap;">
|
||||
{{ $confirmCode }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;min-width:0;">
|
||||
<label class="fl" style="color:#92400e;">Paste code here</label>
|
||||
<input type="text" name="confirm_code" id="confirm-input"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="characters" spellcheck="false"
|
||||
placeholder="Paste the code above"
|
||||
oninput="checkReady()"
|
||||
style="padding:11px 12px;border:1.5px solid #fde68a;
|
||||
font-size:16px;font-weight:700;letter-spacing:.12em;font-family:monospace;
|
||||
text-transform:uppercase;outline:none;background:#fff;">
|
||||
@error('confirm_code')
|
||||
<div style="font-size:12px;color:#dc2626;margin-top:4px;">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn" type="submit" id="submit-btn" disabled style="opacity:.4;cursor:not-allowed;">
|
||||
Submit My Quote →
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style="font-size:11px;color:#cbd5e1;margin-top:16px;text-align:center;">
|
||||
<p style="font-size:11px;color:#cbd5e1;margin-top:18px;text-align:center;">
|
||||
Link expires {{ $invitation->expires_at->format('d M Y') }} · One submission only
|
||||
</p>
|
||||
</div>
|
||||
@ -126,6 +216,20 @@ function calcRow(i, qty) {
|
||||
var ge = document.getElementById('grand-total');
|
||||
if (ge) ge.textContent = 'BD ' + grand.toFixed(3);
|
||||
}
|
||||
|
||||
var _expected = '{{ $confirmCode }}';
|
||||
function checkReady() {
|
||||
var terms = document.getElementById('terms-cb');
|
||||
var inp = document.getElementById('confirm-input');
|
||||
var btn = document.getElementById('submit-btn');
|
||||
if (!terms || !inp || !btn) return;
|
||||
var codeOk = inp.value.trim().toUpperCase() === _expected;
|
||||
var ok = terms.checked && codeOk;
|
||||
btn.disabled = !ok;
|
||||
btn.style.opacity = ok ? '1' : '.4';
|
||||
btn.style.cursor = ok ? 'pointer' : 'not-allowed';
|
||||
inp.style.borderColor = inp.value.length > 0 ? (codeOk ? '#16a34a' : '#ef4444') : '#fde68a';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -3,25 +3,89 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Quote Submitted</title>
|
||||
<title>Quote Submitted — Thank You</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0;}
|
||||
body{font-family:system-ui,-apple-system,sans-serif;background:#f0fdf4;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;}
|
||||
.card{background:#fff;border-radius:20px;box-shadow:0 8px 32px rgba(0,0,0,.10);padding:52px 44px;max-width:480px;width:100%;text-align:center;}
|
||||
body{font-family:system-ui,-apple-system,sans-serif;min-height:100vh;}
|
||||
@keyframes popIn{from{opacity:0;transform:scale(.88) translateY(18px);}to{opacity:1;transform:scale(1) translateY(0);}}
|
||||
@keyframes checkDraw{from{stroke-dashoffset:60;}to{stroke-dashoffset:0;}}
|
||||
@keyframes ringPulse{0%,100%{transform:scale(1);opacity:.3;}50%{transform:scale(1.2);opacity:.1;}}
|
||||
.detail-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:13px;padding:8px 0;}
|
||||
|
||||
/* ── Desktop: centered card ── */
|
||||
@media(min-width:641px){
|
||||
body{background:linear-gradient(135deg,#f0fdf4 0%,#ecfdf5 50%,#d1fae5 100%);display:flex;align-items:center;justify-content:center;padding:28px 16px;}
|
||||
.card{background:#fff;border-radius:24px;box-shadow:0 20px 60px rgba(0,0,0,.10),0 4px 16px rgba(0,0,0,.06);max-width:500px;width:100%;overflow:hidden;animation:popIn .5s cubic-bezier(.34,1.56,.64,1) forwards;}
|
||||
.inner{padding:44px 40px 36px;text-align:center;}
|
||||
.footer-bar{padding:14px 40px;background:#f8fafc;border-top:1px solid #f1f5f9;text-align:center;}
|
||||
}
|
||||
|
||||
/* ── Mobile: full page ── */
|
||||
@media(max-width:640px){
|
||||
body{background:#fff;display:flex;flex-direction:column;}
|
||||
.card{background:#fff;border-radius:0;box-shadow:none;width:100%;flex:1;display:flex;flex-direction:column;}
|
||||
.inner{padding:36px 20px 28px;text-align:center;flex:1;}
|
||||
.footer-bar{padding:14px 20px;background:#f8fafc;border-top:1px solid #f1f5f9;text-align:center;}
|
||||
.icon-wrap{margin-bottom:20px !important;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div style="font-size:60px;margin-bottom:20px;">✅</div>
|
||||
<div style="font-size:24px;font-weight:700;color:#15803d;margin-bottom:10px;">Quote Submitted!</div>
|
||||
<div style="font-size:14px;color:#475569;line-height:1.65;">
|
||||
Thank you, <strong>{{ $invitation->supplier->name }}</strong>. Your quote for
|
||||
<strong>{{ $invitation->purchaseRequest->request_number }}</strong> has been received
|
||||
and is under review. You will be contacted if you are selected.
|
||||
|
||||
<div style="height:6px;background:linear-gradient(90deg,#16a34a,#22c55e,#4ade80);"></div>
|
||||
|
||||
<div class="inner">
|
||||
|
||||
{{-- Animated check --}}
|
||||
<div class="icon-wrap" style="position:relative;display:inline-block;margin-bottom:28px;">
|
||||
<div style="position:absolute;inset:-12px;border-radius:50%;background:#dcfce7;animation:ringPulse 2.5s ease-in-out infinite;"></div>
|
||||
<div style="width:80px;height:80px;border-radius:50%;background:linear-gradient(135deg,#16a34a,#22c55e);display:flex;align-items:center;justify-content:center;position:relative;box-shadow:0 8px 24px rgba(22,163,74,.3);">
|
||||
<svg width="38" height="38" viewBox="0 0 38 38" fill="none">
|
||||
<path d="M10 19.5L16 25.5L28 13" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
|
||||
style="stroke-dasharray:60;stroke-dashoffset:60;animation:checkDraw .6s .35s ease forwards;"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="margin-top:28px;padding:14px;background:#f0fdf4;border-radius:10px;font-size:12px;color:#64748b;">
|
||||
Submitted on {{ now()->format('d M Y, H:i') }}
|
||||
</div>
|
||||
|
||||
<div style="font-size:11px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#16a34a;margin-bottom:8px;">Quote Received</div>
|
||||
<h1 style="font-size:26px;font-weight:800;color:#0f172a;margin-bottom:14px;line-height:1.25;">
|
||||
Thank you,<br>{{ $invitation->supplier->name }}!
|
||||
</h1>
|
||||
<p style="font-size:15px;color:#475569;line-height:1.75;max-width:360px;margin:0 auto 24px;">
|
||||
Your quote for <strong style="color:#0f172a;">{{ $invitation->purchaseRequest->request_number }}</strong>
|
||||
has been successfully received. Our team will review all submitted quotes and get back to you shortly.
|
||||
</p>
|
||||
|
||||
{{-- Details --}}
|
||||
<div style="background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:12px;padding:4px 20px;text-align:left;margin-bottom:24px;">
|
||||
<div class="detail-row">
|
||||
<span style="color:#64748b;">Reference</span>
|
||||
<span style="font-weight:700;color:#0f172a;">{{ $invitation->purchaseRequest->request_number }}</span>
|
||||
</div>
|
||||
<div class="detail-row" style="border-top:1px solid #f1f5f9;">
|
||||
<span style="color:#64748b;">Submitted</span>
|
||||
<span style="font-weight:600;color:#0f172a;">{{ now()->format('d M Y, H:i') }}</span>
|
||||
</div>
|
||||
@if($invitation->purchaseRequest->project_name)
|
||||
<div class="detail-row" style="border-top:1px solid #f1f5f9;">
|
||||
<span style="color:#64748b;">Project</span>
|
||||
<span style="font-weight:600;color:#0f172a;">{{ $invitation->purchaseRequest->project_name }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<p style="font-size:13px;color:#94a3b8;line-height:1.65;">
|
||||
This link has now been closed. No further action is required.<br>
|
||||
We appreciate your time and look forward to working with you.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer-bar">
|
||||
<span style="font-size:11px;color:#cbd5e1;font-weight:500;letter-spacing:.03em;">SteelERP · Procurement Portal</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -369,9 +369,10 @@ $allDeptsJson = json_encode($allDeptsData);
|
||||
<div style="margin-top:1rem;background:#f0f9ff;border:1px solid #bae6fd;border-radius:0.5rem;padding:0.75rem 1rem;">
|
||||
<p style="font-size:12px;color:#0369a1;margin:0;line-height:1.7;">
|
||||
<strong>Expected format (2 tabs):</strong><br>
|
||||
<strong>Projects</strong> tab — columns: <em>Company Name</em> | <em>Project Name</em><br>
|
||||
<strong>Departments</strong> tab — columns: <em>Company Name</em> | <em>Department Name</em><br>
|
||||
Companies are created automatically if they don't exist. Duplicates are skipped.
|
||||
<strong>Projects</strong> tab — <em>Company Name</em> | <em>Project Name</em> | <em>Location Name</em> | <em>Address</em> | <em>Latitude</em> | <em>Longitude</em><br>
|
||||
↳ Leave Location Name blank for project-only rows. Add multiple rows per project for multiple locations.<br>
|
||||
<strong>Departments</strong> tab — <em>Company Name</em> | <em>Department Name</em><br>
|
||||
Companies are created automatically. Address, Latitude, Longitude are optional. Duplicates are skipped.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -43,6 +43,20 @@ Route::post('/rfq/{token}', [RfqPortalController::class, 'submit'])->name('rfq.s
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
Route::get('/notifications/unread', fn() => response()->json([
|
||||
'count' => auth()->user()->unreadNotifications()->count(),
|
||||
'items' => auth()->user()->unreadNotifications()->latest()->take(10)->get()->map(fn($n) => [
|
||||
'id' => $n->id,
|
||||
'message' => $n->data['message'] ?? '',
|
||||
'url' => $n->data['url'] ?? null,
|
||||
'ago' => $n->created_at->diffForHumans(),
|
||||
]),
|
||||
]))->name('notifications.unread');
|
||||
|
||||
Route::post('/notifications/read-all', fn() => response()->json(
|
||||
tap(auth()->user()->unreadNotifications()->update(['read_at' => now()]))
|
||||
))->name('notifications.read-all');
|
||||
|
||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user