renew and other crud functionality
This commit is contained in:
@@ -9,7 +9,10 @@ class BuildingController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$buildings = Building::with('owner')->latest()->paginate(15);
|
||||
$buildings = Building::with('owner')
|
||||
->when($request->search, fn($q) => $q->where('building_name', 'like', "%{$request->search}%"))
|
||||
->latest()
|
||||
->paginate(15);
|
||||
return Inertia::render('Buildings/Index', ['buildings' => $buildings]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\Unit;
|
||||
use App\Models\Owner;
|
||||
use App\Models\Subunit;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -31,4 +34,75 @@ class OwnerContractController extends Controller
|
||||
$ownerContract->delete();
|
||||
return redirect()->route('owner-contracts.index');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// app/Http/Controllers/OwnerContractController.php - add these methods
|
||||
public function create()
|
||||
{
|
||||
$units = Unit::with('building')->get();
|
||||
$owners = Owner::all();
|
||||
$subunits = Subunit::all();
|
||||
|
||||
return Inertia::render('OwnerContracts/Create', [
|
||||
'units' => $units,
|
||||
'owners' => $owners,
|
||||
'subunits' => $subunits
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'unit_id' => 'required|exists:units,id',
|
||||
'person_id' => 'required|exists:owners,id',
|
||||
'contract_type' => 'required',
|
||||
'contract_number' => 'required|unique:contracts',
|
||||
'ejari_no' => 'required',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date',
|
||||
'duration_months' => 'required|integer',
|
||||
'total_amount' => 'required|numeric',
|
||||
'status' => 'required',
|
||||
'notes' => 'nullable',
|
||||
'subunit_ids' => 'nullable|array',
|
||||
'payments' => 'required|array'
|
||||
]);
|
||||
|
||||
$contract = Contract::create($validated);
|
||||
|
||||
if ($request->subunit_ids) {
|
||||
$contract->subunits()->attach($request->subunit_ids);
|
||||
}
|
||||
|
||||
foreach ($request->payments as $payment) {
|
||||
$contract->payments()->create($payment);
|
||||
}
|
||||
|
||||
return redirect()->route('owner-contracts.index');
|
||||
}
|
||||
|
||||
public function renew(Contract $ownerContract)
|
||||
{
|
||||
$units = Unit::with('building')->get();
|
||||
$owners = Owner::all();
|
||||
$subunits = Subunit::all();
|
||||
|
||||
$renewData = [
|
||||
'unit_id' => $ownerContract->unit_id,
|
||||
'person_id' => $ownerContract->person_id,
|
||||
'ejari_no' => $ownerContract->ejari_no,
|
||||
'duration_months' => $ownerContract->duration_months,
|
||||
'total_amount' => $ownerContract->total_amount,
|
||||
'start_date' => $ownerContract->end_date->addDay()->format('Y-m-d'),
|
||||
'subunit_ids' => $ownerContract->subunits->pluck('id')->toArray()
|
||||
];
|
||||
|
||||
return Inertia::render('OwnerContracts/Create', [
|
||||
'units' => $units,
|
||||
'owners' => $owners,
|
||||
'subunits' => $subunits,
|
||||
'renewData' => $renewData
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,16 @@ use Inertia\Inertia;
|
||||
|
||||
class OwnerController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$owners = Owner::latest()->paginate(15);
|
||||
public function index(Request $request)
|
||||
{
|
||||
$owners = Owner::when($request->search, fn($q) =>
|
||||
$q->where('name', 'like', "%{$request->search}%")
|
||||
->orWhere('phone', 'like', "%{$request->search}%")
|
||||
)
|
||||
->latest()
|
||||
->paginate(15);
|
||||
return Inertia::render('Owners/Index', ['owners' => $owners]);
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
|
||||
@@ -6,14 +6,19 @@ use Inertia\Inertia;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$payments = Payment::with('contract.unit.building', 'contract.person')
|
||||
->when($request->search, fn($q) =>
|
||||
$q->whereHas('contract', fn($q2) =>
|
||||
$q2->where('contract_number', 'like', "%{$request->search}%")
|
||||
)
|
||||
)
|
||||
->when($request->status, fn($q) => $q->where('status', $request->status))
|
||||
->when($request->type, fn($q) => $q->where('payment_type', $request->type))
|
||||
->latest()
|
||||
->paginate(20);
|
||||
|
||||
return Inertia::render('Payments/Index', ['payments' => $payments]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,18 @@ use Inertia\Inertia;
|
||||
|
||||
class SubunitController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$subunits = Subunit::with('unit.building')->latest()->paginate(15);
|
||||
public function index(Request $request)
|
||||
{
|
||||
$subunits = Subunit::with('unit.building')
|
||||
->when($request->search, fn($q) =>
|
||||
$q->whereHas('unit.building', fn($q2) =>
|
||||
$q2->where('building_name', 'like', "%{$request->search}%")
|
||||
)
|
||||
)
|
||||
->latest()
|
||||
->paginate(15);
|
||||
return Inertia::render('Subunits/Index', ['subunits' => $subunits]);
|
||||
}
|
||||
|
||||
}
|
||||
public function create()
|
||||
{
|
||||
$units = Unit::with('building')->get();
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Contract;
|
||||
use App\Models\Unit;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\Subunit;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -22,7 +25,7 @@ class TenantContractController extends Controller
|
||||
|
||||
public function show(Contract $tenantContract)
|
||||
{
|
||||
$tenantContract->load(['unit.building.owner', 'person', 'payments', 'subunits']);
|
||||
$tenantContract->load(['unit.building.tenant', 'person', 'payments', 'subunits']);
|
||||
return Inertia::render('TenantContracts/Show', ['contract' => $tenantContract]);
|
||||
}
|
||||
|
||||
@@ -31,4 +34,73 @@ class TenantContractController extends Controller
|
||||
$tenantContract->delete();
|
||||
return redirect()->route('tenant-contracts.index');
|
||||
}
|
||||
|
||||
// app/Http/Controllers/tenantContractController.php - add these methods
|
||||
public function create()
|
||||
{
|
||||
$units = Unit::with('building')->get();
|
||||
$tenants = Tenant::all();
|
||||
$subunits = Subunit::all();
|
||||
|
||||
return Inertia::render('TenantContracts/Create', [
|
||||
'units' => $units,
|
||||
'tenants' => $tenants,
|
||||
'subunits' => $subunits
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'unit_id' => 'required|exists:units,id',
|
||||
'person_id' => 'required|exists:tenants,id',
|
||||
'contract_type' => 'required',
|
||||
'contract_number' => 'required|unique:contracts',
|
||||
'ejari_no' => 'required',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date',
|
||||
'duration_months' => 'required|integer',
|
||||
'total_amount' => 'required|numeric',
|
||||
'status' => 'required',
|
||||
'notes' => 'nullable',
|
||||
'subunit_ids' => 'nullable|array',
|
||||
'payments' => 'required|array'
|
||||
]);
|
||||
|
||||
$contract = Contract::create($validated);
|
||||
|
||||
if ($request->subunit_ids) {
|
||||
$contract->subunits()->attach($request->subunit_ids);
|
||||
}
|
||||
|
||||
foreach ($request->payments as $payment) {
|
||||
$contract->payments()->create($payment);
|
||||
}
|
||||
|
||||
return redirect()->route('tenant-contracts.index');
|
||||
}
|
||||
|
||||
public function renew(Contract $tenantContract)
|
||||
{
|
||||
$units = Unit::with('building')->get();
|
||||
$tenants = Tenant::all();
|
||||
$subunits = Subunit::all();
|
||||
|
||||
$renewData = [
|
||||
'unit_id' => $tenantContract->unit_id,
|
||||
'person_id' => $tenantContract->person_id,
|
||||
'ejari_no' => $tenantContract->ejari_no,
|
||||
'duration_months' => $tenantContract->duration_months,
|
||||
'total_amount' => $tenantContract->total_amount,
|
||||
'start_date' => $tenantContract->end_date->addDay()->format('Y-m-d'),
|
||||
'subunit_ids' => $tenantContract->subunits->pluck('id')->toArray()
|
||||
];
|
||||
|
||||
return Inertia::render('TenantContracts/Create', [
|
||||
'units' => $units,
|
||||
'tenants' => $tenants,
|
||||
'subunits' => $subunits,
|
||||
'renewData' => $renewData
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,16 @@ use Inertia\Inertia;
|
||||
|
||||
class TenantController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$tenants = Tenant::latest()->paginate(15);
|
||||
public function index(Request $request)
|
||||
{
|
||||
$tenants = Tenant::when($request->search, fn($q) =>
|
||||
$q->where('name', 'like', "%{$request->search}%")
|
||||
->orWhere('phone', 'like', "%{$request->search}%")
|
||||
)
|
||||
->latest()
|
||||
->paginate(15);
|
||||
return Inertia::render('Tenants/Index', ['tenants' => $tenants]);
|
||||
}
|
||||
|
||||
}
|
||||
public function create()
|
||||
{
|
||||
return Inertia::render('Tenants/Create');
|
||||
|
||||
@@ -7,11 +7,18 @@ use Inertia\Inertia;
|
||||
|
||||
class UnitController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$units = Unit::with('building')->latest()->paginate(15);
|
||||
public function index(Request $request)
|
||||
{
|
||||
$units = Unit::with('building')
|
||||
->when($request->search, fn($q) =>
|
||||
$q->whereHas('building', fn($q2) =>
|
||||
$q2->where('building_name', 'like', "%{$request->search}%")
|
||||
)
|
||||
)
|
||||
->latest()
|
||||
->paginate(15);
|
||||
return Inertia::render('Units/Index', ['units' => $units]);
|
||||
}
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ class DatabaseSeeder extends Seeder
|
||||
$building2 = Building::create(['owner_id' => $owner2->id, 'building_name' => 'Sky Tower', 'address' => 'Sheikh Zayed Road', 'city' => 'Dubai', 'status' => 'occupied']);
|
||||
|
||||
// Units
|
||||
$unit1 = Unit::create(['building_id' => $building1->id, 'unit_no' => '1205', 'floor_no' => '12', 'area_sqft' => 1200, 'assets' => ['AC', 'Fridge', 'Washing Machine'], 'status' => 'occupied']);
|
||||
$unit1 = Unit::create(['building_id' => $building2->id, 'unit_no' => '1205', 'floor_no' => '12', 'area_sqft' => 1200, 'assets' => ['AC', 'Fridge', 'Washing Machine'], 'status' => 'occupied']);
|
||||
$unit2 = Unit::create(['building_id' => $building1->id, 'unit_no' => '805', 'floor_no' => '8', 'area_sqft' => 800, 'assets' => ['AC'], 'status' => 'occupied']);
|
||||
$unit3 = Unit::create(['building_id' => $building2->id, 'unit_no' => '305', 'floor_no' => '3', 'area_sqft' => 1500, 'assets' => ['AC', 'Fridge', 'Bed'], 'status' => 'vacant']);
|
||||
|
||||
@@ -49,7 +49,7 @@ class DatabaseSeeder extends Seeder
|
||||
$sub2 = Subunit::create(['unit_id' => $unit1->id, 'subunit_no' => 'R2', 'subunit_type' => 'room', 'area_sqft' => 350, 'status' => 'vacant']);
|
||||
$sub3 = Subunit::create(['unit_id' => $unit2->id, 'subunit_no' => 'R1', 'subunit_type' => 'room', 'area_sqft' => 400, 'status' => 'occupied']);
|
||||
|
||||
// Owner Contracts
|
||||
// Owner Contractss
|
||||
$ownerContract1 = Contract::create([
|
||||
'unit_id' => $unit1->id,
|
||||
'person_id' => $owner1->id,
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-[#f6f7f8] flex">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-white border-r border-slate-200 fixed h-full overflow-y-auto">
|
||||
<div class="p-6 border-b border-slate-200">
|
||||
<h1 class="text-xl font-bold text-slate-900">Real Estate</h1>
|
||||
<aside class="bg-white border-r border-slate-200 fixed h-full overflow-y-auto transition-all duration-300" :class="sidebarOpen ? 'w-64' : 'w-16'">
|
||||
<div class="p-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<h1 v-show="sidebarOpen" class="text-xl font-bold text-slate-900">Real Estate</h1>
|
||||
<button @click="sidebarOpen = !sidebarOpen" class="p-2 hover:bg-slate-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="p-4 space-y-1">
|
||||
<Link :href="route('dashboard')" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-colors" :class="route().current('dashboard') ? 'bg-[#137fec] text-white' : 'hover:bg-slate-50 text-slate-900'">
|
||||
<span class="text-sm font-medium">Home</span>
|
||||
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
<span v-show="sidebarOpen" class="text-sm font-medium">Home</span>
|
||||
</Link>
|
||||
|
||||
<!-- Contracts Dropdown -->
|
||||
<div>
|
||||
<button @click="contractsOpen = !contractsOpen" class="w-full flex items-center justify-between px-4 py-3 rounded-lg hover:bg-slate-50 text-slate-900">
|
||||
<span class="text-sm font-medium">Contracts</span>
|
||||
<svg class="w-4 h-4 transition-transform" :class="{'rotate-180': contractsOpen}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<span v-show="sidebarOpen" class="text-sm font-medium">Contracts</span>
|
||||
</div>
|
||||
<svg v-show="sidebarOpen" class="w-4 h-4 transition-transform" :class="{'rotate-180': contractsOpen}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-show="contractsOpen" class="ml-4 mt-1 space-y-1">
|
||||
<div v-show="contractsOpen && sidebarOpen" class="ml-4 mt-1 space-y-1">
|
||||
<Link :href="route('owner-contracts.index')" class="flex items-center px-4 py-2 rounded-lg text-sm transition-colors" :class="route().current('owner-contracts.*') ? 'bg-[#137fec] text-white' : 'hover:bg-slate-50 text-slate-700'">
|
||||
Owner Contracts
|
||||
</Link>
|
||||
@@ -31,12 +44,17 @@
|
||||
<!-- Properties Dropdown -->
|
||||
<div>
|
||||
<button @click="propertiesOpen = !propertiesOpen" class="w-full flex items-center justify-between px-4 py-3 rounded-lg hover:bg-slate-50 text-slate-900">
|
||||
<span class="text-sm font-medium">Properties</span>
|
||||
<svg class="w-4 h-4 transition-transform" :class="{'rotate-180': propertiesOpen}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" 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>
|
||||
<span v-show="sidebarOpen" class="text-sm font-medium">Properties</span>
|
||||
</div>
|
||||
<svg v-show="sidebarOpen" class="w-4 h-4 transition-transform" :class="{'rotate-180': propertiesOpen}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-show="propertiesOpen" class="ml-4 mt-1 space-y-1">
|
||||
<div v-show="propertiesOpen && sidebarOpen" class="ml-4 mt-1 space-y-1">
|
||||
<Link :href="route('buildings.index')" class="flex items-center px-4 py-2 rounded-lg text-sm transition-colors" :class="route().current('buildings.*') ? 'bg-[#137fec] text-white' : 'hover:bg-slate-50 text-slate-700'">
|
||||
Buildings
|
||||
</Link>
|
||||
@@ -50,21 +68,30 @@
|
||||
</div>
|
||||
|
||||
<Link :href="route('owners.index')" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-colors" :class="route().current('owners.*') ? 'bg-[#137fec] text-white' : 'hover:bg-slate-50 text-slate-900'">
|
||||
<span class="text-sm font-medium">Owners</span>
|
||||
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
<span v-show="sidebarOpen" class="text-sm font-medium">Owners</span>
|
||||
</Link>
|
||||
|
||||
<Link :href="route('tenants.index')" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-colors" :class="route().current('tenants.*') ? 'bg-[#137fec] text-white' : 'hover:bg-slate-50 text-slate-900'">
|
||||
<span class="text-sm font-medium">Tenants</span>
|
||||
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
<span v-show="sidebarOpen" class="text-sm font-medium">Tenants</span>
|
||||
</Link>
|
||||
|
||||
<Link :href="route('payments.index')" class="flex items-center gap-3 px-4 py-3 rounded-lg transition-colors" :class="route().current('payments.*') ? 'bg-[#137fec] text-white' : 'hover:bg-slate-50 text-slate-900'">
|
||||
<span class="text-sm font-medium">Payments</span>
|
||||
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span v-show="sidebarOpen" class="text-sm font-medium">Payments</span>
|
||||
</Link>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="ml-64 flex-1">
|
||||
<div class="flex-1 transition-all duration-300" :class="sidebarOpen ? 'ml-64' : 'ml-16'">
|
||||
<!-- Top Bar -->
|
||||
<header class="bg-white border-b border-slate-200 px-6 py-4 sticky top-0 z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -89,6 +116,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { Link, router } from '@inertiajs/vue3'
|
||||
|
||||
const sidebarOpen = ref(true)
|
||||
const contractsOpen = ref(true)
|
||||
const propertiesOpen = ref(true)
|
||||
|
||||
|
||||
@@ -13,7 +13,15 @@
|
||||
Add Building
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<input
|
||||
v-model="search"
|
||||
@input="filterResults"
|
||||
type="text"
|
||||
placeholder="Search by building..."
|
||||
class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#137fec]"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-slate-50">
|
||||
@@ -34,8 +42,10 @@
|
||||
<td class="px-6 py-4 text-sm text-slate-900">{{ building.city }}</td>
|
||||
<td class="px-6 py-4"><span class="px-2 py-1 text-xs font-bold rounded-md bg-green-50 text-green-700">{{ building.status }}</span></td>
|
||||
<td class="px-6 py-4 text-sm space-x-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('buildings.edit', building.id)" class="text-[#137fec] hover:underline">Edit</Link>
|
||||
<button @click="destroy(building.id)" class="text-red-600 hover:underline">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -56,4 +66,10 @@ const destroy = (id) => {
|
||||
router.delete(route('buildings.destroy', id))
|
||||
}
|
||||
}
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filterResults = () => {
|
||||
router.get(route('buildings.index'), { search: search.value }, { preserveState: true })
|
||||
}
|
||||
</script>
|
||||
230
resources/js/Pages/OwnerContracts/Create.vue
Normal file
230
resources/js/Pages/OwnerContracts/Create.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<AppLayout>
|
||||
<template #breadcrumb>
|
||||
<Link href="/" class="hover:text-[#137fec]">Home</Link>
|
||||
<span class="mx-2">/</span>
|
||||
<Link :href="route('owner-contracts.index')" class="hover:text-[#137fec]">Owner Contracts</Link>
|
||||
<span class="mx-2">/</span>
|
||||
<span>Create</span>
|
||||
</template>
|
||||
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="text-2xl font-bold text-slate-900 mb-6">Add Owner Contract</h1>
|
||||
|
||||
<form @submit.prevent="submit" class="bg-white rounded-xl border border-slate-200 p-6 space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Unit</label>
|
||||
<select v-model="form.unit_id" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required>
|
||||
<option value="">Select Unit</option>
|
||||
<option v-for="unit in units" :key="unit.id" :value="unit.id">
|
||||
{{ unit.building.building_name }} - Unit {{ unit.unit_no }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Owner</label>
|
||||
<select v-model="form.person_id" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required>
|
||||
<option value="">Select Owner</option>
|
||||
<option v-for="owner in owners" :key="owner.id" :value="owner.id">{{ owner.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Contract Number</label>
|
||||
<input v-model="form.contract_number" type="text" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Ejari Number</label>
|
||||
<input v-model="form.ejari_no" type="text" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Start Date</label>
|
||||
<input v-model="form.start_date" type="date" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Duration (months)</label>
|
||||
<input v-model="form.duration_months" @input="calculateEndDate" type="number" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">End Date</label>
|
||||
<input v-model="form.end_date" type="date" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Total Amount (AED)</label>
|
||||
<input v-model="form.total_amount" @input="distributePayments" type="number" step="0.01" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Status</label>
|
||||
<select v-model="form.status" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required>
|
||||
<option value="active">Active</option>
|
||||
<option value="expired">Expired</option>
|
||||
<option value="terminated">Terminated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Number of Payments</label>
|
||||
<input v-model="numPayments" @input="updatePayments" type="number" min="1" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.unit_id">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Subunits (optional)</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label v-for="subunit in availableSubunits" :key="subunit.id" class="flex items-center gap-2 px-3 py-2 border border-slate-200 rounded-lg cursor-pointer hover:bg-slate-50">
|
||||
<input type="checkbox" :value="subunit.id" v-model="form.subunit_ids" class="rounded" />
|
||||
<span class="text-sm">{{ subunit.subunit_type }} {{ subunit.subunit_no }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Notes</label>
|
||||
<textarea v-model="form.notes" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div v-if="form.payments.length" class="space-y-4">
|
||||
<h3 class="text-lg font-bold text-slate-900">Payments</h3>
|
||||
<p v-if="paymentError" class="text-red-600 text-sm">{{ paymentError }}</p>
|
||||
|
||||
<div v-for="(payment, index) in form.payments" :key="index" class="p-4 border border-slate-200 rounded-lg space-y-3">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Payment Type</label>
|
||||
<select v-model="payment.payment_type" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]">
|
||||
<option value="cheque">Cheque</option>
|
||||
<option value="online">Online</option>
|
||||
<option value="cash">Cash</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Amount (AED)</label>
|
||||
<input v-model="payment.amount" @input="validateTotal" type="number" step="0.01" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Due Date</label>
|
||||
<input v-model="payment.due_date" type="date" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="payment.payment_type === 'cheque'" class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Cheque Number</label>
|
||||
<input v-model="payment.payment_number" type="text" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Bank Name</label>
|
||||
<input v-model="payment.bank_name" type="text" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Status</label>
|
||||
<select v-model="payment.status" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]">
|
||||
<option value="pending">Pending</option>
|
||||
<option value="collected">Collected</option>
|
||||
<option value="cleared">Cleared</option>
|
||||
<option value="bounced">Bounced</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 pt-4">
|
||||
<button type="submit" :disabled="!!paymentError || form.processing" class="px-6 py-2 bg-[#137fec] text-white rounded-lg font-medium hover:bg-[#137fec]/90 disabled:opacity-50">Save Contract</button>
|
||||
<Link :href="route('owner-contracts.index')" class="px-6 py-2 border border-slate-200 text-slate-600 rounded-lg font-medium hover:bg-slate-50">Cancel</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useForm, Link } from '@inertiajs/vue3'
|
||||
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||
|
||||
const props = defineProps({
|
||||
units: Array,
|
||||
owners: Array,
|
||||
subunits: Array,
|
||||
renewData: Object
|
||||
})
|
||||
|
||||
const numPayments = ref(props.renewData?.payments?.length || 1)
|
||||
const paymentError = ref('')
|
||||
|
||||
const form = useForm({
|
||||
unit_id: props.renewData?.unit_id || '',
|
||||
person_id: props.renewData?.person_id || '',
|
||||
contract_type: 'owner',
|
||||
contract_number: '',
|
||||
ejari_no: props.renewData?.ejari_no || '',
|
||||
start_date: props.renewData?.start_date || '',
|
||||
end_date: '',
|
||||
duration_months: props.renewData?.duration_months || 12,
|
||||
total_amount: props.renewData?.total_amount || '',
|
||||
status: 'active',
|
||||
notes: '',
|
||||
subunit_ids: props.renewData?.subunit_ids || [],
|
||||
payments: []
|
||||
})
|
||||
|
||||
const availableSubunits = computed(() => {
|
||||
return props.subunits.filter(s => s.unit_id == form.unit_id)
|
||||
})
|
||||
|
||||
const calculateEndDate = () => {
|
||||
if (form.start_date && form.duration_months) {
|
||||
const start = new Date(form.start_date)
|
||||
start.setMonth(start.getMonth() + parseInt(form.duration_months))
|
||||
form.end_date = start.toISOString().split('T')[0]
|
||||
}
|
||||
}
|
||||
|
||||
const updatePayments = () => {
|
||||
const count = parseInt(numPayments.value)
|
||||
form.payments = Array.from({ length: count }, (_, i) => ({
|
||||
payment_type: 'cheque',
|
||||
payment_number: '',
|
||||
bank_name: '',
|
||||
amount: 0,
|
||||
due_date: '',
|
||||
status: 'pending'
|
||||
}))
|
||||
distributePayments()
|
||||
}
|
||||
|
||||
const distributePayments = () => {
|
||||
if (form.total_amount && form.payments.length) {
|
||||
const perPayment = (parseFloat(form.total_amount) / form.payments.length).toFixed(2)
|
||||
form.payments.forEach(p => p.amount = perPayment)
|
||||
}
|
||||
validateTotal()
|
||||
}
|
||||
|
||||
const validateTotal = () => {
|
||||
const sum = form.payments.reduce((acc, p) => acc + parseFloat(p.amount || 0), 0)
|
||||
const total = parseFloat(form.total_amount || 0)
|
||||
if (Math.abs(sum - total) > 0.01) {
|
||||
paymentError.value = `Payment total (${sum.toFixed(2)}) doesn't match contract amount (${total.toFixed(2)})`
|
||||
} else {
|
||||
paymentError.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => form.start_date, calculateEndDate)
|
||||
watch(() => form.duration_months, calculateEndDate)
|
||||
|
||||
const submit = () => form.post(route('owner-contracts.store'))
|
||||
</script>
|
||||
@@ -6,7 +6,14 @@
|
||||
<span>Owner Contracts</span>
|
||||
</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-slate-900">Owner Contracts</h1>
|
||||
<Link :href="route('owner-contracts.create')" class="px-4 py-2 bg-[#137fec] text-white rounded-lg font-medium hover:bg-[#137fec]/90">
|
||||
Add Contract
|
||||
</Link>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<div class="flex gap-4">
|
||||
<input
|
||||
@@ -83,6 +90,12 @@
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
<button
|
||||
@click="renewContract(contract.id)"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Renew
|
||||
</button>
|
||||
<button
|
||||
@click="deleteContract(contract.id)"
|
||||
class="px-4 py-2 border border-red-200 text-red-600 rounded-lg text-sm font-medium hover:bg-red-50 transition-colors"
|
||||
@@ -136,6 +149,10 @@ const deleteContract = (id) => {
|
||||
}
|
||||
}
|
||||
|
||||
const renewContract = (id) => {
|
||||
router.get(`/owner-contracts/${id}/renew`)
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString('en-GB')
|
||||
}
|
||||
|
||||
@@ -11,7 +11,15 @@
|
||||
<h1 class="text-2xl font-bold text-slate-900">Owners</h1>
|
||||
<Link :href="route('owners.create')" class="px-4 py-2 bg-[#137fec] text-white rounded-lg font-medium hover:bg-[#137fec]/90">Add Owner</Link>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<input
|
||||
v-model="search"
|
||||
@input="filterResults"
|
||||
type="text"
|
||||
placeholder="Search by owner name..."
|
||||
class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#137fec]"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-slate-50">
|
||||
@@ -30,8 +38,10 @@
|
||||
<td class="px-6 py-4 text-sm text-slate-900">{{ owner.email }}</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-900">{{ owner.nationality }}</td>
|
||||
<td class="px-6 py-4 text-sm space-x-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('owners.edit', owner.id)" class="text-[#137fec] hover:underline">Edit</Link>
|
||||
<button @click="destroy(owner.id)" class="text-red-600 hover:underline">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -48,4 +58,10 @@ defineProps({ owners: Object })
|
||||
const destroy = (id) => {
|
||||
if (confirm('Delete this owner?')) router.delete(route('owners.destroy', id))
|
||||
}
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filterResults = () => {
|
||||
router.get(route('owners.index'), { search: search.value }, { preserveState: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,22 +8,40 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
<h1 class="text-2xl font-bold text-slate-900">All Payments</h1>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<input
|
||||
v-model="search"
|
||||
@input="filterResults"
|
||||
type="text"
|
||||
placeholder="Search by contract ID..."
|
||||
class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#137fec]"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<div class="flex gap-4">
|
||||
<select v-model="filters.type" @change="filter" class="px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]">
|
||||
<select
|
||||
v-model="filters.type"
|
||||
@change="filter"
|
||||
class="px-4 py-2 pr-10 border border-slate-200 rounded-lg
|
||||
focus:ring-2 focus:ring-[#137fec] appearance-none bg-white">
|
||||
<option value="">All Types</option>
|
||||
<option value="cheque">Cheque</option>
|
||||
<option value="online">Online</option>
|
||||
<option value="cash">Cash</option>
|
||||
</select>
|
||||
<select v-model="filters.status" @change="filter" class="px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]">
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-model="filters.status"
|
||||
@change="filter"
|
||||
class="px-4 py-2 pr-10 border border-slate-200 rounded-lg
|
||||
focus:ring-2 focus:ring-[#137fec] appearance-none bg-white">
|
||||
<option value="">All Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="collected">Collected</option>
|
||||
<option value="cleared">Cleared</option>
|
||||
<option value="bounced">Bounced</option>
|
||||
</select>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,4 +119,10 @@ const filter = () => {
|
||||
|
||||
const formatDate = (date) => new Date(date).toLocaleDateString('en-GB')
|
||||
const formatMoney = (amount) => new Intl.NumberFormat('en-US').format(amount)
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filterResults = () => {
|
||||
router.get(route('payments.index'), { search: search.value }, { preserveState: true })
|
||||
}
|
||||
</script>
|
||||
@@ -13,7 +13,15 @@
|
||||
Add Subunit
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<input
|
||||
v-model="search"
|
||||
@input="filterResults"
|
||||
type="text"
|
||||
placeholder="Search by building..."
|
||||
class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#137fec]"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-slate-50">
|
||||
@@ -35,10 +43,12 @@
|
||||
<td class="px-6 py-4 text-sm text-slate-900">{{ subunit.subunit_type }}</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-900">{{ subunit.area_sqft }}</td>
|
||||
<td class="px-6 py-4"><span class="px-2 py-1 text-xs font-bold rounded-md bg-green-50 text-green-700">{{ subunit.status }}</span></td>
|
||||
<td class="px-6 py-4 text-sm space-x-2">
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('subunits.edit', subunit.id)" class="text-[#137fec] hover:underline">Edit</Link>
|
||||
<button @click="destroy(subunit.id)" class="text-red-600 hover:underline">Delete</button>
|
||||
</td>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -58,4 +68,10 @@ const destroy = (id) => {
|
||||
router.delete(route('subunits.destroy', id))
|
||||
}
|
||||
}
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filterResults = () => {
|
||||
router.get(route('subunits.index'), { search: search.value }, { preserveState: true })
|
||||
}
|
||||
</script>
|
||||
230
resources/js/Pages/TenantContracts/Create.vue
Normal file
230
resources/js/Pages/TenantContracts/Create.vue
Normal file
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<AppLayout>
|
||||
<template #breadcrumb>
|
||||
<Link href="/" class="hover:text-[#137fec]">Home</Link>
|
||||
<span class="mx-2">/</span>
|
||||
<Link :href="route('tenant-contracts.index')" class="hover:text-[#137fec]">Tenant Contracts</Link>
|
||||
<span class="mx-2">/</span>
|
||||
<span>Create</span>
|
||||
</template>
|
||||
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="text-2xl font-bold text-slate-900 mb-6">Add Tenant Contract</h1>
|
||||
|
||||
<form @submit.prevent="submit" class="bg-white rounded-xl border border-slate-200 p-6 space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Unit</label>
|
||||
<select v-model="form.unit_id" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required>
|
||||
<option value="">Select Unit</option>
|
||||
<option v-for="unit in units" :key="unit.id" :value="unit.id">
|
||||
{{ unit.building.building_name }} - Unit {{ unit.unit_no }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Tenant</label>
|
||||
<select v-model="form.person_id" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required>
|
||||
<option value="">Select Tenant</option>
|
||||
<option v-for="tenant in tenants" :key="tenant.id" :value="tenant.id">{{ tenant.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Contract Number</label>
|
||||
<input v-model="form.contract_number" type="text" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Ejari Number</label>
|
||||
<input v-model="form.ejari_no" type="text" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Start Date</label>
|
||||
<input v-model="form.start_date" type="date" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Duration (months)</label>
|
||||
<input v-model="form.duration_months" @input="calculateEndDate" type="number" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">End Date</label>
|
||||
<input v-model="form.end_date" type="date" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Total Amount (AED)</label>
|
||||
<input v-model="form.total_amount" @input="distributePayments" type="number" step="0.01" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Status</label>
|
||||
<select v-model="form.status" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required>
|
||||
<option value="active">Active</option>
|
||||
<option value="expired">Expired</option>
|
||||
<option value="terminated">Terminated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Number of Payments</label>
|
||||
<input v-model="numPayments" @input="updatePayments" type="number" min="1" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.unit_id">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Subunits (optional)</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label v-for="subunit in availableSubunits" :key="subunit.id" class="flex items-center gap-2 px-3 py-2 border border-slate-200 rounded-lg cursor-pointer hover:bg-slate-50">
|
||||
<input type="checkbox" :value="subunit.id" v-model="form.subunit_ids" class="rounded" />
|
||||
<span class="text-sm">{{ subunit.subunit_type }} {{ subunit.subunit_no }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-2">Notes</label>
|
||||
<textarea v-model="form.notes" class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div v-if="form.payments.length" class="space-y-4">
|
||||
<h3 class="text-lg font-bold text-slate-900">Payments</h3>
|
||||
<p v-if="paymentError" class="text-red-600 text-sm">{{ paymentError }}</p>
|
||||
|
||||
<div v-for="(payment, index) in form.payments" :key="index" class="p-4 border border-slate-200 rounded-lg space-y-3">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Payment Type</label>
|
||||
<select v-model="payment.payment_type" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]">
|
||||
<option value="cheque">Cheque</option>
|
||||
<option value="online">Online</option>
|
||||
<option value="cash">Cash</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Amount (AED)</label>
|
||||
<input v-model="payment.amount" @input="validateTotal" type="number" step="0.01" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Due Date</label>
|
||||
<input v-model="payment.due_date" type="date" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="payment.payment_type === 'cheque'" class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Cheque Number</label>
|
||||
<input v-model="payment.payment_number" type="text" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Bank Name</label>
|
||||
<input v-model="payment.bank_name" type="text" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Status</label>
|
||||
<select v-model="payment.status" class="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-[#137fec]">
|
||||
<option value="pending">Pending</option>
|
||||
<option value="collected">Collected</option>
|
||||
<option value="cleared">Cleared</option>
|
||||
<option value="bounced">Bounced</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 pt-4">
|
||||
<button type="submit" :disabled="!!paymentError || form.processing" class="px-6 py-2 bg-[#137fec] text-white rounded-lg font-medium hover:bg-[#137fec]/90 disabled:opacity-50">Save Contract</button>
|
||||
<Link :href="route('tenant-contracts.index')" class="px-6 py-2 border border-slate-200 text-slate-600 rounded-lg font-medium hover:bg-slate-50">Cancel</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useForm, Link } from '@inertiajs/vue3'
|
||||
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||
|
||||
const props = defineProps({
|
||||
units: Array,
|
||||
tenants: Array,
|
||||
subunits: Array,
|
||||
renewData: Object
|
||||
})
|
||||
|
||||
const numPayments = ref(props.renewData?.payments?.length || 1)
|
||||
const paymentError = ref('')
|
||||
|
||||
const form = useForm({
|
||||
unit_id: props.renewData?.unit_id || '',
|
||||
person_id: props.renewData?.person_id || '',
|
||||
contract_type: 'tenant',
|
||||
contract_number: '',
|
||||
ejari_no: props.renewData?.ejari_no || '',
|
||||
start_date: props.renewData?.start_date || '',
|
||||
end_date: '',
|
||||
duration_months: props.renewData?.duration_months || 12,
|
||||
total_amount: props.renewData?.total_amount || '',
|
||||
status: 'active',
|
||||
notes: '',
|
||||
subunit_ids: props.renewData?.subunit_ids || [],
|
||||
payments: []
|
||||
})
|
||||
|
||||
const availableSubunits = computed(() => {
|
||||
return props.subunits.filter(s => s.unit_id == form.unit_id)
|
||||
})
|
||||
|
||||
const calculateEndDate = () => {
|
||||
if (form.start_date && form.duration_months) {
|
||||
const start = new Date(form.start_date)
|
||||
start.setMonth(start.getMonth() + parseInt(form.duration_months))
|
||||
form.end_date = start.toISOString().split('T')[0]
|
||||
}
|
||||
}
|
||||
|
||||
const updatePayments = () => {
|
||||
const count = parseInt(numPayments.value)
|
||||
form.payments = Array.from({ length: count }, (_, i) => ({
|
||||
payment_type: 'cheque',
|
||||
payment_number: '',
|
||||
bank_name: '',
|
||||
amount: 0,
|
||||
due_date: '',
|
||||
status: 'pending'
|
||||
}))
|
||||
distributePayments()
|
||||
}
|
||||
|
||||
const distributePayments = () => {
|
||||
if (form.total_amount && form.payments.length) {
|
||||
const perPayment = (parseFloat(form.total_amount) / form.payments.length).toFixed(2)
|
||||
form.payments.forEach(p => p.amount = perPayment)
|
||||
}
|
||||
validateTotal()
|
||||
}
|
||||
|
||||
const validateTotal = () => {
|
||||
const sum = form.payments.reduce((acc, p) => acc + parseFloat(p.amount || 0), 0)
|
||||
const total = parseFloat(form.total_amount || 0)
|
||||
if (Math.abs(sum - total) > 0.01) {
|
||||
paymentError.value = `Payment total (${sum.toFixed(2)}) doesn't match contract amount (${total.toFixed(2)})`
|
||||
} else {
|
||||
paymentError.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => form.start_date, calculateEndDate)
|
||||
watch(() => form.duration_months, calculateEndDate)
|
||||
|
||||
const submit = () => form.post(route('tenant-contracts.store'))
|
||||
</script>
|
||||
@@ -7,6 +7,13 @@
|
||||
</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-slate-900">Tenant Contracts</h1>
|
||||
<Link :href="route('tenant-contracts.create')" class="px-4 py-2 bg-[#137fec] text-white rounded-lg font-medium hover:bg-[#137fec]/90">
|
||||
Add Contract
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<div class="flex gap-4">
|
||||
<input
|
||||
@@ -83,6 +90,12 @@
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
<button
|
||||
@click="renewContract(contract.id)"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Renew
|
||||
</button>
|
||||
<button
|
||||
@click="deleteContract(contract.id)"
|
||||
class="px-4 py-2 border border-red-200 text-red-600 rounded-lg text-sm font-medium hover:bg-red-50 transition-colors"
|
||||
@@ -136,6 +149,10 @@ const deleteContract = (id) => {
|
||||
}
|
||||
}
|
||||
|
||||
const renewContract = (id) => {
|
||||
router.get(`/tenant-contracts/${id}/renew`)
|
||||
}
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString('en-GB')
|
||||
}
|
||||
|
||||
@@ -11,7 +11,15 @@
|
||||
<h1 class="text-2xl font-bold text-slate-900">Tenants</h1>
|
||||
<Link :href="route('tenants.create')" class="px-4 py-2 bg-[#137fec] text-white rounded-lg font-medium hover:bg-[#137fec]/90">Add Tenant</Link>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<input
|
||||
v-model="search"
|
||||
@input="filterResults"
|
||||
type="text"
|
||||
placeholder="Search by tenant..."
|
||||
class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#137fec]"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-slate-50">
|
||||
@@ -30,8 +38,10 @@
|
||||
<td class="px-6 py-4 text-sm text-slate-900">{{ tenant.email }}</td>
|
||||
<td class="px-6 py-4 text-sm text-slate-900">{{ tenant.nationality }}</td>
|
||||
<td class="px-6 py-4 text-sm space-x-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('tenants.edit', tenant.id)" class="text-[#137fec] hover:underline">Edit</Link>
|
||||
<button @click="destroy(tenant.id)" class="text-red-600 hover:underline">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -48,4 +58,10 @@ defineProps({ tenants: Object })
|
||||
const destroy = (id) => {
|
||||
if (confirm('Delete this tenant?')) router.delete(route('tenants.destroy', id))
|
||||
}
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filterResults = () => {
|
||||
router.get(route('tenants.index'), { search: search.value }, { preserveState: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,6 +14,16 @@
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<input
|
||||
v-model="search"
|
||||
@input="filterResults"
|
||||
type="text"
|
||||
placeholder="Search by building..."
|
||||
class="w-full px-4 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#137fec]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-slate-50">
|
||||
@@ -34,8 +44,10 @@
|
||||
<td class="px-6 py-4 text-sm text-slate-900">{{ unit.area_sqft }}</td>
|
||||
<td class="px-6 py-4"><span class="px-2 py-1 text-xs font-bold rounded-md bg-green-50 text-green-700">{{ unit.status }}</span></td>
|
||||
<td class="px-6 py-4 text-sm space-x-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('units.edit', unit.id)" class="text-[#137fec] hover:underline">Edit</Link>
|
||||
<button @click="destroy(unit.id)" class="text-red-600 hover:underline">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -56,4 +68,10 @@ const destroy = (id) => {
|
||||
router.delete(route('units.destroy', id))
|
||||
}
|
||||
}
|
||||
|
||||
const search = ref('')
|
||||
|
||||
const filterResults = () => {
|
||||
router.get(route('units.index'), { search: search.value }, { preserveState: true })
|
||||
}
|
||||
</script>
|
||||
@@ -30,4 +30,8 @@ Route::middleware('auth')->group(function () {
|
||||
Route::resource('tenants', TenantController::class);
|
||||
|
||||
Route::resource('payments', PaymentController::class);
|
||||
|
||||
// routes/web.php - add these routes
|
||||
Route::get('owner-contracts/{contract}/renew', [OwnerContractController::class, 'renew'])->name('owner-contracts.renew');
|
||||
Route::get('tenant-contracts/{contract}/renew', [TenantContractController::class, 'renew'])->name('tenant-contracts.renew');
|
||||
});
|
||||
Reference in New Issue
Block a user