Menu

Change language

GitHub repo

Cash Flow Treasury Dashboard

blockfinance

Fluxo and Mercury style corporate cash flow treasury dashboard with 30-day runway projection, liquidity buffer monitor, inflow/outflow breakdown, and connected bank accounts table.

Installation

$npx uipkge-ng@latest add cash-flow-treasury-dashboard
Manifest:https://uipkge.dev/r/angular/cash-flow-treasury-dashboard.jsonInstalls to:src/app/components/blocks/cash-flow-treasury-dashboard/

Variants

Loading interactive previews…

Schema

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

HealthCard
interface HealthCard {
  title: string
  value: string
  highlight?: string
  badgeText: string
  badgeVariant: 'default' | 'secondary' | 'outline' | 'destructive'
  badgeClass?: string
  description: string
  metricFootnote: string
  progressValue: number
  // Carried for parity with the React/Vue data; the card chrome renders no icon.
  icon: unknown
}
FlowItem
interface FlowItem {
  id: string
  name: string
  amount: string
  rawAmount: number
  percentage: number
  category: string
  detail: string
  color: string
}
BankAccount
interface BankAccount {
  id: string
  name: string
  institution: string
  accountType: string
  accountMask: string
  routingMask: string
  purpose: string
  apy: string
  balance: string
  balanceRaw: number
  status: 'Active' | 'Yield Sweeping' | 'Ringfenced' | 'Reserve Locked'
  statusType: 'success' | 'info' | 'warning'
  isSweepAccount?: boolean
}

Files installed (2)

  • src/app/components/blocks/cash-flow-treasury-dashboard/cash-flow-treasury-dashboard.component.ts41.7 kB
    // Fluxo and Mercury style corporate cash flow treasury dashboard: 30-day runway
    // projection with a confidence corridor, liquidity buffer monitor, inflow/outflow
    // breakdown, and a connected bank accounts table. Port of the React block 1:1.
    import {
      ChangeDetectionStrategy,
      Component,
      DestroyRef,
      EventEmitter,
      Input,
      Output,
      inject,
      signal,
    } from '@angular/core'
    import {
      ArrowDownLeft,
      ArrowUpRight,
      Building2,
      CheckCircle2,
      Flame,
      Landmark,
      Lock,
      LucideAngularModule,
      RefreshCw,
      Send,
      Shield,
      ShieldCheck,
      TrendingUp,
      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 { UiProgressComponent } from '@/ui/progress/progress.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export type TreasuryTab = '30d' | '60d' | '90d'
    
    interface HealthCard {
      title: string
      value: string
      highlight?: string
      badgeText: string
      badgeVariant: 'default' | 'secondary' | 'outline' | 'destructive'
      badgeClass?: string
      description: string
      metricFootnote: string
      progressValue: number
      // Carried for parity with the React/Vue data; the card chrome renders no icon.
      icon: unknown
    }
    
    const healthCards: HealthCard[] = [
      {
        title: 'Total Cash Runway',
        value: '28.4 Months Runway',
        badgeText: 'Optimal Runway',
        badgeVariant: 'outline',
        badgeClass: 'bg-success/10 text-success border-success/20',
        description: 'At current net burn rate ($170.8k/mo)',
        metricFootnote: 'Zero dilution horizon through Q4 2028',
        progressValue: 94,
        icon: ShieldCheck,
      },
      {
        title: 'Net Monthly Burn',
        value: '-$170,800.00 / mo',
        badgeText: '-12% vs budget',
        badgeVariant: 'outline',
        badgeClass: 'bg-success/10 text-success border-success/20',
        description: 'Gross outflow $415.8k offset by $245.0k inflow',
        metricFootnote: '$23,200 under monthly board approved cap',
        progressValue: 74,
        icon: Flame,
      },
      {
        title: 'Projected 30-Day Inflow',
        value: '+$245,000.00',
        badgeText: '+18.4% YoY',
        badgeVariant: 'outline',
        badgeClass: 'bg-success/10 text-success border-success/20',
        description: 'From customer subscriptions & ARR renewals',
        metricFootnote: '96.2% contracted recurring enterprise revenue',
        progressValue: 88,
        icon: TrendingUp,
      },
      {
        title: 'Yield on Treasury Funds',
        value: '5.14% APY',
        highlight: '$20,800/mo interest earned',
        badgeText: 'Yield Active',
        badgeVariant: 'outline',
        badgeClass: 'bg-success/10 text-success border-success/20',
        description: '$2.85M deployed in US Treasury bills & sweeps',
        metricFootnote: '$249,600 annualized passive treasury income',
        progressValue: 98,
        icon: TrendingUp,
      },
    ]
    
    interface FlowItem {
      id: string
      name: string
      amount: string
      rawAmount: number
      percentage: number
      category: string
      detail: string
      color: string
    }
    
    const inflowBreakdown: FlowItem[] = [
      {
        id: 'in-arr',
        name: 'Customer ARR',
        amount: '+$210,000.00',
        rawAmount: 210000,
        percentage: 85.7,
        category: 'Subscription ARR',
        detail: 'Enterprise multi-seat renewals and monthly cloud subscriptions',
        color: 'bg-success',
      },
      {
        id: 'in-yield',
        name: 'Treasury Yield',
        amount: '+$20,800.00',
        rawAmount: 20800,
        percentage: 8.5,
        category: 'Money Market Sweep',
        detail: 'Accrued interest from 5.14% APY government sweeps & T-Bills',
        color: 'bg-teal-500',
      },
      {
        id: 'in-rebates',
        name: 'Partner Rebates',
        amount: '+$14,200.00',
        rawAmount: 14200,
        percentage: 5.8,
        category: 'Interchange & Cashback',
        detail: 'Corporate card cashback and cloud partner referral volume',
        color: 'bg-cyan-500',
      },
    ]
    
    const outflowBreakdown: FlowItem[] = [
      {
        id: 'out-payroll',
        name: 'Payroll & Benefits',
        amount: '-$138,400.00',
        rawAmount: 138400,
        percentage: 81.0,
        category: 'Compensation',
        detail: '42 full-time employees, health insurance, and 401(k) matching',
        color: 'bg-destructive',
      },
      {
        id: 'out-infra',
        name: 'Cloud Infrastructure',
        amount: '-$24,800.00',
        rawAmount: 24800,
        percentage: 14.5,
        category: 'Hosting & AI APIs',
        detail: 'AWS compute clusters, Vercel edge deployment, & OpenAI inference',
        color: 'bg-warning',
      },
      {
        id: 'out-office',
        name: 'Office & Taxes',
        amount: '-$7,600.00',
        rawAmount: 7600,
        percentage: 4.5,
        category: 'G&A and Compliance',
        detail: 'San Francisco hub lease, state registrations & tax prep reserves',
        color: 'bg-slate-500 dark:bg-slate-400',
      },
    ]
    
    interface BankAccount {
      id: string
      name: string
      institution: string
      accountType: string
      accountMask: string
      routingMask: string
      purpose: string
      apy: string
      balance: string
      balanceRaw: number
      status: 'Active' | 'Yield Sweeping' | 'Ringfenced' | 'Reserve Locked'
      statusType: 'success' | 'info' | 'warning'
      isSweepAccount?: boolean
    }
    
    const bankAccounts: BankAccount[] = [
      {
        id: 'acc-operating',
        name: 'Chase Corporate Operating',
        institution: 'JPMorgan Chase Bank',
        accountType: 'Operating Checking',
        accountMask: '•••• 4892',
        routingMask: '•••• 0210',
        purpose: 'Primary AP, vendor payouts & incoming customer wires',
        apy: '0.05% APY',
        balance: '$1,420,500.00',
        balanceRaw: 1420500,
        status: 'Active',
        statusType: 'success',
      },
      {
        id: 'acc-sweep',
        name: 'Morgan Stanley Treasury Sweep',
        institution: 'Morgan Stanley Private Bank',
        accountType: 'High-Yield Treasury Sweep',
        accountMask: '•••• 9210',
        routingMask: '•••• 0119',
        purpose: 'Automated daily sweep of cash over $1.0M into 4-week US T-Bills',
        apy: '5.14% APY',
        balance: '$2,850,000.00',
        balanceRaw: 2850000,
        status: 'Yield Sweeping',
        statusType: 'success',
        isSweepAccount: true,
      },
      {
        id: 'acc-payroll',
        name: 'SVB Payroll Escrow',
        institution: 'Silicon Valley Bank',
        accountType: 'Payroll Escrow',
        accountMask: '•••• 1049',
        routingMask: '•••• 1211',
        purpose: 'Ringfenced 2-cycle buffer for bi-weekly employee compensation',
        apy: '1.20% APY',
        balance: '$380,240.00',
        balanceRaw: 380240,
        status: 'Ringfenced',
        statusType: 'info',
      },
      {
        id: 'acc-tax',
        name: 'Citi Corporate Tax Reserve',
        institution: 'Citibank N.A.',
        accountType: 'Tax Reserve',
        accountMask: '•••• 8832',
        routingMask: '•••• 0008',
        purpose: 'Q3 estimated federal/state taxes & corporate franchise reserves',
        apy: '2.10% APY',
        balance: '$199,500.00',
        balanceRaw: 199500,
        status: 'Reserve Locked',
        statusType: 'warning',
      },
    ]
    
    const trajectoryMilestones = [
      {
        day: 'Day 1',
        date: 'Aug 21',
        title: 'Starting Liquidity',
        amount: '$4,850,240.00',
        note: 'Base cash balance',
        cx: 50,
        cy: 130,
      },
      {
        day: 'Day 7',
        date: 'Aug 28',
        title: 'Weekly ARR Inflow',
        amount: '+$52,500.00',
        note: 'SaaS recurring billing',
        cx: 180,
        cy: 123,
      },
      {
        day: 'Day 15',
        date: 'Sep 05',
        title: 'Mid-Month Payroll Run',
        amount: '-$69,200.00',
        note: 'Bi-weekly payroll disbursal',
        cx: 330,
        cy: 144,
      },
      {
        day: 'Day 22',
        date: 'Sep 12',
        title: 'Enterprise ARR Renewals',
        amount: '+$185,000.00',
        note: 'Annual contract renewals',
        cx: 490,
        cy: 89,
      },
      {
        day: 'Day 30',
        date: 'Sep 20',
        title: 'Projected Ending Balance',
        amount: '$4,924,440.00',
        note: '+$74,200 net increase',
        cx: 650,
        cy: 108,
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-cash-flow-treasury-dashboard, [ui-cash-flow-treasury-dashboard]',
      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,
        UiProgressComponent,
        UiSeparatorComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="cash-flow-treasury-dashboard" [class]="rootClass">
          <!-- Top Action Feedback Banner -->
          @if (actionFeedback()) {
            <div
              class="bg-primary/10 border-primary/20 text-foreground flex flex-wrap items-center justify-between gap-x-2 rounded-lg border px-4 py-3 text-xs shadow-xs"
            >
              <div class="flex items-center gap-2.5">
                <lucide-icon [img]="CheckCircle2" class="text-primary size-4 shrink-0" aria-hidden="true" />
                <span class="font-medium">{{ actionFeedback() }}</span>
              </div>
              <button
                type="button"
                class="text-muted-foreground hover:text-foreground inline-flex size-5 items-center justify-center rounded-sm"
                (click)="dismissFeedback()"
              >
                <span class="sr-only">Close message</span>
                &times;
              </button>
            </div>
          }
    
          <!-- Header Section -->
          <div class="bg-card border-border relative overflow-hidden rounded-xl border p-6 shadow-xs">
            <div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
              <!-- Company & Title -->
              <div class="space-y-2">
                <div class="flex flex-wrap items-center gap-2.5">
                  <span class="text-muted-foreground font-mono text-xs font-semibold tracking-wider uppercase">
                    UIPKGE Technologies Inc.
                  </span>
                  <span
                    ui-badge
                    variant="outline"
                    class="border-success/20 bg-success/10 text-success gap-1.5 text-xs font-medium"
                  >
                    <span class="bg-success size-1.5 animate-pulse rounded-full" aria-hidden="true"></span>
                    Live FDIC Insured Sweeps ($5M Aggregate)
                  </span>
                </div>
    
                <div>
                  <h1 class="text-foreground text-2xl font-semibold tracking-tight sm:text-3xl">
                    Treasury &amp; Cash Flow Management
                  </h1>
                  <p class="text-muted-foreground mt-0.5 text-xs sm:text-sm">
                    Real-time liquidity monitoring, 30-day runway projection, and automated 5.14% APY yield sweep desk.
                  </p>
                </div>
              </div>
    
              <!-- Header Action Controls -->
              <div class="flex flex-wrap items-center gap-2.5">
                <button ui-button variant="outline" class="gap-2 text-xs font-medium sm:text-sm" (click)="handleDeposit()">
                  <lucide-icon [img]="ArrowDownLeft" class="size-4" aria-hidden="true" />
                  Deposit Funds
                </button>
                <button ui-button class="gap-2 text-xs font-medium sm:text-sm" (click)="handleTransfer()">
                  <lucide-icon [img]="ArrowUpRight" class="size-4" aria-hidden="true" />
                  Initiate Wire / Transfer
                </button>
              </div>
            </div>
    
            <ui-separator class="my-5" />
    
            <!-- Net Total Liquidity Hero Metric Row -->
            <div class="grid grid-cols-1 gap-6 md:grid-cols-12 md:items-end">
              <div class="space-y-1.5 md:col-span-6 lg:col-span-5">
                <div class="text-muted-foreground text-xs font-medium">Net Total Liquidity</div>
                <div class="flex flex-wrap items-baseline gap-3">
                  <span class="text-foreground text-3xl font-semibold tracking-tight tabular-nums sm:text-4xl">
                    $4,850,240.00
                  </span>
                  <span
                    ui-badge
                    variant="outline"
                    class="border-success/20 bg-success/10 text-success gap-1 px-2 py-0.5 text-xs font-semibold"
                  >
                    <lucide-icon [img]="TrendingUp" class="size-3" aria-hidden="true" />
                    +$74,200.00 (+1.55%) this month
                  </span>
                </div>
                <p class="text-muted-foreground text-xs">Aggregated across 4 corporate custody &amp; sweep accounts</p>
              </div>
    
              <!-- Liquidity Allocation Mini Pills -->
              <div class="grid grid-cols-1 gap-2.5 sm:grid-cols-3 md:col-span-6 lg:col-span-7">
                <div class="border-border bg-muted/40 rounded-lg border p-2.5">
                  <span class="text-muted-foreground block text-xs">Available Cash</span>
                  <span class="text-foreground font-mono text-sm font-semibold tabular-nums">$1,420,500.00</span>
                  <span class="text-muted-foreground mt-0.5 block text-xs">29.3% in Operating</span>
                </div>
    
                <div class="border-border bg-muted/40 rounded-lg border p-2.5">
                  <div class="flex flex-wrap items-center justify-between gap-x-2">
                    <span class="text-muted-foreground block text-xs">Yield Sweep</span>
                    <span ui-badge variant="outline" class="border-success/30 text-success px-1 py-0 text-xs font-normal">
                      5.14% APY
                    </span>
                  </div>
                  <span class="text-foreground font-mono text-sm font-semibold tabular-nums">$2,850,000.00</span>
                  <span class="text-muted-foreground mt-0.5 block text-xs">58.8% in T-Bills</span>
                </div>
    
                <div class="border-border bg-muted/40 rounded-lg border p-2.5">
                  <span class="text-muted-foreground block text-xs">Escrow &amp; Reserves</span>
                  <span class="text-foreground font-mono text-sm font-semibold tabular-nums">$579,740.00</span>
                  <span class="text-muted-foreground mt-0.5 block text-xs">11.9% Ringfenced</span>
                </div>
              </div>
            </div>
          </div>
    
          <!-- 4 Treasury Health Metric Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            @for (card of healthCards; track card.title) {
              <div ui-card class="border-border relative flex flex-col justify-between overflow-hidden shadow-xs">
                <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                  <div ui-card-title class="text-muted-foreground text-xs font-medium tracking-tight">{{ card.title }}</div>
                  <span ui-badge [variant]="card.badgeVariant" [class]="'text-xs font-medium ' + (card.badgeClass ?? '')">
                    {{ card.badgeText }}
                  </span>
                </div>
    
                <div ui-card-content class="space-y-3">
                  <div>
                    <div [class]="healthValueClass(card.title)">{{ card.value }}</div>
                    @if (card.highlight) {
                      <p class="text-success mt-0.5 text-xs font-semibold">{{ card.highlight }}</p>
                    }
                    <p class="text-muted-foreground mt-1 text-xs leading-normal">{{ card.description }}</p>
                  </div>
    
                  <div class="space-y-1.5 border-t pt-2.5">
                    <ui-progress [value]="card.progressValue" class="h-1.5" />
                    <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
                      <span>{{ card.metricFootnote }}</span>
                    </div>
                  </div>
                </div>
              </div>
            }
          </div>
    
          <!-- 30-Day Cash Flow Projection & Inflow/Outflow Breakdown Section -->
          <div class="grid grid-cols-1 gap-6 lg:grid-cols-12">
            <!-- 30-Day Cash Flow Projection Chart (7 Cols on LG) -->
            <div ui-card class="border-border flex flex-col justify-between shadow-xs lg:col-span-7">
              <div ui-card-header class="border-b pb-4">
                <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                  <div class="space-y-0.5">
                    <div class="flex items-center gap-2">
                      <div ui-card-title class="text-base font-semibold tracking-tight">30-Day Cash Flow Projection</div>
                      <span ui-badge variant="outline" class="border-success/20 bg-success/10 text-success text-xs">
                        95% Confidence Band
                      </span>
                    </div>
                    <div ui-card-description class="text-xs">
                      Trajectory from <strong class="text-foreground">$4,850,240.00</strong> to projected
                      <strong class="text-foreground">$4,924,440.00</strong> balance.
                    </div>
                  </div>
    
                  <!-- Timeframe selector -->
                  <div class="flex items-center rounded-lg border p-0.5 text-xs">
                    @for (tab of timeframeTabs; track tab.value) {
                      <button type="button" [class]="timeframeTabClass(tab.value)" (click)="setActiveTab(tab.value)">
                        {{ tab.label }}
                      </button>
                    }
                  </div>
                </div>
              </div>
    
              <div ui-card-content class="space-y-4 pt-5">
                <!-- Responsive Projection SVG Curve with Confidence Corridor -->
                <div class="bg-muted/10 relative w-full overflow-x-auto rounded-lg border p-2">
                  <svg
                    viewBox="0 0 700 220"
                    class="h-56 w-full max-w-[560px] min-w-full overflow-visible"
                    preserveAspectRatio="none"
                    aria-label="30-day corporate cash projection graph"
                  >
                    <defs>
                      <linearGradient id="cfBandGradientNg" x1="0" y1="0" x2="0" y2="1">
                        <stop offset="0%" stop-color="#10B981" stop-opacity="0.22" />
                        <stop offset="100%" stop-color="#10B981" stop-opacity="0.02" />
                      </linearGradient>
                      <linearGradient id="cfLineGradientNg" x1="0" y1="0" x2="1" y2="0">
                        <stop offset="0%" stop-color="#10B981" />
                        <stop offset="45%" stop-color="#06B6D4" />
                        <stop offset="100%" stop-color="#10B981" />
                      </linearGradient>
                    </defs>
    
                    <!-- Horizontal Grid Guidelines -->
                    <g stroke="currentColor" class="text-border/60" stroke-width="1" stroke-dasharray="3 3">
                      <line x1="40" y1="55" x2="670" y2="55" />
                      <line x1="40" y1="115" x2="670" y2="115" />
                      <line x1="40" y1="175" x2="670" y2="175" />
                    </g>
    
                    <!-- Guideline Labels -->
                    <g class="fill-muted-foreground font-mono text-xs" font-size="12">
                      <text x="4" y="58">$5.10M</text>
                      <text x="4" y="118">$4.85M</text>
                      <text x="4" y="178">$4.60M</text>
                    </g>
    
                    <!-- 95% Confidence Band Polygon Area -->
                    <path
                      d="M 50,120 C 110,110 140,95 180,90 C 240,85 280,105 330,105 C 390,105 430,55 490,55 C 550,55 590,75 650,75 L 650,140 C 590,140 550,125 490,125 C 430,125 390,175 330,175 C 280,175 240,155 180,155 C 140,155 110,140 50,140 Z"
                      fill="url(#cfBandGradientNg)"
                    />
    
                    <!-- Upper Confidence Bound Line (Dashed) -->
                    <path
                      d="M 50,120 C 110,110 140,95 180,90 C 240,85 280,105 330,105 C 390,105 430,55 490,55 C 550,55 590,75 650,75"
                      fill="none"
                      stroke="#10B981"
                      stroke-width="1.5"
                      stroke-dasharray="4 4"
                      opacity="0.6"
                    />
    
                    <!-- Lower Confidence Bound Line (Dashed) -->
                    <path
                      d="M 50,140 C 110,140 140,155 180,155 C 240,155 280,175 330,175 C 390,175 430,125 490,125 C 550,125 590,140 650,140"
                      fill="none"
                      stroke="#10B981"
                      stroke-width="1.5"
                      stroke-dasharray="4 4"
                      opacity="0.4"
                    />
    
                    <!-- Median Expected Trajectory Main Curve -->
                    <path
                      d="M 50,130 C 110,128 140,123 180,123 C 240,123 280,144 330,144 C 390,144 430,89 490,89 C 550,89 590,108 650,108"
                      fill="none"
                      stroke="url(#cfLineGradientNg)"
                      stroke-width="3"
                      stroke-linecap="round"
                    />
    
                    <!-- Milestone Markers on Curve -->
                    @for (m of milestones; track m.day; let idx = $index) {
                      <g class="cursor-pointer" (mouseenter)="setActiveMilestone(idx)">
                        <circle
                          [attr.cx]="m.cx"
                          [attr.cy]="m.cy"
                          r="6"
                          class="fill-background stroke-emerald-500"
                          stroke-width="3"
                        />
                        @if (activeMilestone() === idx) {
                          <circle
                            [attr.cx]="m.cx"
                            [attr.cy]="m.cy"
                            r="11"
                            class="fill-success/20 stroke-emerald-400"
                            stroke-width="1"
                          />
                        }
                      </g>
                    }
                  </svg>
    
                  <!-- Bottom X-Axis Milestone Labels -->
                  <div class="mt-2 grid max-w-[560px] min-w-full grid-cols-5 gap-1 border-t pt-2 text-center">
                    @for (m of milestones; track m.day; let idx = $index) {
                      <div [class]="milestoneCellClass(idx)" (mouseenter)="setActiveMilestone(idx)">
                        <span class="text-foreground block text-xs font-semibold">{{ m.day }}</span>
                        <span class="text-muted-foreground block text-xs">{{ m.date }}</span>
                        <span [class]="milestoneAmountClass(m.amount)">{{ m.amount }}</span>
                      </div>
                    }
                  </div>
                </div>
    
                <!-- Chart Key Legend & Milestone Detail Bar -->
                <div class="flex flex-wrap items-center justify-between gap-3 text-xs">
                  <div class="flex flex-wrap items-center gap-4">
                    <div class="flex items-center gap-1.5">
                      <span class="bg-success size-2.5 rounded-full"></span>
                      <span class="text-foreground font-medium">Expected Trajectory</span>
                    </div>
                    <div class="flex items-center gap-1.5">
                      <span class="border-success bg-success/20 h-2 w-4 rounded-xs border border-dashed"></span>
                      <span class="text-muted-foreground">95% Confidence Corridor</span>
                    </div>
                    <div class="flex items-center gap-1.5">
                      <span class="bg-background border-success size-2 rounded-full border-2"></span>
                      <span class="text-muted-foreground">Cash Events</span>
                    </div>
                  </div>
    
                  <div class="text-muted-foreground font-mono">
                    Net 30-Day Delta: <strong class="text-success">+$74,200.00</strong>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- Inflow & Outflow Breakdown Side Column (5 Cols on LG) -->
            <div class="space-y-4 lg:col-span-5">
              <!-- Inflow Breakdown Card -->
              <div ui-card class="border-border shadow-xs">
                <div ui-card-header class="pb-3">
                  <div class="flex flex-wrap items-center justify-between gap-x-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-success/10 text-success flex size-7 items-center justify-center rounded-md">
                        <lucide-icon [img]="ArrowDownLeft" class="size-4" aria-hidden="true" />
                      </div>
                      <div>
                        <div ui-card-title class="text-sm font-semibold tracking-tight">Projected 30D Inflow</div>
                        <div ui-card-description class="text-xs">Total incoming cash: +$245,000.00</div>
                      </div>
                    </div>
                    <span
                      ui-badge
                      variant="outline"
                      class="border-success/20 bg-success/10 text-success text-xs font-semibold"
                    >
                      100%
                    </span>
                  </div>
                </div>
                <div ui-card-content class="space-y-3">
                  @for (item of inflow; track item.id) {
                    <div class="space-y-1.5">
                      <div class="flex flex-wrap items-center justify-between gap-x-2 text-xs">
                        <div class="flex items-center gap-2">
                          <span class="text-foreground font-medium">{{ item.name }}</span>
                          <span class="text-muted-foreground font-mono text-xs">({{ item.percentage }}%)</span>
                        </div>
                        <span class="text-success font-mono font-semibold tabular-nums">{{ item.amount }}</span>
                      </div>
                      <ui-progress [value]="item.percentage" class="h-2" />
                      <p class="text-muted-foreground text-xs">{{ item.detail }}</p>
                    </div>
                  }
                </div>
              </div>
    
              <!-- Outflow Breakdown Card -->
              <div ui-card class="border-border shadow-xs">
                <div ui-card-header class="pb-3">
                  <div class="flex flex-wrap items-center justify-between gap-x-2">
                    <div class="flex items-center gap-2">
                      <div class="bg-destructive/10 text-destructive flex size-7 items-center justify-center rounded-md">
                        <lucide-icon [img]="ArrowUpRight" class="size-4" aria-hidden="true" />
                      </div>
                      <div>
                        <div ui-card-title class="text-sm font-semibold tracking-tight">Projected 30D Outflow</div>
                        <div ui-card-description class="text-xs">Total operational burn: -$170,800.00</div>
                      </div>
                    </div>
                    <span
                      ui-badge
                      variant="outline"
                      class="border-destructive/20 bg-destructive/10 text-destructive text-xs font-semibold"
                    >
                      100%
                    </span>
                  </div>
                </div>
                <div ui-card-content class="space-y-3">
                  @for (item of outflow; track item.id) {
                    <div class="space-y-1.5">
                      <div class="flex flex-wrap items-center justify-between gap-x-2 text-xs">
                        <div class="flex items-center gap-2">
                          <span class="text-foreground font-medium">{{ item.name }}</span>
                          <span class="text-muted-foreground font-mono text-xs">({{ item.percentage }}%)</span>
                        </div>
                        <span class="text-destructive font-mono font-semibold tabular-nums">{{ item.amount }}</span>
                      </div>
                      <ui-progress [value]="item.percentage" class="h-2" />
                      <p class="text-muted-foreground text-xs">{{ item.detail }}</p>
                    </div>
                  }
                </div>
              </div>
            </div>
          </div>
    
          <!-- Connected Corporate Bank Accounts Table -->
          <div class="bg-card border-border overflow-hidden rounded-xl border shadow-xs">
            <div class="flex flex-col gap-3 border-b p-5 sm:flex-row sm:items-center sm:justify-between">
              <div class="space-y-1">
                <div class="flex items-center gap-2.5">
                  <lucide-icon [img]="Landmark" class="text-muted-foreground size-5" aria-hidden="true" />
                  <h3 class="text-foreground text-base font-semibold tracking-tight sm:text-lg">
                    Connected Corporate Bank &amp; Treasury Accounts
                  </h3>
                </div>
                <p class="text-muted-foreground text-xs">
                  4 institutional accounts connected via real-time Open Banking / Plaid Exchange
                </p>
              </div>
    
              <div class="flex flex-wrap items-center gap-2.5">
                <button ui-button size="sm" variant="outline" [class]="sweepButtonClass" (click)="handleManageSweep()">
                  <lucide-icon [img]="Zap" class="size-3.5" aria-hidden="true" />
                  <span>Auto-Sweep Rule: {{ isAutoSweepActive() ? 'Active (5.14% APY)' : 'Paused' }}</span>
                </button>
    
                <button ui-button size="sm" variant="outline" class="gap-1.5 text-xs" (click)="refreshBalances()">
                  <lucide-icon [img]="RefreshCw" class="size-3.5" aria-hidden="true" />
                  Refresh Balances
                </button>
              </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-[240px]">Institution &amp; Account</th>
                    <th ui-table-head class="min-w-[220px]">Purpose &amp; Routing</th>
                    <th ui-table-head class="text-center">APY / Yield</th>
                    <th ui-table-head class="text-right">Available Balance</th>
                    <th ui-table-head class="text-center">Status</th>
                    <th ui-table-head class="text-right">Actions</th>
                  </tr>
                </thead>
                <tbody ui-table-body>
                  @for (account of accounts; track account.id) {
                    <tr ui-table-row class="hover:bg-muted/30">
                      <!-- Account & Institution -->
                      <td ui-table-cell>
                        <div class="flex items-center gap-3">
                          <div
                            class="border-border/80 bg-background flex size-10 shrink-0 items-center justify-center rounded-lg border shadow-xs"
                          >
                            @switch (account.id) {
                              @case ('acc-operating') {
                                <svg viewBox="0 0 24 24" class="size-5 shrink-0 text-[#117ACA]" fill="currentColor">
                                  <path
                                    d="M12 2.5L7.5 7h4.8L16.2 3.1 12 2.5zm9.5 9.5L17 7.5v4.8l3.9 3.9.6-4.2zm-9.5 9.5L16.5 17h-4.8L7.8 20.9l4.2.6zm-9.5-9.5L7 16.5v-4.8L3.1 7.8 2.5 12z"
                                  />
                                </svg>
                              }
                              @case ('acc-sweep') {
                                <svg viewBox="0 0 24 24" class="size-5 shrink-0" fill="none">
                                  <rect width="24" height="24" rx="4" fill="#002D62" />
                                  <path d="M4 17V7l5 5-5 5zm8 0V7l5 5-5 5z" fill="#00A3E0" />
                                  <circle cx="18" cy="12" r="2" fill="#FFFFFF" />
                                </svg>
                              }
                              @case ('acc-payroll') {
                                <svg viewBox="0 0 24 24" class="size-5 shrink-0" fill="none">
                                  <rect width="24" height="24" rx="4" fill="#005587" />
                                  <path d="M6 15l6-9 6 9-3 1.5-3-4.5-3 4.5L6 15z" fill="#FFFFFF" />
                                  <path d="M9 16.5l3-4.5 3 4.5-3 1.5-3-1.5z" fill="#00A3E0" />
                                </svg>
                              }
                              @case ('acc-tax') {
                                <svg viewBox="0 0 24 24" class="size-5 shrink-0" fill="none">
                                  <path
                                    d="M12 4.5C8 4.5 4.5 6.8 4 9.5h2.5c.5-1.5 3-2.5 5.5-2.5s5 1 5.5 2.5H20c-.5-2.7-4-5-8-5z"
                                    fill="#EC111A"
                                  />
                                  <rect x="4" y="11" width="3" height="8" rx="0.5" fill="#003B70" />
                                  <circle cx="12" cy="15" r="4" fill="#003B70" />
                                  <rect x="17" y="11" width="3" height="8" rx="0.5" fill="#003B70" />
                                </svg>
                              }
                              @default {
                                <lucide-icon [img]="Building2" class="text-muted-foreground size-5" />
                              }
                            }
                          </div>
    
                          <div class="min-w-0 space-y-0.5">
                            <div class="flex items-center gap-2">
                              <span class="text-foreground truncate text-sm font-semibold">{{ account.name }}</span>
                            </div>
                            <div class="flex items-center gap-2">
                              <span class="text-muted-foreground truncate text-xs">{{ account.institution }}</span>
                              <span class="text-muted-foreground font-mono text-xs">{{ account.accountMask }}</span>
                            </div>
                          </div>
                        </div>
                      </td>
    
                      <!-- Purpose -->
                      <td ui-table-cell>
                        <div class="space-y-0.5">
                          <span class="text-foreground block text-xs font-medium">{{ account.accountType }}</span>
                          <span class="text-muted-foreground block text-xs leading-tight">{{ account.purpose }}</span>
                        </div>
                      </td>
    
                      <!-- APY Rate -->
                      <td ui-table-cell class="text-center">
                        @if (account.isSweepAccount) {
                          <span
                            ui-badge
                            variant="outline"
                            class="border-success/30 bg-success/10 text-success font-mono text-xs font-semibold"
                          >
                            {{ account.apy }}
                          </span>
                        } @else {
                          <span class="text-muted-foreground font-mono text-xs">{{ account.apy }}</span>
                        }
                      </td>
    
                      <!-- Balance -->
                      <td ui-table-cell class="text-right">
                        <span class="text-foreground font-mono text-sm font-semibold tabular-nums">{{
                          account.balance
                        }}</span>
                      </td>
    
                      <!-- Status -->
                      <td ui-table-cell class="text-center">
                        @if (account.statusType === 'success') {
                          <span
                            ui-badge
                            variant="outline"
                            class="border-success/20 bg-success/10 text-success gap-1 text-xs font-medium"
                          >
                            <span class="bg-success size-1.5 rounded-full" aria-hidden="true"></span>
                            {{ account.status }}
                          </span>
                        } @else if (account.statusType === 'info') {
                          <span
                            ui-badge
                            variant="outline"
                            class="border-info/20 bg-info/10 text-info gap-1 text-xs font-medium"
                          >
                            <span class="bg-info size-1.5 rounded-full" aria-hidden="true"></span>
                            {{ account.status }}
                          </span>
                        } @else {
                          <span
                            ui-badge
                            variant="outline"
                            class="border-warning/20 bg-warning/10 text-warning gap-1 text-xs font-medium"
                          >
                            <lucide-icon [img]="Lock" class="size-3" aria-hidden="true" />
                            {{ account.status }}
                          </span>
                        }
                      </td>
    
                      <!-- Actions -->
                      <td ui-table-cell class="text-right">
                        <div class="flex items-center justify-end gap-1.5">
                          @if (account.isSweepAccount) {
                            <button
                              ui-button
                              size="sm"
                              variant="outline"
                              class="h-8 gap-1 text-xs"
                              (click)="handleManageSweep()"
                            >
                              <lucide-icon [img]="Zap" class="text-success size-3" aria-hidden="true" />
                              Configure Sweep
                            </button>
                          } @else {
                            <button
                              ui-button
                              size="sm"
                              variant="outline"
                              class="h-8 text-xs"
                              (click)="handleTransfer(account.name)"
                            >
                              Transfer
                            </button>
                          }
                        </div>
                      </td>
                    </tr>
                  }
                </tbody>
              </table>
            </div>
    
            <!-- Table Summary Footer -->
            <div class="bg-muted/20 flex flex-col items-center justify-between gap-3 border-t p-4 sm:flex-row">
              <div class="flex items-center gap-2">
                <lucide-icon [img]="Shield" class="text-muted-foreground size-4" aria-hidden="true" />
                <p class="text-muted-foreground text-xs">
                  Custody verified through Member FINRA / SIPC &amp; FDIC depository institutions.
                </p>
              </div>
              <div class="flex items-center gap-3">
                <span class="text-muted-foreground font-mono text-xs">
                  Net Liquidity: <strong class="text-foreground">$4,850,240.00</strong>
                </span>
                <button ui-button size="sm" class="h-8 text-xs font-medium" (click)="handleTransfer()">
                  <lucide-icon [img]="Send" class="mr-1.5 size-3.5" aria-hidden="true" />
                  Transfer Between Accounts
                </button>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiCashFlowTreasuryDashboardComponent {
      protected readonly ArrowDownLeft = ArrowDownLeft
      protected readonly ArrowUpRight = ArrowUpRight
      protected readonly Building2 = Building2
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Landmark = Landmark
      protected readonly Lock = Lock
      protected readonly RefreshCw = RefreshCw
      protected readonly Send = Send
      protected readonly Shield = Shield
      protected readonly TrendingUp = TrendingUp
      protected readonly Zap = Zap
    
      protected readonly healthCards = healthCards
      protected readonly inflow = inflowBreakdown
      protected readonly outflow = outflowBreakdown
      protected readonly accounts = bankAccounts
      protected readonly milestones = trajectoryMilestones
      protected readonly timeframeTabs: { value: TreasuryTab; label: string }[] = [
        { value: '30d', label: '30D Projection' },
        { value: '60d', label: '60D' },
        { value: '90d', label: '90D' },
      ]
    
      @Input('class') className?: string
    
      @Output() readonly deposit = new EventEmitter<void>()
      @Output() readonly transfer = new EventEmitter<string | undefined>()
      @Output() readonly manageSweep = new EventEmitter<void>()
    
      readonly activeTab = signal<TreasuryTab>('30d')
      readonly activeMilestone = signal<number | null>(null)
      readonly actionFeedback = signal<string | null>(null)
      readonly isAutoSweepActive = signal(true)
    
      private feedbackTimer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        inject(DestroyRef).onDestroy(() => clearTimeout(this.feedbackTimer))
      }
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      get sweepButtonClass(): string {
        return cn(
          'gap-1.5 text-xs',
          this.isAutoSweepActive() ? 'border-success/30 bg-success/10 text-success' : 'text-muted-foreground',
        )
      }
    
      timeframeTabClass(tab: TreasuryTab): string {
        return cn(
          'rounded-md px-2.5 py-1 font-medium transition-colors',
          this.activeTab() === tab
            ? 'bg-primary text-primary-foreground shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      healthValueClass(title: string): string {
        return cn(
          'text-foreground text-2xl font-semibold tracking-tight tabular-nums',
          title === 'Yield on Treasury Funds' && 'text-success',
        )
      }
    
      milestoneCellClass(idx: number): string {
        return cn(
          'rounded-md p-1 transition-colors',
          this.activeMilestone() === idx ? 'bg-muted ring-border/80 ring-1' : 'hover:bg-muted/40',
        )
      }
    
      milestoneAmountClass(amount: string): string {
        return cn(
          'font-mono text-xs font-medium tabular-nums',
          amount.startsWith('+') ? 'text-success' : amount.startsWith('-') ? 'text-destructive' : 'text-foreground',
        )
      }
    
      setActiveTab(tab: TreasuryTab): void {
        this.activeTab.set(tab)
      }
    
      setActiveMilestone(idx: number | null): void {
        this.activeMilestone.set(idx)
      }
    
      dismissFeedback(): void {
        this.actionFeedback.set(null)
      }
    
      private triggerNotification(msg: string): void {
        this.actionFeedback.set(msg)
        clearTimeout(this.feedbackTimer)
        this.feedbackTimer = setTimeout(() => {
          this.actionFeedback.update((prev) => (prev === msg ? null : prev))
        }, 4000)
      }
    
      handleDeposit(): void {
        this.deposit.emit()
        this.triggerNotification('Deposit modal opened. Direct ACH & Wire instructions ready for UIPKGE Technologies Inc.')
      }
    
      handleTransfer(accountName?: string): void {
        this.transfer.emit(accountName)
        this.triggerNotification(
          accountName
            ? `Transfer interface initiated from ${accountName}. Real-time settlement active.`
            : 'Initiating wire & multi-account transfer portal.',
        )
      }
    
      handleManageSweep(): void {
        const next = !this.isAutoSweepActive()
        this.isAutoSweepActive.set(next)
        this.manageSweep.emit()
        this.triggerNotification(
          next
            ? 'Auto-Sweep rule ACTIVE: Daily excess over $1.0M swept into 5.14% APY Treasury Bills.'
            : 'Auto-Sweep rule PAUSED. Funds will remain in standard operating checking.',
        )
      }
    
      refreshBalances(): void {
        this.triggerNotification('Bank balances refreshed from Federal Reserve Fedwire & NACHA feeds.')
      }
    }
    
  • src/app/components/blocks/cash-flow-treasury-dashboard/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/cash-flow-treasury-dashboard.json