feat: add companies as top-level container for projects with departments
- Company → Project → (Locations + Departments) hierarchy - Company CRUD (add, edit, delete) with AJAX - Projects are created under a company via inline strip - Two-column project body: Locations | Departments - Stats show companies, projects, locations, departments, active projects - Dynamic stat counters update on add/delete without page reload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
60d57af630
commit
a425e12349
@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Settings;
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Settings\Company;
|
||||||
use App\Models\Settings\Department;
|
use App\Models\Settings\Department;
|
||||||
use App\Models\Settings\Location;
|
use App\Models\Settings\Location;
|
||||||
use App\Models\Settings\ProjectSetting;
|
use App\Models\Settings\ProjectSetting;
|
||||||
@ -12,33 +13,60 @@ class ProjectSettingController extends Controller
|
|||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$projects = ProjectSetting::with([
|
$companies = Company::with([
|
||||||
'locations' => fn ($q) => $q->orderBy('name'),
|
'projects.locations' => fn ($q) => $q->orderBy('name'),
|
||||||
'departments' => fn ($q) => $q->orderBy('name'),
|
'projects.departments' => fn ($q) => $q->orderBy('name'),
|
||||||
])->orderBy('name')->get();
|
])->orderBy('name')->get();
|
||||||
|
|
||||||
|
$allProjects = $companies->flatMap(fn ($c) => $c->projects);
|
||||||
|
|
||||||
$stats = [
|
$stats = [
|
||||||
'total_projects' => $projects->count(),
|
'total_companies' => $companies->count(),
|
||||||
'active_projects' => $projects->where('is_active', true)->count(),
|
'total_projects' => $allProjects->count(),
|
||||||
'total_locations' => $projects->sum(fn ($p) => $p->locations->count()),
|
'active_projects' => $allProjects->where('is_active', true)->count(),
|
||||||
'active_locations' => $projects->sum(fn ($p) => $p->locations->where('is_active', true)->count()),
|
'total_locations' => $allProjects->sum(fn ($p) => $p->locations->count()),
|
||||||
'total_departments' => $projects->sum(fn ($p) => $p->departments->count()),
|
'total_departments' => $allProjects->sum(fn ($p) => $p->departments->count()),
|
||||||
];
|
];
|
||||||
|
|
||||||
return view('settings.projects.index', compact('projects', 'stats'));
|
return view('settings.projects.index', compact('companies', 'stats'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validate(['name' => 'required|string|max:255|unique:settings_projects,name']);
|
$validated = $request->validate([
|
||||||
$project = ProjectSetting::create(['name' => $validated['name'], 'is_active' => true]);
|
'name' => 'required|string|max:255|unique:settings_projects,name',
|
||||||
|
'company_id' => 'required|exists:settings_companies,id',
|
||||||
|
]);
|
||||||
|
$project = ProjectSetting::create(['name' => $validated['name'], 'company_id' => $validated['company_id'], 'is_active' => true]);
|
||||||
return response()->json(['project' => [
|
return response()->json(['project' => [
|
||||||
'id' => $project->id,
|
'id' => $project->id,
|
||||||
'name' => $project->name,
|
'name' => $project->name,
|
||||||
'is_active' => $project->is_active,
|
'is_active' => $project->is_active,
|
||||||
|
'company_id' => $project->company_id,
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Company CRUD ──────────────────────────────────────────────────────────
|
||||||
|
public function storeCompany(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate(['name' => 'required|string|max:255|unique:settings_companies,name']);
|
||||||
|
$company = Company::create(['name' => $validated['name'], 'is_active' => true]);
|
||||||
|
return response()->json(['company' => ['id' => $company->id, 'name' => $company->name, 'is_active' => $company->is_active]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateCompany(Request $request, Company $company)
|
||||||
|
{
|
||||||
|
$validated = $request->validate(['name' => 'required|string|max:255|unique:settings_companies,name,' . $company->id]);
|
||||||
|
$company->update(['name' => $validated['name'], 'is_active' => $request->boolean('is_active', true)]);
|
||||||
|
return response()->json(['company' => ['id' => $company->id, 'name' => $company->name, 'is_active' => $company->is_active]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyCompany(Company $company)
|
||||||
|
{
|
||||||
|
$company->delete();
|
||||||
|
return response()->json(['ok' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
public function update(Request $request, ProjectSetting $project)
|
public function update(Request $request, ProjectSetting $project)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
|
|||||||
19
app/Models/Settings/Company.php
Normal file
19
app/Models/Settings/Company.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Settings;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Company extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'settings_companies';
|
||||||
|
|
||||||
|
protected $fillable = ['name', 'is_active'];
|
||||||
|
|
||||||
|
protected $casts = ['is_active' => 'boolean'];
|
||||||
|
|
||||||
|
public function projects(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProjectSetting::class, 'company_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ class ProjectSetting extends Model
|
|||||||
{
|
{
|
||||||
protected $table = 'settings_projects';
|
protected $table = 'settings_projects';
|
||||||
|
|
||||||
protected $fillable = ['name', 'is_active'];
|
protected $fillable = ['name', 'is_active', 'company_id'];
|
||||||
|
|
||||||
protected $casts = ['is_active' => 'boolean'];
|
protected $casts = ['is_active' => 'boolean'];
|
||||||
|
|
||||||
@ -17,6 +17,11 @@ class ProjectSetting extends Model
|
|||||||
return $query->where('is_active', true);
|
return $query->where('is_active', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Company::class, 'company_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function locations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
public function locations(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(\App\Models\Settings\Location::class, 'project_id');
|
return $this->hasMany(\App\Models\Settings\Location::class, 'project_id');
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
<?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_companies', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name', 255)->unique();
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('settings_projects', function (Blueprint $table) {
|
||||||
|
$table->foreignId('company_id')->nullable()->after('id')
|
||||||
|
->constrained('settings_companies')->nullOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign all existing projects to a "General" company
|
||||||
|
if (\DB::table('settings_projects')->exists()) {
|
||||||
|
$id = \DB::table('settings_companies')->insertGetId(['name' => 'General', 'is_active' => 1, 'created_at' => now(), 'updated_at' => now()]);
|
||||||
|
\DB::table('settings_projects')->update(['company_id' => $id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('settings_projects', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['company_id']);
|
||||||
|
$table->dropColumn('company_id');
|
||||||
|
});
|
||||||
|
Schema::dropIfExists('settings_companies');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -40,25 +40,25 @@
|
|||||||
|
|
||||||
{{-- Stat boxes --}}
|
{{-- Stat boxes --}}
|
||||||
<div style="display:grid; grid-template-columns:repeat(5,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 #6366f1;">
|
||||||
|
<div style="display:flex; align-items:center; gap:12px;">
|
||||||
|
<div style="width:40px;height:40px;background:#eef2ff;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||||
|
<svg width="18" height="18" fill="none" stroke="#6366f1" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div id="stat-companies" style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['total_companies'] }}</div>
|
||||||
|
<div style="font-size:12px;color:#64748b;margin-top:3px;">Companies</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="stat-card" style="border-top:3px solid #3b82f6;">
|
<div class="stat-card" style="border-top:3px solid #3b82f6;">
|
||||||
<div style="display:flex; align-items:center; gap:12px;">
|
<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;">
|
<div style="width:40px;height:40px;background:#eff6ff;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||||
<svg width="18" height="18" fill="none" stroke="#3b82f6" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
<svg width="18" height="18" fill="none" stroke="#3b82f6" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['total_projects'] }}</div>
|
<div id="stat-projects" style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['total_projects'] }}</div>
|
||||||
<div style="font-size:12px;color:#64748b;margin-top:3px;">Total Projects</div>
|
<div style="font-size:12px;color:#64748b;margin-top:3px;">Projects</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card" style="border-top:3px solid #22c55e;">
|
|
||||||
<div style="display:flex; align-items:center; gap:12px;">
|
|
||||||
<div style="width:40px;height:40px;background:#f0fdf4;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
|
||||||
<svg width="18" height="18" fill="none" stroke="#22c55e" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['active_projects'] }}</div>
|
|
||||||
<div style="font-size:12px;color:#64748b;margin-top:3px;">Active Projects</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['total_locations'] }}</div>
|
<div style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['total_locations'] }}</div>
|
||||||
<div style="font-size:12px;color:#64748b;margin-top:3px;">Total Locations</div>
|
<div style="font-size:12px;color:#64748b;margin-top:3px;">Locations</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -84,39 +84,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" style="border-top:3px solid #f59e0b;">
|
<div class="stat-card" style="border-top:3px solid #22c55e;">
|
||||||
<div style="display:flex; align-items:center; gap:12px;">
|
<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;">
|
<div style="width:40px;height:40px;background:#f0fdf4;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||||
<svg width="18" height="18" fill="none" stroke="#f59e0b" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
|
<svg width="18" height="18" fill="none" stroke="#22c55e" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['active_locations'] }}</div>
|
<div style="font-size:28px;font-weight:700;color:#1e293b;line-height:1;">{{ $stats['active_projects'] }}</div>
|
||||||
<div style="font-size:12px;color:#64748b;margin-top:3px;">Active Locations</div>
|
<div style="font-size:12px;color:#64748b;margin-top:3px;">Active Projects</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Add Project --}}
|
{{-- Add Company --}}
|
||||||
<div class="card card-body mb-5" style="padding:1.125rem 1.25rem;">
|
<div class="card card-body mb-5" style="padding:1.125rem 1.25rem;">
|
||||||
<div style="display:flex; gap:10px; align-items:flex-start;">
|
<div style="display:flex; gap:10px; align-items:flex-start;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<input id="new-project-input" type="text" class="form-input" style="width:100%;"
|
<input id="new-company-input" type="text" class="form-input" style="width:100%;"
|
||||||
placeholder="New project name…"
|
placeholder="New company name…"
|
||||||
onkeydown="if(event.key==='Enter') addProject()">
|
onkeydown="if(event.key==='Enter') addCompany()">
|
||||||
<p id="new-project-error" class="field-error"></p>
|
<p id="new-company-error" class="field-error"></p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onclick="addProject()" class="btn-primary" style="white-space:nowrap; flex-shrink:0; padding:0.5rem 1.25rem;">
|
<button type="button" onclick="addCompany()" class="btn-primary" style="white-space:nowrap; flex-shrink:0; padding:0.5rem 1.25rem;">
|
||||||
+ Add Project
|
+ Add Company
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Build location + department data maps for safe JS passing --}}
|
{{-- Build data maps for safe JS passing --}}
|
||||||
@php
|
@php
|
||||||
$allLocsData = [];
|
$allLocsData = [];
|
||||||
$allDeptsData = [];
|
$allDeptsData = [];
|
||||||
foreach ($projects as $proj) {
|
foreach ($companies as $company) {
|
||||||
|
foreach ($company->projects as $proj) {
|
||||||
foreach ($proj->locations as $loc) {
|
foreach ($proj->locations as $loc) {
|
||||||
$allLocsData[$loc->id] = [
|
$allLocsData[$loc->id] = [
|
||||||
'id' => $loc->id,
|
'id' => $loc->id,
|
||||||
@ -134,30 +135,74 @@ foreach ($projects as $proj) {
|
|||||||
'is_active' => $dept->is_active,
|
'is_active' => $dept->is_active,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$allLocsJson = json_encode($allLocsData);
|
$allLocsJson = json_encode($allLocsData);
|
||||||
$allDeptsJson = json_encode($allDeptsData);
|
$allDeptsJson = json_encode($allDeptsData);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
{{-- Projects accordion --}}
|
{{-- Companies list --}}
|
||||||
<div id="projects-list">
|
<div id="companies-list">
|
||||||
@forelse($projects as $project)
|
@forelse($companies as $company)
|
||||||
|
|
||||||
<div class="proj-card" id="proj-card-{{ $project->id }}">
|
{{-- Company card --}}
|
||||||
|
<div id="company-card-{{ $company->id }}" style="margin-bottom:1.25rem;">
|
||||||
|
{{-- Company header --}}
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.75rem 1.25rem;background:linear-gradient(135deg,#eef2ff,#e0e7ff);border:1px solid #c7d2fe;border-radius:0.875rem 0.875rem 0 0;border-bottom:none;">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<svg width="18" height="18" fill="none" stroke="#6366f1" viewBox="0 0 24 24" style="flex-shrink:0;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px;align-items:center;">
|
||||||
|
<button type="button" onclick="openAddProject({{ $company->id }})"
|
||||||
|
class="btn-primary" style="padding:4px 12px;font-size:12px;">+ Add 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) }}')"
|
||||||
|
class="btn-danger btn-sm">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Header --}}
|
{{-- Company edit strip --}}
|
||||||
<div class="proj-header">
|
<div class="proj-edit-wrap" id="company-edit-{{ $company->id }}" style="border-radius:0;border-top:1px solid #c7d2fe;border-left:1px solid #c7d2fe;border-right:1px solid #c7d2fe;">
|
||||||
<div style="display:flex; align-items:center; gap:10px; flex:1; min-width:0;">
|
<input id="edit-company-name-{{ $company->id }}" type="text" class="form-input" style="flex:1;font-size:13px;">
|
||||||
<svg width="15" height="15" fill="none" stroke="{{ $project->is_active ? '#2563eb' : '#9ca3af' }}" viewBox="0 0 24 24" style="flex-shrink:0;" id="proj-icon-{{ $project->id }}">
|
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#374151;white-space:nowrap;cursor:pointer;">
|
||||||
|
<input type="checkbox" id="edit-company-active-{{ $company->id }}" {{ $company->is_active ? 'checked' : '' }} style="width:14px;height:14px;">
|
||||||
|
Active
|
||||||
|
</label>
|
||||||
|
<button type="button" onclick="saveCompany({{ $company->id }})" class="btn-primary" style="padding:5px 14px;font-size:12px;white-space:nowrap;">Save</button>
|
||||||
|
<button type="button" onclick="closeEditCompany({{ $company->id }})" class="btn-secondary btn-sm">Cancel</button>
|
||||||
|
<p id="edit-company-error-{{ $company->id }}" class="field-error" style="margin:0;"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Add project strip --}}
|
||||||
|
<div class="proj-edit-wrap" id="add-proj-strip-{{ $company->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-{{ $company->id }}" type="text" class="form-input" style="flex:1;font-size:13px;" placeholder="New project name…"
|
||||||
|
onkeydown="if(event.key==='Enter') saveAddProject({{ $company->id }}); if(event.key==='Escape') closeAddProject({{ $company->id }})">
|
||||||
|
<button type="button" onclick="saveAddProject({{ $company->id }})" class="btn-primary" style="padding:5px 14px;font-size:12px;white-space:nowrap;">Save</button>
|
||||||
|
<button type="button" onclick="closeAddProject({{ $company->id }})" class="btn-secondary btn-sm">Cancel</button>
|
||||||
|
<p id="add-proj-error-{{ $company->id }}" class="field-error" style="margin:0;"></p>
|
||||||
|
</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;">
|
||||||
|
|
||||||
|
@forelse($company->projects as $project)
|
||||||
|
@php $isLast = $loop->last; @endphp
|
||||||
|
<div id="proj-card-{{ $project->id }}" style="border-bottom:{{ $isLast ? 'none' : '1px solid #e2e8f0' }};">
|
||||||
|
|
||||||
|
{{-- Project header --}}
|
||||||
|
<div class="proj-header" style="background:#fafafa;border-bottom:1px solid #f1f5f9;">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0;">
|
||||||
|
<svg width="14" height="14" fill="none" stroke="{{ $project->is_active ? '#2563eb' : '#9ca3af' }}" viewBox="0 0 24 24" style="flex-shrink:0;" id="proj-icon-{{ $project->id }}">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span id="proj-name-{{ $project->id }}" style="font-size:14px; font-weight:600; color:#1e293b;">{{ $project->name }}</span>
|
<span id="proj-name-{{ $project->id }}" style="font-size:13px;font-weight:600;color:#1e293b;">{{ $project->name }}</span>
|
||||||
<span id="proj-inactive-badge-{{ $project->id }}" class="badge-inactive" style="{{ $project->is_active ? 'display:none' : '' }}">Inactive</span>
|
<span id="proj-inactive-badge-{{ $project->id }}" class="badge-inactive" style="{{ $project->is_active ? 'display:none' : '' }}">Inactive</span>
|
||||||
<span id="proj-loc-count-{{ $project->id }}" style="font-size:12px; color:#94a3b8; margin-left:2px;">
|
|
||||||
{{ $project->locations->count() }} {{ Str::plural('location', $project->locations->count()) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:6px; flex-shrink:0;" onclick="event.stopPropagation()">
|
<div style="display:flex;gap:6px;flex-shrink:0;">
|
||||||
<button type="button" onclick="openEditProject({{ $project->id }}, '{{ addslashes($project->name) }}', {{ $project->is_active ? 'true' : 'false' }})"
|
<button type="button" onclick="openEditProject({{ $project->id }}, '{{ addslashes($project->name) }}', {{ $project->is_active ? 'true' : 'false' }})"
|
||||||
class="btn-secondary btn-sm">Edit</button>
|
class="btn-secondary btn-sm">Edit</button>
|
||||||
<button type="button" onclick="deleteProject({{ $project->id }}, '{{ addslashes($project->name) }}')"
|
<button type="button" onclick="deleteProject({{ $project->id }}, '{{ addslashes($project->name) }}')"
|
||||||
@ -165,141 +210,114 @@ $allDeptsJson = json_encode($allDeptsData);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Project edit form --}}
|
{{-- Project edit strip --}}
|
||||||
<div class="proj-edit-wrap" id="proj-edit-{{ $project->id }}">
|
<div class="proj-edit-wrap" id="proj-edit-{{ $project->id }}">
|
||||||
<input id="edit-proj-name-{{ $project->id }}" type="text" class="form-input" style="flex:1; font-size:13px;">
|
<input id="edit-proj-name-{{ $project->id }}" type="text" class="form-input" style="flex:1;font-size:13px;">
|
||||||
<label style="display:flex; align-items:center; gap:4px; font-size:12px; color:#374151; white-space:nowrap; cursor:pointer;">
|
<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#374151;white-space:nowrap;cursor:pointer;">
|
||||||
<input type="checkbox" id="edit-proj-active-{{ $project->id }}" {{ $project->is_active ? 'checked' : '' }} style="width:14px; height:14px;">
|
<input type="checkbox" id="edit-proj-active-{{ $project->id }}" {{ $project->is_active ? 'checked' : '' }} style="width:14px;height:14px;">
|
||||||
Active
|
Active
|
||||||
</label>
|
</label>
|
||||||
<button type="button" onclick="saveProject({{ $project->id }})" class="btn-primary" style="padding:5px 14px; font-size:12px; white-space:nowrap;">Save</button>
|
<button type="button" onclick="saveProject({{ $project->id }})" class="btn-primary" style="padding:5px 14px;font-size:12px;white-space:nowrap;">Save</button>
|
||||||
<button type="button" onclick="closeEditProject({{ $project->id }})" class="btn-secondary btn-sm">Cancel</button>
|
<button type="button" onclick="closeEditProject({{ $project->id }})" class="btn-secondary btn-sm">Cancel</button>
|
||||||
<p id="edit-proj-error-{{ $project->id }}" class="field-error" style="margin:0;"></p>
|
<p id="edit-proj-error-{{ $project->id }}" class="field-error" style="margin:0;"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Body --}}
|
{{-- Project body: Locations + Departments --}}
|
||||||
<div class="proj-body" id="proj-body-{{ $project->id }}">
|
<div class="proj-body" id="proj-body-{{ $project->id }}">
|
||||||
<div class="proj-body-inner">
|
<div class="proj-body-inner">
|
||||||
|
{{-- Locations --}}
|
||||||
{{-- ── Locations column ── --}}
|
|
||||||
<div class="proj-section">
|
<div class="proj-section">
|
||||||
<div class="proj-section-header">
|
<div class="proj-section-header">
|
||||||
<span class="proj-section-title">
|
<span class="proj-section-title">Locations</span>
|
||||||
<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>
|
<button type="button" onclick="openLocModal(null, {{ $project->id }})" class="btn-primary" style="padding:3px 10px;font-size:11px;">+ Add</button>
|
||||||
Locations
|
|
||||||
</span>
|
|
||||||
<button type="button" onclick="openLocModal(null, {{ $project->id }})"
|
|
||||||
class="btn-primary" style="padding:3px 10px; font-size:11px;">+ Add</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="loc-list-{{ $project->id }}">
|
<div id="loc-list-{{ $project->id }}">
|
||||||
@forelse($project->locations as $location)
|
@forelse($project->locations as $location)
|
||||||
<div id="loc-wrap-{{ $location->id }}">
|
<div id="loc-wrap-{{ $location->id }}">
|
||||||
<div class="loc-row" id="loc-row-{{ $location->id }}">
|
<div class="loc-row" id="loc-row-{{ $location->id }}">
|
||||||
<div style="display:flex; align-items:flex-start; gap:8px; flex:1; min-width:0;">
|
<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;">
|
<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>
|
||||||
<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"/>
|
<div style="flex:1;min-width:0;">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
|
||||||
</svg>
|
<span id="loc-name-{{ $location->id }}" style="font-size:13px;font-weight:600;color:#1e293b;">{{ $location->name }}</span>
|
||||||
<div style="flex:1; min-width:0;">
|
|
||||||
<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap;">
|
|
||||||
<span id="loc-name-{{ $location->id }}" style="font-size:13px; font-weight:600; color:#1e293b;">{{ $location->name }}</span>
|
|
||||||
<span id="loc-inactive-{{ $location->id }}" class="badge-inactive" style="{{ $location->is_active ? 'display:none' : '' }}">Inactive</span>
|
<span id="loc-inactive-{{ $location->id }}" class="badge-inactive" style="{{ $location->is_active ? 'display:none' : '' }}">Inactive</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="loc-address-{{ $location->id }}" style="font-size:12px; color:#64748b; margin-top:2px;{{ $location->address ? '' : 'display:none;' }}">{{ $location->address }}</div>
|
<div id="loc-address-{{ $location->id }}" style="font-size:12px;color:#64748b;margin-top:2px;{{ $location->address ? '' : 'display:none;' }}">{{ $location->address }}</div>
|
||||||
<div id="loc-gps-{{ $location->id }}" style="font-size:11px; color:#94a3b8; margin-top:1px; font-family:monospace;{{ ($location->latitude && $location->longitude) ? '' : 'display:none;' }}">
|
<div id="loc-gps-{{ $location->id }}" style="font-size:11px;color:#94a3b8;margin-top:1px;font-family:monospace;{{ ($location->latitude && $location->longitude) ? '' : 'display:none;' }}">
|
||||||
@if($location->latitude && $location->longitude)
|
@if($location->latitude && $location->longitude){{ number_format((float)$location->latitude,6) }}°, {{ number_format((float)$location->longitude,6) }}°@endif
|
||||||
{{ number_format((float)$location->latitude, 6) }}°, {{ number_format((float)$location->longitude, 6) }}°
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:4px; flex-shrink:0; margin-top:1px;">
|
<div style="display:flex;gap:4px;flex-shrink:0;margin-top:1px;">
|
||||||
<button type="button" onclick="openLocModal({{ $location->id }}, {{ $project->id }})"
|
<button type="button" onclick="openLocModal({{ $location->id }}, {{ $project->id }})" class="btn-secondary btn-sm" style="padding:3px 8px;font-size:12px;">Edit</button>
|
||||||
class="btn-secondary btn-sm" style="padding:3px 8px; font-size:12px;">Edit</button>
|
<button type="button" onclick="deleteLoc({{ $project->id }}, {{ $location->id }}, '{{ addslashes($location->name) }}')" class="btn-danger btn-sm" style="padding:3px 8px;font-size:12px;">Delete</button>
|
||||||
<button type="button" onclick="deleteLoc({{ $project->id }}, {{ $location->id }}, '{{ addslashes($location->name) }}')"
|
|
||||||
class="btn-danger btn-sm" style="padding:3px 8px; font-size:12px;">Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
<div id="loc-empty-{{ $project->id }}" style="padding:14px 1.25rem; color:#9ca3af; font-size:13px;">
|
<div id="loc-empty-{{ $project->id }}" style="padding:12px 1.25rem;color:#9ca3af;font-size:13px;">No locations yet.</div>
|
||||||
No locations yet.
|
|
||||||
</div>
|
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{-- Departments --}}
|
||||||
{{-- ── Departments column ── --}}
|
|
||||||
<div class="proj-section">
|
<div class="proj-section">
|
||||||
<div class="proj-section-header">
|
<div class="proj-section-header">
|
||||||
<span class="proj-section-title">
|
<span class="proj-section-title">Departments</span>
|
||||||
<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>
|
<button type="button" onclick="openAddDept({{ $project->id }})" class="btn-primary" style="padding:3px 10px;font-size:11px;">+ Add</button>
|
||||||
Departments
|
|
||||||
</span>
|
|
||||||
<button type="button" onclick="openAddDept({{ $project->id }})"
|
|
||||||
class="btn-primary" style="padding:3px 10px; font-size:11px;">+ Add</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Inline add row --}}
|
|
||||||
<div class="dept-edit-row" id="dept-add-row-{{ $project->id }}">
|
<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…"
|
<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 }})">
|
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="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>
|
<button type="button" onclick="closeAddDept({{ $project->id }})" class="btn-secondary btn-sm">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dept-list-{{ $project->id }}">
|
<div id="dept-list-{{ $project->id }}">
|
||||||
@forelse($project->departments as $dept)
|
@forelse($project->departments as $dept)
|
||||||
<div id="dept-wrap-{{ $dept->id }}">
|
<div id="dept-wrap-{{ $dept->id }}">
|
||||||
{{-- View row --}}
|
|
||||||
<div class="dept-row" id="dept-row-{{ $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;">
|
<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;">
|
<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>
|
||||||
<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"/>
|
<span id="dept-name-{{ $dept->id }}" style="font-size:13px;font-weight:600;color:#1e293b;">{{ $dept->name }}</span>
|
||||||
</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>
|
<span id="dept-inactive-{{ $dept->id }}" class="badge-inactive" style="{{ $dept->is_active ? 'display:none' : '' }}">Inactive</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:4px; flex-shrink:0;">
|
<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' }})"
|
<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>
|
||||||
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>
|
||||||
<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>
|
</div>
|
||||||
{{-- Inline edit row --}}
|
|
||||||
<div class="dept-edit-row" id="dept-edit-row-{{ $dept->id }}">
|
<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;"
|
<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 }})">
|
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;">
|
<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;">
|
<input type="checkbox" id="dept-edit-active-{{ $dept->id }}" {{ $dept->is_active ? 'checked' : '' }} style="width:13px;height:13px;">
|
||||||
Active
|
Active
|
||||||
</label>
|
</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="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>
|
<button type="button" onclick="closeEditDept({{ $dept->id }})" class="btn-secondary btn-sm">✕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
<div id="dept-empty-{{ $project->id }}" style="padding:14px 1.25rem; color:#9ca3af; font-size:13px;">
|
<div id="dept-empty-{{ $project->id }}" style="padding:12px 1.25rem;color:#9ca3af;font-size:13px;">No departments yet.</div>
|
||||||
No departments yet.
|
|
||||||
</div>
|
|
||||||
@endforelse
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>{{-- end proj-body-inner --}}
|
</div>{{-- end proj-body-inner --}}
|
||||||
</div>{{-- end proj-body --}}
|
</div>{{-- end proj-body --}}
|
||||||
</div>{{-- end proj-card --}}
|
</div>{{-- end proj-card --}}
|
||||||
|
|
||||||
@empty
|
@empty
|
||||||
<div id="no-projects-msg" class="card card-body" style="text-align:center; padding:3rem; color:#9ca3af;">
|
<div id="no-proj-msg-{{ $company->id }}" style="padding:1.5rem;text-align:center;color:#9ca3af;font-size:13px;">
|
||||||
<svg width="40" height="40" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="margin:0 auto 12px; display:block; opacity:.3;">
|
No projects yet — click "+ Add Project" above.
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
|
||||||
</svg>
|
|
||||||
No projects yet. Add your first project above.
|
|
||||||
</div>
|
</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
|
</div>{{-- end proj-list --}}
|
||||||
|
</div>{{-- end company-card --}}
|
||||||
|
|
||||||
|
@empty
|
||||||
|
<div class="card card-body" style="text-align:center;padding:3rem;color:#9ca3af;">
|
||||||
|
No companies yet. Add your first company above.
|
||||||
</div>
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>{{-- end companies-list --}}
|
||||||
|
|
||||||
{{-- ═══════════════ Location Map Modal ═══════════════ --}}
|
{{-- ═══════════════ Location Map Modal ═══════════════ --}}
|
||||||
<div id="loc-modal-overlay" onclick="if(event.target===this)closeLocModal()">
|
<div id="loc-modal-overlay" onclick="if(event.target===this)closeLocModal()">
|
||||||
@ -602,28 +620,111 @@ function saveLocModal() {
|
|||||||
.catch(function(e) { genErr.textContent = firstError(e); genErr.style.display = 'block'; });
|
.catch(function(e) { genErr.textContent = firstError(e); genErr.style.display = 'block'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Project CRUD ─────────────────────────────────────────────────────────────
|
// ── Company CRUD ──────────────────────────────────────────────────────────────
|
||||||
function addProject() {
|
function addCompany() {
|
||||||
var input = document.getElementById('new-project-input');
|
var input = document.getElementById('new-company-input');
|
||||||
var err = document.getElementById('new-project-error');
|
var err = document.getElementById('new-company-error');
|
||||||
var name = input.value.trim();
|
var name = input.value.trim();
|
||||||
err.style.display = 'none';
|
err.style.display = 'none';
|
||||||
if (!name) { err.textContent = 'Project name is required.'; err.style.display = 'block'; return; }
|
if (!name) { err.textContent = 'Company name is required.'; err.style.display = 'block'; return; }
|
||||||
api(BASE, 'POST', { name: name })
|
api(BASE + '/companies', 'POST', { name: name })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
input.value = '';
|
input.value = '';
|
||||||
var p = data.project;
|
var c = data.company;
|
||||||
var noMsg = document.getElementById('no-projects-msg');
|
var list = document.getElementById('companies-list');
|
||||||
if (noMsg) noMsg.remove();
|
var emptyEl = list.querySelector('.card.card-body');
|
||||||
var list = document.getElementById('projects-list');
|
if (emptyEl) emptyEl.remove();
|
||||||
var div = document.createElement('div');
|
var div = document.createElement('div');
|
||||||
div.innerHTML = buildProjectCard(p);
|
div.innerHTML = buildCompanyCard(c);
|
||||||
list.appendChild(div.firstElementChild);
|
list.appendChild(div.firstElementChild);
|
||||||
showToast('Project "' + esc(p.name) + '" added.', 'success');
|
updateStat('stat-companies', 1);
|
||||||
|
showToast('Company "' + esc(c.name) + '" added.', 'success');
|
||||||
})
|
})
|
||||||
.catch(function(e) { err.textContent = firstError(e); err.style.display = 'block'; });
|
.catch(function(e) { err.textContent = firstError(e); err.style.display = 'block'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEditCompany(id, name, isActive) {
|
||||||
|
document.getElementById('company-edit-' + id).classList.add('open');
|
||||||
|
document.getElementById('edit-company-name-' + id).value = name;
|
||||||
|
document.getElementById('edit-company-active-' + id).checked = isActive;
|
||||||
|
var errEl = document.getElementById('edit-company-error-' + id);
|
||||||
|
if (errEl) errEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
function closeEditCompany(id) { document.getElementById('company-edit-' + id).classList.remove('open'); }
|
||||||
|
|
||||||
|
function saveCompany(id) {
|
||||||
|
var name = document.getElementById('edit-company-name-' + id).value.trim();
|
||||||
|
var isActive = document.getElementById('edit-company-active-' + id).checked;
|
||||||
|
var errEl = document.getElementById('edit-company-error-' + id);
|
||||||
|
errEl.style.display = 'none';
|
||||||
|
if (!name) { errEl.textContent = 'Name is required.'; errEl.style.display = 'block'; return; }
|
||||||
|
api(BASE + '/companies/' + id, 'PATCH', { name: name, is_active: isActive ? 1 : 0 })
|
||||||
|
.then(function(data) {
|
||||||
|
var c = data.company;
|
||||||
|
closeEditCompany(id);
|
||||||
|
document.getElementById('company-name-' + id).textContent = c.name;
|
||||||
|
var badge = document.getElementById('company-inactive-' + id);
|
||||||
|
if (badge) badge.style.display = c.is_active ? 'none' : '';
|
||||||
|
showToast('Company updated.', 'success');
|
||||||
|
})
|
||||||
|
.catch(function(e) { errEl.textContent = firstError(e); errEl.style.display = 'block'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCompany(id, name) {
|
||||||
|
confirmAction('Delete Company', 'Delete "' + name + '" and all its projects?', function() {
|
||||||
|
api(BASE + '/companies/' + id, 'DELETE')
|
||||||
|
.then(function() {
|
||||||
|
var card = document.getElementById('company-card-' + id);
|
||||||
|
if (card) card.remove();
|
||||||
|
var list = document.getElementById('companies-list');
|
||||||
|
if (list && !list.querySelector('[id^="company-card-"]')) {
|
||||||
|
list.innerHTML = '<div class="card card-body" style="text-align:center;padding:3rem;color:#9ca3af;">No companies yet. Add your first company above.</div>';
|
||||||
|
}
|
||||||
|
updateStat('stat-companies', -1);
|
||||||
|
showToast('Company "' + esc(name) + '" deleted.', 'success');
|
||||||
|
})
|
||||||
|
.catch(function(e) { showToast(firstError(e), 'error'); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Add Project under Company ─────────────────────────────────────────────────
|
||||||
|
function openAddProject(companyId) {
|
||||||
|
var strip = document.getElementById('add-proj-strip-' + companyId);
|
||||||
|
strip.classList.add('open');
|
||||||
|
var input = document.getElementById('add-proj-name-' + companyId);
|
||||||
|
input.value = '';
|
||||||
|
input.focus();
|
||||||
|
var errEl = document.getElementById('add-proj-error-' + companyId);
|
||||||
|
if (errEl) errEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
function closeAddProject(companyId) {
|
||||||
|
document.getElementById('add-proj-strip-' + companyId).classList.remove('open');
|
||||||
|
}
|
||||||
|
function saveAddProject(companyId) {
|
||||||
|
var input = document.getElementById('add-proj-name-' + companyId);
|
||||||
|
var errEl = document.getElementById('add-proj-error-' + companyId);
|
||||||
|
var name = input.value.trim();
|
||||||
|
errEl.style.display = 'none';
|
||||||
|
if (!name) { errEl.textContent = 'Project name is required.'; errEl.style.display = 'block'; return; }
|
||||||
|
api(BASE, 'POST', { name: name, company_id: companyId })
|
||||||
|
.then(function(data) {
|
||||||
|
input.value = '';
|
||||||
|
closeAddProject(companyId);
|
||||||
|
var p = data.project;
|
||||||
|
var projList = document.getElementById('proj-list-' + companyId);
|
||||||
|
var noMsg = document.getElementById('no-proj-msg-' + companyId);
|
||||||
|
if (noMsg) noMsg.remove();
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = buildProjectCard(p, companyId);
|
||||||
|
projList.appendChild(div.firstElementChild);
|
||||||
|
updateCompanyProjCount(companyId);
|
||||||
|
updateStat('stat-projects', 1);
|
||||||
|
showToast('Project "' + esc(p.name) + '" added.', 'success');
|
||||||
|
})
|
||||||
|
.catch(function(e) { errEl.textContent = firstError(e); errEl.style.display = 'block'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Project CRUD ──────────────────────────────────────────────────────────────
|
||||||
function openEditProject(id, name, isActive) {
|
function openEditProject(id, name, isActive) {
|
||||||
document.getElementById('proj-edit-' + id).classList.add('open');
|
document.getElementById('proj-edit-' + id).classList.add('open');
|
||||||
document.getElementById('edit-proj-name-' + id).value = name;
|
document.getElementById('edit-proj-name-' + id).value = name;
|
||||||
@ -654,21 +755,123 @@ function saveProject(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteProject(id, name) {
|
function deleteProject(id, name) {
|
||||||
confirmAction('Delete Project', 'Delete "' + name + '" and all its locations?', function() {
|
confirmAction('Delete Project', 'Delete "' + name + '" and all its locations and departments?', function() {
|
||||||
api(BASE + '/' + id, 'DELETE')
|
api(BASE + '/' + id, 'DELETE')
|
||||||
.then(function() {
|
.then(function() {
|
||||||
var card = document.getElementById('proj-card-' + id);
|
var card = document.getElementById('proj-card-' + id);
|
||||||
if (card) card.remove();
|
if (card) {
|
||||||
if (!document.querySelector('.proj-card')) {
|
var projList = card.closest('[id^="proj-list-"]');
|
||||||
document.getElementById('projects-list').innerHTML =
|
card.remove();
|
||||||
'<div id="no-projects-msg" class="card card-body" style="text-align:center;padding:3rem;color:#9ca3af;">No projects yet. Add your first project above.</div>';
|
if (projList) {
|
||||||
|
if (!projList.querySelector('[id^="proj-card-"]')) {
|
||||||
|
var coId = projList.id.replace('proj-list-', '');
|
||||||
|
projList.innerHTML = '<div id="no-proj-msg-' + coId + '" style="padding:1.5rem;text-align:center;color:#9ca3af;font-size:13px;">No projects yet — click "+ Add Project" above.</div>';
|
||||||
}
|
}
|
||||||
|
var coCard = projList.closest('[id^="company-card-"]');
|
||||||
|
if (coCard) updateCompanyProjCount(coCard.id.replace('company-card-', ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateStat('stat-projects', -1);
|
||||||
showToast('Project "' + esc(name) + '" deleted.', 'success');
|
showToast('Project "' + esc(name) + '" deleted.', 'success');
|
||||||
})
|
})
|
||||||
.catch(function(e) { showToast(firstError(e), 'error'); });
|
.catch(function(e) { showToast(firstError(e), 'error'); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Stat & count helpers ──────────────────────────────────────────────────────
|
||||||
|
function updateStat(statId, delta) {
|
||||||
|
var el = document.getElementById(statId);
|
||||||
|
if (el) el.textContent = Math.max(0, (parseInt(el.textContent, 10) || 0) + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCompanyProjCount(companyId) {
|
||||||
|
var projList = document.getElementById('proj-list-' + companyId);
|
||||||
|
var count = projList ? projList.querySelectorAll('[id^="proj-card-"]').length : 0;
|
||||||
|
var el = document.getElementById('company-proj-count-' + companyId);
|
||||||
|
if (el) el.textContent = count + ' ' + (count === 1 ? 'project' : 'projects');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JS Card Builders ──────────────────────────────────────────────────────────
|
||||||
|
function buildCompanyCard(c) {
|
||||||
|
var cName = esc(c.name);
|
||||||
|
var rawName = (c.name || '').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||||
|
return '<div id="company-card-' + c.id + '" style="margin-bottom:1.25rem;">'
|
||||||
|
+ '<div style="display:flex;align-items:center;justify-content:space-between;padding:0.75rem 1.25rem;background:linear-gradient(135deg,#eef2ff,#e0e7ff);border:1px solid #c7d2fe;border-radius:0.875rem 0.875rem 0 0;border-bottom:none;">'
|
||||||
|
+ '<div style="display:flex;align-items:center;gap:10px;">'
|
||||||
|
+ '<svg width="18" height="18" fill="none" stroke="#6366f1" viewBox="0 0 24 24" style="flex-shrink:0;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>'
|
||||||
|
+ '<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>'
|
||||||
|
+ '</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="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>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="proj-edit-wrap" id="company-edit-' + c.id + '" style="border-radius:0;border-top:1px solid #c7d2fe;border-left:1px solid #c7d2fe;border-right:1px solid #c7d2fe;">'
|
||||||
|
+ '<input id="edit-company-name-' + c.id + '" type="text" class="form-input" style="flex:1;font-size:13px;">'
|
||||||
|
+ '<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#374151;white-space:nowrap;cursor:pointer;"><input type="checkbox" id="edit-company-active-' + c.id + '" checked style="width:14px;height:14px;"> Active</label>'
|
||||||
|
+ '<button type="button" onclick="saveCompany(' + c.id + ')" class="btn-primary" style="padding:5px 14px;font-size:12px;white-space:nowrap;">Save</button>'
|
||||||
|
+ '<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 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>'
|
||||||
|
+ '<button type="button" onclick="closeAddProject(' + c.id + ')" class="btn-secondary btn-sm">Cancel</button>'
|
||||||
|
+ '<p id="add-proj-error-' + c.id + '" class="field-error" style="margin:0;"></p>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div id="proj-list-' + c.id + '" style="border:1px solid #c7d2fe;border-top:none;border-radius:0 0 0.875rem 0.875rem;overflow:hidden;">'
|
||||||
|
+ '<div id="no-proj-msg-' + c.id + '" style="padding:1.5rem;text-align:center;color:#9ca3af;font-size:13px;">No projects yet — click "+ Add Project" above.</div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProjectCard(p, companyId) {
|
||||||
|
var pName = esc(p.name);
|
||||||
|
var rawName = (p.name || '').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||||
|
return '<div id="proj-card-' + p.id + '" style="border-bottom:1px solid #e2e8f0;">'
|
||||||
|
+ '<div class="proj-header" style="background:#fafafa;border-bottom:1px solid #f1f5f9;">'
|
||||||
|
+ '<div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0;">'
|
||||||
|
+ '<svg width="14" height="14" fill="none" stroke="#2563eb" viewBox="0 0 24 24" style="flex-shrink:0;" id="proj-icon-' + p.id + '"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>'
|
||||||
|
+ '<span id="proj-name-' + p.id + '" style="font-size:13px;font-weight:600;color:#1e293b;">' + pName + '</span>'
|
||||||
|
+ '<span id="proj-inactive-badge-' + p.id + '" class="badge-inactive" style="display:none;">Inactive</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div style="display:flex;gap:6px;flex-shrink:0;">'
|
||||||
|
+ '<button type="button" onclick="openEditProject(' + p.id + ', \'' + rawName + '\', true)" class="btn-secondary btn-sm">Edit</button>'
|
||||||
|
+ '<button type="button" onclick="deleteProject(' + p.id + ', \'' + rawName + '\')" class="btn-danger btn-sm">Delete</button>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="proj-edit-wrap" id="proj-edit-' + p.id + '">'
|
||||||
|
+ '<input id="edit-proj-name-' + p.id + '" type="text" class="form-input" style="flex:1;font-size:13px;">'
|
||||||
|
+ '<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#374151;white-space:nowrap;cursor:pointer;"><input type="checkbox" id="edit-proj-active-' + p.id + '" checked style="width:14px;height:14px;"> Active</label>'
|
||||||
|
+ '<button type="button" onclick="saveProject(' + p.id + ')" class="btn-primary" style="padding:5px 14px;font-size:12px;white-space:nowrap;">Save</button>'
|
||||||
|
+ '<button type="button" onclick="closeEditProject(' + p.id + ')" class="btn-secondary btn-sm">Cancel</button>'
|
||||||
|
+ '<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>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
// ── Location Delete ───────────────────────────────────────────────────────────
|
// ── Location Delete ───────────────────────────────────────────────────────────
|
||||||
function deleteLoc(projectId, locId, name) {
|
function deleteLoc(projectId, locId, name) {
|
||||||
confirmAction('Delete Location', 'Delete "' + name + '"?', function() {
|
confirmAction('Delete Location', 'Delete "' + name + '"?', function() {
|
||||||
@ -712,51 +915,6 @@ function insertLocInOrder(projectId, newWrapEl) {
|
|||||||
if (!inserted) list.appendChild(newWrapEl);
|
if (!inserted) list.appendChild(newWrapEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProjectCard(p) {
|
|
||||||
var pName = esc(p.name);
|
|
||||||
var rawName = p.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
|
||||||
return '<div class="proj-card" id="proj-card-' + p.id + '">'
|
|
||||||
+ '<div class="proj-header">'
|
|
||||||
+ '<div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0;">'
|
|
||||||
+ '<svg width="15" height="15" fill="none" stroke="#2563eb" viewBox="0 0 24 24" style="flex-shrink:0;" id="proj-icon-' + p.id + '"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>'
|
|
||||||
+ '<span id="proj-name-' + p.id + '" style="font-size:14px;font-weight:600;color:#1e293b;">' + pName + '</span>'
|
|
||||||
+ '<span id="proj-inactive-badge-' + p.id + '" class="badge-inactive" style="display:none;">Inactive</span>'
|
|
||||||
+ '<span id="proj-loc-count-' + p.id + '" style="font-size:12px;color:#94a3b8;margin-left:2px;">0 locations</span>'
|
|
||||||
+ '</div>'
|
|
||||||
+ '<div style="display:flex;gap:6px;flex-shrink:0;" onclick="event.stopPropagation()">'
|
|
||||||
+ '<button type="button" onclick="openEditProject(' + p.id + ', \'' + rawName + '\', true)" class="btn-secondary btn-sm">Edit</button>'
|
|
||||||
+ '<button type="button" onclick="deleteProject(' + p.id + ', \'' + rawName + '\')" class="btn-danger btn-sm">Delete</button>'
|
|
||||||
+ '</div>'
|
|
||||||
+ '</div>'
|
|
||||||
+ '<div class="proj-edit-wrap" id="proj-edit-' + p.id + '">'
|
|
||||||
+ '<input id="edit-proj-name-' + p.id + '" type="text" class="form-input" style="flex:1;font-size:13px;">'
|
|
||||||
+ '<label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#374151;white-space:nowrap;cursor:pointer;"><input type="checkbox" id="edit-proj-active-' + p.id + '" checked style="width:14px;height:14px;"> Active</label>'
|
|
||||||
+ '<button type="button" onclick="saveProject(' + p.id + ')" class="btn-primary" style="padding:5px 14px;font-size:12px;white-space:nowrap;">Save</button>'
|
|
||||||
+ '<button type="button" onclick="closeEditProject(' + p.id + ')" class="btn-secondary btn-sm">Cancel</button>'
|
|
||||||
+ '<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 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 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 ───────────────────────────────────────────────────────────
|
// ── Department CRUD ───────────────────────────────────────────────────────────
|
||||||
function openAddDept(projectId) {
|
function openAddDept(projectId) {
|
||||||
var row = document.getElementById('dept-add-row-' + projectId);
|
var row = document.getElementById('dept-add-row-' + projectId);
|
||||||
|
|||||||
@ -136,6 +136,9 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
// Projects settings
|
// Projects settings
|
||||||
Route::get('settings/projects', [ProjectSettingController::class, 'index'])->name('settings.projects.index');
|
Route::get('settings/projects', [ProjectSettingController::class, 'index'])->name('settings.projects.index');
|
||||||
Route::post('settings/projects', [ProjectSettingController::class, 'store'])->name('settings.projects.store');
|
Route::post('settings/projects', [ProjectSettingController::class, 'store'])->name('settings.projects.store');
|
||||||
|
Route::post('settings/projects/companies', [ProjectSettingController::class, 'storeCompany'])->name('settings.projects.companies.store');
|
||||||
|
Route::patch('settings/projects/companies/{company}', [ProjectSettingController::class, 'updateCompany'])->name('settings.projects.companies.update');
|
||||||
|
Route::delete('settings/projects/companies/{company}', [ProjectSettingController::class, 'destroyCompany'])->name('settings.projects.companies.destroy');
|
||||||
Route::patch('settings/projects/{project}', [ProjectSettingController::class, 'update'])->name('settings.projects.update');
|
Route::patch('settings/projects/{project}', [ProjectSettingController::class, 'update'])->name('settings.projects.update');
|
||||||
Route::delete('settings/projects/{project}', [ProjectSettingController::class, 'destroy'])->name('settings.projects.destroy');
|
Route::delete('settings/projects/{project}', [ProjectSettingController::class, 'destroy'])->name('settings.projects.destroy');
|
||||||
Route::post('settings/projects/{project}/locations', [ProjectSettingController::class, 'storeLocation'])->name('settings.projects.locations.store');
|
Route::post('settings/projects/{project}/locations', [ProjectSettingController::class, 'storeLocation'])->name('settings.projects.locations.store');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user