Employees move through leave type and date selection, then reason entry, then a final review-and-submit step.
Project — Leave Requests
Build a leave request system with a 3-step submit form, manager approval/rejection page, and overlap validation.
What You Are Building
A leave request system where employees submit leave requests and managers approve or reject them. Employees see their own requests. Managers (admin role) see all pending requests and can approve or reject. The request has a status that moves through: draft → submitted → approved / rejected. Cancelled by employee before approval is also allowed.
- Employee / MemberSubmit multi-step leave requests, review personal request history, and cancel eligible items before approval.
- Manager / AdminOpen the manager review page, approve requests, or reject them with a required reason.
How this product should work
The top-level explanation here should make the two sides obvious: employee submission on one side and manager review on the other, with clear state transitions between them.
The employee-facing list shows status history clearly and exposes cancel only where the request is still eligible.
Managers filter requests by status, employee, and leave type, then review all submitted items in one place.
Approve uses confirmation, reject requires a reason, and both actions must disappear once the request leaves submitted status.
Features to Build
| # | Feature | Est. Time |
|---|---|---|
| 1 | Xano tables + endpoints (submit, list, approve, reject, cancel) | 2h |
| 2 | Employee: multi-step submit form (WeWeb) | 2h |
| 3 | Employee: My Requests list page | 1h |
| 4 | Manager: All Requests list page (with approve/reject) | 1h 30m |
| 5 | Status badges + all states | 30m |
1. Xano Database Tables
leave_requests Table
| Column | Type | Rules |
|---|---|---|
| id | int (auto) | PK |
| workspace_id | int | FK → workspaces.id |
| employee_id | int | FK → users.id, set from auth token |
| leave_type | enum | required: 'annual', 'sick', 'personal', 'unpaid' |
| start_date | date | required |
| end_date | date | required, must be >= start_date |
| total_days | int | computed: end_date - start_date + 1 |
| reason | text | optional, max 500 chars |
| status | enum | required: 'submitted', 'approved', 'rejected', 'cancelled', default: 'submitted' |
| reviewed_by | int | FK → users.id, nullable — set on approve/reject |
| reviewed_at | timestamp | nullable |
| review_notes | text | optional — manager note on approve/reject |
| created_at | timestamp | auto |
leave_types Reference Table (optional — or use hardcoded enum)
annual → Annual Leave
sick → Sick Leave
personal → Personal Leave
unpaid → Unpaid Leave
2. Xano Endpoints
POST /api/leave-requests — Submit Request
Auth: Required (any workspace member)
Body: { workspace_id, leave_type, start_date, end_date, reason }
Validation (in order):
1. Auth check
2. Workspace membership check (any member via check_workspace_permission)
3. leave_type is valid
4. start_date is not in the past
5. end_date >= start_date
6. total_days > 0
7. No overlapping approved/submitted request for same employee:
check no existing request where status IN ('submitted','approved')
AND date ranges overlap
Logic:
Set employee_id from auth token
Set total_days = end_date - start_date + 1
Set status = 'submitted'
Insert record
Error codes: VALIDATION_ERROR, BUSINESS_RULE_VIOLATION
Response: { "data": { request } }
GET /api/leave-requests/my — My Requests
Auth: Required
Query params: status (optional filter), page, per_page (default 20)
Logic:
1. Auth check
2. Return requests WHERE employee_id = auth_user_id, ordered by created_at DESC
Response: { "data": [...], "total": N }
GET /api/leave-requests — All Requests (Admin/Manager)
Auth: Required — admin/owner role only
Query params: workspace_id, status (optional), employee_id (optional), page, per_page
Logic:
1. Auth check
2. check_workspace_permission(required_role: 'admin')
3. Return all requests for workspace with filters
Response: { "data": [...], "total": N }
POST /api/leave-requests/{id}/approve — Approve
Auth: Required — admin/owner only
Body: { review_notes (optional) }
Validation:
1. Auth + workspace admin permission
2. Request exists and belongs to workspace
3. Status must be 'submitted' (cannot approve already-approved or rejected)
Logic:
Update status = 'approved', reviewed_by = auth_user_id, reviewed_at = now()
Response: { "data": { updated request } }
Error: BUSINESS_RULE_VIOLATION if not in 'submitted' status
POST /api/leave-requests/{id}/reject — Reject
Same structure as approve, but status = 'rejected'
Body: { review_notes (required on reject — reason must be given) }
Validation: review_notes required for rejection
POST /api/leave-requests/{id}/cancel — Cancel (Employee)
Auth: Required
Validation:
1. Auth check
2. Request belongs to auth user (not just workspace — only own requests)
3. Status must be 'submitted' (cannot cancel approved/rejected)
Logic: Update status = 'cancelled'
Error: FORBIDDEN if not own request; BUSINESS_RULE_VIOLATION if not submitted
Run checklist: New API Endpoint (Xano) for each endpoint · Reusable Function
3. Employee: Multi-Step Submit Form
A 3-step WeWeb form to submit a leave request.
Step 1 — Leave Type & Dates
Fields:
[ ] Leave Type: radio buttons or dropdown (Annual / Sick / Personal / Unpaid)
[ ] Start Date: date picker, min: today
[ ] End Date: date picker, min: start_date (dynamically updates when start_date changes)
[ ] Total Days: computed display (end - start + 1), shown read-only
Validation before Next:
[ ] Leave type selected
[ ] Start date not in the past
[ ] End date >= start date
Step 2 — Reason
Fields:
[ ] Reason: textarea, optional, max 500 chars, show character count
[ ] Summary card: shows selected type, dates, total days (read-only recap)
Validation before Next:
[ ] No validation required — reason is optional
Step 3 — Review & Submit
Progress Indicator
Run checklist: Multi-Step Forms · WeWeb Page Build
4. Employee: My Requests Page
URL: /workspace/{id}/my-leaves
[ ] List of the logged-in employee's own requests only
[ ] Columns: Leave Type · Start Date · End Date · Days · Status badge · Actions
[ ] Actions: Cancel button (only shown when status = 'submitted')
[ ] Filter: Status dropdown
[ ] Status badges: submitted=blue, approved=green, rejected=red, cancelled=grey
[ ] Cancel: confirmation dialog "Cancel this leave request?" + "Yes, Cancel" button
[ ] Pagination: 20 per page
[ ] Empty state: "You have no leave requests." + "Submit a Request" button
STATES:
[ ] Loading skeleton · Empty · Error with retry
5. Manager: All Requests Page
URL: /workspace/{id}/admin/leaves
[ ] Visible only to admin/owner role — 403 state for members
[ ] List of all workspace requests
[ ] Filter: Status · Employee (dropdown of members) · Leave Type
[ ] Active filter chips with X to remove
[ ] Columns: Employee Name · Leave Type · Dates · Days · Submitted On · Status badge · Actions
[ ] Actions per row (when status = 'submitted'):
"Approve" (green button) · "Reject" (red button)
[ ] Clicking Approve: confirmation "Approve [Name]'s [N]-day Annual Leave?"
[ ] Clicking Reject: opens a small modal asking for rejection reason (required)
[ ] After approve/reject: row status badge updates in place, action buttons disappear
[ ] Pagination: 25 per page
[ ] "Showing X–Y of Z requests" total count
STATES: Loading · Empty · Empty (filter) · Error
Run checklist: Permissions & Role-Based UI · Modals & Dialogs
What You Should NOT Do
× Skip the overlapping request check — two approved requests for the same days must be blocked
× Let the cancel endpoint succeed for a request that belongs to another user
× Let the approve/reject endpoint succeed for a member-role user
× Let the employee see all workspace requests — only their own in "my" endpoint
× Submit the form all at once on Step 1 — each step must validate before advancing
× Clear form data when user clicks Back (data must be preserved)
× Show the manager view to a member-role WeWeb user (check role on page load)