Menu

Change language

GitHub repo

Tenant Rent Ledger

blockreal-estate

Property management resident portal with tenant header, 4 financial overview cards (monthly rent, next due date, autopay status, escrow deposit), autopay toggle with date schedule selection, and a complete 6-record payment ledger with receipt actions.

Installation

$npx uipkge-ng@latest add tenant-rent-ledger
Manifest:https://uipkge.dev/r/angular/tenant-rent-ledger.jsonInstalls to:src/app/components/blocks/tenant-rent-ledger/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

PaymentRecord
interface PaymentRecord {
  id: string
  period: string
  description: string
  dueDate: string
  paidDate: string
  method: string
  methodType: 'ach' | 'card' | 'wire'
  amount: string
  status: 'paid' | 'waived'
  statusLabel: string
}
FinancialCard
interface FinancialCard {
  title: string
  value: string
  unit: string
  description: string
  icon: typeof Home
  iconColor: string
}

Files installed (2)

  • src/app/components/blocks/tenant-rent-ledger/tenant-rent-ledger.component.ts20.5 kB
    // Property management resident portal with tenant header, 4 financial
    // overview cards (monthly rent, next due date, autopay status, escrow deposit),
    // autopay toggle with date schedule selection, and a complete 6-record payment
    // ledger with receipt actions. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import {
      Building2,
      Calendar,
      CheckCircle2,
      Clock,
      CreditCard,
      Download,
      Home,
      Landmark,
      LucideAngularModule,
      ShieldCheck,
      Zap,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import { UiSwitchComponent } from '@/ui/switch/switch.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export interface PaymentRecord {
      id: string
      period: string
      description: string
      dueDate: string
      paidDate: string
      method: string
      methodType: 'ach' | 'card' | 'wire'
      amount: string
      status: 'paid' | 'waived'
      statusLabel: string
    }
    
    interface FinancialCard {
      title: string
      value: string
      unit: string
      description: string
      icon: typeof Home
      iconColor: string
    }
    
    const financialCards: FinancialCard[] = [
      {
        title: 'Monthly Base Rent',
        value: '$3,450.00',
        unit: '/ mo',
        description: 'Fixed lease rate through Jul 2027',
        icon: Home,
        iconColor: 'bg-primary/10 text-primary',
      },
      {
        title: 'Next Due Date',
        value: 'Sep 01, 2026',
        unit: '',
        description: 'Due in 11 days · Grace period to Sep 5',
        icon: Calendar,
        iconColor: 'bg-info/10 text-info',
      },
      {
        title: 'Autopay Status',
        value: 'Active',
        unit: '',
        description: 'Scheduled for 1st of month with Visa •••• 4892',
        icon: Zap,
        iconColor: 'bg-success/10 text-success',
      },
      {
        title: 'Security Deposit on File',
        value: '$3,450.00',
        unit: '',
        description: 'Held in Escrow (Interest Bearing)',
        icon: ShieldCheck,
        iconColor: 'bg-warning/10 text-warning',
      },
    ]
    
    const paymentRecords: PaymentRecord[] = [
      {
        id: 'pay-2026-08',
        period: 'August 2026 Rent',
        description: 'Base Rent ($3,400.00) + High-Speed Fiber ($50.00)',
        dueDate: 'Aug 01, 2026',
        paidDate: 'Aug 01, 2026',
        method: 'ACH Transfer · Chase •••• 4892',
        methodType: 'ach',
        amount: '$3,450.00',
        status: 'paid',
        statusLabel: 'Paid on Time',
      },
      {
        id: 'pay-2026-07',
        period: 'July 2026 Rent',
        description: 'Base Rent ($3,400.00) + Assigned Parking Spot #4B ($50.00)',
        dueDate: 'Jul 01, 2026',
        paidDate: 'Jul 01, 2026',
        method: 'ACH Transfer · Chase •••• 4892',
        methodType: 'ach',
        amount: '$3,450.00',
        status: 'paid',
        statusLabel: 'Paid on Time',
      },
      {
        id: 'pay-2026-06',
        period: 'June 2026 Rent',
        description: 'Base Rent ($3,400.00) + Water & Sewer Allocation ($50.00)',
        dueDate: 'Jun 01, 2026',
        paidDate: 'Jun 03, 2026',
        method: 'Visa •••• 4892',
        methodType: 'card',
        amount: '$3,450.00',
        status: 'waived',
        statusLabel: 'Late Fee Waived',
      },
      {
        id: 'pay-2026-05',
        period: 'May 2026 Rent',
        description: 'Base Rent ($3,400.00) + Storage Locker #12 ($50.00)',
        dueDate: 'May 01, 2026',
        paidDate: 'May 01, 2026',
        method: 'ACH Transfer · Chase •••• 4892',
        methodType: 'ach',
        amount: '$3,450.00',
        status: 'paid',
        statusLabel: 'Paid on Time',
      },
      {
        id: 'pay-2026-04',
        period: 'April 2026 Rent',
        description: 'First Full Month Base Rent ($3,400.00) + Pet Rent ($50.00)',
        dueDate: 'Apr 01, 2026',
        paidDate: 'Apr 01, 2026',
        method: 'ACH Transfer · Chase •••• 4892',
        methodType: 'ach',
        amount: '$3,450.00',
        status: 'paid',
        statusLabel: 'Paid on Time',
      },
      {
        id: 'pay-2026-03-dep',
        period: 'Security Deposit & Move-In Fee',
        description: '100% Refundable Security Escrow Deposit + Key Fob Activation',
        dueDate: 'Mar 15, 2026',
        paidDate: 'Mar 15, 2026',
        method: 'Wire Transfer · Wells Fargo •••• 1044',
        methodType: 'wire',
        amount: '$3,450.00',
        status: 'paid',
        statusLabel: 'Paid on Time',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-tenant-rent-ledger, [ui-tenant-rent-ledger]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiSeparatorComponent,
        UiSwitchComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="tenant-rent-ledger" [class]="rootClass">
          <!-- Resident & Unit Header -->
          <div
            class="bg-card flex flex-col gap-4 rounded-xl border p-5 shadow-xs sm:flex-row sm:items-center sm:justify-between"
          >
            <div class="space-y-1.5">
              <div class="flex flex-wrap items-center gap-2.5">
                <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">Elena Rostova</h2>
                <span ui-badge variant="secondary" class="text-xs font-semibold"> Unit 4B </span>
                <span ui-badge variant="outline" class="border-success/20 bg-success/10 text-success text-xs font-medium">
                  <lucide-icon [img]="CheckCircle2" class="text-success size-3" />
                  $0.00 Paid in Full
                </span>
              </div>
              <div class="text-muted-foreground flex items-center gap-1.5 text-xs sm:text-sm">
                <lucide-icon [img]="Building2" class="text-muted-foreground size-4 shrink-0" />
                <span>Pacific Heights Luxury Residences, San Francisco</span>
              </div>
            </div>
            <div class="flex items-center gap-3 self-start sm:self-auto">
              <button ui-button class="font-medium shadow-xs">
                <lucide-icon [img]="CreditCard" class="size-4" />
                Pay Rent Now
              </button>
            </div>
          </div>
    
          <!-- 4 Financial Overview Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            @for (card of financeCards; track card.title) {
              <div ui-card class="shadow-xs">
                <div ui-card-content class="space-y-2 p-5">
                  <div class="flex items-center justify-between gap-2">
                    <p class="text-muted-foreground text-xs font-medium">{{ card.title }}</p>
                    <div [class]="cardIconClass(card)">
                      <lucide-icon [img]="card.icon" class="size-4" />
                    </div>
                  </div>
                  <div>
                    <div class="flex items-baseline gap-1">
                      <p class="text-foreground text-2xl font-bold tracking-tight tabular-nums">{{ card.value }}</p>
                      @if (card.unit) {
                        <span class="text-muted-foreground text-xs font-normal">{{ card.unit }}</span>
                      }
                    </div>
                    <p class="text-muted-foreground mt-1 text-xs">{{ card.description }}</p>
                  </div>
                </div>
              </div>
            }
          </div>
    
          <!-- Autopay & Payment Method Card -->
          <div ui-card class="shadow-xs">
            <div ui-card-header>
              <div class="flex items-center justify-between">
                <div>
                  <h3 ui-card-title class="text-base font-semibold">Autopay &amp; Payment Preferences</h3>
                  <p ui-card-description class="text-xs">
                    Configure automatic monthly rent deduction and manage saved funding methods.
                  </p>
                </div>
                @if (autopayEnabled()) {
                  <span ui-badge variant="outline" class="border-success/20 bg-success/10 text-success text-xs font-medium">
                    Autopay Active
                  </span>
                } @else {
                  <span ui-badge variant="secondary" class="text-xs font-medium"> Autopay Paused </span>
                }
              </div>
            </div>
            <div ui-card-content class="space-y-5">
              <!-- Toggle Autopay Switch -->
              <div
                class="bg-muted/30 flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between"
              >
                <div class="space-y-0.5">
                  <label for="autopay-toggle" class="text-foreground cursor-pointer text-sm font-medium">
                    Enable Automatic Rent Deductions
                  </label>
                  <p class="text-muted-foreground text-xs">
                    Automatically charge your primary payment method on your selected schedule before due date.
                  </p>
                </div>
                <button
                  ui-switch
                  id="autopay-toggle"
                  [checked]="autopayEnabled()"
                  (checkedChange)="autopayEnabled.set($event)"
                ></button>
              </div>
    
              <div ui-separator></div>
    
              <!-- Primary Bank Account / Card -->
              <div class="space-y-3">
                <div class="flex items-center justify-between">
                  <h4 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                    Primary Payment Method
                  </h4>
                  <button ui-button variant="ghost" size="xs" class="text-primary hover:text-primary text-xs font-medium">
                    Change Method
                  </button>
                </div>
                <div class="flex flex-col gap-3 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between">
                  <div class="flex items-center gap-3.5">
                    <div
                      class="bg-background flex size-10 shrink-0 items-center justify-center rounded-lg border shadow-xs"
                    >
                      <lucide-icon [img]="CreditCard" class="text-foreground size-5" />
                    </div>
                    <div class="space-y-0.5">
                      <div class="flex items-center gap-2">
                        <span class="text-foreground text-sm font-semibold tabular-nums">Visa •••• 4892</span>
                        <span ui-badge variant="secondary" class="text-xs font-normal">Default</span>
                      </div>
                      <p class="text-muted-foreground text-xs">Elena Rostova · Expires 08/28 · Bank of America</p>
                    </div>
                  </div>
                  <div class="flex items-center gap-2">
                    <span ui-badge variant="outline" class="border-success/20 bg-success/10 text-success text-xs">
                      <lucide-icon [img]="CheckCircle2" class="text-success size-3" />
                      Verified for ACH &amp; Debit
                    </span>
                  </div>
                </div>
              </div>
    
              <div ui-separator></div>
    
              <!-- Autopay Date Selector (1st vs 5th) -->
              <div class="space-y-3">
                <div class="space-y-0.5">
                  <h4 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                    Scheduled Deduction Date
                  </h4>
                  <p class="text-muted-foreground text-xs">
                    Select when rent will be debited from your account each cycle.
                  </p>
                </div>
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                  <button
                    type="button"
                    [disabled]="!autopayEnabled()"
                    [class]="scheduleOptionClass('1st')"
                    (click)="autopayDay.set('1st')"
                  >
                    <div [class]="scheduleRadioClass('1st')">
                      @if (autopayDay() === '1st' && autopayEnabled()) {
                        <span class="bg-background size-1.5 rounded-full"></span>
                      }
                    </div>
                    <div class="space-y-1">
                      <div class="flex items-center gap-2">
                        <span class="text-foreground text-sm font-medium">1st of the month</span>
                        <span
                          ui-badge
                          variant="outline"
                          class="border-success/20 bg-success/10 text-success text-xs font-medium"
                        >
                          Recommended
                        </span>
                      </div>
                      <p class="text-muted-foreground text-xs">
                        Debited promptly on the rent due date. Ensures zero delinquency risk.
                      </p>
                    </div>
                  </button>
    
                  <button
                    type="button"
                    [disabled]="!autopayEnabled()"
                    [class]="scheduleOptionClass('5th')"
                    (click)="autopayDay.set('5th')"
                  >
                    <div [class]="scheduleRadioClass('5th')">
                      @if (autopayDay() === '5th' && autopayEnabled()) {
                        <span class="bg-background size-1.5 rounded-full"></span>
                      }
                    </div>
                    <div class="space-y-1">
                      <div class="flex items-center gap-2">
                        <span class="text-foreground text-sm font-medium">5th of the month</span>
                        <span ui-badge variant="secondary" class="text-xs font-normal"> Grace Period </span>
                      </div>
                      <p class="text-muted-foreground text-xs">
                        Processes within the standard 5-day building grace window before late fees.
                      </p>
                    </div>
                  </button>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Payment History Ledger Table -->
          <div class="space-y-3">
            <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <h3 class="text-foreground text-base font-semibold tracking-tight">Payment History &amp; Ledger</h3>
                <p class="text-muted-foreground text-xs">
                  Complete transaction record of monthly rent charges, utility allocations, and security deposits.
                </p>
              </div>
              <button
                ui-button
                aria-label="Download attachment"
                variant="outline"
                size="sm"
                class="self-start sm:self-auto"
              >
                <lucide-icon [img]="Download" class="size-4" />
                Download Statement (PDF)
              </button>
            </div>
    
            <div class="bg-card overflow-x-auto rounded-lg border shadow-xs">
              <table ui-table>
                <thead ui-table-header>
                  <tr ui-table-row>
                    <th ui-table-head>Billing Period &amp; Description</th>
                    <th ui-table-head>Due Date</th>
                    <th ui-table-head>Paid Date</th>
                    <th ui-table-head>Payment Method</th>
                    <th ui-table-head class="text-right">Amount Paid</th>
                    <th ui-table-head>Status</th>
                    <th ui-table-head class="text-right"><span class="sr-only">Actions</span></th>
                  </tr>
                </thead>
                <tbody ui-table-body>
                  @for (record of records; track record.id) {
                    <tr ui-table-row class="hover:bg-muted/50">
                      <td ui-table-cell>
                        <div class="text-foreground text-sm font-medium">{{ record.period }}</div>
                        <div class="text-muted-foreground text-xs">{{ record.description }}</div>
                      </td>
                      <td ui-table-cell class="text-muted-foreground text-sm tabular-nums">{{ record.dueDate }}</td>
                      <td ui-table-cell class="text-foreground text-sm font-medium tabular-nums">{{ record.paidDate }}</td>
                      <td ui-table-cell>
                        <div class="text-foreground flex items-center gap-1.5 text-xs sm:text-sm">
                          @if (record.methodType === 'ach') {
                            <lucide-icon [img]="Landmark" class="text-muted-foreground size-3.5 shrink-0" />
                          } @else if (record.methodType === 'card') {
                            <lucide-icon [img]="CreditCard" class="text-muted-foreground size-3.5 shrink-0" />
                          } @else {
                            <lucide-icon [img]="ShieldCheck" class="text-muted-foreground size-3.5 shrink-0" />
                          }
                          <span class="truncate">{{ record.method }}</span>
                        </div>
                      </td>
                      <td ui-table-cell class="text-foreground text-right text-sm font-bold tabular-nums">
                        {{ record.amount }}
                      </td>
                      <td ui-table-cell>
                        @if (record.status === 'paid') {
                          <span
                            ui-badge
                            variant="outline"
                            class="border-success/20 bg-success/10 text-success text-xs font-medium whitespace-normal"
                          >
                            <lucide-icon [img]="CheckCircle2" class="text-success size-3" />
                            {{ record.statusLabel }}
                          </span>
                        } @else {
                          <span ui-badge variant="outline" class="border-info/20 bg-info/10 text-info text-xs font-medium">
                            <lucide-icon [img]="Clock" class="text-info size-3" />
                            {{ record.statusLabel }}
                          </span>
                        }
                      </td>
                      <td ui-table-cell class="text-right">
                        <button
                          ui-button
                          variant="ghost"
                          size="sm"
                          class="text-muted-foreground hover:text-foreground h-8 gap-1.5 text-xs"
                          aria-label="Download receipt PDF"
                        >
                          <lucide-icon [img]="Download" class="size-3.5" />
                          <span>Receipt</span>
                        </button>
                      </td>
                    </tr>
                  }
                </tbody>
              </table>
            </div>
    
            <div
              class="text-muted-foreground flex flex-col gap-3 pt-1 text-xs sm:flex-row sm:items-center sm:justify-between"
            >
              <p>
                Showing <span class="text-foreground font-medium">6</span> of
                <span class="text-foreground font-medium">6</span> lease payments · Lease Ref:
                <span class="font-mono">PH-4B-2026</span>
              </p>
              <div class="flex items-center gap-2">
                <button ui-button variant="outline" size="xs" disabled>Previous</button>
                <button ui-button variant="outline" size="xs" disabled>Next</button>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiTenantRentLedgerComponent {
      protected readonly Building2 = Building2
      protected readonly Calendar = Calendar
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly CreditCard = CreditCard
      protected readonly Download = Download
      protected readonly Home = Home
      protected readonly Landmark = Landmark
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Zap = Zap
    
      protected readonly financeCards = financialCards
      protected readonly records = paymentRecords
    
      @Input('class') className?: string
    
      readonly autopayEnabled = signal(true)
      readonly autopayDay = signal<'1st' | '5th'>('1st')
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      cardIconClass(card: FinancialCard): string {
        return cn('flex size-8 shrink-0 items-center justify-center rounded-lg', card.iconColor)
      }
    
      scheduleOptionClass(day: '1st' | '5th'): string {
        return cn(
          'focus-visible:ring-ring flex cursor-pointer items-start gap-3 rounded-lg border p-3.5 text-left transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
          this.autopayDay() === day && this.autopayEnabled()
            ? 'border-primary bg-primary/5 ring-primary ring-1'
            : 'border-border hover:bg-muted/50',
        )
      }
    
      scheduleRadioClass(day: '1st' | '5th'): string {
        return cn(
          'mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border transition-colors',
          this.autopayDay() === day && this.autopayEnabled()
            ? 'border-primary bg-primary text-primary-foreground'
            : 'border-muted-foreground/50',
        )
      }
    }
    
  • src/app/components/blocks/tenant-rent-ledger/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/tenant-rent-ledger.json