710 lines
28 KiB
TypeScript
710 lines
28 KiB
TypeScript
// routes/landlord/properties/[id]/index.tsx
|
|
// Property details page with complete payment history and expenses
|
|
|
|
import { define } from "../../../../utils.ts";
|
|
import { page } from "fresh";
|
|
import { getPropertyById } from "../../../../services/propertyService.ts";
|
|
import { getLeasesByProperty } from "../../../../services/leaseService.ts";
|
|
import { getUserById } from "../../../../services/userService.ts";
|
|
import {
|
|
getExpensesByProperty,
|
|
type PropertyExpense,
|
|
} from "../../../../services/expenseService.ts";
|
|
import {
|
|
getRentHistory,
|
|
type RentChange,
|
|
} from "../../../../services/rentChangeService.ts";
|
|
import { db } from "../../../../db/db.ts";
|
|
import { leases, payment_transactions } from "../../../../db/schema.ts";
|
|
import { eq } from "drizzle-orm";
|
|
import type { Lease, Property, User } from "../../../../db/interfaces.ts";
|
|
|
|
interface PropertyDetailsData {
|
|
property: Property | null;
|
|
leases: Lease[];
|
|
payments: Array<{
|
|
id: string;
|
|
amount: number | null;
|
|
payment_date: number | null;
|
|
payment_method: string | null;
|
|
transaction_status: string | null;
|
|
tenant_name?: string;
|
|
}>;
|
|
expenses: PropertyExpense[];
|
|
rentHistory: Array<{
|
|
effective_date: number;
|
|
monthly_rent: number;
|
|
previous_rent: number | null;
|
|
reason: string | null;
|
|
is_current: boolean;
|
|
}>;
|
|
error?: string;
|
|
}
|
|
|
|
function buildPropertyAddress(property: Property | null): string {
|
|
if (!property) return "";
|
|
return [
|
|
property.address_1,
|
|
property.address_2,
|
|
property.city,
|
|
property.state,
|
|
property.zip,
|
|
].filter(Boolean).join(", ");
|
|
}
|
|
|
|
export const handler = define.handlers({
|
|
async GET(ctx) {
|
|
const { id } = ctx.params;
|
|
|
|
try {
|
|
// Fetch property
|
|
const property = await getPropertyById(id);
|
|
|
|
if (!property) {
|
|
return page({
|
|
property: null,
|
|
leases: [],
|
|
payments: [],
|
|
expenses: [],
|
|
rentHistory: [],
|
|
error: "Property not found",
|
|
} as PropertyDetailsData);
|
|
}
|
|
|
|
// Fetch all leases for this property
|
|
const leases = await getLeasesByProperty(id);
|
|
|
|
// Fetch all payment transactions for these leases
|
|
const payments: PropertyDetailsData["payments"] = [];
|
|
|
|
for (const lease of leases) {
|
|
if (lease.id) {
|
|
const leasePayments = await db.select({
|
|
id: payment_transactions.id,
|
|
amount: payment_transactions.amount,
|
|
payment_date: payment_transactions.payment_date,
|
|
payment_method: payment_transactions.payment_method,
|
|
transaction_status: payment_transactions.transaction_status,
|
|
})
|
|
.from(payment_transactions)
|
|
.where(eq(payment_transactions.lease_id, lease.id));
|
|
|
|
// Fetch tenant name for the lease
|
|
let tenant_name = undefined;
|
|
if (lease.tenant_id) {
|
|
const tenant = await getUserById(lease.tenant_id);
|
|
tenant_name = tenant?.name || undefined;
|
|
}
|
|
|
|
for (const payment of leasePayments) {
|
|
payments.push({
|
|
...payment,
|
|
tenant_name,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort payments by date (newest first)
|
|
payments.sort((a, b) => (b.payment_date || 0) - (a.payment_date || 0));
|
|
|
|
// Fetch all expenses for this property
|
|
const expenses = await getExpensesByProperty(id);
|
|
|
|
// Fetch rent history
|
|
const rentHistory = await getRentHistory(id);
|
|
|
|
return page({
|
|
property,
|
|
leases,
|
|
payments,
|
|
expenses,
|
|
rentHistory,
|
|
} as PropertyDetailsData);
|
|
} catch (error) {
|
|
console.error("Error loading property details:", error);
|
|
return page({
|
|
property: null,
|
|
leases: [],
|
|
payments: [],
|
|
expenses: [],
|
|
rentHistory: [],
|
|
error: "Failed to load property details",
|
|
} as PropertyDetailsData);
|
|
}
|
|
},
|
|
});
|
|
|
|
export default define.page(
|
|
function PropertyDetailsPage({ data }: { data: PropertyDetailsData }) {
|
|
const { property, leases, payments, expenses, rentHistory, error } = data;
|
|
|
|
if (error) {
|
|
return (
|
|
<div class="dashboard-page landlord-route">
|
|
<div class="dashboard-error-page">
|
|
<div class="dashboard-error-inner">
|
|
<h1 class="dashboard-error-title">Error</h1>
|
|
<p class="dashboard-error-text">{error}</p>
|
|
<a href="/landlord/properties" class="dashboard-error-link">
|
|
Back to Properties
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!property) {
|
|
return (
|
|
<div class="dashboard-page landlord-route">
|
|
<div class="dashboard-error-page">
|
|
<div class="dashboard-error-inner">
|
|
<h1 class="dashboard-error-title">Property Not Found</h1>
|
|
<p class="dashboard-error-text">
|
|
The property you're looking for doesn't exist.
|
|
</p>
|
|
<a href="/landlord/properties" class="dashboard-error-link">
|
|
Back to Properties
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const propertyAddress = buildPropertyAddress(property);
|
|
const totalPayments = payments.reduce((sum, p) => sum + (p.amount || 0), 0);
|
|
const totalExpenses = expenses.reduce((sum, e) => sum + (e.amount || 0), 0);
|
|
const netIncome = totalPayments - totalExpenses;
|
|
|
|
// Calculate expenses by category
|
|
const expensesByCategory: Record<string, number> = {};
|
|
for (const expense of expenses) {
|
|
const category = expense.category || "uncategorized";
|
|
expensesByCategory[category] = (expensesByCategory[category] || 0) +
|
|
(expense.amount || 0);
|
|
}
|
|
|
|
return (
|
|
<div class="dashboard-page landlord-route">
|
|
{/* Header */}
|
|
<div class="dashboard-header">
|
|
<div class="dashboard-header-inner">
|
|
<div class="dashboard-header-row">
|
|
<div>
|
|
<h1 class="dashboard-header-title">Property Details</h1>
|
|
<p class="dashboard-header-subtitle">
|
|
{property.title || propertyAddress}
|
|
</p>
|
|
</div>
|
|
<div class="dashboard-header-actions">
|
|
<a
|
|
href="/landlord/properties"
|
|
class="dashboard-btn dashboard-btn--secondary"
|
|
>
|
|
Back to Properties
|
|
</a>
|
|
<a
|
|
href={`/landlord/properties/${property.id}/edit`}
|
|
class="dashboard-btn dashboard-btn--primary"
|
|
>
|
|
Edit Property
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-content">
|
|
{/* Property Info Card */}
|
|
<div class="dashboard-card dashboard-section-mb">
|
|
<div class="dashboard-card-body">
|
|
<h3 class="dashboard-section-title dashboard-section-title--mb">
|
|
Property Information
|
|
</h3>
|
|
<div class="dashboard-property-grid">
|
|
<div>
|
|
<dt class="dashboard-stat-label">Address</dt>
|
|
<dd class="dashboard-stat-value">
|
|
{propertyAddress || "N/A"}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="dashboard-stat-label">Monthly Rent</dt>
|
|
<dd class="dashboard-stat-value">
|
|
${property.monthly_rent?.toLocaleString() || "N/A"}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="dashboard-stat-label">Status</dt>
|
|
<dd>
|
|
<span
|
|
class={`dashboard-badge capitalize ${
|
|
property.rental_status === "occupied"
|
|
? "dashboard-badge--green"
|
|
: property.rental_status === "available"
|
|
? "dashboard-badge--blue"
|
|
: "dashboard-badge--yellow"
|
|
}`}
|
|
>
|
|
{property.rental_status || "unknown"}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="dashboard-stat-label">Bedrooms / Bathrooms</dt>
|
|
<dd class="dashboard-stat-value">
|
|
{property.bedrooms || 0}bd / {property.bathrooms || 0}ba
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="dashboard-stat-label">Square Footage</dt>
|
|
<dd class="dashboard-stat-value">
|
|
{property.square_footage?.toLocaleString() || "N/A"} sq ft
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="dashboard-stat-label">Property Type</dt>
|
|
<dd class="dashboard-stat-value capitalize">
|
|
{property.property_type || "N/A"}
|
|
</dd>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Financial Summary */}
|
|
<div class="dashboard-stats-grid dashboard-section-mb">
|
|
<div class="dashboard-card">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-stat-card">
|
|
<div class="dashboard-stat-icon-wrap">
|
|
<svg
|
|
class="dashboard-stat-icon dashboard-stat-icon--green"
|
|
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>
|
|
</div>
|
|
<div class="dashboard-stat-content">
|
|
<dl>
|
|
<dt class="dashboard-stat-label">
|
|
Total Payments Received
|
|
</dt>
|
|
<dd class="dashboard-stat-value">
|
|
${totalPayments.toLocaleString()}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-card">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-stat-card">
|
|
<div class="dashboard-stat-icon-wrap">
|
|
<svg
|
|
class="dashboard-stat-icon dashboard-stat-icon--red"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="dashboard-stat-content">
|
|
<dl>
|
|
<dt class="dashboard-stat-label">Total Expenses</dt>
|
|
<dd class="dashboard-stat-value">
|
|
${totalExpenses.toLocaleString()}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-card">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-stat-card">
|
|
<div class="dashboard-stat-icon-wrap">
|
|
<svg
|
|
class="dashboard-stat-icon dashboard-stat-icon--blue"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="dashboard-stat-content">
|
|
<dl>
|
|
<dt class="dashboard-stat-label">Net Income</dt>
|
|
<dd
|
|
class={`dashboard-stat-value ${
|
|
netIncome >= 0 ? "text-success" : "text-danger"
|
|
}`}
|
|
>
|
|
${netIncome.toLocaleString()}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-card">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-stat-card">
|
|
<div class="dashboard-stat-icon-wrap">
|
|
<svg
|
|
class="dashboard-stat-icon"
|
|
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>
|
|
</div>
|
|
<div class="dashboard-stat-content">
|
|
<dl>
|
|
<dt class="dashboard-stat-label">Active Leases</dt>
|
|
<dd class="dashboard-stat-value">
|
|
{leases.filter((l) => l.lease_status === "active")
|
|
.length}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Two column layout */}
|
|
<div class="dashboard-two-col">
|
|
{/* Payment History */}
|
|
<div class="dashboard-card dashboard-card--no-overflow dashboard-card--full-width">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-section-header">
|
|
<h3 class="dashboard-section-title">Payment History</h3>
|
|
<span class="dashboard-section-subtitle">
|
|
{payments.length} payments received
|
|
</span>
|
|
</div>
|
|
<div class="dashboard-table-wrap">
|
|
<table class="dashboard-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="dashboard-table-th">Date</th>
|
|
<th class="dashboard-table-th">Tenant</th>
|
|
<th class="dashboard-table-th">Method</th>
|
|
<th class="dashboard-table-th">Amount</th>
|
|
<th class="dashboard-table-th">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{payments && payments.length > 0
|
|
? (
|
|
payments.map((payment) => (
|
|
<tr key={payment.id}>
|
|
<td class="dashboard-table-td">
|
|
{payment.payment_date
|
|
? new Date(payment.payment_date)
|
|
.toLocaleDateString()
|
|
: "N/A"}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
{payment.tenant_name || "Unknown"}
|
|
</td>
|
|
<td class="dashboard-table-td capitalize">
|
|
{payment.payment_method || "N/A"}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
${payment.amount?.toLocaleString() || "0"}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
<span
|
|
class={`dashboard-badge ${
|
|
payment.transaction_status === "completed"
|
|
? "dashboard-badge--green"
|
|
: payment.transaction_status === "pending"
|
|
? "dashboard-badge--yellow"
|
|
: "dashboard-badge--red"
|
|
}`}
|
|
>
|
|
{payment.transaction_status || "unknown"}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)
|
|
: (
|
|
<tr>
|
|
<td colspan={5} class="dashboard-table-empty">
|
|
No payment history yet
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expenses by Category */}
|
|
{Object.keys(expensesByCategory).length > 0 && (
|
|
<div class="dashboard-card">
|
|
<div class="dashboard-card-body">
|
|
<h3 class="dashboard-section-title dashboard-section-title--mb">
|
|
Expenses by Category
|
|
</h3>
|
|
<div class="dashboard-list-spaced">
|
|
{Object.entries(expensesByCategory)
|
|
.sort(([, a], [, b]) => b - a)
|
|
.map(([category, amount]) => (
|
|
<div key={category} class="dashboard-row">
|
|
<div>
|
|
<p class="dashboard-row-label capitalize">
|
|
{category}
|
|
</p>
|
|
</div>
|
|
<span class="dashboard-row-value">
|
|
${amount.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Expense History */}
|
|
<div class="dashboard-card dashboard-card--no-overflow dashboard-card--full-width">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-section-header">
|
|
<h3 class="dashboard-section-title">Expense History</h3>
|
|
<a
|
|
href={`/landlord/properties/${property.id}/expenses/add`}
|
|
class="dashboard-section-link"
|
|
>
|
|
Add Expense
|
|
</a>
|
|
</div>
|
|
<div class="dashboard-table-wrap">
|
|
<table class="dashboard-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="dashboard-table-th">Date</th>
|
|
<th class="dashboard-table-th">Category</th>
|
|
<th class="dashboard-table-th">Description</th>
|
|
<th class="dashboard-table-th">Vendor</th>
|
|
<th class="dashboard-table-th">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{expenses && expenses.length > 0
|
|
? (
|
|
expenses.map((expense) => (
|
|
<tr key={expense.id}>
|
|
<td class="dashboard-table-td">
|
|
{expense.expense_date
|
|
? new Date(expense.expense_date)
|
|
.toLocaleDateString()
|
|
: "N/A"}
|
|
</td>
|
|
<td class="dashboard-table-td capitalize">
|
|
{expense.category || "N/A"}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
{expense.description || "-"}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
{expense.vendor || "-"}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
${expense.amount?.toLocaleString() || "0"}
|
|
</td>
|
|
</tr>
|
|
))
|
|
)
|
|
: (
|
|
<tr>
|
|
<td colspan={5} class="dashboard-table-empty">
|
|
No expenses recorded yet
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active Leases */}
|
|
<div class="dashboard-card dashboard-card--no-overflow dashboard-card--full-width">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-section-header">
|
|
<h3 class="dashboard-section-title">Active Leases</h3>
|
|
</div>
|
|
<div class="dashboard-list-spaced">
|
|
{leases && leases.length > 0
|
|
? (
|
|
leases
|
|
.filter((l) => l.lease_status === "active")
|
|
.map((lease) => (
|
|
<div key={lease.id} class="dashboard-row">
|
|
<div>
|
|
<p class="dashboard-row-label">
|
|
{lease.title ||
|
|
`Lease #${lease.id.slice(0, 8)}`}
|
|
</p>
|
|
<p class="dashboard-row-muted">
|
|
{lease.start_date
|
|
? new Date(lease.start_date)
|
|
.toLocaleDateString()
|
|
: ""} - {lease.end_date
|
|
? new Date(lease.end_date)
|
|
.toLocaleDateString()
|
|
: ""}
|
|
</p>
|
|
</div>
|
|
<span class="dashboard-row-value">
|
|
${lease.rent?.toLocaleString() || "0"}/mo
|
|
</span>
|
|
</div>
|
|
))
|
|
)
|
|
: (
|
|
<div class="dashboard-empty">
|
|
<p class="dashboard-empty-text">No active leases</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Rent History */}
|
|
<div class="dashboard-card dashboard-card--no-overflow dashboard-card--full-width">
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-section-header">
|
|
<h3 class="dashboard-section-title">Rent History</h3>
|
|
<a
|
|
href={`/landlord/properties/${property.id}/rent/change`}
|
|
class="dashboard-section-link"
|
|
>
|
|
Change Rent
|
|
</a>
|
|
</div>
|
|
<div class="dashboard-table-wrap">
|
|
<table class="dashboard-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="dashboard-table-th">Effective Date</th>
|
|
<th class="dashboard-table-th">Previous Rent</th>
|
|
<th class="dashboard-table-th">New Rent</th>
|
|
<th class="dashboard-table-th">Change</th>
|
|
<th class="dashboard-table-th">Reason</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rentHistory && rentHistory.length > 0
|
|
? (
|
|
rentHistory.map((entry, index) => {
|
|
const previousRent = entry.previous_rent || 0;
|
|
const newRent = entry.monthly_rent || 0;
|
|
const changeAmount = newRent - previousRent;
|
|
const isIncrease = changeAmount > 0;
|
|
const isDecrease = changeAmount < 0;
|
|
|
|
return (
|
|
<tr key={index}>
|
|
<td class="dashboard-table-td">
|
|
{entry.effective_date
|
|
? new Date(entry.effective_date)
|
|
.toLocaleDateString()
|
|
: "N/A"}
|
|
{entry.is_current && (
|
|
<span
|
|
class="dashboard-badge dashboard-badge--green"
|
|
style={{ marginLeft: "8px" }}
|
|
>
|
|
Current
|
|
</span>
|
|
)}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
${previousRent.toLocaleString()}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
${newRent.toLocaleString()}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
{previousRent > 0 && (
|
|
<span
|
|
class={isIncrease
|
|
? "text-success"
|
|
: isDecrease
|
|
? "text-danger"
|
|
: ""}
|
|
>
|
|
{isIncrease && "+"}
|
|
${changeAmount.toLocaleString()}
|
|
{changeAmount !== 0 && ` (${
|
|
Math.round(
|
|
(changeAmount / previousRent) * 100,
|
|
)
|
|
}%)`}
|
|
</span>
|
|
)}
|
|
{previousRent === 0 && <span>-</span>}
|
|
</td>
|
|
<td class="dashboard-table-td">
|
|
{entry.reason || "-"}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
)
|
|
: (
|
|
<tr>
|
|
<td colspan={5} class="dashboard-table-empty">
|
|
No rent history available
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
);
|