Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
e72bb7b4ed Autosave: 20260217-062619 2026-02-17 06:26:19 +00:00
38 changed files with 1359 additions and 189 deletions

View File

@ -1,3 +1,60 @@
from django.contrib import admin from django.contrib import admin
from .models import (
Department, Shift, Employee, BiometricDevice,
AttendanceLog, DailyAttendance, LeaveType,
LeaveRequest, LeaveBalance
)
# Register your models here. @admin.register(Department)
class DepartmentAdmin(admin.ModelAdmin):
list_display = ('name', 'code')
@admin.register(Shift)
class ShiftAdmin(admin.ModelAdmin):
list_display = ('name', 'start_time', 'end_time')
@admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin):
list_display = ('employee_id', 'biometric_id', 'first_name', 'last_name', 'department', 'position', 'is_active')
list_filter = ('department', 'shift', 'is_active')
search_fields = ('employee_id', 'biometric_id', 'first_name', 'last_name', 'position')
@admin.register(BiometricDevice)
class BiometricDeviceAdmin(admin.ModelAdmin):
list_display = ('name', 'ip_address', 'port', 'is_active', 'last_sync')
list_filter = ('is_active',)
@admin.register(AttendanceLog)
class AttendanceLogAdmin(admin.ModelAdmin):
list_display = ('employee', 'timestamp', 'device')
list_filter = ('device', 'timestamp')
search_fields = ('employee__first_name', 'employee__last_name', 'employee__employee_id', 'employee__biometric_id')
@admin.register(DailyAttendance)
class DailyAttendanceAdmin(admin.ModelAdmin):
list_display = ('employee', 'date', 'check_in', 'check_out', 'status', 'is_late', 'worked_hours')
list_filter = ('date', 'status', 'is_late')
search_fields = ('employee__first_name', 'employee__last_name', 'employee__employee_id')
@admin.register(LeaveType)
class LeaveTypeAdmin(admin.ModelAdmin):
list_display = ('name', 'total_days', 'color_code')
@admin.register(LeaveRequest)
class LeaveRequestAdmin(admin.ModelAdmin):
list_display = ('employee', 'leave_type', 'start_date', 'end_date', 'status', 'applied_on')
list_filter = ('status', 'leave_type', 'start_date')
actions = ['approve_requests', 'reject_requests']
def approve_requests(self, request, queryset):
queryset.update(status='approved', approved_by=request.user)
approve_requests.short_description = "Approve selected leave requests"
def reject_requests(self, request, queryset):
queryset.update(status='rejected')
reject_requests.short_description = "Reject selected leave requests"
@admin.register(LeaveBalance)
class LeaveBalanceAdmin(admin.ModelAdmin):
list_display = ('employee', 'leave_type', 'allocated', 'used', 'remaining')
list_filter = ('leave_type',)

View File

Binary file not shown.

View File

View File

@ -0,0 +1,62 @@
from django.core.management.base import BaseCommand
from core.models import Employee, BiometricDevice, AttendanceLog
from core.utils import sync_all_daily_summaries
from zk import ZK, const
from django.utils import timezone
import datetime
class Command(BaseCommand):
help = 'Fetches attendance logs from ZKTeco biometric devices'
def handle(self, *args, **options):
devices = BiometricDevice.objects.filter(is_active=True)
if not devices.exists():
self.stdout.write(self.style.WARNING('No active biometric devices found.'))
return
for device in devices:
self.stdout.write(f'Connecting to device: {device.name} ({device.ip_address})...')
zk = ZK(device.ip_address, port=device.port, timeout=5, password=0, force_udp=False, ommit_ping=False)
conn = None
try:
conn = zk.connect()
# Disable device while fetching data
conn.disable_device()
logs = conn.get_attendance()
count = 0
for log in logs:
try:
employee = Employee.objects.get(biometric_id=log.user_id)
# Avoid duplicates using unique_together constraint logic
obj, created = AttendanceLog.objects.get_or_create(
employee=employee,
timestamp=log.timestamp,
defaults={
'device': device,
'uid': log.uid
}
)
if created:
count += 1
except Employee.DoesNotExist:
# self.stdout.write(self.style.WARNING(f'Employee with biometric ID {log.user_id} not found.'))
continue
device.last_sync = timezone.now()
device.save()
self.stdout.write(self.style.SUCCESS(f'Successfully synced {count} new logs from {device.name}.'))
# Re-enable device
conn.enable_device()
except Exception as e:
self.stdout.write(self.style.ERROR(f'Failed to connect to {device.name}: {str(e)}'))
finally:
if conn:
conn.disconnect()
# After all devices synced, update daily summaries
self.stdout.write('Updating daily attendance summaries...')
sync_all_daily_summaries()
self.stdout.write(self.style.SUCCESS('All summaries updated.'))

View File

@ -0,0 +1,81 @@
from django.core.management.base import BaseCommand
from core.models import Employee, Department, Shift, DailyAttendance, LeaveType, LeaveRequest
from django.utils import timezone
import datetime
class Command(BaseCommand):
help = 'Populates the database with demo data'
def handle(self, *args, **options):
# 1. Create Department
dept, _ = Department.objects.get_or_create(name='IT Department', code='IT', defaults={'description': 'Information Technology'})
# 2. Create Shift
shift, _ = Shift.objects.get_or_create(
name='General Shift',
defaults={'start_time': datetime.time(9, 0), 'end_time': datetime.time(17, 0)}
)
# 3. Create Employees
emp1, _ = Employee.objects.get_or_create(
employee_id='EMP001',
defaults={
'first_name': 'John',
'last_name': 'Doe',
'department': dept,
'shift': shift,
'position': 'Software Engineer',
'joined_date': datetime.date(2023, 1, 1),
'is_active': True
}
)
emp2, _ = Employee.objects.get_or_create(
employee_id='EMP002',
defaults={
'first_name': 'Jane',
'last_name': 'Smith',
'department': dept,
'shift': shift,
'position': 'Product Manager',
'joined_date': datetime.date(2023, 2, 1),
'is_active': True
}
)
# 4. Create Leave Types
lt, _ = LeaveType.objects.get_or_create(name='Sick Leave', defaults={'total_days': 12, 'color_code': '#e74c3c'})
# 5. Create Daily Attendance for today
today = timezone.now().date()
DailyAttendance.objects.get_or_create(
employee=emp1,
date=today,
defaults={
'check_in': datetime.time(9, 5),
'status': 'present',
'worked_hours': 8.0
}
)
DailyAttendance.objects.get_or_create(
employee=emp2,
date=today,
defaults={
'check_in': datetime.time(9, 45),
'status': 'late',
'is_late': True,
'worked_hours': 7.25
}
)
# 6. Create Pending Leave
LeaveRequest.objects.get_or_create(
employee=emp1,
leave_type=lt,
start_date=today + datetime.timedelta(days=5),
end_date=today + datetime.timedelta(days=7),
defaults={'reason': 'Medical checkup', 'status': 'pending'}
)
self.stdout.write(self.style.SUCCESS('Successfully populated demo data.'))

View File

@ -0,0 +1,50 @@
# Generated by Django 5.2.7 on 2026-02-17 05:58
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Department',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('code', models.CharField(max_length=10, unique=True)),
('description', models.TextField(blank=True)),
],
),
migrations.CreateModel(
name='Shift',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('start_time', models.TimeField()),
('end_time', models.TimeField()),
],
),
migrations.CreateModel(
name='Employee',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('employee_id', models.CharField(max_length=20, unique=True)),
('first_name', models.CharField(max_length=50)),
('last_name', models.CharField(max_length=50)),
('position', models.CharField(max_length=100)),
('joined_date', models.DateField()),
('is_active', models.BooleanField(default=True)),
('department', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.department')),
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('shift', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.shift')),
],
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 5.2.7 on 2026-02-17 06:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='BiometricDevice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('ip_address', models.GenericIPAddressField()),
('port', models.IntegerField(default=4370)),
('is_active', models.BooleanField(default=True)),
('last_sync', models.DateTimeField(blank=True, null=True)),
],
),
migrations.AddField(
model_name='employee',
name='biometric_id',
field=models.CharField(blank=True, help_text='ID on the ZKTeco device', max_length=20, null=True, unique=True),
),
migrations.CreateModel(
name='AttendanceLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField()),
('uid', models.IntegerField(help_text='Internal ID from device log')),
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.employee')),
('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.biometricdevice')),
],
options={
'ordering': ['-timestamp'],
'unique_together': {('employee', 'timestamp')},
},
),
]

View File

@ -0,0 +1,68 @@
# Generated by Django 5.2.7 on 2026-02-17 06:18
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_biometricdevice_employee_biometric_id_attendancelog'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='LeaveType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('color_code', models.CharField(default='#3498db', max_length=7)),
('total_days', models.IntegerField(default=0)),
],
),
migrations.CreateModel(
name='LeaveRequest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_date', models.DateField()),
('end_date', models.DateField()),
('reason', models.TextField()),
('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=20)),
('applied_on', models.DateTimeField(auto_now_add=True)),
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approver', to=settings.AUTH_USER_MODEL)),
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.employee')),
('leave_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.leavetype')),
],
),
migrations.CreateModel(
name='DailyAttendance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('check_in', models.TimeField(blank=True, null=True)),
('check_out', models.TimeField(blank=True, null=True)),
('status', models.CharField(choices=[('present', 'Present'), ('absent', 'Absent'), ('late', 'Late'), ('half_day', 'Half Day'), ('on_leave', 'On Leave')], default='absent', max_length=20)),
('is_late', models.BooleanField(default=False)),
('worked_hours', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)),
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.employee')),
],
options={
'unique_together': {('employee', 'date')},
},
),
migrations.CreateModel(
name='LeaveBalance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('allocated', models.IntegerField()),
('used', models.IntegerField(default=0)),
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.employee')),
('leave_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.leavetype')),
],
options={
'unique_together': {('employee', 'leave_type')},
},
),
]

View File

@ -1,3 +1,114 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User
# Create your models here. class Department(models.Model):
name = models.CharField(max_length=100)
code = models.CharField(max_length=10, unique=True)
description = models.TextField(blank=True)
def __str__(self):
return self.name
class Shift(models.Model):
name = models.CharField(max_length=50) # e.g., Morning, Night, General
start_time = models.TimeField()
end_time = models.TimeField()
def __str__(self):
return f"{self.name} ({self.start_time} - {self.end_time})"
class Employee(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True)
employee_id = models.CharField(max_length=20, unique=True)
biometric_id = models.CharField(max_length=20, unique=True, null=True, blank=True, help_text="ID on the ZKTeco device")
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True)
shift = models.ForeignKey(Shift, on_delete=models.SET_NULL, null=True)
position = models.CharField(max_length=100)
joined_date = models.DateField()
is_active = models.BooleanField(default=True)
def __str__(self):
return f"{self.first_name} {self.last_name} ({self.employee_id})"
class BiometricDevice(models.Model):
name = models.CharField(max_length=100)
ip_address = models.GenericIPAddressField()
port = models.IntegerField(default=4370)
is_active = models.BooleanField(default=True)
last_sync = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"{self.name} ({self.ip_address})"
class AttendanceLog(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
timestamp = models.DateTimeField()
device = models.ForeignKey(BiometricDevice, on_delete=models.SET_NULL, null=True)
uid = models.IntegerField(help_text="Internal ID from device log")
class Meta:
unique_together = ('employee', 'timestamp')
ordering = ['-timestamp']
def __str__(self):
return f"{self.employee} - {self.timestamp}"
class DailyAttendance(models.Model):
STATUS_CHOICES = (
('present', 'Present'),
('absent', 'Absent'),
('late', 'Late'),
('half_day', 'Half Day'),
('on_leave', 'On Leave'),
)
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
date = models.DateField()
check_in = models.TimeField(null=True, blank=True)
check_out = models.TimeField(null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='absent')
is_late = models.BooleanField(default=False)
worked_hours = models.DecimalField(max_digits=5, decimal_places=2, default=0.00)
class Meta:
unique_together = ('employee', 'date')
class LeaveType(models.Model):
name = models.CharField(max_length=50)
color_code = models.CharField(max_length=7, default='#3498db') # Hex code for UI
total_days = models.IntegerField(default=0)
def __str__(self):
return self.name
class LeaveRequest(models.Model):
STATUS_CHOICES = (
('pending', 'Pending'),
('approved', 'Approved'),
('rejected', 'Rejected'),
)
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
leave_type = models.ForeignKey(LeaveType, on_delete=models.CASCADE)
start_date = models.DateField()
end_date = models.DateField()
reason = models.TextField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
applied_on = models.DateTimeField(auto_now_add=True)
approved_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='approver')
def __str__(self):
return f"{self.employee} - {self.leave_type} ({self.start_date})"
class LeaveBalance(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
leave_type = models.ForeignKey(LeaveType, on_delete=models.CASCADE)
allocated = models.IntegerField()
used = models.IntegerField(default=0)
@property
def remaining(self):
return self.allocated - self.used
class Meta:
unique_together = ('employee', 'leave_type')

View File

@ -3,23 +3,70 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if project_description %} <title>{% block title %}Hospital HRMS{% endblock %}</title>
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
<meta property="twitter:description" content="{{ project_description }}">
{% endif %}
{% if project_image_url %}
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
{% load static %} {% load static %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}"> <!-- Bootstrap 5 CSS -->
<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">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<!-- Sidebar -->
<aside class="sidebar">
<a href="/" class="sidebar-brand">
<i class="fas fa-hospital-user"></i>
<span>MediHR</span>
</a>
<nav class="nav-menu">
<a href="{% url 'home' %}" class="nav-link {% if request.resolver_match.url_name == 'home' %}active{% endif %}">
<i class="fas fa-th-large"></i>
<span>Dashboard</span>
</a>
<a href="{% url 'attendance_list' %}" class="nav-link {% if request.resolver_match.url_name == 'attendance_list' %}active{% endif %}">
<i class="fas fa-user-clock"></i>
<span>Attendance</span>
</a>
<a href="{% url 'leave_list' %}" class="nav-link {% if request.resolver_match.url_name == 'leave_list' %}active{% endif %}">
<i class="fas fa-calendar-minus"></i>
<span>Leave Requests</span>
</a>
<a href="{% url 'employee_list' %}" class="nav-link {% if request.resolver_match.url_name == 'employee_list' %}active{% endif %}">
<i class="fas fa-users"></i>
<span>Employees</span>
</a>
<hr style="border-color: rgba(255,255,255,0.1)">
<a href="/admin/" class="nav-link">
<i class="fas fa-user-shield"></i>
<span>Admin Panel</span>
</a>
</nav>
<div class="mt-auto pt-4 border-top" style="border-color: rgba(255,255,255,0.1) !important">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary d-flex align-items-center justify-content-center text-white" style="width: 32px; height: 32px; font-size: 0.8rem">HR</div>
<div class="ms-2">
<div class="small fw-bold text-white">HR Officer</div>
<div class="text-muted" style="font-size: 0.7rem">System Administrator</div>
</div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block title %}Attendance History | MediHR HRMS{% endblock %}
{% block content %}
<div class="top-bar">
<div>
<h1 class="h4 fw-bold mb-1">Attendance History</h1>
<p class="text-muted small mb-0">Detailed view of all daily attendance records.</p>
</div>
</div>
<div class="table-card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Date</th>
<th>Employee</th>
<th>Check In</th>
<th>Check Out</th>
<th>Status</th>
<th>Worked Hours</th>
</tr>
</thead>
<tbody>
{% for record in attendances %}
<tr>
<td class="fw-bold">{{ record.date|date:"d M Y" }}</td>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-2" style="width: 28px; height: 28px; font-size: 0.7rem">
{{ record.employee.first_name|first }}{{ record.employee.last_name|first }}
</div>
<div>{{ record.employee.first_name }} {{ record.employee.last_name }}</div>
</div>
</td>
<td>{{ record.check_in|time:"H:i"|default:"--" }}</td>
<td>{{ record.check_out|time:"H:i"|default:"--" }}</td>
<td>
<span class="status-pill badge-{{ record.status }}">
{{ record.get_status_display }}
</span>
</td>
<td>{{ record.worked_hours }}h</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-muted">No attendance records found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}Employees | MediHR HRMS{% endblock %}
{% block content %}
<div class="top-bar">
<div>
<h1 class="h4 fw-bold mb-1">Employee Directory</h1>
<p class="text-muted small mb-0">Manage your staff and their assignments.</p>
</div>
<a href="/admin/core/employee/add/" class="btn btn-primary btn-sm rounded-pill px-3">
<i class="fas fa-plus me-1"></i> Add Employee
</a>
</div>
<div class="table-card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Employee ID</th>
<th>Name</th>
<th>Department</th>
<th>Position</th>
<th>Shift</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for emp in employees %}
<tr>
<td class="fw-bold">{{ emp.employee_id }}</td>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary-subtle d-flex align-items-center justify-content-center text-primary me-2" style="width: 32px; height: 32px; font-weight: 700;">
{{ emp.first_name|first }}{{ emp.last_name|first }}
</div>
<div>{{ emp.first_name }} {{ emp.last_name }}</div>
</div>
</td>
<td>{{ emp.department.name|default:"--" }}</td>
<td>{{ emp.position }}</td>
<td><span class="badge bg-light text-dark border">{{ emp.shift.name|default:"--" }}</span></td>
<td>
{% if emp.is_active %}
<span class="badge bg-success-subtle text-success border border-success-subtle">Active</span>
{% else %}
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">Inactive</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-muted">No employees found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -1,145 +1,168 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ project_name }}{% endblock %} {% block title %}Dashboard | MediHR HRMS{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<main> <div class="top-bar">
<div class="card"> <div>
<h1>Analyzing your requirements and generating your app…</h1> <h1 class="h4 fw-bold mb-1">General Dashboard</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <p class="text-muted small mb-0">Overview of today's workplace activity.</p>
<span class="sr-only">Loading…</span>
</div> </div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p> <div class="date-badge">
<p class="hint">This page will refresh automatically as the plan is implemented.</p> <div class="d-flex align-items-center">
<p class="runtime"> <i class="far fa-calendar-alt me-2 text-primary"></i>
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code> <span>{{ current_time|date:"D, d M Y" }} AD</span>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div> </div>
</main> <div class="vr"></div>
<footer> <div class="d-flex align-items-center">
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) <i class="fas fa-calendar-day me-2 text-warning"></i>
</footer> <span>{{ nepali_date }}</span>
</div>
</div>
</div>
<!-- Stats Overview -->
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="stat-card">
<div class="card-icon icon-blue">
<i class="fas fa-users"></i>
</div>
<h6 class="text-muted small fw-bold text-uppercase mb-1">Total Staff</h6>
<h3 class="fw-bold mb-0">{{ total_employees }}</h3>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="card-icon icon-green">
<i class="fas fa-user-check"></i>
</div>
<h6 class="text-muted small fw-bold text-uppercase mb-1">Present Today</h6>
<h3 class="fw-bold mb-0 text-success">{{ present_today|default:"0" }}</h3>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="card-icon icon-orange">
<i class="fas fa-user-clock"></i>
</div>
<h6 class="text-muted small fw-bold text-uppercase mb-1">Late Arrivals</h6>
<h3 class="fw-bold mb-0 text-warning">{{ late_today|default:"0" }}</h3>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="card-icon icon-red">
<i class="fas fa-calendar-times"></i>
</div>
<h6 class="text-muted small fw-bold text-uppercase mb-1">On Leave</h6>
<h3 class="fw-bold mb-0 text-danger">{{ on_leave_today|default:"0" }}</h3>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<!-- Attendance Table -->
<div class="col-lg-8">
<div class="section-header d-flex justify-content-between align-items-center mb-3">
<h5 class="fw-bold mb-0">Recent Attendance Logs</h5>
<a href="#" class="btn btn-sm btn-outline-primary rounded-pill">View All</a>
</div>
<div class="table-card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Employee</th>
<th>Time</th>
<th>Status</th>
<th>Worked Hours</th>
</tr>
</thead>
<tbody>
{% for record in recent_attendance %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-3" style="width: 32px; height: 32px; font-weight: 600; font-size: 0.75rem">
{{ record.employee.first_name|first }}{{ record.employee.last_name|first }}
</div>
<div>
<div class="fw-bold">{{ record.employee.first_name }} {{ record.employee.last_name }}</div>
<div class="text-muted small">{{ record.employee.department.name }}</div>
</div>
</div>
</td>
<td>
<div class="fw-500">{{ record.check_in|time:"H:i" }}</div>
<div class="text-muted small">{{ record.date|date:"d M Y" }}</div>
</td>
<td>
<span class="status-pill badge-{{ record.status }}">
{{ record.get_status_display }}
</span>
</td>
<td>{{ record.worked_hours }}h</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-5 text-muted">
<i class="fas fa-info-circle mb-2 d-block fa-2x"></i>
No attendance records found for today.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Quick Actions & Pending Leaves -->
<div class="col-lg-4">
<div class="section-header mb-3">
<h5 class="fw-bold mb-0">Pending Requests</h5>
</div>
<div class="stat-card mb-4">
{% for leave in pending_leaves %}
<div class="d-flex align-items-center mb-3 pb-3 border-bottom last-child-no-border">
<div class="rounded-circle bg-primary-subtle p-2 me-3">
<i class="fas fa-file-alt text-primary"></i>
</div>
<div class="flex-grow-1">
<div class="fw-bold small">{{ leave.employee.first_name }} {{ leave.employee.last_name }}</div>
<div class="text-muted extra-small">{{ leave.leave_type.name }} • {{ leave.start_date }}</div>
</div>
<div>
<a href="/admin/core/leaverequest/{{ leave.id }}/change/" class="btn btn-sm btn-light border">Review</a>
</div>
</div>
{% empty %}
<div class="text-center py-4 text-muted small">
All caught up! No pending requests.
</div>
{% endfor %}
</div>
<div class="section-header mb-3">
<h5 class="fw-bold mb-0">Quick Actions</h5>
</div>
<div class="list-group shadow-sm">
<a href="/admin/core/employee/add/" class="list-group-item list-group-item-action py-3">
<i class="fas fa-user-plus me-2 text-primary"></i> Register New Staff
</a>
<a href="/admin/core/leaverequest/add/" class="list-group-item list-group-item-action py-3">
<i class="fas fa-calendar-plus me-2 text-success"></i> Apply Leave
</a>
<button class="list-group-item list-group-item-action py-3 text-start" onclick="location.reload()">
<i class="fas fa-sync me-2 text-info"></i> Sync Device Logs
</button>
</div>
</div>
</div>
<style>
.extra-small { font-size: 0.75rem; }
.last-child-no-border:last-child { border-bottom: none !important; }
</style>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block title %}Leave Requests | MediHR HRMS{% endblock %}
{% block content %}
<div class="top-bar">
<div>
<h1 class="h4 fw-bold mb-1">Leave Requests</h1>
<p class="text-muted small mb-0">Manage and track employee leave applications.</p>
</div>
<a href="/admin/core/leaverequest/add/" class="btn btn-primary btn-sm rounded-pill px-3">
<i class="fas fa-plus me-1"></i> New Request
</a>
</div>
<div class="table-card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Employee</th>
<th>Type</th>
<th>Duration</th>
<th>Reason</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for leave in leaves %}
<tr>
<td>
<div class="fw-bold">{{ leave.employee.first_name }} {{ leave.employee.last_name }}</div>
<div class="text-muted small">ID: {{ leave.employee.employee_id }}</div>
</td>
<td><span class="badge bg-info-subtle text-info border border-info-subtle">{{ leave.leave_type.name }}</span></td>
<td>
<div class="small fw-500 text-dark">{{ leave.start_date }}</div>
<div class="text-muted small">to {{ leave.end_date }}</div>
</td>
<td><div class="text-truncate" style="max-width: 200px;" title="{{ leave.reason }}">{{ leave.reason }}</div></td>
<td>
{% if leave.status == 'pending' %}
<span class="badge bg-warning text-dark">Pending</span>
{% elif leave.status == 'approved' %}
<span class="badge bg-success">Approved</span>
{% else %}
<span class="badge bg-danger">Rejected</span>
{% endif %}
</td>
<td>
<a href="/admin/core/leaverequest/{{ leave.id }}/change/" class="btn btn-sm btn-light border">
<i class="fas fa-edit"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-5 text-muted">No leave requests found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -1,7 +1,10 @@
from django.urls import path from django.urls import path
from .views import home from .views import home, attendance_list, leave_list, employee_list
urlpatterns = [ urlpatterns = [
path("", home, name="home"), path("", home, name="home"),
path("attendance/", attendance_list, name="attendance_list"),
path("leaves/", leave_list, name="leave_list"),
path("employees/", employee_list, name="employee_list"),
] ]

64
core/utils.py Normal file
View File

@ -0,0 +1,64 @@
from datetime import datetime, time, timedelta
from django.utils import timezone
from .models import AttendanceLog, DailyAttendance, Employee, Shift
def calculate_daily_attendance(employee, date):
"""
Calculate summary for a specific employee on a specific date.
"""
logs = AttendanceLog.objects.filter(
employee=employee,
timestamp__date=date
).order_by('timestamp')
if not logs.exists():
# Check if they are on leave (simplified for now)
# In a real app, we'd check LeaveRequest for this date
return None
check_in = logs.first().timestamp.time()
check_out = logs.last().timestamp.time()
# Calculate worked hours
first_log = logs.first().timestamp
last_log = logs.last().timestamp
duration = last_log - first_log
worked_hours = duration.total_seconds() / 3600
# Determine status based on shift
status = 'present'
is_late = False
if employee.shift:
# If check-in is 15 mins later than shift start, mark as late
shift_start_datetime = datetime.combine(date, employee.shift.start_time)
check_in_datetime = datetime.combine(date, check_in)
if check_in_datetime > (shift_start_datetime + timedelta(minutes=15)):
status = 'late'
is_late = True
# Update or create summary
summary, created = DailyAttendance.objects.update_or_create(
employee=employee,
date=date,
defaults={
'check_in': check_in,
'check_out': check_out,
'status': status,
'is_late': is_late,
'worked_hours': round(worked_hours, 2)
}
)
return summary
def sync_all_daily_summaries(date=None):
"""
Utility to refresh summaries for all employees.
"""
if not date:
date = timezone.now().date()
employees = Employee.objects.filter(is_active=True)
for emp in employees:
calculate_daily_attendance(emp, date)

View File

@ -1,25 +1,63 @@
import os import os
import platform import platform
import datetime
import nepali_datetime
from django import get_version as django_version from django import get_version as django_version
from django.shortcuts import render from django.shortcuts import render, redirect
from django.utils import timezone from django.utils import timezone
from .models import Employee, Department, Shift, DailyAttendance, LeaveRequest, AttendanceLog
def get_nepali_date(ad_date):
"""Convert AD date to Nepali BS date string."""
bs_date = nepali_datetime.date.from_datetime_date(ad_date)
return bs_date.strftime("%B %d, %Y BS")
def home(request): def home(request):
"""Render the landing screen with loader and environment details.""" """Render the landing screen with modern HRMS dashboard."""
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now() now = timezone.now()
today = now.date()
# Stats
total_employees = Employee.objects.count()
daily_stats = DailyAttendance.objects.filter(date=today)
present_today = daily_stats.filter(status__in=['present', 'late', 'half_day']).count()
late_today = daily_stats.filter(status='late').count()
on_leave_today = daily_stats.filter(status='on_leave').count()
# Recent Attendance Summary
recent_attendance = DailyAttendance.objects.all().order_by('-date', '-check_in')[:5]
# Pending Leaves
pending_leaves = LeaveRequest.objects.filter(status='pending').order_by('-applied_on')[:5]
nepali_date = get_nepali_date(today)
context = { context = {
"project_name": "New Style", "project_name": "MediHR",
"agent_brand": agent_brand, "total_employees": total_employees,
"django_version": django_version(), "present_today": present_today,
"python_version": platform.python_version(), "late_today": late_today,
"on_leave_today": on_leave_today,
"recent_attendance": recent_attendance,
"pending_leaves": pending_leaves,
"current_time": now, "current_time": now,
"host_name": host_name, "nepali_date": nepali_date,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
} }
return render(request, "core/index.html", context) return render(request, "core/index.html", context)
def attendance_list(request):
"""View to list all daily attendance summaries."""
attendances = DailyAttendance.objects.all().order_by('-date', '-check_in')
return render(request, "core/attendance_list.html", {"attendances": attendances})
def leave_list(request):
"""View to list all leave requests."""
leaves = LeaveRequest.objects.all().order_by('-applied_on')
return render(request, "core/leave_list.html", {"leaves": leaves})
def employee_list(request):
"""View to list all employees."""
employees = Employee.objects.all().order_by('employee_id')
return render(request, "core/employee_list.html", {"employees": employees})

View File

@ -1,9 +1,9 @@
<?php <?php
// Generated by setup_mariadb_project.sh — edit as needed. // Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1'); define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_38483'); define('DB_NAME', 'app_38505');
define('DB_USER', 'app_38483'); define('DB_USER', 'app_38505');
define('DB_PASS', '6ba2e336-6f05-4aa2-a889-a1fee29c6def'); define('DB_PASS', 'd80a8cea-c06f-47fa-90f5-4072d623b537');
function db() { function db() {
static $pdo; static $pdo;

View File

@ -1,4 +1,182 @@
/* Custom styles for the application */ :root {
body { --primary-color: #2563eb;
font-family: system-ui, -apple-system, sans-serif; --primary-hover: #1d4ed8;
--secondary-color: #64748b;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--background-color: #f8fafc;
--card-bg: #ffffff;
--sidebar-bg: #1e293b;
--sidebar-text: #f1f5f9;
--text-main: #1e293b;
--text-muted: #64748b;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--background-color);
color: var(--text-main);
overflow-x: hidden;
margin: 0;
}
/* Sidebar Styles */
.sidebar {
width: 260px;
height: 100vh;
position: fixed;
left: 0;
top: 0;
background-color: var(--sidebar-bg);
color: var(--sidebar-text);
padding: 1.5rem;
z-index: 1000;
display: flex;
flex-direction: column;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 2.5rem;
display: flex;
align-items: center;
color: #fff;
text-decoration: none;
}
.sidebar-brand i {
margin-right: 0.75rem;
color: var(--primary-color);
}
.nav-menu {
flex-grow: 1;
}
.nav-link {
color: #94a3b8;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
margin-bottom: 0.25rem;
transition: all 0.2s;
display: flex;
align-items: center;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.nav-link i {
width: 20px;
margin-right: 0.75rem;
}
/* Main Content Area */
.main-content {
margin-left: 260px;
padding: 2rem;
min-height: 100vh;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
/* Card Styles */
.stat-card {
background: var(--card-bg);
border-radius: 1rem;
padding: 1.5rem;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.icon-blue { background: #dbeafe; color: #1e40af; }
.icon-green { background: #dcfce7; color: #166534; }
.icon-orange { background: #fef3c7; color: #92400e; }
.icon-red { background: #fee2e2; color: #991b1b; }
/* Date Badge */
.date-badge {
background: #fff;
padding: 0.5rem 1rem;
border-radius: 0.75rem;
font-size: 0.875rem;
border: 1px solid #e2e8f0;
display: flex;
gap: 1rem;
font-weight: 500;
}
/* Table Styles */
.table-card {
background: white;
border-radius: 1rem;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.table thead th {
background-color: #f8fafc;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e2e8f0;
}
.table tbody td {
padding: 1rem 1.5rem;
vertical-align: middle;
}
/* Status Badges */
.status-pill {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-present { background-color: #dcfce7; color: #166534; }
.badge-absent { background-color: #fee2e2; color: #991b1b; }
.badge-late { background-color: #fef3c7; color: #92400e; }
.badge-leave { background-color: #dbeafe; color: #1e40af; }
/* Buttons */
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
} }

View File

@ -1,21 +1,182 @@
:root { :root {
--bg-color-start: #6a11cb; --primary-color: #2563eb;
--bg-color-end: #2575fc; --primary-hover: #1d4ed8;
--text-color: #ffffff; --secondary-color: #64748b;
--card-bg-color: rgba(255, 255, 255, 0.01); --success-color: #10b981;
--card-border-color: rgba(255, 255, 255, 0.1); --warning-color: #f59e0b;
--danger-color: #ef4444;
--background-color: #f8fafc;
--card-bg: #ffffff;
--sidebar-bg: #1e293b;
--sidebar-text: #f1f5f9;
--text-main: #1e293b;
--text-muted: #64748b;
} }
body { body {
margin: 0;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); background-color: var(--background-color);
color: var(--text-color); color: var(--text-main);
display: flex; overflow-x: hidden;
justify-content: center; margin: 0;
align-items: center; }
min-height: 100vh;
text-align: center; /* Sidebar Styles */
overflow: hidden; .sidebar {
position: relative; width: 260px;
height: 100vh;
position: fixed;
left: 0;
top: 0;
background-color: var(--sidebar-bg);
color: var(--sidebar-text);
padding: 1.5rem;
z-index: 1000;
display: flex;
flex-direction: column;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 2.5rem;
display: flex;
align-items: center;
color: #fff;
text-decoration: none;
}
.sidebar-brand i {
margin-right: 0.75rem;
color: var(--primary-color);
}
.nav-menu {
flex-grow: 1;
}
.nav-link {
color: #94a3b8;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
margin-bottom: 0.25rem;
transition: all 0.2s;
display: flex;
align-items: center;
text-decoration: none;
}
.nav-link:hover, .nav-link.active {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.nav-link i {
width: 20px;
margin-right: 0.75rem;
}
/* Main Content Area */
.main-content {
margin-left: 260px;
padding: 2rem;
min-height: 100vh;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
/* Card Styles */
.stat-card {
background: var(--card-bg);
border-radius: 1rem;
padding: 1.5rem;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.icon-blue { background: #dbeafe; color: #1e40af; }
.icon-green { background: #dcfce7; color: #166534; }
.icon-orange { background: #fef3c7; color: #92400e; }
.icon-red { background: #fee2e2; color: #991b1b; }
/* Date Badge */
.date-badge {
background: #fff;
padding: 0.5rem 1rem;
border-radius: 0.75rem;
font-size: 0.875rem;
border: 1px solid #e2e8f0;
display: flex;
gap: 1rem;
font-weight: 500;
}
/* Table Styles */
.table-card {
background: white;
border-radius: 1rem;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.table thead th {
background-color: #f8fafc;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e2e8f0;
}
.table tbody td {
padding: 1rem 1.5rem;
vertical-align: middle;
}
/* Status Badges */
.status-pill {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-present { background-color: #dcfce7; color: #166534; }
.badge-absent { background-color: #fee2e2; color: #991b1b; }
.badge-late { background-color: #fef3c7; color: #92400e; }
.badge-leave { background-color: #dbeafe; color: #1e40af; }
/* Buttons */
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
} }