feat: add departments to projects — two-column layout with inline CRUD

This commit is contained in:
Ghassan Yusuf 2026-05-26 17:41:15 +03:00
parent 58b3e9e0de
commit 60d57af630
6 changed files with 362 additions and 56 deletions

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Settings\Department;
use App\Models\Settings\Location;
use App\Models\Settings\ProjectSetting;
use Illuminate\Http\Request;
@ -11,15 +12,17 @@ class ProjectSettingController extends Controller
{
public function index()
{
$projects = ProjectSetting::with(['locations' => function ($q) {
$q->orderBy('name');
}])->orderBy('name')->get();
$projects = ProjectSetting::with([
'locations' => fn ($q) => $q->orderBy('name'),
'departments' => fn ($q) => $q->orderBy('name'),
])->orderBy('name')->get();
$stats = [
'total_projects' => $projects->count(),
'active_projects' => $projects->where('is_active', true)->count(),
'total_locations' => $projects->sum(fn ($p) => $p->locations->count()),
'active_locations' => $projects->sum(fn ($p) => $p->locations->where('is_active', true)->count()),
'total_departments' => $projects->sum(fn ($p) => $p->departments->count()),
];
return view('settings.projects.index', compact('projects', 'stats'));
@ -113,4 +116,24 @@ class ProjectSettingController extends Controller
$location->delete();
return response()->json(['ok' => true]);
}
public function storeDepartment(Request $request, ProjectSetting $project)
{
$validated = $request->validate(['name' => 'required|string|max:255']);
$dept = $project->departments()->create(['name' => $validated['name'], 'is_active' => true]);
return response()->json(['department' => ['id' => $dept->id, 'name' => $dept->name, 'is_active' => $dept->is_active]]);
}
public function updateDepartment(Request $request, ProjectSetting $project, Department $department)
{
$validated = $request->validate(['name' => 'required|string|max:255']);
$department->update(['name' => $validated['name'], 'is_active' => $request->boolean('is_active', true)]);
return response()->json(['department' => ['id' => $department->id, 'name' => $department->name, 'is_active' => $department->is_active]]);
}
public function destroyDepartment(ProjectSetting $project, Department $department)
{
$department->delete();
return response()->json(['ok' => true]);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models\Settings;
use Illuminate\Database\Eloquent\Model;
class Department extends Model
{
protected $table = 'settings_departments';
protected $fillable = ['name', 'project_id', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
public function project(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(ProjectSetting::class, 'project_id');
}
}

View File

@ -21,4 +21,9 @@ class ProjectSetting extends Model
{
return $this->hasMany(\App\Models\Settings\Location::class, 'project_id');
}
public function departments(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(\App\Models\Settings\Department::class, 'project_id');
}
}

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('settings_departments', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('settings_projects')->cascadeOnDelete();
$table->string('name', 255);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('settings_departments');
}
};

View File

@ -8,7 +8,15 @@
.proj-card { border:1px solid #e2e8f0; border-radius:0.875rem; overflow:hidden; margin-bottom:0.75rem; background:white; }
.proj-header { display:flex; align-items:center; justify-content:space-between; padding:0.875rem 1.25rem; background:white; }
.proj-body { display:block; border-top:1px solid #e2e8f0; }
.loc-row { display:flex; align-items:flex-start; justify-content:space-between; padding:0.75rem 1.25rem 0.75rem 2.5rem; border-bottom:1px solid #f1f5f9; }
.proj-body-inner { display:grid; grid-template-columns:1fr 1fr; }
.proj-section { border-right:1px solid #e2e8f0; }
.proj-section:last-child { border-right:none; }
.proj-section-header { display:flex; align-items:center; justify-content:space-between; padding:0.6rem 1.25rem; background:#f8fafc; border-bottom:1px solid #e2e8f0; }
.proj-section-title { font-size:11px; font-weight:700; color:#64748b; text-transform:uppercase; letter-spacing:.05em; }
.loc-row { display:flex; align-items:flex-start; justify-content:space-between; padding:0.65rem 1rem 0.65rem 1.25rem; border-bottom:1px solid #f1f5f9; }
.dept-row { display:flex; align-items:center; justify-content:space-between; padding:0.6rem 1rem 0.6rem 1.25rem; border-bottom:1px solid #f1f5f9; }
.dept-edit-row { display:none; padding:0.5rem 1rem; background:#f0f9ff; border-bottom:1px solid #bae6fd; gap:6px; align-items:center; }
.dept-edit-row.open { display:flex; }
.proj-edit-wrap { display:none; padding:0.75rem 1.25rem; background:#f0f9ff; border-bottom:1px solid #bae6fd; align-items:center; gap:8px; }
.proj-edit-wrap.open { display:flex; }
.badge-inactive { display:inline-block; padding:1px 7px; border-radius:9px; font-size:11px; font-weight:600; background:#fee2e2; color:#dc2626; margin-left:6px; }
@ -25,13 +33,13 @@
{{-- Page header --}}
<div class="mb-5" style="display:flex; align-items:flex-start; justify-content:space-between; flex-wrap:wrap; gap:12px;">
<div>
<h1 class="page-title">Projects &amp; Locations</h1>
<p class="page-subtitle">Manage projects and their physical sub-locations with addresses and GPS coordinates.</p>
<h1 class="page-title">Projects, Locations &amp; Departments</h1>
<p class="page-subtitle">Manage projects with their sub-locations and departments.</p>
</div>
</div>
{{-- Stat boxes --}}
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:28px;">
<div style="display:grid; grid-template-columns:repeat(5,1fr); gap:16px; margin-bottom:28px;">
<div class="stat-card" style="border-top:3px solid #3b82f6;">
<div style="display:flex; align-items:center; gap:12px;">
<div style="width:40px;height:40px;background:#eff6ff;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
@ -65,6 +73,17 @@
</div>
</div>
</div>
<div class="stat-card" style="border-top:3px solid #06b6d4;">
<div style="display:flex; align-items:center; gap:12px;">
<div style="width:40px;height:40px;background:#ecfeff;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<svg width="18" height="18" fill="none" stroke="#06b6d4" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</div>
<div>
<div style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['total_departments'] }}</div>
<div style="font-size:12px;color:#64748b;margin-top:3px;">Departments</div>
</div>
</div>
</div>
<div class="stat-card" style="border-top:3px solid #f59e0b;">
<div style="display:flex; align-items:center; gap:12px;">
<div style="width:40px;height:40px;background:#fffbeb;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
@ -93,9 +112,10 @@
</div>
</div>
{{-- Build location data map for safe JS passing --}}
{{-- Build location + department data maps for safe JS passing --}}
@php
$allLocsData = [];
$allDeptsData = [];
foreach ($projects as $proj) {
foreach ($proj->locations as $loc) {
$allLocsData[$loc->id] = [
@ -107,8 +127,16 @@ foreach ($projects as $proj) {
'is_active' => $loc->is_active,
];
}
foreach ($proj->departments as $dept) {
$allDeptsData[$dept->id] = [
'id' => $dept->id,
'name' => $dept->name,
'is_active' => $dept->is_active,
];
}
}
$allLocsJson = json_encode($allLocsData);
$allDeptsJson = json_encode($allDeptsData);
@endphp
{{-- Projects accordion --}}
@ -151,14 +179,24 @@ $allLocsJson = json_encode($allLocsData);
{{-- Body --}}
<div class="proj-body" id="proj-body-{{ $project->id }}">
<div class="proj-body-inner">
{{-- Location rows (sorted alphabetically by server) --}}
{{-- ── Locations column ── --}}
<div class="proj-section">
<div class="proj-section-header">
<span class="proj-section-title">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="display:inline;vertical-align:middle;margin-right:4px;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Locations
</span>
<button type="button" onclick="openLocModal(null, {{ $project->id }})"
class="btn-primary" style="padding:3px 10px; font-size:11px;">+ Add</button>
</div>
<div id="loc-list-{{ $project->id }}">
@forelse($project->locations as $location)
<div id="loc-wrap-{{ $location->id }}">
<div class="loc-row" id="loc-row-{{ $location->id }}">
<div style="display:flex; align-items:flex-start; gap:10px; flex:1; min-width:0;">
<svg width="14" height="14" fill="none" stroke="{{ $location->is_active ? '#22c55e' : '#9ca3af' }}" viewBox="0 0 24 24" id="loc-icon-{{ $location->id }}" style="flex-shrink:0; margin-top:3px;">
<div style="display:flex; align-items:flex-start; gap:8px; flex:1; min-width:0;">
<svg width="13" height="13" fill="none" stroke="{{ $location->is_active ? '#22c55e' : '#9ca3af' }}" viewBox="0 0 24 24" id="loc-icon-{{ $location->id }}" style="flex-shrink:0; margin-top:3px;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
@ -184,20 +222,72 @@ $allLocsJson = json_encode($allLocsData);
</div>
</div>
@empty
<div id="loc-empty-{{ $project->id }}" style="padding:14px 1.25rem 14px 2.5rem; color:#9ca3af; font-size:13px; border-bottom:1px solid #f1f5f9;">
No locations yet add the first one below.
<div id="loc-empty-{{ $project->id }}" style="padding:14px 1.25rem; color:#9ca3af; font-size:13px;">
No locations yet.
</div>
@endforelse
</div>
{{-- Add Location button --}}
<div style="padding:0.875rem 1.25rem; background:#f8fafc; border-top:1px solid #e2e8f0;">
<button type="button" onclick="openLocModal(null, {{ $project->id }})"
class="btn-primary" style="padding:0.4rem 1rem; font-size:13px;">
+ Add Location to {{ $project->name }}
</button>
</div>
{{-- ── Departments column ── --}}
<div class="proj-section">
<div class="proj-section-header">
<span class="proj-section-title">
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="display:inline;vertical-align:middle;margin-right:4px;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
Departments
</span>
<button type="button" onclick="openAddDept({{ $project->id }})"
class="btn-primary" style="padding:3px 10px; font-size:11px;">+ Add</button>
</div>
{{-- Inline add row --}}
<div class="dept-edit-row" id="dept-add-row-{{ $project->id }}">
<input id="dept-add-name-{{ $project->id }}" type="text" class="form-input" style="flex:1; font-size:12px;" placeholder="Department name…"
onkeydown="if(event.key==='Enter') saveDeptAdd({{ $project->id }}); if(event.key==='Escape') closeAddDept({{ $project->id }})">
<button type="button" onclick="saveDeptAdd({{ $project->id }})" class="btn-primary" style="padding:4px 12px; font-size:12px; white-space:nowrap;">Save</button>
<button type="button" onclick="closeAddDept({{ $project->id }})" class="btn-secondary btn-sm"></button>
</div>
<div id="dept-list-{{ $project->id }}">
@forelse($project->departments as $dept)
<div id="dept-wrap-{{ $dept->id }}">
{{-- View row --}}
<div class="dept-row" id="dept-row-{{ $dept->id }}">
<div style="display:flex; align-items:center; gap:8px; flex:1; min-width:0;">
<svg width="13" height="13" fill="none" stroke="{{ $dept->is_active ? '#06b6d4' : '#9ca3af' }}" viewBox="0 0 24 24" id="dept-icon-{{ $dept->id }}" style="flex-shrink:0;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span id="dept-name-{{ $dept->id }}" style="font-size:13px; font-weight:600; color:#1e293b;">{{ $dept->name }}</span>
<span id="dept-inactive-{{ $dept->id }}" class="badge-inactive" style="{{ $dept->is_active ? 'display:none' : '' }}">Inactive</span>
</div>
<div style="display:flex; gap:4px; flex-shrink:0;">
<button type="button" onclick="openEditDept({{ $dept->id }}, {{ $project->id }}, '{{ addslashes($dept->name) }}', {{ $dept->is_active ? 'true' : 'false' }})"
class="btn-secondary btn-sm" style="padding:3px 8px; font-size:12px;">Edit</button>
<button type="button" onclick="deleteDept({{ $project->id }}, {{ $dept->id }}, '{{ addslashes($dept->name) }}')"
class="btn-danger btn-sm" style="padding:3px 8px; font-size:12px;">Delete</button>
</div>
</div>
{{-- Inline edit row --}}
<div class="dept-edit-row" id="dept-edit-row-{{ $dept->id }}">
<input id="dept-edit-name-{{ $dept->id }}" type="text" class="form-input" style="flex:1; font-size:12px;"
onkeydown="if(event.key==='Enter') saveDeptEdit({{ $dept->id }}, {{ $project->id }}); if(event.key==='Escape') closeEditDept({{ $dept->id }})">
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#374151;white-space:nowrap;cursor:pointer;">
<input type="checkbox" id="dept-edit-active-{{ $dept->id }}" {{ $dept->is_active ? 'checked' : '' }} style="width:13px;height:13px;">
Active
</label>
<button type="button" onclick="saveDeptEdit({{ $dept->id }}, {{ $project->id }})" class="btn-primary" style="padding:4px 12px; font-size:12px; white-space:nowrap;">Save</button>
<button type="button" onclick="closeEditDept({{ $dept->id }})" class="btn-secondary btn-sm"></button>
</div>
</div>
@empty
<div id="dept-empty-{{ $project->id }}" style="padding:14px 1.25rem; color:#9ca3af; font-size:13px;">
No departments yet.
</div>
@endforelse
</div>
</div>
</div>{{-- end proj-body-inner --}}
</div>{{-- end proj-body --}}
</div>{{-- end proj-card --}}
@ -306,6 +396,7 @@ var BASE = '{{ url("settings/projects") }}';
// Location data store (keyed by loc ID) — avoids inline JSON in onclick attributes
var LOC_DATA = {!! $allLocsJson !!};
var DEPT_DATA = {!! $allDeptsJson !!};
// ── Core fetch helper ────────────────────────────────────────────────────────
function api(url, method, data) {
@ -645,12 +736,147 @@ function buildProjectCard(p) {
+ '<p id="edit-proj-error-' + p.id + '" class="field-error" style="margin:0;"></p>'
+ '</div>'
+ '<div class="proj-body" id="proj-body-' + p.id + '" style="display:block;">'
+ '<div id="loc-list-' + p.id + '">'
+ '<div id="loc-empty-' + p.id + '" style="padding:14px 1.25rem 14px 2.5rem;color:#9ca3af;font-size:13px;border-bottom:1px solid #f1f5f9;">No locations yet — add the first one below.</div>'
+ '<div class="proj-body-inner">'
+ '<div class="proj-section">'
+ '<div class="proj-section-header"><span class="proj-section-title">Locations</span>'
+ '<button type="button" onclick="openLocModal(null,' + p.id + ')" class="btn-primary" style="padding:3px 10px;font-size:11px;">+ Add</button></div>'
+ '<div id="loc-list-' + p.id + '"><div id="loc-empty-' + p.id + '" style="padding:14px 1.25rem;color:#9ca3af;font-size:13px;">No locations yet.</div></div>'
+ '</div>'
+ '<div style="padding:0.875rem 1.25rem;background:#f8fafc;border-top:1px solid #e2e8f0;">'
+ '<button type="button" onclick="openLocModal(null, ' + p.id + ')" class="btn-primary" style="padding:0.4rem 1rem;font-size:13px;">+ Add Location to ' + pName + '</button>'
+ '<div class="proj-section">'
+ '<div class="proj-section-header"><span class="proj-section-title">Departments</span>'
+ '<button type="button" onclick="openAddDept(' + p.id + ')" class="btn-primary" style="padding:3px 10px;font-size:11px;">+ Add</button></div>'
+ '<div class="dept-edit-row" id="dept-add-row-' + p.id + '">'
+ '<input id="dept-add-name-' + p.id + '" type="text" class="form-input" style="flex:1;font-size:12px;" placeholder="Department name…" onkeydown="if(event.key===\'Enter\') saveDeptAdd(' + p.id + '); if(event.key===\'Escape\') closeAddDept(' + p.id + ')">'
+ '<button type="button" onclick="saveDeptAdd(' + p.id + ')" class="btn-primary" style="padding:4px 12px;font-size:12px;white-space:nowrap;">Save</button>'
+ '<button type="button" onclick="closeAddDept(' + p.id + ')" class="btn-secondary btn-sm">✕</button>'
+ '</div>'
+ '<div id="dept-list-' + p.id + '"><div id="dept-empty-' + p.id + '" style="padding:14px 1.25rem;color:#9ca3af;font-size:13px;">No departments yet.</div></div>'
+ '</div>'
+ '</div>'
+ '</div>'
+ '</div>';
}
// ── Department CRUD ───────────────────────────────────────────────────────────
function openAddDept(projectId) {
var row = document.getElementById('dept-add-row-' + projectId);
row.classList.add('open');
document.getElementById('dept-add-name-' + projectId).value = '';
document.getElementById('dept-add-name-' + projectId).focus();
}
function closeAddDept(projectId) {
document.getElementById('dept-add-row-' + projectId).classList.remove('open');
}
function saveDeptAdd(projectId) {
var input = document.getElementById('dept-add-name-' + projectId);
var name = input.value.trim();
if (!name) { showToast('Department name is required.', 'warn'); return; }
api(BASE + '/' + projectId + '/departments', 'POST', { name: name })
.then(function(data) {
var dept = data.department;
DEPT_DATA[dept.id] = dept;
closeAddDept(projectId);
var emptyEl = document.getElementById('dept-empty-' + projectId);
if (emptyEl) emptyEl.remove();
var list = document.getElementById('dept-list-' + projectId);
var div = document.createElement('div');
div.innerHTML = buildDeptRow(projectId, dept);
insertDeptInOrder(projectId, div.firstElementChild);
updateDeptCount(projectId);
showToast('Department "' + esc(dept.name) + '" added.', 'success');
})
.catch(function(e) { showToast(firstError(e), 'error'); });
}
function openEditDept(deptId, projectId, name, isActive) {
document.getElementById('dept-row-' + deptId).style.display = 'none';
var row = document.getElementById('dept-edit-row-' + deptId);
row.classList.add('open');
document.getElementById('dept-edit-name-' + deptId).value = name;
document.getElementById('dept-edit-active-' + deptId).checked = isActive;
document.getElementById('dept-edit-name-' + deptId).focus();
}
function closeEditDept(deptId) {
document.getElementById('dept-edit-row-' + deptId).classList.remove('open');
document.getElementById('dept-row-' + deptId).style.display = '';
}
function saveDeptEdit(deptId, projectId) {
var name = document.getElementById('dept-edit-name-' + deptId).value.trim();
var isActive = document.getElementById('dept-edit-active-' + deptId).checked;
if (!name) { showToast('Department name is required.', 'warn'); return; }
api(BASE + '/' + projectId + '/departments/' + deptId, 'PATCH', { name: name, is_active: isActive ? 1 : 0 })
.then(function(data) {
var dept = data.department;
DEPT_DATA[dept.id] = dept;
closeEditDept(deptId);
document.getElementById('dept-name-' + deptId).textContent = dept.name;
var badge = document.getElementById('dept-inactive-' + deptId);
if (badge) badge.style.display = dept.is_active ? 'none' : '';
var icon = document.getElementById('dept-icon-' + deptId);
if (icon) icon.setAttribute('stroke', dept.is_active ? '#06b6d4' : '#9ca3af');
showToast('Department updated.', 'success');
})
.catch(function(e) { showToast(firstError(e), 'error'); });
}
function deleteDept(projectId, deptId, name) {
confirmAction('Delete Department', 'Delete "' + name + '"?', function() {
api(BASE + '/' + projectId + '/departments/' + deptId, 'DELETE')
.then(function() {
delete DEPT_DATA[deptId];
var wrap = document.getElementById('dept-wrap-' + deptId);
if (wrap) wrap.remove();
updateDeptCount(projectId);
var list = document.getElementById('dept-list-' + projectId);
if (list && !list.querySelector('[id^="dept-wrap-"]')) {
list.innerHTML = '<div id="dept-empty-' + projectId + '" style="padding:14px 1.25rem;color:#9ca3af;font-size:13px;">No departments yet.</div>';
}
showToast('Department "' + esc(name) + '" deleted.', 'success');
})
.catch(function(e) { showToast(firstError(e), 'error'); });
});
}
function updateDeptCount(projectId) {
var list = document.getElementById('dept-list-' + projectId);
var count = list ? list.querySelectorAll('[id^="dept-wrap-"]').length : 0;
var el = document.getElementById('proj-dept-count-' + projectId);
if (el) el.textContent = count + ' ' + (count === 1 ? 'dept' : 'depts');
}
function insertDeptInOrder(projectId, newWrapEl) {
var list = document.getElementById('dept-list-' + projectId);
var newName = (newWrapEl.querySelector('[id^="dept-name-"]').textContent || '').trim().toLowerCase();
var wraps = list.querySelectorAll('[id^="dept-wrap-"]');
var inserted = false;
for (var i = 0; i < wraps.length; i++) {
var nameEl = wraps[i].querySelector('[id^="dept-name-"]');
if (nameEl && newName < nameEl.textContent.trim().toLowerCase()) {
list.insertBefore(newWrapEl, wraps[i]);
inserted = true; break;
}
}
if (!inserted) list.appendChild(newWrapEl);
}
function buildDeptRow(projectId, dept) {
var safeName = (dept.name || '').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
return '<div id="dept-wrap-' + dept.id + '">'
+ '<div class="dept-row" id="dept-row-' + dept.id + '">'
+ '<div style="display:flex;align-items:center;gap:8px;flex:1;min-width:0;">'
+ '<svg width="13" height="13" fill="none" stroke="#06b6d4" viewBox="0 0 24 24" id="dept-icon-' + dept.id + '" style="flex-shrink:0;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>'
+ '<span id="dept-name-' + dept.id + '" style="font-size:13px;font-weight:600;color:#1e293b;">' + esc(dept.name) + '</span>'
+ '<span id="dept-inactive-' + dept.id + '" class="badge-inactive" style="display:none;">Inactive</span>'
+ '</div>'
+ '<div style="display:flex;gap:4px;flex-shrink:0;">'
+ '<button type="button" onclick="openEditDept(' + dept.id + ',' + projectId + ',\'' + safeName + '\',true)" class="btn-secondary btn-sm" style="padding:3px 8px;font-size:12px;">Edit</button>'
+ '<button type="button" onclick="deleteDept(' + projectId + ',' + dept.id + ',\'' + safeName + '\')" class="btn-danger btn-sm" style="padding:3px 8px;font-size:12px;">Delete</button>'
+ '</div>'
+ '</div>'
+ '<div class="dept-edit-row" id="dept-edit-row-' + dept.id + '">'
+ '<input id="dept-edit-name-' + dept.id + '" type="text" class="form-input" style="flex:1;font-size:12px;" onkeydown="if(event.key===\'Enter\') saveDeptEdit(' + dept.id + ',' + projectId + '); if(event.key===\'Escape\') closeEditDept(' + dept.id + ')">'
+ '<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#374151;white-space:nowrap;cursor:pointer;"><input type="checkbox" id="dept-edit-active-' + dept.id + '" checked style="width:13px;height:13px;"> Active</label>'
+ '<button type="button" onclick="saveDeptEdit(' + dept.id + ',' + projectId + ')" class="btn-primary" style="padding:4px 12px;font-size:12px;white-space:nowrap;">Save</button>'
+ '<button type="button" onclick="closeEditDept(' + dept.id + ')" class="btn-secondary btn-sm">✕</button>'
+ '</div>'
+ '</div>';
}

View File

@ -141,6 +141,9 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::post('settings/projects/{project}/locations', [ProjectSettingController::class, 'storeLocation'])->name('settings.projects.locations.store');
Route::patch('settings/projects/{project}/locations/{location}', [ProjectSettingController::class, 'updateLocation'])->name('settings.projects.locations.update');
Route::delete('settings/projects/{project}/locations/{location}', [ProjectSettingController::class, 'destroyLocation'])->name('settings.projects.locations.destroy');
Route::post('settings/projects/{project}/departments', [ProjectSettingController::class, 'storeDepartment'])->name('settings.projects.departments.store');
Route::patch('settings/projects/{project}/departments/{department}', [ProjectSettingController::class, 'updateDepartment'])->name('settings.projects.departments.update');
Route::delete('settings/projects/{project}/departments/{department}', [ProjectSettingController::class, 'destroyDepartment'])->name('settings.projects.departments.destroy');
});
});