Menu

Change language

GitHub repo

Payroll Run Summary

blockfinance

Gusto/Rippling style bi-weekly company payroll execution preview and tax reconciliation: cycle header with approval badge and submission controls, 4 primary payroll metric cards (company cost, net pay, employee withholdings, employer taxes), direct deposit ACH funding timeline, and itemized employee payroll table.

Installation

$npx uipkge-ng@latest add payroll-run-summary
Manifest:https://uipkge.dev/r/angular/payroll-run-summary.jsonInstalls to:src/app/components/blocks/payroll-run-summary/

Variants

Loading interactive previews…

Schema

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

EmployeeRecord
interface EmployeeRecord {
  id: string
  name: string
  role: string
  department: string
  payType: 'Salary' | 'Hourly'
  hours: string
  grossPay: string
  deductions: string
  deductionsDetail: string
  netPay: string
  paymentMethod: string
  bankDetail: string
}
TimelineStep
interface TimelineStep {
  step: number
  title: string
  date: string
  time: string
  description: string
  status: string
  statusClass: string
  icon: unknown
  footer: string
}
SummaryCard
interface SummaryCard {
  label: string
  value: string
  badge: string
  description: string
}

Files installed (2)

  • src/app/components/blocks/payroll-run-summary/payroll-run-summary.component.ts17.1 kB
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import {
      Building2,
      Calendar,
      CheckCircle2,
      Clock,
      CreditCard,
      DollarSign,
      Download,
      Landmark,
      LucideAngularModule,
      Receipt,
      Send,
      ShieldCheck,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export interface EmployeeRecord {
      id: string
      name: string
      role: string
      department: string
      payType: 'Salary' | 'Hourly'
      hours: string
      grossPay: string
      deductions: string
      deductionsDetail: string
      netPay: string
      paymentMethod: string
      bankDetail: string
    }
    
    interface TimelineStep {
      step: number
      title: string
      date: string
      time: string
      description: string
      status: string
      statusClass: string
      icon: unknown
      footer: string
    }
    
    interface SummaryCard {
      label: string
      value: string
      badge: string
      description: string
    }
    
    const timelineSteps: TimelineStep[] = [
      {
        step: 1,
        title: 'ACH Debit Date',
        date: 'Wednesday, Aug 19, 2026',
        time: '5:00 PM EST',
        description: 'Company Operating Account (•••• 9021) debited for payroll & tax liability.',
        status: 'Completed',
        statusClass: 'bg-success/10 text-success border-success/20',
        icon: CheckCircle2,
        footer: 'Verified',
      },
      {
        step: 2,
        title: 'Bank Processing',
        date: 'Thursday, Aug 20, 2026',
        time: 'Automated Clearing',
        description: 'Federal Reserve NACHA batch transmission, ACH clearing, and tax escrow lock.',
        status: 'In Transit',
        statusClass: 'bg-info/10 text-info border-info/20',
        icon: Landmark,
        footer: 'Processing',
      },
      {
        step: 3,
        title: 'Employee Payday',
        date: 'Friday, Aug 21, 2026',
        time: '12:01 AM Local',
        description: 'Net funds settled across 42 employee accounts and electronic paystubs released.',
        status: 'Target Payday',
        statusClass: 'bg-success/10 text-success border-success/20',
        icon: DollarSign,
        footer: 'Scheduled',
      },
    ]
    
    const summaryCards: SummaryCard[] = [
      {
        label: 'Total Company Cost',
        value: '$184,250.00',
        badge: 'Cycle #16',
        description: 'Gross pay ($169,670.00) + Employer taxes ($14,580.00)',
      },
      {
        label: 'Net Direct Deposit Pay',
        value: '$138,420.00',
        badge: '42 Employees',
        description: 'Disbursed to 42 employees via automated 100% direct deposit',
      },
      {
        label: 'Employee Withholdings',
        value: '$31,250.00',
        badge: 'Federal & State Tax',
        description: '$31,250.00 · Federal & State Tax, FICA & benefit deductions',
      },
      {
        label: 'Employer Payroll Taxes',
        value: '$14,580.00',
        badge: 'FICA & FUTA',
        description: '$14,580.00 · FICA & FUTA, SUTA employer contributions',
      },
    ]
    
    const employees: EmployeeRecord[] = [
      {
        id: 'emp-1',
        name: 'Elena Rostova',
        role: 'Principal Engineer',
        department: 'Engineering',
        payType: 'Salary',
        hours: '80.0 hrs',
        grossPay: '$8,750.00',
        deductions: '-$2,140.00',
        deductionsDetail: 'Pre-tax 401k & Health',
        netPay: '$6,610.00',
        paymentMethod: 'Direct Deposit',
        bankDetail: 'Chase •••• 4892',
      },
      {
        id: 'emp-2',
        name: 'Marcus Vance',
        role: 'Staff Architect',
        department: 'Engineering',
        payType: 'Salary',
        hours: '80.0 hrs',
        grossPay: '$8,125.00',
        deductions: '-$1,980.00',
        deductionsDetail: 'Pre-tax 401k & HSA',
        netPay: '$6,145.00',
        paymentMethod: 'Direct Deposit',
        bankDetail: 'Wells Fargo •••• 3109',
      },
      {
        id: 'emp-3',
        name: 'David Chen',
        role: 'Senior Full-Stack',
        department: 'Engineering',
        payType: 'Salary',
        hours: '80.0 hrs',
        grossPay: '$6,875.00',
        deductions: '-$1,650.00',
        deductionsDetail: 'Pre-tax 401k & Dental',
        netPay: '$5,225.00',
        paymentMethod: 'Direct Deposit',
        bankDetail: 'Bank of America •••• 7741',
      },
      {
        id: 'emp-4',
        name: 'Sarah Jenkins',
        role: 'Frontend Specialist',
        department: 'Engineering',
        payType: 'Hourly',
        hours: '80.0 hrs',
        grossPay: '$5,492.50',
        deductions: '-$1,280.00',
        deductionsDetail: 'Medical & Vision',
        netPay: '$4,212.50',
        paymentMethod: 'Direct Deposit',
        bankDetail: 'Citibank •••• 9018',
      },
      {
        id: 'emp-5',
        name: 'Alex Rivera',
        role: 'Product Designer',
        department: 'Design',
        payType: 'Salary',
        hours: '80.0 hrs',
        grossPay: '$6,250.00',
        deductions: '-$1,490.00',
        deductionsDetail: 'Pre-tax 401k & Health',
        netPay: '$4,760.00',
        paymentMethod: 'Direct Deposit',
        bankDetail: 'SVB •••• 6523',
      },
    ]
    
    function initials(name: string): string {
      return name
        .split(' ')
        .map((part) => part[0])
        .slice(0, 2)
        .join('')
        .toUpperCase()
    }
    
    /**
     * PayrollRunSummary -- the React block, 1:1: cycle header with approval badge and submit
     * controls, four payroll metric cards, a direct-deposit ACH funding timeline, and an
     * itemized employee compensation table with avatar, pay-type badge, deductions and net pay.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-payroll-run-summary, [ui-payroll-run-summary]',
      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,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiTableComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="payroll-run-summary" [class]="rootClass">
          <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div class="space-y-1.5">
              <div class="flex flex-wrap items-center gap-3">
                <h2 class="text-foreground text-2xl font-bold tracking-tight">Payroll Run Summary</h2>
                <span
                  ui-badge
                  wrap
                  variant="outline"
                  class="border-success/20 bg-success/10 text-success gap-1.5 font-medium"
                >
                  <span class="bg-success size-1.5 rounded-full" aria-hidden="true"></span>
                  Approved & Ready to Submit
                </span>
              </div>
              <p class="text-muted-foreground flex flex-wrap items-center gap-2 text-sm">
                <span class="inline-flex items-center gap-1.5">
                  <lucide-icon [img]="Calendar" class="size-4" />
                  Pay Period: <strong class="text-foreground font-medium">Aug 01, 2026 – Aug 15, 2026</strong>
                </span>
                <span> Pay Date: <strong class="text-foreground font-medium">Aug 21</strong> </span>
              </p>
            </div>
    
            <div class="flex flex-wrap items-center gap-2.5">
              <button ui-button aria-label="Download attachment" variant="outline">
                <lucide-icon [img]="Download" class="size-4" />
                Export Payroll CSV
              </button>
              <button ui-button>
                <lucide-icon [img]="Send" class="size-4" />
                Submit Payroll ($184,250.00)
              </button>
            </div>
          </div>
    
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            @for (card of summaryCards; track card.label) {
              <div ui-card class="relative overflow-hidden shadow-xs">
                <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                  <h3 ui-card-title class="text-muted-foreground text-sm font-medium">{{ card.label }}</h3>
                  <span ui-badge wrap variant="secondary" class="text-xs font-normal">{{ card.badge }}</span>
                </div>
                <div ui-card-content class="space-y-1">
                  <div class="text-foreground text-3xl font-bold tracking-tight tabular-nums">{{ card.value }}</div>
                  <p class="text-muted-foreground text-xs leading-relaxed">{{ card.description }}</p>
                </div>
              </div>
            }
          </div>
    
          <div ui-card class="border-border shadow-xs">
            <div ui-card-header class="border-b pb-3">
              <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                <div class="flex items-center gap-2">
                  <lucide-icon [img]="Clock" class="text-muted-foreground size-4" />
                  <h3 ui-card-title class="text-sm font-semibold tracking-tight">Direct Deposit Funding Timeline</h3>
                </div>
                <span ui-badge wrap variant="outline" class="text-muted-foreground w-fit text-xs font-normal">
                  ACH 2-Day Settlement Window
                </span>
              </div>
            </div>
            <div ui-card-content class="pt-4">
              <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
                @for (item of timelineSteps; track item.step; let idx = $index) {
                  <div class="bg-card relative flex flex-col justify-between rounded-lg border p-4 shadow-xs">
                    <div class="space-y-2">
                      <div class="flex flex-wrap items-center justify-between gap-2">
                        <div class="flex min-w-0 items-center gap-2">
                          <div
                            class="bg-muted text-foreground flex size-7 items-center justify-center rounded-full text-xs font-semibold"
                          >
                            {{ item.step }}
                          </div>
                          <span class="text-foreground text-xs font-semibold tracking-tight">{{ item.title }}</span>
                        </div>
                        <span ui-badge [class]="timelineStatusClass(item)">{{ item.status }}</span>
                      </div>
                      <div class="pt-1">
                        <p class="text-foreground text-xs font-medium">{{ item.date }}</p>
                        <p class="text-muted-foreground text-xs">{{ item.time }}</p>
                      </div>
                      <p class="text-muted-foreground text-xs leading-relaxed">{{ item.description }}</p>
                    </div>
                    <div class="text-muted-foreground mt-3 flex items-center justify-between border-t pt-2 text-xs">
                      <span class="inline-flex items-center gap-1 font-medium">
                        <lucide-icon [img]="asIcon(item.icon)" class="text-foreground/70 size-3.5" />
                        {{ item.footer }}
                      </span>
                      @if (idx < timelineSteps.length - 1) {
                        <span class="text-muted-foreground/50 hidden md:inline-flex">Next: Step {{ idx + 2 }}</span>
                      }
                    </div>
                  </div>
                }
              </div>
            </div>
          </div>
    
          <div class="bg-card overflow-hidden rounded-lg border shadow-xs">
            <div class="flex flex-col gap-2 border-b p-4 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <h3 class="text-foreground text-base font-semibold tracking-tight">Employee Compensation Breakdown</h3>
                <p class="text-muted-foreground mt-0.5 text-xs">
                  5 of 42 employees shown in active batch · 80.0 standard cycle base hours
                </p>
              </div>
              <div class="flex items-center gap-2">
                <span ui-badge wrap variant="secondary" class="text-xs font-medium">42 Total Employees</span>
                <span ui-badge wrap variant="outline" class="border-success/20 text-success text-xs font-medium">
                  Approved & Ready to Submit
                </span>
              </div>
            </div>
    
            <div class="overflow-x-auto">
              <table ui-table>
                <thead ui-table-header>
                  <tr ui-table-row class="bg-muted/40 hover:bg-muted/40">
                    <th ui-table-head class="min-w-[220px]">Employee</th>
                    <th ui-table-head>Pay Type</th>
                    <th ui-table-head class="text-right">Hours</th>
                    <th ui-table-head class="text-right">Gross Pay</th>
                    <th ui-table-head class="min-w-[180px] text-right">Total Deductions</th>
                    <th ui-table-head class="text-right">Net Pay</th>
                    <th ui-table-head class="min-w-[220px]">Payment Method</th>
                  </tr>
                </thead>
                <tbody ui-table-body>
                  @for (emp of employees; track emp.id) {
                    <tr ui-table-row class="hover:bg-muted/30">
                      <td ui-table-cell>
                        <div class="flex items-center gap-3">
                          <ui-avatar class="size-8 border">
                            <ui-avatar-fallback class="text-xs font-medium">{{ initials(emp.name) }}</ui-avatar-fallback>
                          </ui-avatar>
                          <div class="flex min-w-0 flex-col">
                            <div class="flex items-center gap-2">
                              <span class="text-foreground truncate text-sm font-semibold">{{ emp.name }}</span>
                              <span ui-badge wrap variant="outline" class="px-1.5 py-0 text-xs font-normal">
                                {{ emp.department }}
                              </span>
                            </div>
                            <span class="text-muted-foreground truncate text-xs">{{ emp.role }}</span>
                          </div>
                        </div>
                      </td>
                      <td ui-table-cell>
                        <span
                          ui-badge
                          wrap
                          [variant]="emp.payType === 'Salary' ? 'outline' : 'secondary'"
                          class="text-xs font-medium"
                        >
                          {{ emp.payType }}
                        </span>
                      </td>
                      <td ui-table-cell class="text-foreground text-right font-mono text-xs tabular-nums">
                        {{ emp.hours }}
                      </td>
                      <td ui-table-cell class="text-foreground text-right font-medium tabular-nums">{{ emp.grossPay }}</td>
                      <td ui-table-cell class="text-right">
                        <div class="flex flex-col items-end">
                          <span class="text-destructive text-sm font-medium tabular-nums">{{ emp.deductions }}</span>
                          <span class="text-muted-foreground text-xs">{{ emp.deductionsDetail }}</span>
                        </div>
                      </td>
                      <td ui-table-cell class="text-right">
                        <span class="text-success text-sm font-bold tabular-nums">{{ emp.netPay }}</span>
                      </td>
                      <td ui-table-cell>
                        <div class="flex items-center gap-2">
                          <lucide-icon [img]="Landmark" class="text-muted-foreground size-4 shrink-0" />
                          <div class="flex min-w-0 flex-col">
                            <span class="text-foreground truncate text-xs font-medium">{{ emp.paymentMethod }}</span>
                            <span class="text-muted-foreground truncate text-xs">{{ emp.bankDetail }}</span>
                          </div>
                        </div>
                      </td>
                    </tr>
                  }
                </tbody>
              </table>
            </div>
    
            <div class="bg-muted/20 flex flex-col items-center justify-between gap-3 border-t p-4 sm:flex-row">
              <p class="text-muted-foreground text-xs">
                Showing 5 of 42 employees · Reconciliation verified against IRS Form 941 schedules
              </p>
              <div class="flex items-center gap-2">
                <button ui-button variant="outline" size="sm" disabled>Previous</button>
                <button ui-button variant="outline" size="sm">Next</button>
              </div>
            </div>
          </div>
    
          <div class="hidden items-center gap-2">
            <lucide-icon [img]="Building2" />
            <lucide-icon [img]="CreditCard" />
            <lucide-icon [img]="Receipt" />
            <lucide-icon [img]="ShieldCheck" />
          </div>
        </div>
      `,
    })
    export class UiPayrollRunSummaryComponent {
      protected readonly Calendar = Calendar
      protected readonly Clock = Clock
      protected readonly Download = Download
      protected readonly Landmark = Landmark
      protected readonly Send = Send
      protected readonly Building2 = Building2
      protected readonly CreditCard = CreditCard
      protected readonly Receipt = Receipt
      protected readonly ShieldCheck = ShieldCheck
    
      @Input('class') className?: string
    
      protected readonly summaryCards = summaryCards
      protected readonly timelineSteps = timelineSteps
      protected readonly employees = employees
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      initials(name: string): string {
        return initials(name)
      }
    
      asIcon(icon: unknown): never {
        return icon as never
      }
    
      timelineStatusClass(item: TimelineStep): string {
        return cn('text-xs font-medium capitalize', item.statusClass)
      }
    }
    
  • src/app/components/blocks/payroll-run-summary/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/payroll-run-summary.json