renew and other crud functionality

This commit is contained in:
2026-01-11 18:31:03 +00:00
parent 55bf184d53
commit ca516d74c2
21 changed files with 882 additions and 74 deletions

View File

@@ -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>

View 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>

View File

@@ -4,9 +4,16 @@
<Link href="/" class="hover:text-[#137fec]">Home</Link>
<span class="mx-2">/</span>
<span>Owner Contracts</span>
</template>
</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')
}

View File

@@ -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>

View File

@@ -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]">
<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]">
<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
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 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>
</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>

View File

@@ -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">
<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>
<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>
</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>

View 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>

View File

@@ -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')
}

View File

@@ -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>

View File

@@ -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>