77 KiB
Purchase Request Form Upgrade — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add three admin-managed lookup tables (locations, projects, urgency levels), a Settings admin section in the sidebar, and upgrade the purchase request create/edit forms with searchable dropdowns and an interactive urgency popup.
Architecture: Three migrations + models + seeders for lookup tables. Three Settings controllers under app/Http/Controllers/Settings/. PurchaseRequestController passes lookup collections to create/edit views. The urgency picker is rendered as a floating popup using server-rendered JSON (URGENCY_LEVELS JS variable) and vanilla JS — no Alpine.js or Select2 needed. Dropdowns are native <select> elements. Strings (not IDs) are stored in existing purchase_requests columns for historical integrity.
Tech Stack: Laravel 12 / PHP 8.2, Blade, vanilla JS, inline Tailwind styles (per project convention), SQLite
File Map
Create:
database/migrations/2026_05_24_000001_create_settings_locations_table.phpdatabase/migrations/2026_05_24_000002_create_settings_projects_table.phpdatabase/migrations/2026_05_24_000003_create_settings_urgency_levels_table.phpdatabase/seeders/UrgencyLevelSeeder.phpapp/Models/Settings/Location.phpapp/Models/Settings/ProjectSetting.phpapp/Models/Settings/UrgencyLevel.phpapp/Http/Controllers/Settings/LocationController.phpapp/Http/Controllers/Settings/ProjectSettingController.phpapp/Http/Controllers/Settings/UrgencyLevelController.phpresources/views/settings/locations/index.blade.phpresources/views/settings/projects/index.blade.phpresources/views/settings/urgency-levels/index.blade.php
Modify:
routes/web.phpresources/views/layouts/app.blade.phpapp/Http/Controllers/Purchase/PurchaseRequestController.phpresources/views/purchase/requests/create.blade.phpresources/views/purchase/requests/edit.blade.phpdatabase/seeders/DatabaseSeeder.php
Task 1: Three Migrations
Files:
-
Create:
database/migrations/2026_05_24_000001_create_settings_locations_table.php -
Create:
database/migrations/2026_05_24_000002_create_settings_projects_table.php -
Create:
database/migrations/2026_05_24_000003_create_settings_urgency_levels_table.php -
Step 1: Create the three migration files
Run:
php artisan make:migration create_settings_locations_table
php artisan make:migration create_settings_projects_table
php artisan make:migration create_settings_urgency_levels_table
Expected: Three new files in database/migrations/ with timestamps.
- Step 2: Fill in the locations migration
Open the create_settings_locations_table migration file and replace its up() / down() with:
public function up(): void
{
Schema::create('settings_locations', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('settings_locations');
}
- Step 3: Fill in the projects migration
Open the create_settings_projects_table migration file and replace its up() / down() with:
public function up(): void
{
Schema::create('settings_projects', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('settings_projects');
}
- Step 4: Fill in the urgency levels migration
Open the create_settings_urgency_levels_table migration file and replace its up() / down() with:
public function up(): void
{
Schema::create('settings_urgency_levels', function (Blueprint $table) {
$table->id();
$table->string('label', 100);
$table->string('emoji', 10)->default('📋');
$table->string('color_bg', 20)->default('#f8fafc');
$table->string('color_text', 20)->default('#475569');
$table->string('subtitle', 100)->nullable();
$table->unsignedTinyInteger('sort_order')->default(99);
$table->boolean('show_date_picker')->default(false);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('settings_urgency_levels');
}
- Step 5: Run migrations
php artisan migrate
Expected output includes:
Creating table: settings_locations
Creating table: settings_projects
Creating table: settings_urgency_levels
- Step 6: Verify tables exist
php artisan tinker --execute="DB::select(\"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'settings_%'\")"
Expected: 3 rows — settings_locations, settings_projects, settings_urgency_levels.
- Step 7: Commit
git add database/migrations/
git commit -m "feat: add migrations for settings_locations, settings_projects, settings_urgency_levels"
Task 2: Three Models
Files:
-
Create:
app/Models/Settings/Location.php -
Create:
app/Models/Settings/ProjectSetting.php -
Create:
app/Models/Settings/UrgencyLevel.php -
Step 1: Create
app/Models/Settings/Location.php
<?php
namespace App\Models\Settings;
use Illuminate\Database\Eloquent\Model;
class Location extends Model
{
protected $table = 'settings_locations';
protected $fillable = ['name', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}
- Step 2: Create
app/Models/Settings/ProjectSetting.php
<?php
namespace App\Models\Settings;
use Illuminate\Database\Eloquent\Model;
class ProjectSetting extends Model
{
protected $table = 'settings_projects';
protected $fillable = ['name', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}
- Step 3: Create
app/Models/Settings/UrgencyLevel.php
<?php
namespace App\Models\Settings;
use Illuminate\Database\Eloquent\Model;
class UrgencyLevel extends Model
{
protected $table = 'settings_urgency_levels';
protected $fillable = [
'label', 'emoji', 'color_bg', 'color_text',
'subtitle', 'sort_order', 'show_date_picker', 'is_active',
];
protected $casts = [
'show_date_picker' => 'boolean',
'is_active' => 'boolean',
];
public function scopeActive($query)
{
return $query->where('is_active', true)->orderBy('sort_order');
}
}
- Step 4: Verify models resolve
php artisan tinker --execute="echo App\Models\Settings\Location::count();"
Expected: 0
- Step 5: Commit
git add app/Models/Settings/
git commit -m "feat: add Settings models for Location, ProjectSetting, UrgencyLevel"
Task 3: Seeder for Urgency Levels
Files:
-
Create:
database/seeders/UrgencyLevelSeeder.php -
Modify:
database/seeders/DatabaseSeeder.php -
Step 1: Create
database/seeders/UrgencyLevelSeeder.php
<?php
namespace Database\Seeders;
use App\Models\Settings\UrgencyLevel;
use Illuminate\Database\Seeder;
class UrgencyLevelSeeder extends Seeder
{
public function run(): void
{
if (UrgencyLevel::count() > 0) {
return;
}
$levels = [
['label' => 'Critical', 'emoji' => '🚨', 'color_bg' => '#fee2e2', 'color_text' => '#dc2626', 'subtitle' => 'Today', 'sort_order' => 1, 'show_date_picker' => false],
['label' => 'Urgent', 'emoji' => '⚡', 'color_bg' => '#ffedd5', 'color_text' => '#ea580c', 'subtitle' => '1–3 days', 'sort_order' => 2, 'show_date_picker' => false],
['label' => 'Normal', 'emoji' => '📋', 'color_bg' => '#fef9c3', 'color_text' => '#ca8a04', 'subtitle' => 'This week', 'sort_order' => 3, 'show_date_picker' => false],
['label' => 'Planned', 'emoji' => '🗓️', 'color_bg' => '#f0fdf4', 'color_text' => '#16a34a', 'subtitle' => 'Pick date', 'sort_order' => 4, 'show_date_picker' => true],
];
foreach ($levels as $level) {
UrgencyLevel::create(array_merge($level, ['is_active' => true]));
}
}
}
- Step 2: Register seeder in
database/seeders/DatabaseSeeder.php
Add $this->call(UrgencyLevelSeeder::class); inside the run() method, after the existing calls:
public function run(): void
{
// ... existing calls ...
$this->call(UrgencyLevelSeeder::class);
}
- Step 3: Run the seeder
php artisan db:seed --class=UrgencyLevelSeeder
Expected: No errors.
- Step 4: Verify seeded data
php artisan tinker --execute="App\Models\Settings\UrgencyLevel::all(['label','emoji','sort_order'])->each(fn(\$l) => print(\$l->sort_order.' '.\$l->emoji.' '.\$l->label.PHP_EOL));"
Expected:
1 🚨 Critical
2 ⚡ Urgent
3 📋 Normal
4 🗓️ Planned
- Step 5: Commit
git add database/seeders/
git commit -m "feat: add UrgencyLevelSeeder with 4 default urgency levels"
Task 4: Settings Controllers
Files:
-
Create:
app/Http/Controllers/Settings/LocationController.php -
Create:
app/Http/Controllers/Settings/ProjectSettingController.php -
Create:
app/Http/Controllers/Settings/UrgencyLevelController.php -
Step 1: Create
app/Http/Controllers/Settings/LocationController.php
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Settings\Location;
use Illuminate\Http\Request;
class LocationController extends Controller
{
public function index()
{
$locations = Location::orderBy('name')->get();
return view('settings.locations.index', compact('locations'));
}
public function store(Request $request)
{
$request->validate(['name' => 'required|string|max:255|unique:settings_locations,name']);
Location::create(['name' => $request->name, 'is_active' => true]);
return redirect()->route('settings.locations.index')->with('success', 'Location added.');
}
public function update(Request $request, Location $location)
{
$request->validate(['name' => 'required|string|max:255|unique:settings_locations,name,' . $location->id]);
$location->update([
'name' => $request->name,
'is_active' => $request->boolean('is_active', true),
]);
return redirect()->route('settings.locations.index')->with('success', 'Location updated.');
}
public function destroy(Location $location)
{
$location->delete();
return redirect()->route('settings.locations.index')->with('success', 'Location deleted.');
}
}
- Step 2: Create
app/Http/Controllers/Settings/ProjectSettingController.php
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Settings\ProjectSetting;
use Illuminate\Http\Request;
class ProjectSettingController extends Controller
{
public function index()
{
$projects = ProjectSetting::orderBy('name')->get();
return view('settings.projects.index', compact('projects'));
}
public function store(Request $request)
{
$request->validate(['name' => 'required|string|max:255|unique:settings_projects,name']);
ProjectSetting::create(['name' => $request->name, 'is_active' => true]);
return redirect()->route('settings.projects.index')->with('success', 'Project added.');
}
public function update(Request $request, ProjectSetting $project)
{
$request->validate(['name' => 'required|string|max:255|unique:settings_projects,name,' . $project->id]);
$project->update([
'name' => $request->name,
'is_active' => $request->boolean('is_active', true),
]);
return redirect()->route('settings.projects.index')->with('success', 'Project updated.');
}
public function destroy(ProjectSetting $project)
{
$project->delete();
return redirect()->route('settings.projects.index')->with('success', 'Project deleted.');
}
}
- Step 3: Create
app/Http/Controllers/Settings/UrgencyLevelController.php
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Settings\UrgencyLevel;
use Illuminate\Http\Request;
class UrgencyLevelController extends Controller
{
public function index()
{
$urgencyLevels = UrgencyLevel::orderBy('sort_order')->get();
return view('settings.urgency-levels.index', compact('urgencyLevels'));
}
public function store(Request $request)
{
$request->validate([
'label' => 'required|string|max:100|unique:settings_urgency_levels,label',
'emoji' => 'required|string|max:10',
'color_bg' => 'required|string|max:20',
'color_text' => 'required|string|max:20',
'subtitle' => 'nullable|string|max:100',
'sort_order' => 'required|integer|min:0',
]);
UrgencyLevel::create([
'label' => $request->label,
'emoji' => $request->emoji,
'color_bg' => $request->color_bg,
'color_text' => $request->color_text,
'subtitle' => $request->subtitle,
'sort_order' => $request->sort_order,
'show_date_picker' => $request->boolean('show_date_picker'),
'is_active' => true,
]);
return redirect()->route('settings.urgency-levels.index')->with('success', 'Urgency level added.');
}
public function update(Request $request, UrgencyLevel $urgencyLevel)
{
$request->validate([
'label' => 'required|string|max:100|unique:settings_urgency_levels,label,' . $urgencyLevel->id,
'emoji' => 'required|string|max:10',
'color_bg' => 'required|string|max:20',
'color_text' => 'required|string|max:20',
'subtitle' => 'nullable|string|max:100',
'sort_order' => 'required|integer|min:0',
]);
$urgencyLevel->update([
'label' => $request->label,
'emoji' => $request->emoji,
'color_bg' => $request->color_bg,
'color_text' => $request->color_text,
'subtitle' => $request->subtitle,
'sort_order' => $request->sort_order,
'show_date_picker' => $request->boolean('show_date_picker'),
'is_active' => $request->boolean('is_active', true),
]);
return redirect()->route('settings.urgency-levels.index')->with('success', 'Urgency level updated.');
}
public function destroy(UrgencyLevel $urgencyLevel)
{
$urgencyLevel->delete();
return redirect()->route('settings.urgency-levels.index')->with('success', 'Urgency level deleted.');
}
}
- Step 4: Commit
git add app/Http/Controllers/Settings/
git commit -m "feat: add Settings controllers for Location, ProjectSetting, UrgencyLevel"
Task 5: Routes
Files:
-
Modify:
routes/web.php -
Step 1: Add use-imports at top of
routes/web.php
After the existing use App\Http\Controllers\SettingsController; line, add:
use App\Http\Controllers\Settings\LocationController;
use App\Http\Controllers\Settings\ProjectSettingController;
use App\Http\Controllers\Settings\UrgencyLevelController;
- Step 2: Add routes inside the existing
role:Adminmiddleware group
Find the existing settings middleware group (lines ~119-123):
Route::middleware('role:Admin')->group(function () {
Route::get('settings/integrations', ...
Route::post('settings/integrations/whatsapp', ...
Route::post('settings/integrations/test-whatsapp', ...
});
Add the three new resource groups inside that same block:
Route::middleware('role:Admin')->group(function () {
Route::get('settings/integrations', [SettingsController::class, 'integrations'])->name('settings.integrations');
Route::post('settings/integrations/whatsapp', [SettingsController::class, 'updateWhatsapp'])->name('settings.integrations.whatsapp');
Route::post('settings/integrations/test-whatsapp', [SettingsController::class, 'testWhatsappConnection'])->name('settings.integrations.test-whatsapp');
Route::prefix('settings')->name('settings.')->group(function () {
Route::resource('locations', LocationController::class)
->only(['index', 'store', 'update', 'destroy'])
->parameters(['locations' => 'location']);
Route::resource('projects', ProjectSettingController::class)
->only(['index', 'store', 'update', 'destroy'])
->parameters(['projects' => 'project']);
Route::resource('urgency-levels', UrgencyLevelController::class)
->only(['index', 'store', 'update', 'destroy'])
->parameters(['urgency-levels' => 'urgencyLevel']);
});
});
- Step 3: Verify routes registered
php artisan route:list --name=settings.locations
Expected output shows settings.locations.index, settings.locations.store, settings.locations.update, settings.locations.destroy.
- Step 4: Commit
git add routes/web.php
git commit -m "feat: register settings routes for locations, projects, urgency-levels"
Task 6: Settings View — Locations
Files:
-
Create:
resources/views/settings/locations/index.blade.php -
Step 1: Create the directory and view
mkdir -p resources/views/settings/locations
Create resources/views/settings/locations/index.blade.php:
@extends('layouts.app')
@section('title', 'Locations')
@section('content')
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="page-title">Locations</h1>
<p class="page-subtitle">Manage location / site options used in purchase requests.</p>
</div>
<button onclick="document.getElementById('add-form').classList.toggle('hidden')"
class="btn-primary">+ Add Location</button>
</div>
{{-- Add Form --}}
<div id="add-form" class="card card-body mb-6 hidden">
<h2 class="text-sm font-semibold text-gray-700 mb-3">New Location</h2>
<form action="{{ route('settings.locations.store') }}" method="POST" class="flex items-end gap-3">
@csrf
<div class="flex-1">
<label class="form-label">Name <span class="text-red-500">*</span></label>
<input type="text" name="name" value="{{ old('name') }}" required
class="form-input" placeholder="e.g. BaFa - Hidd">
</div>
<button type="submit" class="btn-primary">Save</button>
<button type="button" onclick="document.getElementById('add-form').classList.add('hidden')"
class="btn-secondary">Cancel</button>
</form>
@error('name')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
{{-- List --}}
<div class="card card-body">
<table class="w-full text-sm">
<thead>
<tr class="bg-gray-50 text-gray-600 uppercase text-xs">
<th class="px-4 py-3 text-left">Name</th>
<th class="px-4 py-3 text-center w-24">Status</th>
<th class="px-4 py-3 text-right w-28">Actions</th>
</tr>
</thead>
<tbody>
@forelse($locations as $location)
<tr class="border-t border-gray-100 hover:bg-gray-50" id="row-{{ $location->id }}">
<td class="px-4 py-3 font-medium text-gray-800" id="name-{{ $location->id }}">
{{ $location->name }}
</td>
<td class="px-4 py-3 text-center">
@if($location->is_active)
<span style="background:#dcfce7;color:#16a34a;border-radius:20px;padding:2px 10px;font-size:11px;font-weight:600;">Active</span>
@else
<span style="background:#f1f5f9;color:#64748b;border-radius:20px;padding:2px 10px;font-size:11px;font-weight:600;">Inactive</span>
@endif
</td>
<td class="px-4 py-3 text-right">
<button onclick="openEditModal({{ $location->id }}, '{{ addslashes($location->name) }}', {{ $location->is_active ? 'true' : 'false' }})"
style="color:#2563eb;background:none;border:none;cursor:pointer;font-size:12px;font-weight:600;margin-right:8px;">Edit</button>
<form method="POST" action="{{ route('settings.locations.destroy', $location) }}"
style="display:inline" onsubmit="event.preventDefault(); confirmDelete(this, 'Delete Location?', 'Remove {{ addslashes($location->name) }} from the list.')">
@csrf @method('DELETE')
<button type="submit" style="color:#dc2626;background:none;border:none;cursor:pointer;font-size:12px;font-weight:600;">Delete</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="px-4 py-8 text-center text-gray-400">No locations yet. Add one above.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Edit Modal --}}
<div id="edit-modal" style="display:none;position:fixed;inset:0;z-index:9000;background:rgba(15,23,42,.6);backdrop-filter:blur(4px);align-items:center;justify-content:center;padding:20px;">
<div style="background:#fff;border-radius:16px;width:100%;max-width:420px;box-shadow:0 20px 50px rgba(0,0,0,.2);">
<div style="padding:20px 22px 0;">
<h3 style="font-size:15px;font-weight:700;color:#0f172a;margin-bottom:16px;">Edit Location</h3>
<form id="edit-form" method="POST" action="">
@csrf @method('PATCH')
<div class="mb-4">
<label class="form-label">Name <span class="text-red-500">*</span></label>
<input type="text" name="name" id="edit-name" required class="form-input">
</div>
<div class="mb-4 flex items-center gap-2">
<input type="checkbox" name="is_active" id="edit-active" value="1" style="width:16px;height:16px;">
<label for="edit-active" class="form-label mb-0">Active</label>
</div>
</div>
<div style="padding:16px 22px 20px;display:flex;gap:8px;justify-content:flex-end;">
<button type="button" onclick="closeEditModal()"
style="padding:9px 18px;border-radius:9px;border:1.5px solid #e2e8f0;background:#fff;font-size:13px;font-weight:600;color:#475569;cursor:pointer;">Cancel</button>
<button type="submit"
style="padding:9px 18px;border-radius:9px;border:none;background:#2563eb;font-size:13px;font-weight:600;color:#fff;cursor:pointer;">Save</button>
</div>
</form>
</div>
</div>
<script>
function openEditModal(id, name, isActive) {
document.getElementById('edit-form').action = '/settings/locations/' + id;
document.getElementById('edit-name').value = name;
document.getElementById('edit-active').checked = isActive;
document.getElementById('edit-modal').style.display = 'flex';
}
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
}
document.getElementById('edit-modal').addEventListener('click', function(e) {
if (e.target === this) closeEditModal();
});
</script>
@endsection
- Step 2: Test the page loads
Visit http://localhost:8000/settings/locations (logged in as Admin).
Expected: Empty table with "Add Location" button.
- Step 3: Test add a location
Click "Add Location", fill in "BaFa - Hidd", click Save. Expected: Row appears, success toast shows.
- Step 4: Commit
git add resources/views/settings/locations/
git commit -m "feat: add Settings Locations management page"
Task 7: Settings View — Projects
Files:
-
Create:
resources/views/settings/projects/index.blade.php -
Step 1: Create the directory and view
mkdir -p resources/views/settings/projects
Create resources/views/settings/projects/index.blade.php:
@extends('layouts.app')
@section('title', 'Projects')
@section('content')
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="page-title">Projects</h1>
<p class="page-subtitle">Manage project / site names used in purchase requests.</p>
</div>
<button onclick="document.getElementById('add-form').classList.toggle('hidden')"
class="btn-primary">+ Add Project</button>
</div>
{{-- Add Form --}}
<div id="add-form" class="card card-body mb-6 hidden">
<h2 class="text-sm font-semibold text-gray-700 mb-3">New Project</h2>
<form action="{{ route('settings.projects.store') }}" method="POST" class="flex items-end gap-3">
@csrf
<div class="flex-1">
<label class="form-label">Name <span class="text-red-500">*</span></label>
<input type="text" name="name" value="{{ old('name') }}" required
class="form-input" placeholder="e.g. Steel Tech Co WLL">
</div>
<button type="submit" class="btn-primary">Save</button>
<button type="button" onclick="document.getElementById('add-form').classList.add('hidden')"
class="btn-secondary">Cancel</button>
</form>
@error('name')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
{{-- List --}}
<div class="card card-body">
<table class="w-full text-sm">
<thead>
<tr class="bg-gray-50 text-gray-600 uppercase text-xs">
<th class="px-4 py-3 text-left">Name</th>
<th class="px-4 py-3 text-center w-24">Status</th>
<th class="px-4 py-3 text-right w-28">Actions</th>
</tr>
</thead>
<tbody>
@forelse($projects as $project)
<tr class="border-t border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-800">{{ $project->name }}</td>
<td class="px-4 py-3 text-center">
@if($project->is_active)
<span style="background:#dcfce7;color:#16a34a;border-radius:20px;padding:2px 10px;font-size:11px;font-weight:600;">Active</span>
@else
<span style="background:#f1f5f9;color:#64748b;border-radius:20px;padding:2px 10px;font-size:11px;font-weight:600;">Inactive</span>
@endif
</td>
<td class="px-4 py-3 text-right">
<button onclick="openEditModal({{ $project->id }}, '{{ addslashes($project->name) }}', {{ $project->is_active ? 'true' : 'false' }})"
style="color:#2563eb;background:none;border:none;cursor:pointer;font-size:12px;font-weight:600;margin-right:8px;">Edit</button>
<form method="POST" action="{{ route('settings.projects.destroy', $project) }}"
style="display:inline" onsubmit="event.preventDefault(); confirmDelete(this, 'Delete Project?', 'Remove {{ addslashes($project->name) }} from the list.')">
@csrf @method('DELETE')
<button type="submit" style="color:#dc2626;background:none;border:none;cursor:pointer;font-size:12px;font-weight:600;">Delete</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="px-4 py-8 text-center text-gray-400">No projects yet. Add one above.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Edit Modal --}}
<div id="edit-modal" style="display:none;position:fixed;inset:0;z-index:9000;background:rgba(15,23,42,.6);backdrop-filter:blur(4px);align-items:center;justify-content:center;padding:20px;">
<div style="background:#fff;border-radius:16px;width:100%;max-width:420px;box-shadow:0 20px 50px rgba(0,0,0,.2);">
<div style="padding:20px 22px 0;">
<h3 style="font-size:15px;font-weight:700;color:#0f172a;margin-bottom:16px;">Edit Project</h3>
<form id="edit-form" method="POST" action="">
@csrf @method('PATCH')
<div class="mb-4">
<label class="form-label">Name <span class="text-red-500">*</span></label>
<input type="text" name="name" id="edit-name" required class="form-input">
</div>
<div class="mb-4 flex items-center gap-2">
<input type="checkbox" name="is_active" id="edit-active" value="1" style="width:16px;height:16px;">
<label for="edit-active" class="form-label mb-0">Active</label>
</div>
</div>
<div style="padding:16px 22px 20px;display:flex;gap:8px;justify-content:flex-end;">
<button type="button" onclick="closeEditModal()"
style="padding:9px 18px;border-radius:9px;border:1.5px solid #e2e8f0;background:#fff;font-size:13px;font-weight:600;color:#475569;cursor:pointer;">Cancel</button>
<button type="submit"
style="padding:9px 18px;border-radius:9px;border:none;background:#2563eb;font-size:13px;font-weight:600;color:#fff;cursor:pointer;">Save</button>
</div>
</form>
</div>
</div>
<script>
function openEditModal(id, name, isActive) {
document.getElementById('edit-form').action = '/settings/projects/' + id;
document.getElementById('edit-name').value = name;
document.getElementById('edit-active').checked = isActive;
document.getElementById('edit-modal').style.display = 'flex';
}
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
}
document.getElementById('edit-modal').addEventListener('click', function(e) {
if (e.target === this) closeEditModal();
});
</script>
@endsection
- Step 2: Test the page loads
Visit http://localhost:8000/settings/projects (logged in as Admin).
Expected: Empty table with "Add Project" button.
- Step 3: Commit
git add resources/views/settings/projects/
git commit -m "feat: add Settings Projects management page"
Task 8: Settings View — Urgency Levels
Files:
-
Create:
resources/views/settings/urgency-levels/index.blade.php -
Step 1: Create the directory and view
mkdir -p "resources/views/settings/urgency-levels"
Create resources/views/settings/urgency-levels/index.blade.php:
@extends('layouts.app')
@section('title', 'Urgency Levels')
@section('content')
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="page-title">Urgency Levels</h1>
<p class="page-subtitle">Configure the urgency options shown in purchase request forms.</p>
</div>
<button onclick="document.getElementById('add-form').classList.toggle('hidden')"
class="btn-primary">+ Add Level</button>
</div>
{{-- Live Preview --}}
<div class="card card-body mb-6">
<h2 class="text-sm font-semibold text-gray-700 mb-3">Popup Preview</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:8px;max-width:480px;">
@foreach($urgencyLevels->where('is_active', true) as $level)
<div style="border:1.5px solid {{ $level->color_text }}33;border-radius:10px;padding:10px;text-align:center;background:{{ $level->color_bg }};">
<div style="font-size:22px;margin-bottom:2px;">{{ $level->emoji }}</div>
<div style="font-size:11px;font-weight:700;color:{{ $level->color_text }};">{{ $level->label }}</div>
<div style="font-size:10px;color:#94a3b8;">{{ $level->subtitle }}</div>
</div>
@endforeach
</div>
</div>
{{-- Add Form --}}
<div id="add-form" class="card card-body mb-6 hidden">
<h2 class="text-sm font-semibold text-gray-700 mb-3">New Urgency Level</h2>
<form action="{{ route('settings.urgency-levels.store') }}" method="POST">
@csrf
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3">
<div>
<label class="form-label">Label <span class="text-red-500">*</span></label>
<input type="text" name="label" value="{{ old('label') }}" required class="form-input" placeholder="e.g. Urgent">
</div>
<div>
<label class="form-label">Emoji <span class="text-red-500">*</span></label>
<input type="text" name="emoji" value="{{ old('emoji') }}" required class="form-input" placeholder="⚡">
</div>
<div>
<label class="form-label">Subtitle</label>
<input type="text" name="subtitle" value="{{ old('subtitle') }}" class="form-input" placeholder="1–3 days">
</div>
<div>
<label class="form-label">BG Color <span class="text-red-500">*</span></label>
<input type="text" name="color_bg" value="{{ old('color_bg', '#f8fafc') }}" required class="form-input" placeholder="#ffedd5">
</div>
<div>
<label class="form-label">Text Color <span class="text-red-500">*</span></label>
<input type="text" name="color_text" value="{{ old('color_text', '#475569') }}" required class="form-input" placeholder="#ea580c">
</div>
<div>
<label class="form-label">Sort Order <span class="text-red-500">*</span></label>
<input type="number" name="sort_order" value="{{ old('sort_order', 99) }}" required min="0" class="form-input">
</div>
</div>
<div class="mt-3 flex items-center gap-2">
<input type="checkbox" name="show_date_picker" value="1" id="add-dp" style="width:16px;height:16px;" {{ old('show_date_picker') ? 'checked' : '' }}>
<label for="add-dp" class="form-label mb-0">Show date picker when selected</label>
</div>
<div class="mt-4 flex gap-3">
<button type="submit" class="btn-primary">Save</button>
<button type="button" onclick="document.getElementById('add-form').classList.add('hidden')" class="btn-secondary">Cancel</button>
</div>
</form>
</div>
{{-- List --}}
<div class="card card-body">
<table class="w-full text-sm">
<thead>
<tr class="bg-gray-50 text-gray-600 uppercase text-xs">
<th class="px-4 py-3 text-left w-10">#</th>
<th class="px-4 py-3 text-left">Label</th>
<th class="px-4 py-3 text-left">Subtitle</th>
<th class="px-4 py-3 text-left">Colors</th>
<th class="px-4 py-3 text-center w-24">Status</th>
<th class="px-4 py-3 text-right w-28">Actions</th>
</tr>
</thead>
<tbody>
@forelse($urgencyLevels as $level)
<tr class="border-t border-gray-100 hover:bg-gray-50">
<td class="px-4 py-3 text-gray-400 text-xs">{{ $level->sort_order }}</td>
<td class="px-4 py-3">
<span style="background:{{ $level->color_bg }};color:{{ $level->color_text }};border-radius:20px;padding:3px 10px;font-weight:600;font-size:12px;">
{{ $level->emoji }} {{ $level->label }}
</span>
@if($level->show_date_picker)
<span style="font-size:10px;color:#94a3b8;margin-left:4px;">📅 date picker</span>
@endif
</td>
<td class="px-4 py-3 text-gray-500 text-xs">{{ $level->subtitle }}</td>
<td class="px-4 py-3">
<span style="display:inline-block;width:14px;height:14px;border-radius:3px;background:{{ $level->color_bg }};border:1px solid #e2e8f0;vertical-align:middle;"></span>
<span style="display:inline-block;width:14px;height:14px;border-radius:3px;background:{{ $level->color_text }};border:1px solid #e2e8f0;vertical-align:middle;margin-left:2px;"></span>
</td>
<td class="px-4 py-3 text-center">
@if($level->is_active)
<span style="background:#dcfce7;color:#16a34a;border-radius:20px;padding:2px 10px;font-size:11px;font-weight:600;">Active</span>
@else
<span style="background:#f1f5f9;color:#64748b;border-radius:20px;padding:2px 10px;font-size:11px;font-weight:600;">Inactive</span>
@endif
</td>
<td class="px-4 py-3 text-right">
<button onclick="openEditModal({{ $level->id }}, {{ $level->toJson() }})"
style="color:#2563eb;background:none;border:none;cursor:pointer;font-size:12px;font-weight:600;margin-right:8px;">Edit</button>
<form method="POST" action="{{ route('settings.urgency-levels.destroy', $level) }}"
style="display:inline" onsubmit="event.preventDefault(); confirmDelete(this, 'Delete Urgency Level?', 'Remove {{ addslashes($level->label) }} from the list.')">
@csrf @method('DELETE')
<button type="submit" style="color:#dc2626;background:none;border:none;cursor:pointer;font-size:12px;font-weight:600;">Delete</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-8 text-center text-gray-400">No urgency levels yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{-- Edit Modal --}}
<div id="edit-modal" style="display:none;position:fixed;inset:0;z-index:9000;background:rgba(15,23,42,.6);backdrop-filter:blur(4px);align-items:center;justify-content:center;padding:20px;">
<div style="background:#fff;border-radius:16px;width:100%;max-width:500px;box-shadow:0 20px 50px rgba(0,0,0,.2);max-height:90vh;overflow-y:auto;">
<div style="padding:20px 22px 0;">
<h3 style="font-size:15px;font-weight:700;color:#0f172a;margin-bottom:16px;">Edit Urgency Level</h3>
<form id="edit-form" method="POST" action="">
@csrf @method('PATCH')
<div class="grid grid-cols-2 gap-4">
<div>
<label class="form-label">Label <span class="text-red-500">*</span></label>
<input type="text" name="label" id="e-label" required class="form-input">
</div>
<div>
<label class="form-label">Emoji <span class="text-red-500">*</span></label>
<input type="text" name="emoji" id="e-emoji" required class="form-input">
</div>
<div>
<label class="form-label">Subtitle</label>
<input type="text" name="subtitle" id="e-subtitle" class="form-input">
</div>
<div>
<label class="form-label">Sort Order</label>
<input type="number" name="sort_order" id="e-sort" min="0" class="form-input">
</div>
<div>
<label class="form-label">BG Color <span class="text-red-500">*</span></label>
<input type="text" name="color_bg" id="e-bg" required class="form-input">
</div>
<div>
<label class="form-label">Text Color <span class="text-red-500">*</span></label>
<input type="text" name="color_text" id="e-text" required class="form-input">
</div>
</div>
<div class="mt-3 flex items-center gap-2">
<input type="checkbox" name="show_date_picker" id="e-dp" value="1" style="width:16px;height:16px;">
<label for="e-dp" class="form-label mb-0">Show date picker when selected</label>
</div>
<div class="mt-3 flex items-center gap-2">
<input type="checkbox" name="is_active" id="e-active" value="1" style="width:16px;height:16px;">
<label for="e-active" class="form-label mb-0">Active</label>
</div>
</div>
<div style="padding:16px 22px 20px;display:flex;gap:8px;justify-content:flex-end;">
<button type="button" onclick="closeEditModal()"
style="padding:9px 18px;border-radius:9px;border:1.5px solid #e2e8f0;background:#fff;font-size:13px;font-weight:600;color:#475569;cursor:pointer;">Cancel</button>
<button type="submit"
style="padding:9px 18px;border-radius:9px;border:none;background:#2563eb;font-size:13px;font-weight:600;color:#fff;cursor:pointer;">Save</button>
</div>
</form>
</div>
</div>
<script>
function openEditModal(id, level) {
document.getElementById('edit-form').action = '/settings/urgency-levels/' + id;
document.getElementById('e-label').value = level.label;
document.getElementById('e-emoji').value = level.emoji;
document.getElementById('e-subtitle').value = level.subtitle || '';
document.getElementById('e-sort').value = level.sort_order;
document.getElementById('e-bg').value = level.color_bg;
document.getElementById('e-text').value = level.color_text;
document.getElementById('e-dp').checked = !!level.show_date_picker;
document.getElementById('e-active').checked = !!level.is_active;
document.getElementById('edit-modal').style.display = 'flex';
}
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
}
document.getElementById('edit-modal').addEventListener('click', function(e) {
if (e.target === this) closeEditModal();
});
</script>
@endsection
- Step 2: Test the page loads
Visit http://localhost:8000/settings/urgency-levels (logged in as Admin).
Expected: Live preview showing 4 urgency cards, table listing all 4 levels.
- Step 3: Commit
git add resources/views/settings/urgency-levels/
git commit -m "feat: add Settings Urgency Levels management page"
Task 9: Sidebar Links
Files:
-
Modify:
resources/views/layouts/app.blade.php -
Step 1: Add Locations, Projects, and Urgency Levels links under the System section
In resources/views/layouts/app.blade.php, find the existing "Integrations" sidebar link (around line 178). After the Integrations <a> tag and before @endrole, add:
<a href="{{ route('settings.locations.index') }}" style="
display:block; padding:7px 12px 7px 24px; border-radius:7px; margin-bottom:1px;
font-size:13px; text-decoration:none;
{{ request()->routeIs('settings.locations*') ? 'background:#1e293b;color:#fff;font-weight:500;' : 'color:#94a3b8;' }}
" onmouseover="if(!this.style.color.includes('fff'))this.style.color='#e2e8f0'" onmouseout="if(!this.style.background.includes('1e293b'))this.style.color='#94a3b8'">
Locations
</a>
<a href="{{ route('settings.projects.index') }}" style="
display:block; padding:7px 12px 7px 24px; border-radius:7px; margin-bottom:1px;
font-size:13px; text-decoration:none;
{{ request()->routeIs('settings.projects*') ? 'background:#1e293b;color:#fff;font-weight:500;' : 'color:#94a3b8;' }}
" onmouseover="if(!this.style.color.includes('fff'))this.style.color='#e2e8f0'" onmouseout="if(!this.style.background.includes('1e293b'))this.style.color='#94a3b8'">
Projects
</a>
<a href="{{ route('settings.urgency-levels.index') }}" style="
display:block; padding:7px 12px 7px 24px; border-radius:7px; margin-bottom:1px;
font-size:13px; text-decoration:none;
{{ request()->routeIs('settings.urgency-levels*') ? 'background:#1e293b;color:#fff;font-weight:500;' : 'color:#94a3b8;' }}
" onmouseover="if(!this.style.color.includes('fff'))this.style.color='#e2e8f0'" onmouseout="if(!this.style.background.includes('1e293b'))this.style.color='#94a3b8'">
Urgency Levels
</a>
- Step 2: Verify in browser
Log in as Admin, check the sidebar — should show Integrations, Locations, Projects, Urgency Levels under the System section.
- Step 3: Commit
git add resources/views/layouts/app.blade.php
git commit -m "feat: add Settings sidebar links for Locations, Projects, Urgency Levels"
Task 10: Update PurchaseRequestController
Files:
-
Modify:
app/Http/Controllers/Purchase/PurchaseRequestController.php -
Step 1: Add use-imports at the top of the controller
After the existing use statements, add:
use App\Models\Settings\Location;
use App\Models\Settings\ProjectSetting;
use App\Models\Settings\UrgencyLevel;
- Step 2: Update
create()to pass lookup data
Replace:
public function create()
{
return view('purchase.requests.create');
}
With:
public function create()
{
$locations = Location::active()->orderBy('name')->pluck('name');
$projects = ProjectSetting::active()->orderBy('name')->pluck('name');
$urgencyLevels = UrgencyLevel::active()->get(['id', 'label', 'emoji', 'color_bg', 'color_text', 'subtitle', 'show_date_picker']);
return view('purchase.requests.create', compact('locations', 'projects', 'urgencyLevels'));
}
- Step 3: Update
edit()to pass lookup data
Replace:
public function edit(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->load('items');
return view('purchase.requests.edit', compact('purchaseRequest'));
}
With:
public function edit(PurchaseRequest $purchaseRequest)
{
$purchaseRequest->load('items');
$locations = Location::active()->orderBy('name')->pluck('name');
$projects = ProjectSetting::active()->orderBy('name')->pluck('name');
$urgencyLevels = UrgencyLevel::active()->get(['id', 'label', 'emoji', 'color_bg', 'color_text', 'subtitle', 'show_date_picker']);
return view('purchase.requests.edit', compact('purchaseRequest', 'locations', 'projects', 'urgencyLevels'));
}
- Step 4: Verify no errors
php artisan route:list --name=purchase.requests.create
Expected: Route listed with no errors.
- Step 5: Commit
git add app/Http/Controllers/Purchase/PurchaseRequestController.php
git commit -m "feat: pass locations, projects, urgencyLevels to purchase request create/edit"
Task 11: Update create.blade.php
Files:
-
Modify:
resources/views/purchase/requests/create.blade.php -
Step 1: Replace the entire file content
Replace the full content of resources/views/purchase/requests/create.blade.php with:
@extends('layouts.app')
@section('title', 'New Material Purchase Request')
@section('content')
<div class="mb-6">
<h1 class="page-title">New Material Purchase Request</h1>
<p class="page-subtitle"><a href="{{ route('purchase.requests.index') }}" class="text-blue-600 hover:underline">Purchase Requests</a> / New</p>
</div>
@if($errors->any())
<div class="mb-4 px-4 py-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
<ul class="list-disc list-inside space-y-1">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('purchase.requests.store') }}" method="POST" id="mpr-form">
@csrf
{{-- Header Details --}}
<div class="card card-body mb-6">
<h2 class="text-base font-semibold text-gray-700 mb-4">Project / Department Details</h2>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label class="form-label">Date <span class="text-red-500">*</span></label>
<input type="date" name="date" value="{{ old('date', date('Y-m-d')) }}" required class="form-input">
</div>
{{-- Project / Site Name — dropdown --}}
<div>
<label class="form-label">Project / Site Name <span class="text-red-500">*</span></label>
<select name="project_name" required class="form-input">
<option value="">Select project…</option>
@foreach($projects as $p)
<option value="{{ $p }}" {{ old('project_name') === $p ? 'selected' : '' }}>{{ $p }}</option>
@endforeach
</select>
</div>
<div>
<label class="form-label">Requested By <span class="text-red-500">*</span></label>
<input type="text" name="requested_by_name" value="{{ old('requested_by_name') }}" required class="form-input" placeholder="Person's name">
</div>
{{-- Required Date / Urgency — popup picker --}}
<div style="position:relative;">
<label class="form-label">Required Date / Urgency</label>
<div id="urgency-trigger" onclick="toggleUrgencyPopup()" style="
border:1px solid #d1d5db;border-radius:8px;padding:9px 12px;
background:#fff;cursor:pointer;display:flex;align-items:center;
justify-content:space-between;min-height:42px;
">
<span id="urgency-display" style="font-size:14px;color:#9ca3af;">Select urgency…</span>
<svg width="14" height="14" fill="none" stroke="#9ca3af" viewBox="0 0 24 24" style="flex-shrink:0;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
<input type="hidden" name="required_date_text" id="required_date_text" value="{{ old('required_date_text') }}">
{{-- Urgency Popup --}}
<div id="urgency-popup" style="
display:none;position:absolute;top:calc(100% + 6px);left:0;z-index:200;
background:#fff;border-radius:16px;border:1.5px solid #e2e8f0;
box-shadow:0 12px 32px rgba(0,0,0,.14);padding:16px;width:300px;
">
<div style="font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px;">
How soon is this needed?
</div>
<div id="urgency-cards" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;"></div>
<div id="date-section" style="display:none;margin-top:12px;border-top:1px solid #f1f5f9;padding-top:12px;">
<label style="font-size:11px;color:#6b7280;font-weight:500;display:block;margin-bottom:4px;">Specify the required date:</label>
<input type="date" id="planned-date" style="width:100%;border:1.5px solid #3b82f6;border-radius:8px;padding:7px 10px;font-size:13px;" min="{{ date('Y-m-d') }}">
</div>
</div>
</div>
{{-- Location / Site — dropdown --}}
<div>
<label class="form-label">Location / Site</label>
<select name="location" class="form-input">
<option value="">Select location…</option>
@foreach($locations as $loc)
<option value="{{ $loc }}" {{ old('location') === $loc ? 'selected' : '' }}>{{ $loc }}</option>
@endforeach
</select>
</div>
<div>
<label class="form-label">Department</label>
<input type="text" name="department" value="{{ old('department') }}" class="form-input" placeholder="e.g. Operations, Production">
</div>
</div>
</div>
{{-- Material Items --}}
<div class="card card-body mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-gray-700">Material Details</h2>
<button type="button" id="add-row" class="btn-primary btn-sm">+ Add Item</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm border-collapse" id="items-table">
<thead>
<tr class="bg-gray-50 text-gray-600 uppercase text-xs">
<th class="border border-gray-300 px-3 py-2 text-left w-10">S.No</th>
<th class="border border-gray-300 px-3 py-2 text-left">Description of Material <span class="text-red-500">*</span></th>
<th class="border border-gray-300 px-3 py-2 text-left w-24">Unit</th>
<th class="border border-gray-300 px-3 py-2 text-left w-28">Qty Required <span class="text-red-500">*</span></th>
<th class="border border-gray-300 px-3 py-2 text-left w-40">Purpose / Use</th>
<th class="border border-gray-300 px-3 py-2 text-left w-36">Required Date</th>
<th class="border border-gray-300 px-2 py-2 w-10"></th>
</tr>
</thead>
<tbody id="items-body">
@php $oldItems = old('items', [[]]); @endphp
@foreach($oldItems as $idx => $oldItem)
<tr class="item-row">
<td class="border border-gray-300 px-3 py-2 text-center text-gray-500 row-num">{{ $idx + 1 }}</td>
<td class="border border-gray-300 px-2 py-1">
<input type="text" name="items[{{ $idx }}][description]" value="{{ $oldItem['description'] ?? '' }}"
class="w-full border-0 focus:ring-0 text-sm" placeholder="Material description" required>
</td>
<td class="border border-gray-300 px-2 py-1">
<input type="text" name="items[{{ $idx }}][unit]" value="{{ $oldItem['unit'] ?? '' }}"
class="w-full border-0 focus:ring-0 text-sm" placeholder="PCS, NOS, KG…">
</td>
<td class="border border-gray-300 px-2 py-1">
<input type="number" name="items[{{ $idx }}][quantity_required]" value="{{ $oldItem['quantity_required'] ?? '' }}"
min="0.01" step="0.01" class="w-full border-0 focus:ring-0 text-sm" placeholder="0" required>
</td>
<td class="border border-gray-300 px-2 py-1">
<input type="text" name="items[{{ $idx }}][purpose_use]" value="{{ $oldItem['purpose_use'] ?? '' }}"
class="w-full border-0 focus:ring-0 text-sm" placeholder="Purpose…">
</td>
<td class="border border-gray-300 px-2 py-1">
<input type="date" name="items[{{ $idx }}][required_date]" value="{{ $oldItem['required_date'] ?? '' }}"
class="w-full border-0 focus:ring-0 text-sm">
</td>
<td class="border border-gray-300 px-2 py-1 text-center">
<button type="button" class="remove-row text-red-500 hover:text-red-700 font-bold text-lg leading-none">×</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
{{-- Remarks --}}
<div class="card card-body mb-6">
<label class="form-label">Remarks / Notes</label>
<textarea name="remarks" rows="2" class="form-textarea">{{ old('remarks') }}</textarea>
</div>
<div class="flex items-center gap-3">
<button type="submit" class="btn-primary">Submit Request</button>
<a href="{{ route('purchase.requests.index') }}" class="btn-secondary">Cancel</a>
</div>
</form>
<script>
(function () {
// ── Urgency Popup ────────────────────────────────────────
var URGENCY_LEVELS = @json($urgencyLevels);
var hiddenInput = document.getElementById('required_date_text');
var displayEl = document.getElementById('urgency-display');
var popup = document.getElementById('urgency-popup');
var cardsEl = document.getElementById('urgency-cards');
var dateSection = document.getElementById('date-section');
var plannedDate = document.getElementById('planned-date');
var activeLevelId = null;
function buildCards() {
cardsEl.innerHTML = '';
URGENCY_LEVELS.forEach(function (level) {
var card = document.createElement('div');
card.className = 'urg-card';
card.setAttribute('data-id', level.id);
card.style.cssText = 'border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;';
card.innerHTML = '<div style="font-size:22px;margin-bottom:3px;">' + level.emoji + '</div>'
+ '<div style="font-size:11px;font-weight:700;color:' + level.color_text + ';">' + level.label + '</div>'
+ '<div style="font-size:10px;color:#94a3b8;margin-top:1px;">' + (level.subtitle || '') + '</div>';
card.addEventListener('click', function () { pickLevel(level); });
cardsEl.appendChild(card);
});
restoreSelection();
}
function pickLevel(level) {
activeLevelId = level.id;
document.querySelectorAll('.urg-card').forEach(function (c) {
var isActive = parseInt(c.getAttribute('data-id')) === level.id;
c.style.borderColor = isActive ? level.color_text : '#e2e8f0';
c.style.background = isActive ? level.color_bg : '#fff';
});
if (level.show_date_picker) {
dateSection.style.display = 'block';
plannedDate.focus();
} else {
dateSection.style.display = 'none';
commitSelection(level.label, level.emoji, level.color_bg, level.color_text);
closePopup();
}
}
function commitSelection(label, emoji, bg, textColor) {
hiddenInput.value = label;
displayEl.innerHTML = '<span style="background:' + bg + ';color:' + textColor
+ ';border-radius:20px;padding:3px 12px;font-size:13px;font-weight:600;">'
+ emoji + ' ' + label + '</span>';
}
plannedDate.addEventListener('change', function () {
if (!this.value) return;
var level = URGENCY_LEVELS.find(function (l) { return l.id === activeLevelId; });
if (!level) return;
var d = new Date(this.value + 'T00:00:00');
var formatted = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
hiddenInput.value = level.label + ' — ' + this.value;
displayEl.innerHTML = '<span style="background:' + level.color_bg + ';color:' + level.color_text
+ ';border-radius:20px;padding:3px 12px;font-size:13px;font-weight:600;">'
+ level.emoji + ' ' + formatted + '</span>';
closePopup();
});
function restoreSelection() {
var saved = hiddenInput.value;
if (!saved) return;
URGENCY_LEVELS.forEach(function (level) {
if (saved === level.label || saved.indexOf(level.label + ' — ') === 0) {
activeLevelId = level.id;
var card = cardsEl.querySelector('[data-id="' + level.id + '"]');
if (card) {
card.style.borderColor = level.color_text;
card.style.background = level.color_bg;
}
if (level.show_date_picker && saved.indexOf(' — ') !== -1) {
var datePart = saved.split(' — ')[1];
plannedDate.value = datePart;
dateSection.style.display = 'block';
}
var emoji = level.emoji;
var label = level.label;
var displayText = saved.indexOf(' — ') !== -1
? emoji + ' ' + new Date(saved.split(' — ')[1] + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
: emoji + ' ' + label;
displayEl.innerHTML = '<span style="background:' + level.color_bg + ';color:' + level.color_text
+ ';border-radius:20px;padding:3px 12px;font-size:13px;font-weight:600;">'
+ displayText + '</span>';
}
});
}
function closePopup() {
popup.style.display = 'none';
}
window.toggleUrgencyPopup = function () {
popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
};
document.addEventListener('click', function (e) {
var trigger = document.getElementById('urgency-trigger');
if (popup.style.display !== 'none' && !popup.contains(e.target) && !trigger.contains(e.target)) {
closePopup();
}
});
buildCards();
// ── Items table ──────────────────────────────────────────
let rowIndex = {{ count($oldItems ?? [[]]) }};
function renumber() {
document.querySelectorAll('#items-body .row-num').forEach(function (el, i) {
el.textContent = i + 1;
});
}
function newRow() {
var idx = rowIndex++;
var tr = document.createElement('tr');
tr.className = 'item-row';
tr.innerHTML =
'<td class="border border-gray-300 px-3 py-2 text-center text-gray-500 row-num"></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="text" name="items[' + idx + '][description]" class="w-full border-0 focus:ring-0 text-sm" placeholder="Material description" required></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="text" name="items[' + idx + '][unit]" class="w-full border-0 focus:ring-0 text-sm" placeholder="PCS, NOS, KG…"></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="number" name="items[' + idx + '][quantity_required]" min="0.01" step="0.01" class="w-full border-0 focus:ring-0 text-sm" placeholder="0" required></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="text" name="items[' + idx + '][purpose_use]" class="w-full border-0 focus:ring-0 text-sm" placeholder="Purpose…"></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="date" name="items[' + idx + '][required_date]" class="w-full border-0 focus:ring-0 text-sm"></td>'
+ '<td class="border border-gray-300 px-2 py-1 text-center"><button type="button" class="remove-row text-red-500 hover:text-red-700 font-bold text-lg leading-none">×</button></td>';
return tr;
}
document.getElementById('add-row').addEventListener('click', function () {
document.getElementById('items-body').appendChild(newRow());
renumber();
});
document.getElementById('items-body').addEventListener('click', function (e) {
if (e.target.classList.contains('remove-row')) {
var rows = document.querySelectorAll('#items-body .item-row');
if (rows.length > 1) {
e.target.closest('tr').remove();
renumber();
}
}
});
renumber();
})();
</script>
@endsection
- Step 2: Test form loads
Visit http://localhost:8000/purchase/requests/create.
Expected: Project and Location fields show as dropdowns. Urgency field shows clickable trigger.
- Step 3: Test urgency popup
Click the "Select urgency…" field. Expected: Popup opens with 2×2 icon cards. Click "Critical" — popup closes, pill shows "🚨 Critical". Click field again, click "Planned" — date input appears. Pick a date — popup closes with "🗓️ 14 Jun 2026" pill.
- Step 4: Test form submission
Fill all required fields and submit. Expected: Redirects to index with success toast. Verify in tinker:
php artisan tinker --execute="echo App\Models\PurchaseRequest::latest()->first()->required_date_text;"
Expected: The urgency label you picked (e.g. "Critical" or "Planned — 2026-06-14").
- Step 5: Commit
git add resources/views/purchase/requests/create.blade.php
git commit -m "feat: upgrade purchase request create form with dropdowns and urgency popup"
Task 12: Update edit.blade.php
Files:
-
Modify:
resources/views/purchase/requests/edit.blade.php -
Step 1: Replace the Project / Site Name field
Find:
<div>
<label class="form-label">Project / Site Name <span class="text-red-500">*</span></label>
<input type="text" name="project_name"
value="{{ old('project_name', $purchaseRequest->project_name) }}"
required class="form-input" placeholder="e.g. Steel Tech Co WLL">
</div>
Replace with:
<div>
<label class="form-label">Project / Site Name <span class="text-red-500">*</span></label>
<select name="project_name" required class="form-input">
<option value="">Select project…</option>
@foreach($projects as $p)
@php $selected = old('project_name', $purchaseRequest->project_name) === $p ? 'selected' : '' @endphp
<option value="{{ $p }}" {{ $selected }}>{{ $p }}</option>
@endforeach
</select>
</div>
- Step 2: Replace the Required Date / Urgency field
Find:
<div>
<label class="form-label">Required Date / Urgency</label>
<input type="text" name="required_date_text"
value="{{ old('required_date_text', $purchaseRequest->required_date_text) }}"
class="form-input" placeholder="e.g. Urgent, or 2026-06-01">
</div>
Replace with:
<div style="position:relative;">
<label class="form-label">Required Date / Urgency</label>
<div id="urgency-trigger" onclick="toggleUrgencyPopup()" style="
border:1px solid #d1d5db;border-radius:8px;padding:9px 12px;
background:#fff;cursor:pointer;display:flex;align-items:center;
justify-content:space-between;min-height:42px;
">
<span id="urgency-display" style="font-size:14px;color:#9ca3af;">Select urgency…</span>
<svg width="14" height="14" fill="none" stroke="#9ca3af" viewBox="0 0 24 24" style="flex-shrink:0;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
<input type="hidden" name="required_date_text" id="required_date_text"
value="{{ old('required_date_text', $purchaseRequest->required_date_text) }}">
<div id="urgency-popup" style="
display:none;position:absolute;top:calc(100% + 6px);left:0;z-index:200;
background:#fff;border-radius:16px;border:1.5px solid #e2e8f0;
box-shadow:0 12px 32px rgba(0,0,0,.14);padding:16px;width:300px;
">
<div style="font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px;">
How soon is this needed?
</div>
<div id="urgency-cards" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;"></div>
<div id="date-section" style="display:none;margin-top:12px;border-top:1px solid #f1f5f9;padding-top:12px;">
<label style="font-size:11px;color:#6b7280;font-weight:500;display:block;margin-bottom:4px;">Specify the required date:</label>
<input type="date" id="planned-date" style="width:100%;border:1.5px solid #3b82f6;border-radius:8px;padding:7px 10px;font-size:13px;">
</div>
</div>
</div>
- Step 3: Replace the Location / Site field
Find:
<div>
<label class="form-label">Location / Site</label>
<input type="text" name="location"
value="{{ old('location', $purchaseRequest->location) }}"
class="form-input" placeholder="e.g. BaFa - Hidd">
</div>
Replace with:
<div>
<label class="form-label">Location / Site</label>
<select name="location" class="form-input">
<option value="">Select location…</option>
@foreach($locations as $loc)
@php $selected = old('location', $purchaseRequest->location) === $loc ? 'selected' : '' @endphp
<option value="{{ $loc }}" {{ $selected }}>{{ $loc }}</option>
@endforeach
</select>
</div>
- Step 4: Add the urgency popup JS before
@endsection
At the very end of the file (before @endsection), add a <script> block with the exact same urgency JS from Task 11, replacing the @json($urgencyLevels) and the rowIndex initialization:
<script>
(function () {
// ── Urgency Popup ─────────────────────────────────────────
var URGENCY_LEVELS = @json($urgencyLevels);
var hiddenInput = document.getElementById('required_date_text');
var displayEl = document.getElementById('urgency-display');
var popup = document.getElementById('urgency-popup');
var cardsEl = document.getElementById('urgency-cards');
var dateSection = document.getElementById('date-section');
var plannedDate = document.getElementById('planned-date');
var activeLevelId = null;
function buildCards() {
cardsEl.innerHTML = '';
URGENCY_LEVELS.forEach(function (level) {
var card = document.createElement('div');
card.className = 'urg-card';
card.setAttribute('data-id', level.id);
card.style.cssText = 'border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;';
card.innerHTML = '<div style="font-size:22px;margin-bottom:3px;">' + level.emoji + '</div>'
+ '<div style="font-size:11px;font-weight:700;color:' + level.color_text + ';">' + level.label + '</div>'
+ '<div style="font-size:10px;color:#94a3b8;margin-top:1px;">' + (level.subtitle || '') + '</div>';
card.addEventListener('click', function () { pickLevel(level); });
cardsEl.appendChild(card);
});
restoreSelection();
}
function pickLevel(level) {
activeLevelId = level.id;
document.querySelectorAll('.urg-card').forEach(function (c) {
var isActive = parseInt(c.getAttribute('data-id')) === level.id;
c.style.borderColor = isActive ? level.color_text : '#e2e8f0';
c.style.background = isActive ? level.color_bg : '#fff';
});
if (level.show_date_picker) {
dateSection.style.display = 'block';
plannedDate.focus();
} else {
dateSection.style.display = 'none';
commitSelection(level.label, level.emoji, level.color_bg, level.color_text);
closePopup();
}
}
function commitSelection(label, emoji, bg, textColor) {
hiddenInput.value = label;
displayEl.innerHTML = '<span style="background:' + bg + ';color:' + textColor
+ ';border-radius:20px;padding:3px 12px;font-size:13px;font-weight:600;">'
+ emoji + ' ' + label + '</span>';
}
plannedDate.addEventListener('change', function () {
if (!this.value) return;
var level = URGENCY_LEVELS.find(function (l) { return l.id === activeLevelId; });
if (!level) return;
var d = new Date(this.value + 'T00:00:00');
var formatted = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
hiddenInput.value = level.label + ' — ' + this.value;
displayEl.innerHTML = '<span style="background:' + level.color_bg + ';color:' + level.color_text
+ ';border-radius:20px;padding:3px 12px;font-size:13px;font-weight:600;">'
+ level.emoji + ' ' + formatted + '</span>';
closePopup();
});
function restoreSelection() {
var saved = hiddenInput.value;
if (!saved) return;
URGENCY_LEVELS.forEach(function (level) {
if (saved === level.label || saved.indexOf(level.label + ' — ') === 0) {
activeLevelId = level.id;
var card = cardsEl.querySelector('[data-id="' + level.id + '"]');
if (card) {
card.style.borderColor = level.color_text;
card.style.background = level.color_bg;
}
if (level.show_date_picker && saved.indexOf(' — ') !== -1) {
var datePart = saved.split(' — ')[1];
plannedDate.value = datePart;
dateSection.style.display = 'block';
}
var emoji = level.emoji;
var label = level.label;
var displayText = saved.indexOf(' — ') !== -1
? emoji + ' ' + new Date(saved.split(' — ')[1] + 'T00:00:00').toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
: emoji + ' ' + label;
displayEl.innerHTML = '<span style="background:' + level.color_bg + ';color:' + level.color_text
+ ';border-radius:20px;padding:3px 12px;font-size:13px;font-weight:600;">'
+ displayText + '</span>';
}
});
}
function closePopup() {
popup.style.display = 'none';
}
window.toggleUrgencyPopup = function () {
popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
};
document.addEventListener('click', function (e) {
var trigger = document.getElementById('urgency-trigger');
if (popup.style.display !== 'none' && !popup.contains(e.target) && !trigger.contains(e.target)) {
closePopup();
}
});
buildCards();
// ── Items table ───────────────────────────────────────────
let rowIndex = {{ count(old('items', $purchaseRequest->items->toArray())) }};
function renumber() {
document.querySelectorAll('#items-body .row-num').forEach(function (el, i) {
el.textContent = i + 1;
});
}
function newRow() {
var idx = rowIndex++;
var tr = document.createElement('tr');
tr.className = 'item-row';
tr.innerHTML =
'<td class="border border-gray-300 px-3 py-2 text-center text-gray-500 row-num"></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="text" name="items[' + idx + '][description]" class="w-full border-0 focus:ring-0 text-sm" placeholder="Material description" required></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="text" name="items[' + idx + '][unit]" class="w-full border-0 focus:ring-0 text-sm" placeholder="PCS, NOS, KG…"></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="number" name="items[' + idx + '][quantity_required]" min="0.01" step="0.01" class="w-full border-0 focus:ring-0 text-sm" placeholder="0" required></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="text" name="items[' + idx + '][purpose_use]" class="w-full border-0 focus:ring-0 text-sm" placeholder="Purpose…"></td>'
+ '<td class="border border-gray-300 px-2 py-1"><input type="date" name="items[' + idx + '][required_date]" class="w-full border-0 focus:ring-0 text-sm"></td>'
+ '<td class="border border-gray-300 px-2 py-1 text-center"><button type="button" class="remove-row text-red-500 hover:text-red-700 font-bold text-lg leading-none">×</button></td>';
return tr;
}
document.getElementById('add-row').addEventListener('click', function () {
document.getElementById('items-body').appendChild(newRow());
renumber();
});
document.getElementById('items-body').addEventListener('click', function (e) {
if (e.target.classList.contains('remove-row')) {
var rows = document.querySelectorAll('#items-body .item-row');
if (rows.length > 1) {
e.target.closest('tr').remove();
renumber();
}
}
});
renumber();
})();
</script>
- Step 5: Test edit form with existing urgency
Open an existing purchase request that has a required_date_text value (e.g. "Critical") and click Edit.
Expected: The urgency field pre-shows the "🚨 Critical" pill. Opening the popup shows the Critical card highlighted.
- Step 6: Commit
git add resources/views/purchase/requests/edit.blade.php
git commit -m "feat: upgrade purchase request edit form with dropdowns and urgency popup"
Done
At this point:
- Three lookup tables exist and are seeded with default urgency levels
- Admin can manage Locations, Projects, and Urgency Levels from Settings in the sidebar
- Purchase request create and edit forms use dropdown selects for Project and Location
- The "Required Date / Urgency" field opens a polished popup with icon cards; "Planned" shows an inline date picker
- All values are stored as strings in existing
purchase_requestscolumns — no schema changes to that table