# How to Build an Attendance App with Lovable

- Tool: How to Build with Lovable
- Difficulty: Beginner
- Compatibility: Lovable free tier or higher
- Last updated: April 2026

## TL;DR

Build an employee attendance app in Lovable where staff clock in and out with one click, a Postgres trigger flags late arrivals automatically, a calendar shows color-coded daily status dots, a monthly DataTable handles payroll exports, and department Recharts bar charts show attendance patterns — all backed by Supabase with role-based access.

## Before you start

- Lovable account (free tier is sufficient for this build)
- Supabase project with SUPABASE_URL and SUPABASE_ANON_KEY available
- A list of your departments and their standard start times
- Decision on grace period for late detection (common: 15 minutes)
- Employee email addresses for Supabase Auth invitations

## Step-by-step guide

### 1. Create the attendance schema with late detection trigger

Ask Lovable to create the tables and the trigger. The trigger is the key piece — it must run immediately on every clock-in to set the late flag automatically.

```
Create an attendance tracking schema in Supabase.

Tables:
- departments: id, name, standard_start_time (time, e.g. '09:00:00'), standard_end_time (time), grace_period_minutes (int default 15), created_at
- employees: id, user_id (references auth.users, unique), first_name, last_name, email, department_id (references departments), role ('employee' | 'manager' | 'admin'), employment_type ('full_time' | 'part_time' | 'contractor'), is_active (bool default true), created_at
- attendance: id, employee_id (references employees), clock_in (timestamptz), clock_out (timestamptz, nullable), is_late (bool default false), late_by_minutes (int, nullable), notes (text), created_at

Add a UNIQUE constraint on attendance using: CREATE UNIQUE INDEX idx_attendance_employee_day ON attendance(employee_id, date(clock_in AT TIME ZONE 'UTC')).

Create a Postgres trigger function flag_late_arrival() that fires AFTER INSERT on attendance:
1. Fetch the employee's department standard_start_time and grace_period_minutes
2. Compare the time portion of NEW.clock_in (in local time) with standard_start_time + grace_period_minutes
3. If clock_in time exceeds the grace cutoff, UPDATE attendance SET is_late = true, late_by_minutes = (difference in minutes) WHERE id = NEW.id

RLS:
- employees: users can SELECT their own row. Managers can SELECT employees in their department. Admins can SELECT/INSERT/UPDATE/DELETE all.
- attendance: employees can SELECT their own rows and INSERT their own (employee_id must match their employees row). Managers can SELECT their department. Admins full access.
- departments: authenticated users SELECT, admin INSERT/UPDATE/DELETE.
```

> Pro tip: The unique constraint uses date(clock_in AT TIME ZONE 'UTC'). If your employees are in multiple time zones, consider passing the local date from the client and storing it in a separate clock_in_date (date) column. This avoids off-by-one errors at midnight in each time zone.

**Expected result:** Tables are created. The unique index prevents two clock-in records for the same employee on the same date. Inserting an attendance row at 9:20am for a department starting at 9:00am with 15-minute grace sets is_late=true and late_by_minutes=5.

### 2. Build the clock in and clock out UI

The employee's main view is simple: a large clock-in button if they haven't clocked in today, or their clock-in time and a clock-out button if they have. Ask Lovable to build it.

```
Build a clock in/out page at src/pages/Attendance.tsx.

Requirements:
- Fetch today's attendance record for the current user's employee row on mount
- If no record exists (not clocked in yet):
  - Show a large 'Clock In' Button (full width, primary variant) with a Clock icon
  - Show the current time updating live with setInterval every second
  - Show the employee's department name and standard start time
  - Clicking 'Clock In' INSERTs into attendance with clock_in = new Date().toISOString() and the employee_id
  - Handle the unique constraint error gracefully: if insert fails with 'unique' error, show 'You already clocked in today'
- If record exists and clock_out is null (clocked in, not out):
  - Show a green 'Clocked In' Badge with the clock-in time
  - Show a running timer since clock-in
  - Show an is_late Badge if applicable (red 'Late - X minutes')
  - Show a large 'Clock Out' Button (outline variant)
  - Clicking 'Clock Out' UPDATEs the attendance row with clock_out = now()
- If record exists and clock_out is set (completed for today):
  - Show a summary Card: clocked in at X, clocked out at Y, hours worked Z
  - Show a green check with 'Attendance recorded for today'
- Add a notes Input below the buttons (optional, saves on clock-out)
```

**Expected result:** The page shows the correct state based on today's attendance. Clocking in creates the record and switches to the timer view. Clocking out updates the record and shows the day summary.

### 3. Build the monthly calendar view

Managers need a calendar that shows each day's attendance status at a glance with color-coded dots. Ask Lovable to build a custom calendar component.

```
Build a monthly attendance calendar at src/components/AttendanceCalendar.tsx.

Requirements:
- Accept props: attendanceByDate (Record<string, { status: 'present' | 'late' | 'absent' | 'weekend' }>) where key is 'YYYY-MM-DD'
- Use the shadcn/ui Calendar component with components={{ DayContent }} override
- Custom DayContent renders the day number plus a small colored dot below it:
  - present (on-time): green dot
  - late: amber dot
  - absent (workday with no record): red dot
  - weekend: no dot
- On month change, fetch attendance for the new month from Supabase and rebuild the attendanceByDate map
- On a standalone page src/pages/CalendarView.tsx:
  - Employee view: show their own calendar
  - Manager view: add an employee Select dropdown to switch between team members
  - Show a legend below the calendar (colored dots with labels)
  - Show summary stats for the visible month: Present X, Late X, Absent X days
```

**Expected result:** The calendar renders with colored dots. The month navigation fetches new data. The employee selector (manager view) updates the calendar to show the selected employee's data.

### 4. Build the admin attendance DataTable with CSV export

Admins and managers need a filterable table of all attendance records for payroll and HR purposes. Ask Lovable to build it with export functionality.

```
Build an admin attendance page at src/pages/AttendanceAdmin.tsx.

Requirements:
- DataTable using TanStack Table with columns: Employee Name, Department, Date, Clock In, Clock Out, Hours Worked (calculated), Status Badge (On Time=green, Late=amber, Absent=red, No Clock Out=gray), Late By (minutes, only for late records)
- Filter bar above table:
  - Employee multi-select (list from employees table)
  - Department Select
  - Status multi-select (On Time, Late, Absent)
  - Date range Popover with two Calendar pickers (from/to)
  - Clear Filters Button
- Pagination (25 rows per page)
- 'Export CSV' Button that downloads filtered results as a CSV file. Include all visible columns. Filename: 'attendance-{from}-{to}.csv'
- For absent days: these are not rows in the attendance table. Generate them by finding all work days (Mon-Fri) in the date range where no attendance record exists for each active employee.
- Protect this page: only manager and admin roles can access it. Redirect others to /attendance.
```

**Expected result:** The DataTable shows filtered attendance records. The export button downloads a CSV with all displayed columns. Absent days appear correctly as rows despite having no attendance record. Role check prevents non-admin access.

### 5. Build the department attendance chart

A bar chart comparing attendance patterns across departments gives managers a quick overview. Ask Lovable to add it to the admin dashboard.

```
Add a department analytics section to the admin dashboard at src/pages/AdminDashboard.tsx.

Requirements:
- Date range selector (this week / this month / last 30 days / custom)
- Recharts GroupedBarChart with one group of bars per department:
  - Bar 1: On-time arrivals count (green)
  - Bar 2: Late arrivals count (amber)
  - Bar 3: Absent days count (red)
  - X-axis: department names
  - Y-axis: count of occurrences
  - Tooltip showing exact counts and percentages
- Below the chart, a DataTable ranking departments by on-time percentage: Department, Total Days, On Time %, Late %, Absent %
- Four stat Cards above the chart: Total Employees Active, Average Attendance Rate (%), Late Rate (%), Most Punctual Department
- Add a second chart: LineChart showing overall attendance rate (present+late / total expected) per week for the last 12 weeks
```

**Expected result:** The grouped bar chart renders with correct data per department. The ranking table shows percentages. Stat cards show aggregated metrics. Changing the date range updates all charts and stats.

## Complete code example

File: `src/components/ClockInButton.tsx`

```typescript
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { LogIn, LogOut, CheckCircle } from 'lucide-react'
import { supabase } from '@/integrations/supabase/client'
import { useToast } from '@/components/ui/use-toast'

type Rec = { id: string; clock_in: string; clock_out: string | null; is_late: boolean; late_by_minutes: number | null }

export function ClockInButton({ employeeId }: { employeeId: string }) {
  const [record, setRecord] = useState<Rec | null>(null)
  const [now, setNow] = useState(new Date())
  const [loading, setLoading] = useState(false)
  const { toast } = useToast()

  useEffect(() => {
    const today = new Date().toISOString().split('T')[0]
    supabase.from('attendance').select('id,clock_in,clock_out,is_late,late_by_minutes')
      .eq('employee_id', employeeId).gte('clock_in', today).maybeSingle()
      .then(({ data }) => setRecord(data))
    const t = setInterval(() => setNow(new Date()), 1000)
    return () => clearInterval(t)
  }, [employeeId])

  const clockIn = async () => {
    setLoading(true)
    const { data, error } = await supabase.from('attendance')
      .insert({ employee_id: employeeId, clock_in: new Date().toISOString() }).select().single()
    if (error?.code === '23505') toast({ title: 'Already clocked in today', variant: 'destructive' })
    else if (error) toast({ title: 'Clock in failed', variant: 'destructive' })
    else setRecord(data)
    setLoading(false)
  }

  const clockOut = async () => {
    if (!record) return
    setLoading(true)
    const { data, error } = await supabase.from('attendance')
      .update({ clock_out: new Date().toISOString() }).eq('id', record.id).select().single()
    if (error) toast({ title: 'Clock out failed', variant: 'destructive' })
    else setRecord(data)
    setLoading(false)
  }

  const fmt = (iso: string) => new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
  const elapsed = record?.clock_in && !record.clock_out
    ? Math.floor((now.getTime() - new Date(record.clock_in).getTime()) / 60000) : null

  if (record?.clock_out) return (
    <Card><CardContent className="pt-6 text-center">
      <CheckCircle className="mx-auto h-12 w-12 text-green-500 mb-2" />
      <p className="font-semibold">Attendance recorded</p>
      <p className="text-sm text-muted-foreground">{fmt(record.clock_in)} – {fmt(record.clock_out)}</p>
    </CardContent></Card>
  )

  if (record) return (
    <div className="space-y-3">
      <div className="flex gap-2 justify-center">
        <Badge>Clocked in {fmt(record.clock_in)}</Badge>
        {record.is_late && <Badge variant="destructive">Late {record.late_by_minutes}m</Badge>}
      </div>
      {elapsed !== null && <p className="text-center text-muted-foreground text-sm">{Math.floor(elapsed/60)}h {elapsed%60}m</p>}
      <Button onClick={clockOut} disabled={loading} className="w-full" variant="outline">
        <LogOut className="mr-2 h-4 w-4" /> Clock Out
      </Button>
    </div>
  )

  return (
    <div className="space-y-3">
      <p className="text-center text-2xl font-mono">{now.toLocaleTimeString()}</p>
      <Button onClick={clockIn} disabled={loading} className="w-full" size="lg">
        <LogIn className="mr-2 h-4 w-4" /> Clock In
      </Button>
    </div>
  )
}
```

## Common mistakes

- **Allowing employees to clock in multiple times per day** — Without the unique constraint, an employee could accidentally double-click the button or navigate back and create two records. Payroll calculations then show double hours. Fix: The UNIQUE index on (employee_id, date(clock_in)) enforces exactly one attendance record per employee per day at the database level. Handle the '23505' unique violation error code in the client to show a friendly message.
- **Calculating absent days from the absence of attendance records** — Absence is defined by something not happening, which is harder to query than something that did happen. A simple SELECT from attendance never returns absent days. Fix: Generate absent days in your query by cross-joining a date series (generate_series of workdays) with the employees list, then LEFT JOINing attendance. Rows where the attendance columns are null represent absences. Do this in a Postgres function for performance.
- **Storing clock-in times in the user's local time zone** — If employees are in different time zones, or if servers are in a different zone than employees, time comparisons break. 9:00am in London is different from 9:00am in New York. Fix: Always store timestamps in UTC (Supabase timestamptz always stores in UTC). Convert to local time only for display purposes. The late detection trigger should convert to the department's time zone for comparison using AT TIME ZONE.
- **Exposing employee data across departments to all managers** — A manager in the Sales department should not see attendance records for the Engineering department. Without proper RLS, all attendance data is visible to all managers. Fix: The RLS policy for managers should be: auth.uid() IN (SELECT user_id FROM employees WHERE department_id = (SELECT department_id FROM employees WHERE id = attendance.employee_id) AND role = 'manager'). Admins bypass this with a separate admin role check.

## Best practices

- Store all timestamps in UTC. Convert to the user's local time zone only in the display layer. Never store 'local' timestamps as they become ambiguous when time zones change.
- Add the unique index on (employee_id, date(clock_in)) before any data is inserted. Adding it later to a table with duplicate records will fail.
- Handle the clock-in button's loading state by disabling it immediately on click and showing a spinner. Double-clicks on slow connections are a common source of duplicate records.
- For the calendar absent day calculation, compute it in a Postgres function rather than client-side. Generating a date series and left-joining in SQL is faster and simpler than doing it in JavaScript.
- Show the employee's name prominently on the clock-in page. If employees share devices, this confirms they are clocking in as themselves.
- Add a notes field to attendance records for optional context. This is useful for documenting why someone was late (car trouble, doctor appointment) without requiring a formal leave request.

## Frequently asked questions

### Can employees clock in on their phones?

Yes. Lovable apps are fully responsive. The clock-in page works on any mobile browser without installing an app. The large clock-in button is touch-friendly and the live clock updates correctly on mobile. For dedicated mobile access, the app can be added to the home screen as a PWA using Lovable's publish settings.

### What time zone should I use for the late detection trigger?

Store the department's time zone as a column in the departments table (e.g. 'America/New_York'). In the trigger, use clock_in AT TIME ZONE departments.timezone to convert the UTC timestamp before comparing it to standard_start_time. This ensures accurate late detection regardless of where the database server is located.

### How do I handle employees who work night shifts?

Night shifts cross midnight, which the default schema does not handle well. For night shift employees, add a shift_type column to employees and adjust the unique index to be based on the shift start date rather than the clock_in date. The late detection trigger needs to compare against the shift's start time, not the calendar day's start time.

### Can managers edit attendance records to fix mistakes?

Yes. Build an edit dialog on the admin DataTable that allows updating clock_in and clock_out timestamps. Add an edited_by and edited_at column to track who changed the record and when. Require managers to add a notes reason when editing. This audit trail is essential for payroll disputes.

### How does the absent day calculation work for employees hired mid-month?

The absent day query uses the date series from the start of the month (or the employee's created_at date, whichever is later) to the end of the month. Compare the employee's hire date before including earlier days in the absent count. Ask Lovable to add this logic to the get_monthly_attendance_summary function.

### What is the best way to onboard a team of 50 employees?

Use Supabase Auth's invite flow: create employees in bulk by uploading a CSV of email addresses. Each employee receives an invitation email to set their password and activate their account. Their employee record in the employees table is created automatically via a trigger on auth.users INSERT that reads their email to populate the row.

### Does this app work for remote teams without a fixed office?

Yes with some adjustments. For remote teams, remove the late detection requirement or make it optional per employee. Add a work_from ('office' | 'remote' | 'client_site') column to attendance so employees can indicate where they are working. The calendar view and reports include this field so managers can track remote vs in-office days.

### Is there help building a more complex attendance or workforce management system?

RapidDev builds Lovable apps with biometric integrations, complex shift patterns, payroll system connections, and multi-location support. Reach out if your attendance system needs features beyond this guide.

---

Source: https://www.rapidevelopers.com/how-to-build-lovable/attendance-app
© RapidDev — https://www.rapidevelopers.com/how-to-build-lovable/attendance-app
