import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export interface Property {
id: number;
propertyTitle: string;
agencyId: number;
propertyTypeId: number;
propertyTypeName: string;
description: string;
amenities: string;
monthlyRent: number;
location: string;
unitsAvailable: number;
applicationDeadline: string;
status: string;
createdBy: string;
createdDate: string;
isDeleted: boolean;
deletedDate: string;
deletedBy: string;
updatedDate: string;
updatedBy: string;
}
export interface PropertyResponse {
data: Property[];
message: string;
totalCount: number;
pageNumber: number;
pageSize: number;
}
export interface GetAllPropertyParams {
pageSize: number;
pageNumber: number;
searchTerm?: string;
statusFilter?: string;
}
export interface DeleteResponse {
response: string;
}
export const propertyApi = createApi({
reducerPath: "propertyApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://localhost:7083/api/",
prepareHeaders: (headers) => {
const token = localStorage.getItem("token");
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
headers.set("Content-Type", "application/json");
return headers;
},
}),
tagTypes: ["Property"],
endpoints: (builder) => ({
// ==========================================
// GET ALL PROPERTY
// ==========================================
getAllProperty: builder.query<
PropertyResponse,
GetAllPropertyParams
>({
query: ({
pageSize,
pageNumber,
searchTerm = "",
statusFilter = "",
}) => ({
url: "Property/GetAllProperty",
method: "GET",
params: {
PageSize: pageSize,
PageNumber: pageNumber,
SearchTerm: searchTerm,
StatusFilter: statusFilter,
},
}),
providesTags: ["Property"],
}),
// ==========================================
// DELETE PROPERTY
// ==========================================
deleteProperty: builder.mutation<
DeleteResponse,
number
>({
query: (propertyId) => ({
url: "Property/DeleteProperty",
method: "DELETE",
body: {
propertyId,
},
}),
invalidatesTags: ["Property"],
}),
}),
});
export const {
useGetAllPropertyQuery,
useDeletePropertyMutation,
} = propertyApi;
Table
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
FaEdit,
FaTrash,
FaSearch,
FaExclamationTriangle,
FaTimes,
} from "react-icons/fa";
import {
useGetAllPropertyQuery,
useDeletePropertyMutation,
} from "../services/propertyApi";
const PAGE_SIZE = 10;
const PropertyTable = () => {
const navigate = useNavigate();
// ==========================================
// PAGINATION
// ==========================================
const [pageNumber, setPageNumber] = useState(1);
// ==========================================
// SEARCH
// ==========================================
const [searchInput, setSearchInput] = useState("");
const [searchTerm, setSearchTerm] = useState("");
// ==========================================
// STATUS FILTER
// ==========================================
const [statusFilter, setStatusFilter] = useState("");
// ==========================================
// DELETE MODAL
// ==========================================
const [deletePropertyId, setDeletePropertyId] =
useState<number | null>(null);
// ==========================================
// ROLE
// ==========================================
const role = localStorage.getItem("role");
const isTenant =
role?.toLowerCase() === "tenant";
// ==========================================
// GET PROPERTIES
// ==========================================
const {
data,
isLoading,
isFetching,
isError,
} = useGetAllPropertyQuery({
pageSize: PAGE_SIZE,
pageNumber,
searchTerm,
statusFilter,
});
// ==========================================
// DELETE MUTATION
// ==========================================
const [
deleteProperty,
{
isLoading: isDeleting,
},
] = useDeletePropertyMutation();
// ==========================================
// DEBOUNCE SEARCH
// ==========================================
useEffect(() => {
const timer = setTimeout(() => {
setPageNumber(1);
setSearchTerm(searchInput);
}, 500);
return () => clearTimeout(timer);
}, [searchInput]);
// ==========================================
// DATA
// ==========================================
const properties = data?.data ?? [];
const totalCount =
data?.totalCount ?? 0;
const totalPages = Math.ceil(
totalCount / PAGE_SIZE
);
// ==========================================
// APPLY
// ==========================================
const handleApply = (
propertyId: number
) => {
navigate(
`/application-form/${propertyId}`
);
};
// ==========================================
// EDIT
// ==========================================
const handleEdit = (
propertyId: number
) => {
console.log(
"Edit Property ID:",
propertyId
);
// Future:
// navigate(`/property/edit/${propertyId}`);
};
// ==========================================
// OPEN DELETE MODAL
// ==========================================
const handleDeleteClick = (
propertyId: number
) => {
setDeletePropertyId(propertyId);
};
// ==========================================
// CLOSE DELETE MODAL
// ==========================================
const handleCancelDelete = () => {
if (isDeleting) return;
setDeletePropertyId(null);
};
// ==========================================
// CONFIRM DELETE
// ==========================================
const handleConfirmDelete = async () => {
if (deletePropertyId === null) {
return;
}
try {
const result =
await deleteProperty(
deletePropertyId
).unwrap();
console.log(
result.response
);
// Close modal
setDeletePropertyId(null);
/*
* Because deleteProperty has:
*
* invalidatesTags: ["Property"]
*
* RTK Query automatically refetches
* the property list.
*/
} catch (error) {
console.error(
"Failed to delete property:",
error
);
}
};
// ==========================================
// STATUS STYLE
// ==========================================
const getStatusStyle = (
status: string
) => {
switch (
status.toLowerCase()
) {
case "available":
return "bg-green-100 text-green-700";
case "rented":
return "bg-red-100 text-red-700";
case "onhold":
return "bg-yellow-100 text-yellow-700";
default:
return "bg-gray-200 text-gray-700";
}
};
// ==========================================
// LOADING
// ==========================================
if (isLoading) {
return (
<div className="flex justify-center py-10">
<p className="text-gray-700">
Loading properties...
</p>
</div>
);
}
// ==========================================
// ERROR
// ==========================================
if (isError) {
return (
<div className="py-10 text-center text-red-600">
Failed to load properties.
</div>
);
}
return (
<>
{/* ========================================
PROPERTY TABLE
======================================== */}
<div className="w-full rounded-xl border border-gray-300 bg-white shadow-sm">
{/* HEADER */}
<div
className="flex flex-col gap-4
border-b border-gray-300
px-6 py-4
md:flex-row
md:items-center
md:justify-between"
>
<h2 className="text-xl font-semibold text-gray-900">
Properties
</h2>
{/* SEARCH */}
<div className="relative w-full md:w-80">
<FaSearch
className="absolute left-3 top-1/2
-translate-y-1/2
text-gray-500"
size={14}
/>
<input
type="text"
value={searchInput}
onChange={(e) =>
setSearchInput(
e.target.value
)
}
placeholder="Search properties..."
className="w-full rounded-lg
border border-gray-400
bg-white
py-2 pl-9 pr-3
text-gray-900
outline-none
placeholder:text-gray-500
focus:border-blue-500
focus:ring-2
focus:ring-blue-200"
/>
</div>
{/* STATUS FILTER */}
<select
value={statusFilter}
onChange={(e) => {
setStatusFilter(
e.target.value
);
setPageNumber(1);
}}
className="rounded-lg
border border-gray-400
bg-white
px-3 py-2
text-gray-800
outline-none
focus:border-blue-500
focus:ring-2
focus:ring-blue-200"
>
<option value="">
All Status
</option>
<option value="Available">
Available
</option>
<option value="Rented">
Rented
</option>
<option value="OnHold">
OnHold
</option>
</select>
</div>
{/* TABLE */}
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
{/* TABLE HEADER */}
<thead className="bg-gray-300 text-gray-900">
<tr>
<th className="whitespace-nowrap px-6 py-4 font-semibold">
Property Name
</th>
<th className="whitespace-nowrap px-6 py-4 font-semibold">
Monthly Rent
</th>
<th className="whitespace-nowrap px-6 py-4 font-semibold">
Location
</th>
<th className="whitespace-nowrap px-6 py-4 font-semibold">
Units Available
</th>
<th className="whitespace-nowrap px-6 py-4 font-semibold">
Application Deadline
</th>
<th className="whitespace-nowrap px-6 py-4 font-semibold">
Status
</th>
{isTenant && (
<th className="px-6 py-4 text-center font-semibold">
Apply
</th>
)}
{!isTenant && (
<th className="px-6 py-4 text-center font-semibold">
Action
</th>
)}
</tr>
</thead>
{/* TABLE BODY */}
<tbody className="divide-y divide-gray-300">
{properties.length === 0 ? (
<tr>
<td
colSpan={7}
className="px-6 py-12 text-center"
>
<p className="text-gray-700">
No properties found.
</p>
</td>
</tr>
) : (
properties.map(
(property) => (
<tr
key={property.id}
className="transition hover:bg-gray-100"
>
{/* PROPERTY NAME */}
<td className="px-6 py-4 font-medium text-gray-900">
{property.propertyTitle}
</td>
{/* RENT */}
<td className="px-6 py-4 font-medium text-gray-800">
₹
{property.monthlyRent.toLocaleString(
"en-IN"
)}
</td>
{/* LOCATION */}
<td className="px-6 py-4 text-gray-700">
{property.location}
</td>
{/* UNITS */}
<td className="px-6 py-4 text-gray-700">
{property.unitsAvailable}
</td>
{/* DEADLINE */}
<td className="px-6 py-4 text-gray-700">
{new Date(
property.applicationDeadline
).toLocaleDateString(
"en-IN"
)}
</td>
{/* STATUS */}
<td className="px-6 py-4">
<span
className={`inline-flex
rounded-full
px-3 py-1
text-xs
font-semibold
${getStatusStyle(
property.status
)}`}
>
{property.status}
</span>
</td>
{/* TENANT */}
{isTenant && (
<td className="px-6 py-4 text-center">
<button
type="button"
onClick={() =>
handleApply(
property.id
)
}
className="rounded-lg
bg-blue-600
px-4 py-2
font-medium
text-white
transition
hover:bg-blue-700"
>
Apply
</button>
</td>
)}
{/* ADMIN / MANAGER */}
{!isTenant && (
<td className="px-6 py-4">
<div
className="flex
items-center
justify-center
gap-4"
>
{/* EDIT */}
<button
type="button"
title="Edit Property"
onClick={() =>
handleEdit(
property.id
)
}
className="text-blue-600
transition
hover:text-blue-800"
>
<FaEdit
size={18}
/>
</button>
{/* DELETE */}
<button
type="button"
title="Delete Property"
onClick={() =>
handleDeleteClick(
property.id
)
}
className="text-red-600
transition
hover:text-red-800"
>
<FaTrash
size={18}
/>
</button>
</div>
</td>
)}
</tr>
)
)
)}
</tbody>
</table>
</div>
{/* PAGINATION */}
<div
className="flex flex-col gap-3
border-t border-gray-300
px-6 py-4
sm:flex-row
sm:items-center
sm:justify-between"
>
<p className="text-sm text-gray-600">
Page{" "}
<span className="font-semibold text-gray-900">
{pageNumber}
</span>{" "}
of{" "}
<span className="font-semibold text-gray-900">
{totalPages || 1}
</span>
</p>
<div className="flex gap-2">
<button
type="button"
disabled={
pageNumber === 1 ||
isFetching
}
onClick={() =>
setPageNumber(
(prev) => prev - 1
)
}
className="rounded-lg
border border-gray-400
bg-white
px-4 py-2
text-sm
font-medium
text-gray-700
transition
hover:bg-gray-100
disabled:cursor-not-allowed
disabled:opacity-50"
>
Previous
</button>
<button
type="button"
disabled={
pageNumber >=
totalPages ||
isFetching
}
onClick={() =>
setPageNumber(
(prev) => prev + 1
)
}
className="rounded-lg
border border-gray-400
bg-white
px-4 py-2
text-sm
font-medium
text-gray-700
transition
hover:bg-gray-100
disabled:cursor-not-allowed
disabled:opacity-50"
>
Next
</button>
</div>
</div>
</div>
{/* ========================================
DELETE CONFIRMATION MODAL
======================================== */}
{deletePropertyId !== null && (
<div
className="fixed inset-0 z-50
flex items-center
justify-center
bg-black/50
px-4"
>
<div
className="w-full max-w-md
rounded-xl
bg-white
p-6
shadow-2xl"
>
{/* MODAL HEADER */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div
className="flex h-10 w-10
items-center
justify-center
rounded-full
bg-red-100"
>
<FaExclamationTriangle
className="text-red-600"
size={18}
/>
</div>
<h3 className="text-lg font-semibold text-gray-900">
Delete Property
</h3>
</div>
{/* CLOSE */}
<button
type="button"
disabled={isDeleting}
onClick={
handleCancelDelete
}
className="text-gray-500
transition
hover:text-gray-800
disabled:opacity-50"
>
<FaTimes size={18} />
</button>
</div>
{/* MESSAGE */}
<div className="mt-5">
<p className="text-sm leading-6 text-gray-600">
Are you sure you want to delete
this property?
</p>
<p className="mt-2 text-sm font-medium text-red-600">
This action cannot be undone.
</p>
</div>
{/* BUTTONS */}
<div
className="mt-6 flex
justify-end
gap-3"
>
{/* CANCEL */}
<button
type="button"
disabled={isDeleting}
onClick={
handleCancelDelete
}
className="rounded-lg
border border-gray-400
bg-white
px-4 py-2
text-sm
font-medium
text-gray-700
transition
hover:bg-gray-100
disabled:cursor-not-allowed
disabled:opacity-50"
>
Cancel
</button>
{/* CONFIRM DELETE */}
<button
type="button"
disabled={isDeleting}
onClick={
handleConfirmDelete
}
className="rounded-lg
bg-red-600
px-4 py-2
text-sm
font-medium
text-white
transition
hover:bg-red-700
disabled:cursor-not-allowed
disabled:opacity-50"
>
{isDeleting
? "Deleting..."
: "Delete"}
</button>
</div>
</div>
</div>
)}
</>
);
};
export default PropertyTable;```
Top comments (0)