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.
What you're building
Attendance tracking needs two things above all else: it must be impossible to accidentally clock in twice in one day, and late detection must be automatic and consistent.
The schema prevents duplicate clock-ins with a unique partial index: UNIQUE (employee_id, date(clock_in)) — only one attendance record per employee per calendar day. If an employee tries to clock in a second time, Supabase returns a constraint violation and the app shows a clear message.
Late detection is a Postgres AFTER INSERT trigger on attendance. The trigger looks up the employee's department to find the standard_start_time. If the NEW.clock_in time (in local time zone) is more than the configured grace_period_minutes after start time, it sets is_late = true on the inserted row. This happens in the same transaction as the INSERT — there is never a window where an attendance record exists without correct late status.
The calendar view uses shadcn/ui's Calendar component but replaces the default day rendering with a custom DayContent that shows a colored dot based on the day's status. This requires fetching the full month's attendance data when the month changes.
Final result
A clean attendance app where clocking in is a single tap, late detection is automatic, and managers get a clear monthly view without manual calculations.
Tech stack
Prerequisites
- 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
Build steps
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.
1Create an attendance tracking schema in Supabase.23Tables:4- departments: id, name, standard_start_time (time, e.g. '09:00:00'), standard_end_time (time), grace_period_minutes (int default 15), created_at5- 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_at6- 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_at78Add a UNIQUE constraint on attendance using: CREATE UNIQUE INDEX idx_attendance_employee_day ON attendance(employee_id, date(clock_in AT TIME ZONE 'UTC')).910Create a Postgres trigger function flag_late_arrival() that fires AFTER INSERT on attendance:111. Fetch the employee's department standard_start_time and grace_period_minutes122. Compare the time portion of NEW.clock_in (in local time) with standard_start_time + grace_period_minutes133. If clock_in time exceeds the grace cutoff, UPDATE attendance SET is_late = true, late_by_minutes = (difference in minutes) WHERE id = NEW.id1415RLS:16- employees: users can SELECT their own row. Managers can SELECT employees in their department. Admins can SELECT/INSERT/UPDATE/DELETE all.17- 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.18- 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.
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.
1Build a clock in/out page at src/pages/Attendance.tsx.23Requirements:4- Fetch today's attendance record for the current user's employee row on mount5- If no record exists (not clocked in yet):6 - Show a large 'Clock In' Button (full width, primary variant) with a Clock icon7 - Show the current time updating live with setInterval every second8 - Show the employee's department name and standard start time9 - Clicking 'Clock In' INSERTs into attendance with clock_in = new Date().toISOString() and the employee_id10 - Handle the unique constraint error gracefully: if insert fails with 'unique' error, show 'You already clocked in today'11- If record exists and clock_out is null (clocked in, not out):12 - Show a green 'Clocked In' Badge with the clock-in time13 - Show a running timer since clock-in14 - Show an is_late Badge if applicable (red 'Late - X minutes')15 - Show a large 'Clock Out' Button (outline variant)16 - Clicking 'Clock Out' UPDATEs the attendance row with clock_out = now()17- If record exists and clock_out is set (completed for today):18 - Show a summary Card: clocked in at X, clocked out at Y, hours worked Z19 - Show a green check with 'Attendance recorded for today'20- 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.
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.
1Build a monthly attendance calendar at src/components/AttendanceCalendar.tsx.23Requirements:4- Accept props: attendanceByDate (Record<string, { status: 'present' | 'late' | 'absent' | 'weekend' }>) where key is 'YYYY-MM-DD'5- Use the shadcn/ui Calendar component with components={{ DayContent }} override6- Custom DayContent renders the day number plus a small colored dot below it:7 - present (on-time): green dot8 - late: amber dot9 - absent (workday with no record): red dot10 - weekend: no dot11- On month change, fetch attendance for the new month from Supabase and rebuild the attendanceByDate map12- On a standalone page src/pages/CalendarView.tsx:13 - Employee view: show their own calendar14 - Manager view: add an employee Select dropdown to switch between team members15 - Show a legend below the calendar (colored dots with labels)16 - Show summary stats for the visible month: Present X, Late X, Absent X daysExpected 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.
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.
1Build an admin attendance page at src/pages/AttendanceAdmin.tsx.23Requirements:4- 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)5- Filter bar above table:6 - Employee multi-select (list from employees table)7 - Department Select8 - Status multi-select (On Time, Late, Absent)9 - Date range Popover with two Calendar pickers (from/to)10 - Clear Filters Button11- Pagination (25 rows per page)12- 'Export CSV' Button that downloads filtered results as a CSV file. Include all visible columns. Filename: 'attendance-{from}-{to}.csv'13- 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.14- 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.
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.
1Add a department analytics section to the admin dashboard at src/pages/AdminDashboard.tsx.23Requirements:4- Date range selector (this week / this month / last 30 days / custom)5- Recharts GroupedBarChart with one group of bars per department:6 - Bar 1: On-time arrivals count (green)7 - Bar 2: Late arrivals count (amber)8 - Bar 3: Absent days count (red)9 - X-axis: department names10 - Y-axis: count of occurrences11 - Tooltip showing exact counts and percentages12- Below the chart, a DataTable ranking departments by on-time percentage: Department, Total Days, On Time %, Late %, Absent %13- Four stat Cards above the chart: Total Employees Active, Average Attendance Rate (%), Late Rate (%), Most Punctual Department14- Add a second chart: LineChart showing overall attendance rate (present+late / total expected) per week for the last 12 weeksExpected 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
1import { useState, useEffect } from 'react'2import { Button } from '@/components/ui/button'3import { Badge } from '@/components/ui/badge'4import { Card, CardContent } from '@/components/ui/card'5import { LogIn, LogOut, CheckCircle } from 'lucide-react'6import { supabase } from '@/integrations/supabase/client'7import { useToast } from '@/components/ui/use-toast'89type Rec = { id: string; clock_in: string; clock_out: string | null; is_late: boolean; late_by_minutes: number | null }1011export function ClockInButton({ employeeId }: { employeeId: string }) {12 const [record, setRecord] = useState<Rec | null>(null)13 const [now, setNow] = useState(new Date())14 const [loading, setLoading] = useState(false)15 const { toast } = useToast()1617 useEffect(() => {18 const today = new Date().toISOString().split('T')[0]19 supabase.from('attendance').select('id,clock_in,clock_out,is_late,late_by_minutes')20 .eq('employee_id', employeeId).gte('clock_in', today).maybeSingle()21 .then(({ data }) => setRecord(data))22 const t = setInterval(() => setNow(new Date()), 1000)23 return () => clearInterval(t)24 }, [employeeId])2526 const clockIn = async () => {27 setLoading(true)28 const { data, error } = await supabase.from('attendance')29 .insert({ employee_id: employeeId, clock_in: new Date().toISOString() }).select().single()30 if (error?.code === '23505') toast({ title: 'Already clocked in today', variant: 'destructive' })31 else if (error) toast({ title: 'Clock in failed', variant: 'destructive' })32 else setRecord(data)33 setLoading(false)34 }3536 const clockOut = async () => {37 if (!record) return38 setLoading(true)39 const { data, error } = await supabase.from('attendance')40 .update({ clock_out: new Date().toISOString() }).eq('id', record.id).select().single()41 if (error) toast({ title: 'Clock out failed', variant: 'destructive' })42 else setRecord(data)43 setLoading(false)44 }4546 const fmt = (iso: string) => new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })47 const elapsed = record?.clock_in && !record.clock_out48 ? Math.floor((now.getTime() - new Date(record.clock_in).getTime()) / 60000) : null4950 if (record?.clock_out) return (51 <Card><CardContent className="pt-6 text-center">52 <CheckCircle className="mx-auto h-12 w-12 text-green-500 mb-2" />53 <p className="font-semibold">Attendance recorded</p>54 <p className="text-sm text-muted-foreground">{fmt(record.clock_in)} – {fmt(record.clock_out)}</p>55 </CardContent></Card>56 )5758 if (record) return (59 <div className="space-y-3">60 <div className="flex gap-2 justify-center">61 <Badge>Clocked in {fmt(record.clock_in)}</Badge>62 {record.is_late && <Badge variant="destructive">Late {record.late_by_minutes}m</Badge>}63 </div>64 {elapsed !== null && <p className="text-center text-muted-foreground text-sm">{Math.floor(elapsed/60)}h {elapsed%60}m</p>}65 <Button onClick={clockOut} disabled={loading} className="w-full" variant="outline">66 <LogOut className="mr-2 h-4 w-4" /> Clock Out67 </Button>68 </div>69 )7071 return (72 <div className="space-y-3">73 <p className="text-center text-2xl font-mono">{now.toLocaleTimeString()}</p>74 <Button onClick={clockIn} disabled={loading} className="w-full" size="lg">75 <LogIn className="mr-2 h-4 w-4" /> Clock In76 </Button>77 </div>78 )79}Customization ideas
QR code clock-in for shared devices
Generate a unique QR code per employee (encoding their employee_id plus a rotating daily token). Mount a shared tablet at the office entrance. Employees scan their QR code to clock in without typing anything. Validate the token server-side in an Edge Function before creating the attendance record.
Location verification for remote teams
Use the browser Geolocation API to capture coordinates when clocking in. Add a locations table with approved office addresses and a radius in meters. Compare the clock-in coordinates against approved locations in the Edge Function. Flag clock-ins that are outside the approved radius for manager review.
Overtime calculation
Add an overtime_hours computed column to attendance: MAX(0, hours_worked - 8). Add an overtime_multiplier to departments (e.g. 1.5). Build a payroll summary page that calculates regular hours, overtime hours, and estimated pay for each employee for a selected pay period.
Leave management integration
Add a leave_requests table with types (vacation, sick, personal) and status (pending, approved, rejected). When calculating absent days on the attendance report, check if the employee has an approved leave request for that day. Approved leaves show as a distinct 'leave' dot color on the calendar instead of 'absent'.
Attendance notification emails
Set up a pg_cron job at 10am every workday. It queries employees who have not clocked in yet and their manager's email. Send reminder emails via an Edge Function calling Resend. Include a link to the clock-in page. This reduces manual follow-up for managers.
Common pitfalls
Pitfall: Allowing employees to clock in multiple times per day
How to avoid: 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.
Pitfall: Calculating absent days from the absence of attendance records
How to avoid: 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.
Pitfall: Storing clock-in times in the user's local time zone
How to avoid: 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.
Pitfall: Exposing employee data across departments to all managers
How to avoid: 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.
AI prompts to try
Copy these prompts to build this project faster.
I'm building an attendance app in Supabase. I have an attendance table with columns employee_id, clock_in (timestamptz), clock_out, is_late (bool). I need a Postgres trigger function flag_late_arrival() that fires AFTER INSERT on attendance. It should look up the employee's department, find the standard_start_time and grace_period_minutes, compare clock_in (converted to the department's time zone) against the cutoff, and UPDATE the new row's is_late and late_by_minutes columns if the employee is late. Show me the full plpgsql function.
Add a 'Team Today' view to the admin dashboard. Show all employees organized by department as a grid of status Cards. Each card shows the employee's name, their current status today (clocked in, clocked out, not clocked in), their clock-in time if applicable, and a late badge if flagged. Auto-refresh every 5 minutes. This gives managers a live room view of who is in the office without checking individual records.
In Supabase, write a SQL function get_monthly_attendance_summary(p_employee_id uuid, p_year int, p_month int) that returns a table of (work_day date, clock_in timestamptz, clock_out timestamptz, hours_worked decimal, is_late bool, late_by_minutes int, status text). Generate all workdays (Mon-Fri) in the given month using generate_series, LEFT JOIN attendance for each day, and set status to 'present', 'late', or 'absent' accordingly. Use this as a Supabase RPC function for the calendar view and CSV export.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation