Back to templates

hr-employees

HR Employees

Use with destination tables shaped for employee rosters or staff directories.

Handle employee roster imports with the same guided upload, mapping, review, and correction flow customers expect from mature HR products.

Who this is for

Best for HR, people ops, and workforce-tool founders onboarding employee directories into a Supabase app.

Sample import idea

An HR manager uploads an employee spreadsheet, checks departments and dates, corrects a handful of rows inline, and imports the valid staff records safely.

Sample headers

full_name, email, department, job_title, start_date, salary

Canonical fields

These are the typed fields the template already understands across the widget, validation rules, dashboard config, and playground.

Full name

full_name

textrequired

Email

email

emailrequired

Department

department

textoptional

Job title

job_title

textoptional

Start date

start_date

dateoptional

Salary

salary

numberoptional

Widget flow

The same sequence appears in the hosted widget and the public playground.

  1. 1

    Upload CSV or Excel

    Upload a spreadsheet and start the browser-side flow immediately.

  2. 2

    Confirm smart column mapping

    Confirm the suggested mapping instead of rebuilding a schema by hand.

  3. 3

    Review validation results

    See what fails clearly and repair bad values inline.

  4. 4

    Fix invalid values inline

    Keep real imports safe with server authority and quotas.

  5. 5

    Import with a clear summary

    End with a simple inserted vs failed summary customers can trust.

React drop-in example

After you create a kit, replace the example token with the public embed token from your kit.

'use client'

import { useEffect, useRef } from 'react'

const IMPORTFLOW_APP_ORIGIN = "https://your-importflow-domain.example"
const IMPORTFLOW_TOKEN = "if_pub_xxxxxxxxx"
const IMPORTFLOW_APPEARANCE = {} as const
const IMPORTFLOW_SCRIPT_ID = 'importflow-embed-loader'

type ImportFlowSummary = {
  importJobId: string
  requestedRows: number
  validRows: number
  insertedRows: number
  failedRows: number
  completedAt: string
}

type ImportFlowMount = {
  destroy: () => void
}

type ImportFlowGlobal = {
  mount: (options: {
    token: string
    target: HTMLElement
    theme?: 'light' | 'dark' | 'system'
    primaryColor?: string
    surfaceColor?: string
    textColor?: string
    radius?: 'none' | 'sm' | 'md' | 'lg' | 'xl'
    onComplete?: (summary: ImportFlowSummary) => void
    onClose?: (event: { reason: string; phase: string }) => void
  }) => ImportFlowMount
}

declare global {
  interface Window {
    ImportFlow?: ImportFlowGlobal
  }
}

function clearTarget(target: HTMLElement) {
  target.replaceChildren()
}

function loadImportFlowScript() {
  if (window.ImportFlow) {
    return Promise.resolve()
  }

  const existing = document.getElementById(IMPORTFLOW_SCRIPT_ID) as HTMLScriptElement | null
  if (existing) {
    return new Promise<void>((resolve, reject) => {
      existing.addEventListener('load', () => resolve(), { once: true })
      existing.addEventListener('error', () => reject(new Error('ImportFlow failed to load.')), {
        once: true,
      })
    })
  }

  return new Promise<void>((resolve, reject) => {
    const script = document.createElement('script')
    script.id = IMPORTFLOW_SCRIPT_ID
    script.src = new URL('/embed/v1/importflow.js', IMPORTFLOW_APP_ORIGIN).toString()
    script.async = true
    script.addEventListener('load', () => resolve(), { once: true })
    script.addEventListener('error', () => reject(new Error('ImportFlow failed to load.')), {
      once: true,
    })
    document.head.appendChild(script)
  })
}

export function ImportFlowDropIn() {
  const targetRef = useRef<HTMLDivElement | null>(null)
  const mountRef = useRef<ImportFlowMount | null>(null)

  useEffect(() => {
    const target = targetRef.current
    if (!target) {
      return
    }

    let cancelled = false
    mountRef.current?.destroy()
    mountRef.current = null
    clearTarget(target)

    loadImportFlowScript()
      .then(() => {
        if (cancelled || !window.ImportFlow) {
          return
        }

        mountRef.current?.destroy()
        clearTarget(target)
        mountRef.current = window.ImportFlow.mount({
          token: IMPORTFLOW_TOKEN,
          target,
          ...IMPORTFLOW_APPEARANCE,
          onComplete(summary) {
            console.log('ImportFlow complete', summary)
          },
          onClose(event) {
            console.log('ImportFlow closed', event)
          },
        })
      })
      .catch((error: unknown) => {
        if (!cancelled) {
          console.error(error)
        }
      })

    return () => {
      cancelled = true
      mountRef.current?.destroy()
      mountRef.current = null
      clearTarget(target)
    }
  }, [])

  return <div ref={targetRef} />
}

JavaScript embed example

The same real hosted loader works without React by mounting into a target element.

<div id="importflow-widget"></div>
<script src="https://your-importflow-domain.example/embed/v1/importflow.js"></script>
<script>
  const importflowAppearance = {}
  const importflow = window.ImportFlow.mount({
    token: 'if_pub_xxxxxxxxx',
    target: '#importflow-widget',
    ...importflowAppearance,
    onReady(payload) {
      console.log('ImportFlow ready', payload)
    },
    onComplete(summary) {
      console.log('ImportFlow complete', summary)
    },
    onClose(event) {
      console.log('ImportFlow closed', event)
    },
  })

  // Optional cleanup if your app dynamically removes this view:
  // importflow.destroy()
</script>

See it before you wire it

Open the safe playground and try the full flow for HR Employees.

The playground uses the same browser-side parsing, mapping, review, and inline correction experience while keeping completion safely local.