Menu

Change language

GitHub repo

ROI — Calculator

blockmarketing

Input-driven ROI estimate where team size, close days, and analyst rate drive a computed saving, with the assumptions stated beneath the figure.

Installation

$npx uipkge-ng@latest add roi-calculator-inputs
Manifest:https://uipkge.dev/r/angular/roi-calculator-inputs.jsonInstalls to:src/app/components/blocks/roi-calculator-inputs/

Variants

Loading interactive previews…

Schema

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

RoiInput
interface RoiInput {
  id: 'analysts' | 'days' | 'rate'
  label: string
  min: number
  max: number
  step: number
}

Files installed (2)

  • src/app/components/blocks/roi-calculator-inputs/roi-calculator-inputs.component.ts5.8 kB
    // Input-driven ROI estimate: team size, close days, and analyst rate drive a computed
    // saving, with the assumptions stated beneath the figure. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import { ArrowRight, LucideAngularModule } 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 } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import { UiSliderComponent } from '@/ui/slider/slider.component'
    
    const DAYS_SAVED_SHARE = 0.55
    const CLOSES_PER_YEAR = 12
    
    interface RoiInput {
      id: 'analysts' | 'days' | 'rate'
      label: string
      min: number
      max: number
      step: number
    }
    
    const INPUTS: RoiInput[] = [
      { id: 'analysts', label: 'People involved in close', min: 1, max: 40, step: 1 },
      { id: 'days', label: 'Days the close takes today', min: 2, max: 20, step: 1 },
      { id: 'rate', label: 'Fully-loaded day rate', min: 200, max: 1200, step: 20 },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-roi-calculator-inputs, [ui-roi-calculator-inputs]',
      standalone: true,
      // React renders the <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiSeparatorComponent,
        UiSliderComponent,
      ],
      template: `
        <section data-slot="roi-calculator-inputs" [class]="rootClass">
          <div class="mx-auto max-w-4xl px-6 py-20 lg:py-28">
            <div class="max-w-2xl">
              <span ui-badge variant="secondary">Estimate</span>
              <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">What the close currently costs you</h2>
              <p class="text-muted-foreground mt-3 text-lg">
                Three inputs, one arithmetic model, stated in full underneath. Adjust anything that looks wrong.
              </p>
            </div>
    
            <div ui-card class="mt-10">
              <div ui-card-content class="grid gap-8 p-6 sm:grid-cols-[1.2fr_1fr] sm:p-8">
                <div class="space-y-6">
                  @for (input of INPUTS; track input.id) {
                    <div>
                      <div class="flex items-baseline justify-between gap-4">
                        <label [attr.for]="input.id" class="text-sm font-medium">{{ input.label }}</label>
                        <span class="font-mono text-sm tabular-nums">{{ displayValue(input.id) }}</span>
                      </div>
                      <ui-slider
                        [value]="modelValue(input.id)"
                        (valueChange)="setModel(input.id, $event)"
                        [min]="input.min"
                        [max]="input.max"
                        [step]="input.step"
                        class="mt-3"
                        [attr.aria-label]="input.label"
                      />
                    </div>
                  }
                </div>
    
                <div class="border-border bg-muted/30 flex flex-col justify-center rounded-lg border p-6 text-center">
                  <p class="text-muted-foreground text-xs tracking-wide uppercase">Estimated annual saving</p>
                  <p class="font-display mt-2 text-4xl font-bold tracking-tight tabular-nums">{{ formatted() }}</p>
                  <p class="text-muted-foreground mt-2 text-xs">
                    {{ daysSaved() }} days saved per close, {{ closesPerYear }} closes a year
                  </p>
                  <button ui-button class="mt-5">
                    Check it against your numbers
                    <lucide-icon [img]="ArrowRight" class="ml-2 size-4" />
                  </button>
                </div>
              </div>
    
              <ui-separator />
    
              <div class="px-6 py-4 sm:px-8">
                <p class="text-muted-foreground text-xs leading-relaxed">
                  <span class="font-medium">The model. </span>
                  Saving = days saved × closes per year × people × day rate, where days saved assumes
                  {{ sharePercent }}% of close time is reconciliation that certified definitions remove. That share is the
                  median across 240 rollouts; yours will differ, and the figure is only as good as that assumption.
                </p>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiRoiCalculatorInputsComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly INPUTS = INPUTS
      protected readonly closesPerYear = CLOSES_PER_YEAR
      protected readonly sharePercent = Math.round(DAYS_SAVED_SHARE * 100)
    
      @Input('class') className?: string
    
      readonly analysts = signal<number[]>([6])
      readonly closeDays = signal<number[]>([9])
      readonly rate = signal<number[]>([520])
    
      readonly daysSaved = computed(() => Math.round((this.closeDays()[0] ?? 0) * DAYS_SAVED_SHARE * 10) / 10)
      readonly annualSaving = computed(() =>
        Math.round(this.daysSaved() * CLOSES_PER_YEAR * (this.analysts()[0] ?? 0) * (this.rate()[0] ?? 0)),
      )
      readonly formatted = computed(() => `£${this.annualSaving().toLocaleString('en-GB')}`)
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    
      modelValue(id: RoiInput['id']): number[] {
        if (id === 'analysts') return this.analysts()
        if (id === 'days') return this.closeDays()
        return this.rate()
      }
    
      setModel(id: RoiInput['id'], value: number[]): void {
        if (id === 'analysts') this.analysts.set(value)
        else if (id === 'days') this.closeDays.set(value)
        else this.rate.set(value)
      }
    
      displayValue(id: RoiInput['id']): string {
        if (id === 'analysts') return `${this.analysts()[0]}`
        if (id === 'days') return `${this.closeDays()[0]} days`
        return `£${this.rate()[0]}`
      }
    }
    
  • src/app/components/blocks/roi-calculator-inputs/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/roi-calculator-inputs.json