feat: move departments from projects to companies

Departments now belong to a Company, not a Project.
- New migration: recreates settings_departments with company_id (migrates existing data)
- Department model: company_id FK, company() relation
- Company model: departments() hasMany relation
- ProjectSetting model: removes departments() relation
- Controller: dept methods now take Company instead of ProjectSetting
- Routes: department CRUD moves to companies/{company}/departments
- View: departments section appears on each company card (purple theme);
  project body is now locations-only (single column)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ghassan Yusuf 2026-05-26 18:26:27 +03:00
parent 9a268dc79b
commit 12e07480a0
7 changed files with 206 additions and 153 deletions

View File

@ -14,8 +14,8 @@ class ProjectSettingController extends Controller
public function index()
{
$companies = Company::with([
'projects.locations' => fn ($q) => $q->orderBy('name'),
'projects.departments' => fn ($q) => $q->orderBy('name'),
'projects.locations' => fn ($q) => $q->orderBy('name'),
'departments' => fn ($q) => $q->orderBy('name'),
])->orderBy('name')->get();
$allProjects = $companies->flatMap(fn ($c) => $c->projects);
@ -25,7 +25,7 @@ class ProjectSettingController extends Controller
'total_projects' => $allProjects->count(),
'active_projects' => $allProjects->where('is_active', true)->count(),
'total_locations' => $allProjects->sum(fn ($p) => $p->locations->count()),
'total_departments' => $allProjects->sum(fn ($p) => $p->departments->count()),
'total_departments' => $companies->sum(fn ($c) => $c->departments->count()),
];
return view('settings.projects.index', compact('companies', 'stats'));
@ -148,21 +148,21 @@ class ProjectSettingController extends Controller
return response()->json(['ok' => true]);
}
public function storeDepartment(Request $request, ProjectSetting $project)
public function storeDepartment(Request $request, Company $company)
{
$validated = $request->validate(['name' => 'required|string|max:255']);
$dept = $project->departments()->create(['name' => $validated['name'], 'is_active' => true]);
$dept = $company->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)
public function updateDepartment(Request $request, Company $company, 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)
public function destroyDepartment(Company $company, Department $department)
{
$department->delete();
return response()->json(['ok' => true]);

View File

@ -16,4 +16,9 @@ class Company extends Model
{
return $this->hasMany(ProjectSetting::class, 'company_id');
}
public function departments(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Department::class, 'company_id');
}
}

View File

@ -8,12 +8,12 @@ class Department extends Model
{
protected $table = 'settings_departments';
protected $fillable = ['name', 'project_id', 'is_active'];
protected $fillable = ['name', 'company_id', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
public function project(): \Illuminate\Database\Eloquent\Relations\BelongsTo
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(ProjectSetting::class, 'project_id');
return $this->belongsTo(Company::class, 'company_id');
}
}

View File

@ -27,8 +27,4 @@ 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,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Recreate table with company_id instead of project_id (SQLite-safe)
Schema::create('settings_departments_new', function (Blueprint $table) {
$table->id();
$table->foreignId('company_id')->constrained('settings_companies')->onDelete('cascade');
$table->string('name');
$table->boolean('is_active')->default(true);
$table->timestamps();
});
// Migrate: map each department's project_id → that project's company_id
DB::statement('
INSERT INTO settings_departments_new (id, company_id, name, is_active, created_at, updated_at)
SELECT d.id, p.company_id, d.name, d.is_active, d.created_at, d.updated_at
FROM settings_departments d
LEFT JOIN settings_projects p ON p.id = d.project_id
WHERE p.company_id IS NOT NULL
');
Schema::drop('settings_departments');
Schema::rename('settings_departments_new', 'settings_departments');
}
public function down(): void
{
Schema::create('settings_departments_old', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('settings_projects')->onDelete('cascade');
$table->string('name');
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::drop('settings_departments');
Schema::rename('settings_departments_old', 'settings_departments');
}
};

View File

@ -117,6 +117,14 @@
$allLocsData = [];
$allDeptsData = [];
foreach ($companies as $company) {
foreach ($company->departments as $dept) {
$allDeptsData[$dept->id] = [
'id' => $dept->id,
'name' => $dept->name,
'is_active' => $dept->is_active,
'company_id' => $dept->company_id,
];
}
foreach ($company->projects as $proj) {
foreach ($proj->locations as $loc) {
$allLocsData[$loc->id] = [
@ -128,13 +136,6 @@ foreach ($companies as $company) {
'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);
@ -154,10 +155,13 @@ $allDeptsJson = json_encode($allDeptsData);
<span id="company-name-{{ $company->id }}" style="font-size:15px;font-weight:700;color:#3730a3;">{{ $company->name }}</span>
<span id="company-inactive-{{ $company->id }}" class="badge-inactive" style="{{ $company->is_active ? 'display:none' : '' }}">Inactive</span>
<span id="company-proj-count-{{ $company->id }}" style="font-size:12px;color:#6366f1;opacity:.7;">{{ $company->projects->count() }} {{ Str::plural('project', $company->projects->count()) }}</span>
<span id="company-dept-count-{{ $company->id }}" style="font-size:12px;color:#7c3aed;opacity:.7;">· {{ $company->departments->count() }} {{ Str::plural('dept', $company->departments->count()) }}</span>
</div>
<div style="display:flex;gap:6px;align-items:center;">
<button type="button" onclick="openAddCoDept({{ $company->id }})"
class="btn-secondary btn-sm" style="border-color:#a78bfa;color:#6d28d9;">+ Dept</button>
<button type="button" onclick="openAddProject({{ $company->id }})"
class="btn-primary" style="padding:4px 12px;font-size:12px;">+ Add Project</button>
class="btn-primary" style="padding:4px 12px;font-size:12px;">+ Project</button>
<button type="button" onclick="openEditCompany({{ $company->id }}, '{{ addslashes($company->name) }}', {{ $company->is_active ? 'true' : 'false' }})"
class="btn-secondary btn-sm">Edit</button>
<button type="button" onclick="deleteCompany({{ $company->id }}, '{{ addslashes($company->name) }}')"
@ -186,6 +190,49 @@ $allDeptsJson = json_encode($allDeptsData);
<p id="add-proj-error-{{ $company->id }}" class="field-error" style="margin:0;"></p>
</div>
{{-- Departments section --}}
<div style="border:1px solid #c7d2fe;border-top:none;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.5rem 1.25rem;background:#f5f3ff;border-bottom:1px solid #ede9fe;">
<span style="font-size:11px;font-weight:700;color:#6d28d9;text-transform:uppercase;letter-spacing:.05em;">Departments</span>
</div>
{{-- Add dept inline row --}}
<div class="dept-edit-row" id="co-dept-add-row-{{ $company->id }}">
<input id="co-dept-add-name-{{ $company->id }}" type="text" class="form-input" style="flex:1;font-size:12px;" placeholder="Department name…"
onkeydown="if(event.key==='Enter') saveCoDeptAdd({{ $company->id }}); if(event.key==='Escape') closeAddCoDept({{ $company->id }})">
<button type="button" onclick="saveCoDeptAdd({{ $company->id }})" class="btn-primary" style="padding:4px 12px;font-size:12px;white-space:nowrap;">Save</button>
<button type="button" onclick="closeAddCoDept({{ $company->id }})" class="btn-secondary btn-sm"></button>
</div>
<div id="co-dept-list-{{ $company->id }}">
@forelse($company->departments as $dept)
<div id="co-dept-wrap-{{ $dept->id }}">
<div class="dept-row" id="co-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 ? '#7c3aed' : '#9ca3af' }}" viewBox="0 0 24 24" id="co-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="co-dept-name-{{ $dept->id }}" style="font-size:13px;font-weight:600;color:#1e293b;">{{ $dept->name }}</span>
<span id="co-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="openEditCoDept({{ $dept->id }}, {{ $company->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="deleteCoDept({{ $company->id }}, {{ $dept->id }}, '{{ addslashes($dept->name) }}')" class="btn-danger btn-sm" style="padding:3px 8px;font-size:12px;">Delete</button>
</div>
</div>
<div class="dept-edit-row" id="co-dept-edit-row-{{ $dept->id }}">
<input id="co-dept-edit-name-{{ $dept->id }}" type="text" class="form-input" style="flex:1;font-size:12px;"
onkeydown="if(event.key==='Enter') saveEditCoDept({{ $dept->id }}, {{ $company->id }}); if(event.key==='Escape') closeEditCoDept({{ $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="co-dept-edit-active-{{ $dept->id }}" {{ $dept->is_active ? 'checked' : '' }} style="width:13px;height:13px;">
Active
</label>
<button type="button" onclick="saveEditCoDept({{ $dept->id }}, {{ $company->id }})" class="btn-primary" style="padding:4px 12px;font-size:12px;white-space:nowrap;">Save</button>
<button type="button" onclick="closeEditCoDept({{ $dept->id }})" class="btn-secondary btn-sm"></button>
</div>
</div>
@empty
<div id="co-dept-empty-{{ $company->id }}" style="padding:10px 1.25rem;color:#9ca3af;font-size:13px;">No departments yet.</div>
@endforelse
</div>
</div>
{{-- Projects container --}}
<div id="proj-list-{{ $company->id }}" style="border:1px solid #c7d2fe;border-top:none;border-radius:0 0 0.875rem 0.875rem;overflow:hidden;">
@ -227,11 +274,9 @@ $allDeptsJson = json_encode($allDeptsData);
<p id="edit-proj-error-{{ $project->id }}" class="field-error" style="margin:0;"></p>
</div>
{{-- Project body: Locations + Departments --}}
{{-- Project body: Locations --}}
<div class="proj-body" id="proj-body-{{ $project->id }}">
<div class="proj-body-inner">
{{-- Locations --}}
<div class="proj-section">
<div class="proj-section" style="border-right:none;">
<div class="proj-section-header">
<span class="proj-section-title">Locations</span>
<button type="button" onclick="openLocModal(null, {{ $project->id }})" class="btn-primary" style="padding:3px 10px;font-size:11px;">+ Add</button>
@ -264,49 +309,6 @@ $allDeptsJson = json_encode($allDeptsData);
@endforelse
</div>
</div>
{{-- Departments --}}
<div class="proj-section">
<div class="proj-section-header">
<span class="proj-section-title">Departments</span>
<button type="button" onclick="openAddDept({{ $project->id }})" class="btn-primary" style="padding:3px 10px;font-size:11px;">+ Add</button>
</div>
<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 }}">
<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>
<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:12px 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 --}}
@empty
@ -832,9 +834,11 @@ function buildCompanyCard(c) {
+ '<span id="company-name-' + c.id + '" style="font-size:15px;font-weight:700;color:#3730a3;">' + cName + '</span>'
+ '<span id="company-inactive-' + c.id + '" class="badge-inactive" style="display:none;">Inactive</span>'
+ '<span id="company-proj-count-' + c.id + '" style="font-size:12px;color:#6366f1;opacity:.7;">0 projects</span>'
+ '<span id="company-dept-count-' + c.id + '" style="font-size:12px;color:#7c3aed;opacity:.7;">· 0 depts</span>'
+ '</div>'
+ '<div style="display:flex;gap:6px;align-items:center;">'
+ '<button type="button" onclick="openAddProject(' + c.id + ')" class="btn-primary" style="padding:4px 12px;font-size:12px;">+ Add Project</button>'
+ '<button type="button" onclick="openAddCoDept(' + c.id + ')" class="btn-secondary btn-sm" style="border-color:#a78bfa;color:#6d28d9;">+ Dept</button>'
+ '<button type="button" onclick="openAddProject(' + c.id + ')" class="btn-primary" style="padding:4px 12px;font-size:12px;">+ Project</button>'
+ '<button type="button" onclick="openEditCompany(' + c.id + ', \'' + rawName + '\', true)" class="btn-secondary btn-sm">Edit</button>'
+ '<button type="button" onclick="deleteCompany(' + c.id + ', \'' + rawName + '\')" class="btn-danger btn-sm">Delete</button>'
+ '</div>'
@ -846,6 +850,17 @@ function buildCompanyCard(c) {
+ '<button type="button" onclick="closeEditCompany(' + c.id + ')" class="btn-secondary btn-sm">Cancel</button>'
+ '<p id="edit-company-error-' + c.id + '" class="field-error" style="margin:0;"></p>'
+ '</div>'
+ '<div style="border:1px solid #c7d2fe;border-top:none;">'
+ '<div style="display:flex;align-items:center;justify-content:space-between;padding:0.5rem 1.25rem;background:#f5f3ff;border-bottom:1px solid #ede9fe;">'
+ '<span style="font-size:11px;font-weight:700;color:#6d28d9;text-transform:uppercase;letter-spacing:.05em;">Departments</span>'
+ '</div>'
+ '<div class="dept-edit-row" id="co-dept-add-row-' + c.id + '">'
+ '<input id="co-dept-add-name-' + c.id + '" type="text" class="form-input" style="flex:1;font-size:12px;" placeholder="Department name…" onkeydown="if(event.key===\'Enter\') saveCoDeptAdd(' + c.id + '); if(event.key===\'Escape\') closeAddCoDept(' + c.id + ')">'
+ '<button type="button" onclick="saveCoDeptAdd(' + c.id + ')" class="btn-primary" style="padding:4px 12px;font-size:12px;white-space:nowrap;">Save</button>'
+ '<button type="button" onclick="closeAddCoDept(' + c.id + ')" class="btn-secondary btn-sm">✕</button>'
+ '</div>'
+ '<div id="co-dept-list-' + c.id + '"><div id="co-dept-empty-' + c.id + '" style="padding:10px 1.25rem;color:#9ca3af;font-size:13px;">No departments yet.</div></div>'
+ '</div>'
+ '<div class="proj-edit-wrap" id="add-proj-strip-' + c.id + '" style="border-radius:0;border-left:1px solid #c7d2fe;border-right:1px solid #c7d2fe;background:#f0fdf4;border-color:#bbf7d0;">'
+ '<input id="add-proj-name-' + c.id + '" type="text" class="form-input" style="flex:1;font-size:13px;" placeholder="New project name…" onkeydown="if(event.key===\'Enter\') saveAddProject(' + c.id + '); if(event.key===\'Escape\') closeAddProject(' + c.id + ')">'
+ '<button type="button" onclick="saveAddProject(' + c.id + ')" class="btn-primary" style="padding:5px 14px;font-size:12px;white-space:nowrap;">Save</button>'
@ -885,22 +900,10 @@ function buildProjectCard(p, companyId) {
+ '<p id="edit-proj-error-' + p.id + '" class="field-error" style="margin:0;"></p>'
+ '</div>'
+ '<div class="proj-body" id="proj-body-' + p.id + '">'
+ '<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:12px 1.25rem;color:#9ca3af;font-size:13px;">No locations yet.</div></div>'
+ '</div>'
+ '<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:12px 1.25rem;color:#9ca3af;font-size:13px;">No departments yet.</div></div>'
+ '</div>'
+ '<div class="proj-section" style="border-right:none;">'
+ '<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:12px 1.25rem;color:#9ca3af;font-size:13px;">No locations yet.</div></div>'
+ '</div>'
+ '</div>'
+ '</div>';
@ -949,78 +952,80 @@ function insertLocInOrder(projectId, newWrapEl) {
if (!inserted) list.appendChild(newWrapEl);
}
// ── Department CRUD ───────────────────────────────────────────────────────────
function openAddDept(projectId) {
var row = document.getElementById('dept-add-row-' + projectId);
// ── Company Department CRUD ───────────────────────────────────────────────────
var CO_DEPT_BASE = BASE + '/companies';
function openAddCoDept(companyId) {
var row = document.getElementById('co-dept-add-row-' + companyId);
row.classList.add('open');
document.getElementById('dept-add-name-' + projectId).value = '';
document.getElementById('dept-add-name-' + projectId).focus();
document.getElementById('co-dept-add-name-' + companyId).value = '';
document.getElementById('co-dept-add-name-' + companyId).focus();
}
function closeAddDept(projectId) {
document.getElementById('dept-add-row-' + projectId).classList.remove('open');
function closeAddCoDept(companyId) {
document.getElementById('co-dept-add-row-' + companyId).classList.remove('open');
}
function saveDeptAdd(projectId) {
var input = document.getElementById('dept-add-name-' + projectId);
function saveCoDeptAdd(companyId) {
var input = document.getElementById('co-dept-add-name-' + companyId);
var name = input.value.trim();
if (!name) { showToast('Department name is required.', 'warn'); return; }
api(BASE + '/' + projectId + '/departments', 'POST', { name: name })
api(CO_DEPT_BASE + '/' + companyId + '/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);
closeAddCoDept(companyId);
var emptyEl = document.getElementById('co-dept-empty-' + companyId);
if (emptyEl) emptyEl.remove();
var list = document.getElementById('dept-list-' + projectId);
var list = document.getElementById('co-dept-list-' + companyId);
var div = document.createElement('div');
div.innerHTML = buildDeptRow(projectId, dept);
insertDeptInOrder(projectId, div.firstElementChild);
updateDeptCount(projectId);
div.innerHTML = buildCoDeptRow(companyId, dept);
insertCoDeptInOrder(companyId, div.firstElementChild);
updateCoDeptCount(companyId);
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);
function openEditCoDept(deptId, companyId, name, isActive) {
document.getElementById('co-dept-row-' + deptId).style.display = 'none';
var row = document.getElementById('co-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();
document.getElementById('co-dept-edit-name-' + deptId).value = name;
document.getElementById('co-dept-edit-active-' + deptId).checked = isActive;
document.getElementById('co-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 closeEditCoDept(deptId) {
document.getElementById('co-dept-edit-row-' + deptId).classList.remove('open');
document.getElementById('co-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;
function saveEditCoDept(deptId, companyId) {
var name = document.getElementById('co-dept-edit-name-' + deptId).value.trim();
var isActive = document.getElementById('co-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 })
api(CO_DEPT_BASE + '/' + companyId + '/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);
closeEditCoDept(deptId);
document.getElementById('co-dept-name-' + deptId).textContent = dept.name;
var badge = document.getElementById('co-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');
var icon = document.getElementById('co-dept-icon-' + deptId);
if (icon) icon.setAttribute('stroke', dept.is_active ? '#7c3aed' : '#9ca3af');
showToast('Department updated.', 'success');
})
.catch(function(e) { showToast(firstError(e), 'error'); });
}
function deleteDept(projectId, deptId, name) {
function deleteCoDept(companyId, deptId, name) {
confirmAction('Delete Department', 'Delete "' + name + '"?', function() {
api(BASE + '/' + projectId + '/departments/' + deptId, 'DELETE')
api(CO_DEPT_BASE + '/' + companyId + '/departments/' + deptId, 'DELETE')
.then(function() {
delete DEPT_DATA[deptId];
var wrap = document.getElementById('dept-wrap-' + deptId);
var wrap = document.getElementById('co-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>';
updateCoDeptCount(companyId);
var list = document.getElementById('co-dept-list-' + companyId);
if (list && !list.querySelector('[id^="co-dept-wrap-"]')) {
list.innerHTML = '<div id="co-dept-empty-' + companyId + '" style="padding:10px 1.25rem;color:#9ca3af;font-size:13px;">No departments yet.</div>';
}
showToast('Department "' + esc(name) + '" deleted.', 'success');
})
@ -1028,20 +1033,20 @@ function deleteDept(projectId, deptId, name) {
});
}
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 updateCoDeptCount(companyId) {
var list = document.getElementById('co-dept-list-' + companyId);
var count = list ? list.querySelectorAll('[id^="co-dept-wrap-"]').length : 0;
var el = document.getElementById('company-dept-count-' + companyId);
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-"]');
function insertCoDeptInOrder(companyId, newWrapEl) {
var list = document.getElementById('co-dept-list-' + companyId);
var newName = (newWrapEl.querySelector('[id^="co-dept-name-"]').textContent || '').trim().toLowerCase();
var wraps = list.querySelectorAll('[id^="co-dept-wrap-"]');
var inserted = false;
for (var i = 0; i < wraps.length; i++) {
var nameEl = wraps[i].querySelector('[id^="dept-name-"]');
var nameEl = wraps[i].querySelector('[id^="co-dept-name-"]');
if (nameEl && newName < nameEl.textContent.trim().toLowerCase()) {
list.insertBefore(newWrapEl, wraps[i]);
inserted = true; break;
@ -1050,25 +1055,25 @@ function insertDeptInOrder(projectId, newWrapEl) {
if (!inserted) list.appendChild(newWrapEl);
}
function buildDeptRow(projectId, dept) {
function buildCoDeptRow(companyId, 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 + '">'
return '<div id="co-dept-wrap-' + dept.id + '">'
+ '<div class="dept-row" id="co-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>'
+ '<svg width="13" height="13" fill="none" stroke="#7c3aed" viewBox="0 0 24 24" id="co-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="co-dept-name-' + dept.id + '" style="font-size:13px;font-weight:600;color:#1e293b;">' + esc(dept.name) + '</span>'
+ '<span id="co-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>'
+ '<button type="button" onclick="openEditCoDept(' + dept.id + ',' + companyId + ',\'' + safeName + '\',true)" class="btn-secondary btn-sm" style="padding:3px 8px;font-size:12px;">Edit</button>'
+ '<button type="button" onclick="deleteCoDept(' + companyId + ',' + 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 class="dept-edit-row" id="co-dept-edit-row-' + dept.id + '">'
+ '<input id="co-dept-edit-name-' + dept.id + '" type="text" class="form-input" style="flex:1;font-size:12px;" onkeydown="if(event.key===\'Enter\') saveEditCoDept(' + dept.id + ',' + companyId + '); if(event.key===\'Escape\') closeEditCoDept(' + 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="co-dept-edit-active-' + dept.id + '" checked style="width:13px;height:13px;"> Active</label>'
+ '<button type="button" onclick="saveEditCoDept(' + dept.id + ',' + companyId + ')" class="btn-primary" style="padding:4px 12px;font-size:12px;white-space:nowrap;">Save</button>'
+ '<button type="button" onclick="closeEditCoDept(' + dept.id + ')" class="btn-secondary btn-sm">✕</button>'
+ '</div>'
+ '</div>';
}

View File

@ -144,9 +144,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');
Route::post('settings/projects/companies/{company}/departments', [ProjectSettingController::class, 'storeDepartment'])->name('settings.projects.companies.departments.store');
Route::patch('settings/projects/companies/{company}/departments/{department}', [ProjectSettingController::class, 'updateDepartment'])->name('settings.projects.companies.departments.update');
Route::delete('settings/projects/companies/{company}/departments/{department}', [ProjectSettingController::class, 'destroyDepartment'])->name('settings.projects.companies.departments.destroy');
});
});