π Build Complete CRM HRM Attendance System 2026: Multi-Tech Stack Guide
Step-by-step guide to build a professional attendance management portal with PHP Laravel, React, .NET, jQuery, and Bootstrap. Includes database design, multi-tenancy, and toast notifications.
Blogs Team
Development Experts β’ 2026 Edition
1 System Overview: What We're Building
A complete CRM HRM Attendance System that combines employee management, attendance tracking, leave management, and reporting in one platform. Available in multiple tech stacks.
π Key Features
- Employee check-in/out with photo
- Geo-fencing location validation
- Leave request & approval workflow
- Real-time attendance dashboard
- Automated payroll reports
- Multi-tenant SaaS architecture
- Toast notifications
- Role-based access control
Perfect for: HR departments, remote teams,δΈε° businesses, and SaaS startups wanting to build their own attendance solution.
π Tech Stack Options
PHP Laravel, .NET Core, Node.js
React, jQuery, Bootstrap 5
MySQL, PostgreSQL, SQL Server
Shared, VPS, Cloud (AWS/Azure)
2 Database Design: MySQL Schema
π Core Tables
-- Users table
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
employee_id VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
department_id INT,
role ENUM('admin', 'manager', 'employee') DEFAULT 'employee',
status ENUM('active', 'inactive') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Departments
CREATE TABLE departments (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
manager_id INT
);
-- Attendance
CREATE TABLE attendance (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
date DATE NOT NULL,
check_in DATETIME NOT NULL,
check_out DATETIME,
status ENUM('present', 'absent', 'late', 'half-day'),
working_hours DECIMAL(5,2),
check_in_photo VARCHAR(255),
check_in_location VARCHAR(255),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Leave requests
CREATE TABLE leave_requests (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
leave_type ENUM('annual', 'sick', 'personal') NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
reason TEXT,
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
π Relationships
- users.department_id β departments.id
- attendance.user_id β users.id
- leave_requests.user_id β users.id
- departments.manager_id β users.id
π Sample Query
-- Get today's attendance summary
SELECT
d.name as department,
COUNT(u.id) as total,
SUM(CASE WHEN a.status = 'present' THEN 1 END) as present,
SUM(CASE WHEN a.status = 'late' THEN 1 END) as late,
SUM(CASE WHEN a.status = 'absent' THEN 1 END) as absent
FROM departments d
LEFT JOIN users u ON u.department_id = d.id
LEFT JOIN attendance a ON a.user_id = u.id
AND a.date = CURDATE()
GROUP BY d.id;
3 PHP Laravel Implementation
π¦ Installation
# Create Laravel project composer create-project laravel/laravel attendance-system # Install required packages composer require laravel/sanctum composer require spatie/laravel-permission composer require maatwebsite/excel # Generate models and migrations php artisan make:model Attendance -m php artisan make:model LeaveRequest -m php artisan make:controller API/AttendanceController
π Attendance Model
<?php
// app/Models/Attendance.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Attendance extends Model
{
protected $fillable = [
'user_id', 'date', 'check_in', 'check_out',
'status', 'working_hours', 'check_in_photo',
'check_in_location', 'check_in_ip'
];
protected $casts = [
'date' => 'date',
'check_in' => 'datetime',
'check_out' => 'datetime'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function scopeToday($query)
{
return $query->whereDate('date', today());
}
}
π― Check-in Controller
<?php
// app/Http/Controllers/API/AttendanceController.php
namespace App\Http\Controllers\API;
use App\Models\Attendance;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
class AttendanceController extends Controller
{
public function checkIn(Request $request)
{
$request->validate([
'photo' => 'required|image|max:2048',
'latitude' => 'required|numeric',
'longitude' => 'required|numeric'
]);
// Check if already checked in today
$existing = Attendance::where('user_id', auth()->id())
->whereDate('date', Carbon::today())
->first();
if ($existing) {
return response()->json([
'message' => 'Already checked in today'
], 400);
}
// Save photo
$photoPath = $request->file('photo')
->store('attendance/' . auth()->id(), 'public');
// Create attendance record
$attendance = Attendance::create([
'user_id' => auth()->id(),
'date' => Carbon::today(),
'check_in' => Carbon::now(),
'status' => $this->determineStatus(),
'check_in_photo' => $photoPath,
'check_in_location' => json_encode([
'lat' => $request->latitude,
'lng' => $request->longitude
]),
'check_in_ip' => $request->ip()
]);
return response()->json([
'message' => 'Check-in successful',
'data' => $attendance
]);
}
private function determineStatus()
{
$officeStart = Carbon::createFromTime(9, 0, 0); // 9 AM
$now = Carbon::now();
if ($now->lte($officeStart)) {
return 'present';
} elseif ($now->lte($officeStart->copy()->addMinutes(15))) {
return 'present'; // Grace period
} else {
return 'late';
}
}
}
4 React.js Frontend
βοΈ Check-in Component
// components/CheckIn.jsx
import React, { useState, useRef } from 'react';
import axios from 'axios';
import Webcam from 'react-webcam';
function CheckIn() {
const [loading, setLoading] = useState(false);
const [location, setLocation] = useState(null);
const webcamRef = useRef(null);
const getLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setLocation({
lat: position.coords.latitude,
lng: position.coords.longitude
});
},
(error) => {
alert('Please enable location access');
}
);
}
};
const capturePhoto = () => {
const imageSrc = webcamRef.current.getScreenshot();
return dataURLtoFile(imageSrc, 'checkin.jpg');
};
const dataURLtoFile = (dataurl, filename) => {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
};
const handleCheckIn = async () => {
setLoading(true);
const photo = capturePhoto();
const formData = new FormData();
formData.append('photo', photo);
formData.append('latitude', location.lat);
formData.append('longitude', location.lng);
try {
const response = await axios.post('/api/attendance/checkin',
formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
alert('Check-in successful!');
} catch (error) {
alert(error.response?.data?.message || 'Check-in failed');
} finally {
setLoading(false);
}
};
return (
<div className="card p-4">
<h3>Attendance Check-in</h3>
<Webcam
ref={webcamRef}
screenshotFormat="image/jpeg"
className="w-100 mb-3 rounded"
/>
<button
onClick={getLocation}
className="btn btn-info mb-3"
disabled={location}
>
{location ? 'Location Captured β' : 'Get Location'}
</button>
<button
onClick={handleCheckIn}
className="btn btn-primary btn-lg"
disabled={!location || loading}
>
{loading ? 'Processing...' : 'Check In'}
</button>
</div>
);
}
export default CheckIn;
π Dashboard Component
// components/Dashboard.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
function Dashboard() {
const [summary, setSummary] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchSummary();
}, []);
const fetchSummary = async () => {
try {
const response = await axios.get('/api/attendance/summary');
setSummary(response.data);
} catch (error) {
console.error('Failed to fetch summary', error);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
const chartData = {
labels: summary?.departments.map(d => d.name),
datasets: [
{
label: 'Present',
data: summary?.departments.map(d => d.present),
backgroundColor: '#10b981'
},
{
label: 'Late',
data: summary?.departments.map(d => d.late),
backgroundColor: '#f59e0b'
},
{
label: 'Absent',
data: summary?.departments.map(d => d.absent),
backgroundColor: '#ef4444'
}
]
};
const options = {
responsive: true,
plugins: {
legend: {
position: 'top'
},
title: {
display: true,
text: 'Daily Attendance Summary'
}
}
};
return (
<div className="dashboard">
<div className="row mb-4">
<div className="col-md-3">
<div className="card bg-success text-white">
<div className="card-body">
<h5>Present</h5>
<h2>{summary?.total.present}</h2>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card bg-warning text-white">
<div className="card-body">
<h5>Late</h5>
<h2>{summary?.total.late}</h2>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card bg-danger text-white">
<div className="card-body">
<h5>Absent</h5>
<h2>{summary?.total.absent}</h2>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card bg-info text-white">
<div className="card-body">
<h5>On Leave</h5>
<h2>{summary?.total.leave}</h2>
</div>
</div>
</div>
</div>
<div className="card p-4">
<Bar options={options} data={chartData} />
</div>
</div>
);
}
export default Dashboard;
5 .NET Core Web API
π· Model Class
// Models/Attendance.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AttendanceSystem.Models
{
public class Attendance
{
[Key]
public int Id { get; set; }
[Required]
public string UserId { get; set; }
[ForeignKey("UserId")]
public virtual ApplicationUser User { get; set; }
[Required]
public DateTime Date { get; set; }
public DateTime CheckIn { get; set; }
public DateTime? CheckOut { get; set; }
[StringLength(20)]
public string Status { get; set; }
[Column(TypeName = "decimal(5,2)")]
public decimal WorkingHours { get; set; }
public string CheckInPhoto { get; set; }
public string CheckInLocation { get; set; }
public string CheckInIP { get; set; }
}
}
π― API Controller
// Controllers/AttendanceController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using AttendanceSystem.Data;
using AttendanceSystem.Models;
using System.Security.Claims;
namespace AttendanceSystem.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AttendanceController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly IWebHostEnvironment _env;
public AttendanceController(
ApplicationDbContext context,
IWebHostEnvironment env)
{
_context = context;
_env = env;
}
[HttpPost("checkin")]
public async Task CheckIn(
[FromForm] CheckInDto model)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// Check existing
var existing = await _context.Attendances
.AnyAsync(a => a.UserId == userId &&
a.Date == DateTime.Today);
if (existing)
{
return BadRequest(new {
message = "Already checked in today"
});
}
// Save photo
string photoPath = null;
if (model.Photo != null)
{
string uploadsFolder = Path.Combine(
_env.WebRootPath,
"uploads",
"attendance"
);
Directory.CreateDirectory(uploadsFolder);
string fileName = Guid.NewGuid().ToString()
+ Path.GetExtension(model.Photo.FileName);
string filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await model.Photo.CopyToAsync(stream);
}
photoPath = Path.Combine("uploads", "attendance", fileName);
}
var attendance = new Attendance
{
UserId = userId,
Date = DateTime.Today,
CheckIn = DateTime.Now,
Status = DetermineStatus(),
CheckInPhoto = photoPath,
CheckInLocation = $"{model.Latitude},{model.Longitude}",
CheckInIP = HttpContext.Connection.RemoteIpAddress?.ToString()
};
_context.Attendances.Add(attendance);
await _context.SaveChangesAsync();
return Ok(new
{
message = "Check-in successful",
data = attendance
});
}
private string DetermineStatus()
{
var officeStart = new TimeSpan(9, 0, 0);
var now = DateTime.Now.TimeOfDay;
if (now <= officeStart.Add(TimeSpan.FromMinutes(15)))
return "present";
return now <= officeStart.Add(TimeSpan.FromMinutes(30))
? "late" : "absent";
}
}
}
6 jQuery + Bootstrap Quick Implementation
π HTML Structure
<!-- attendance.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Attendance System</title>
<!-- Bootstrap 5 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="container mt-5">
<div class="row">
<div class="col-md-6 mx-auto">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Employee Check-in</h3>
</div>
<div class="card-body">
<video id="video" class="w-100 mb-3 rounded" autoplay></video>
<canvas id="canvas" style="display:none;"></canvas>
<button id="captureBtn" class="btn btn-info w-100 mb-3">
<i class="fas fa-camera me-2"></i>Capture Photo
</button>
<button id="locationBtn" class="btn btn-warning w-100 mb-3">
<i class="fas fa-map-marker-alt me-2"></i>Get Location
</button>
<button id="checkinBtn" class="btn btn-success btn-lg w-100" disabled>
<i class="fas fa-fingerprint me-2"></i>Check In
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
π jQuery AJAX
<script>
$(document).ready(function() {
let photoData = null;
let locationData = null;
// Initialize camera
navigator.mediaDevices.getUserMedia({ video: true })
.then(function(stream) {
$('#video')[0].srcObject = stream;
})
.catch(function(error) {
alert('Camera access denied');
});
// Capture photo
$('#captureBtn').click(function() {
const video = $('#video')[0];
const canvas = $('#canvas')[0];
const context = canvas.getContext('2d');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
context.drawImage(video, 0, 0, canvas.width, canvas.height);
photoData = canvas.toDataURL('image/jpeg');
$(this).html('<i class="fas fa-check me-2"></i>Photo Captured').removeClass('btn-info').addClass('btn-success');
enableCheckIn();
});
// Get location
$('#locationBtn').click(function() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function(position) {
locationData = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
$(this).html('<i class="fas fa-check me-2"></i>Location Captured')
.removeClass('btn-warning')
.addClass('btn-success');
enableCheckIn();
}.bind(this),
function(error) {
alert('Please enable location access');
}
);
}
});
function enableCheckIn() {
if (photoData && locationData) {
$('#checkinBtn').prop('disabled', false);
}
}
// Check in
$('#checkinBtn').click(function() {
const btn = $(this);
btn.html('<i class="fas fa-spinner fa-spin me-2"></i>Processing...').prop('disabled', true);
// Convert base64 to blob
const block = photoData.split(';');
const contentType = block[0].split(':')[1];
const realData = block[1].split(',')[1];
const blob = b64toBlob(realData, contentType);
const formData = new FormData();
formData.append('photo', blob, 'checkin.jpg');
formData.append('latitude', locationData.lat);
formData.append('longitude', locationData.lng);
$.ajax({
url: '/api/attendance/checkin.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
alert('Check-in successful!');
location.reload();
},
error: function(xhr) {
alert(xhr.responseJSON?.message || 'Check-in failed');
btn.html('<i class="fas fa-fingerprint me-2"></i>Check In').prop('disabled', false);
}
});
});
function b64toBlob(b64Data, contentType) {
const sliceSize = 512;
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: contentType });
}
});
</script>
7 SaaS Multi-Tenant Architecture
π’ Tenants Table
-- Tenants table for SaaS
CREATE TABLE tenants (
id INT PRIMARY KEY AUTO_INCREMENT,
company_name VARCHAR(255) NOT NULL,
subdomain VARCHAR(100) UNIQUE NOT NULL,
custom_domain VARCHAR(255) UNIQUE,
email VARCHAR(255) NOT NULL,
plan ENUM('basic', 'pro', 'enterprise') DEFAULT 'basic',
status ENUM('active', 'suspended') DEFAULT 'active',
settings JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Add tenant_id to all tables
ALTER TABLE users ADD COLUMN tenant_id INT NOT NULL;
ALTER TABLE departments ADD COLUMN tenant_id INT NOT NULL;
ALTER TABLE attendance ADD COLUMN tenant_id INT NOT NULL;
-- Subscription plans
CREATE TABLE subscriptions (
id INT PRIMARY KEY AUTO_INCREMENT,
tenant_id INT NOT NULL,
plan_id VARCHAR(50) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
amount DECIMAL(10,2),
status ENUM('active', 'expired') DEFAULT 'active',
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
π Laravel Tenant Middleware
<?php
// app/Http/Middleware/TenantMiddleware.php
namespace App\Http\Middleware;
use Closure;
use App\Models\Tenant;
class TenantMiddleware
{
public function handle($request, Closure $next)
{
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
// Skip main domain
if (in_array($subdomain, ['app', 'www', 'demo'])) {
return $next($request);
}
// Find tenant
$tenant = Tenant::where('subdomain', $subdomain)
->where('status', 'active')
->first();
if (!$tenant) {
abort(404, 'Tenant not found');
}
// Check subscription
if (!$tenant->hasActiveSubscription()) {
return redirect()->route('subscription.expired');
}
// Bind tenant to request
$request->merge(['tenant' => $tenant]);
app()->instance('current_tenant', $tenant);
return $next($request);
}
}
π Plan Features Configuration
<?php
// config/plans.php
return [
'basic' => [
'name' => 'Basic',
'price' => 29,
'features' => [
'max_employees' => 50,
'attendance_tracking' => true,
'leave_management' => true,
'reports' => 'basic',
'api_access' => false,
'support' => 'email'
]
],
'pro' => [
'name' => 'Professional',
'price' => 79,
'features' => [
'max_employees' => 500,
'attendance_tracking' => true,
'leave_management' => true,
'reports' => 'advanced',
'api_access' => true,
'biometric_integration' => true,
'support' => 'priority'
]
],
'enterprise' => [
'name' => 'Enterprise',
'price' => 199,
'features' => [
'max_employees' => 'unlimited',
'attendance_tracking' => true,
'leave_management' => true,
'reports' => 'custom',
'api_access' => true,
'biometric_integration' => true,
'dedicated_support' => true,
'custom_branding' => true
]
]
];
8 Toast Notifications System
π Vanilla JavaScript Toast
// toast.js
class Toast {
constructor() {
this.container = document.createElement('div');
this.container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
`;
document.body.appendChild(this.container);
}
show(message, type = 'info', duration = 3000) {
const toast = document.createElement('div');
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6'
};
const icons = {
success: 'β',
error: 'β',
warning: 'β ',
info: 'βΉ'
};
toast.style.cssText = `
background: white;
border-left: 4px solid ${colors[type]};
border-radius: 8px;
padding: 12px 20px;
margin-bottom: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
display: flex;
align-items: center;
gap: 10px;
min-width: 300px;
animation: slideIn 0.3s ease;
`;
toast.innerHTML = `
<span style="font-weight: bold; color: ${colors[type]}">
${icons[type]}
</span>
<span style="flex: 1">${message}</span>
<button onclick="this.parentElement.remove()"
style="background: none; border: none; cursor: pointer; font-size: 18px">
Γ
</button>
`;
this.container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, duration);
}
}
// Add animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
const toast = new Toast();
βοΈ React Toast Hook
// hooks/useToast.js
import { useState, useCallback } from 'react';
export const useToast = () => {
const [toasts, setToasts] = useState([]);
const addToast = useCallback((message, type = 'info', duration = 3000) => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, duration);
}, []);
const removeToast = useCallback((id) => {
setToasts(prev => prev.filter(t => t.id !== id));
}, []);
return { toasts, addToast, removeToast };
};
// components/ToastContainer.jsx
import React from 'react';
import { useToast } from '../hooks/useToast';
const ToastContainer = () => {
const { toasts, removeToast } = useToast();
const getStyles = (type) => {
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6'
};
return {
borderLeft: `4px solid ${colors[type]}`,
color: colors[type]
};
};
return (
<div style={{
position: 'fixed',
top: 20,
right: 20,
zIndex: 9999
}}>
{toasts.map(toast => (
<div
key={toast.id}
style={{
...getStyles(toast.type),
background: 'white',
borderRadius: 8,
padding: '12px 20px',
marginBottom: 10,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
display: 'flex',
alignItems: 'center',
gap: 10,
minWidth: 300,
animation: 'slideIn 0.3s ease'
}}
>
<span style={{ fontWeight: 'bold' }}>
{toast.type === 'success' && 'β'}
{toast.type === 'error' && 'β'}
{toast.type === 'warning' && 'β '}
{toast.type === 'info' && 'βΉ'}
</span>
<span style={{ flex: 1 }}>{toast.message}</span>
<button
onClick={() => removeToast(toast.id)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: 18
}}
>
Γ
</button>
</div>
))}
</div>
);
};
export default ToastContainer;
Live Demo:
Click buttons to see how toasts would appear in your app
π Quick Reference: Tech Stack Comparison
PHP Laravel
Best for rapid dev
React
Real-time UI
.NET Core
Enterprise ready
jQuery
Quick prototypes
Frequently Asked Questions
Which tech stack should I choose?
For startups: Laravel + jQuery. For real-time apps: React + Node.js. For enterprise: .NET Core + React. Choose based on your team's expertise.
How to handle biometric integration?
Use device SDKs (ZKTeco, Suprema) or implement mobile app with fingerprint/face recognition. Most devices provide REST APIs.
How to implement geo-fencing?
Store office coordinates, get user location via browser/mobile GPS, calculate distance using Haversine formula, validate within allowed radius (e.g., 100m).
How to handle leave management?
Define leave types, set quotas per employee, implement approval workflow (employee β manager β HR), auto-calculate balances, integrate with attendance.
How to generate payroll reports?
Calculate: Basic Salary + Allowances - Deductions = Net Pay. Track present days, late days, overtime hours, leave without pay, and apply company policies.
How to make it SaaS ready?
Implement multi-tenancy with tenant_id in all tables, subdomain routing, plan-based feature restrictions, subscription management, and isolated data per client.