Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e72bb7b4ed |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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',)
|
||||||
|
|||||||
0
core/management/__init__.py
Normal file
0
core/management/__init__.py
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/management/commands/__pycache__/setup_demo.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/setup_demo.cpython-311.pyc
Normal file
Binary file not shown.
62
core/management/commands/fetch_attendance.py
Normal file
62
core/management/commands/fetch_attendance.py
Normal 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.'))
|
||||||
81
core/management/commands/setup_demo.py
Normal file
81
core/management/commands/setup_demo.py
Normal 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.'))
|
||||||
50
core/migrations/0001_initial.py
Normal file
50
core/migrations/0001_initial.py
Normal 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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
113
core/models.py
113
core/models.py
@ -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')
|
||||||
|
|||||||
@ -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>
|
||||||
{% block content %}{% endblock %}
|
<!-- 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 %}
|
||||||
|
</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>
|
||||||
|
|||||||
56
core/templates/core/attendance_list.html
Normal file
56
core/templates/core/attendance_list.html
Normal 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 %}
|
||||||
61
core/templates/core/employee_list.html
Normal file
61
core/templates/core/employee_list.html
Normal 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 %}
|
||||||
@ -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>
|
</div>
|
||||||
</p>
|
<div class="vr"></div>
|
||||||
</div>
|
<div class="d-flex align-items-center">
|
||||||
</main>
|
<i class="fas fa-calendar-day me-2 text-warning"></i>
|
||||||
<footer>
|
<span>{{ nepali_date }}</span>
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
{% endblock %}
|
</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 %}
|
||||||
|
|||||||
66
core/templates/core/leave_list.html
Normal file
66
core/templates/core/leave_list.html
Normal 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 %}
|
||||||
@ -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
64
core/utils.py
Normal 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)
|
||||||
@ -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})
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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-color: var(--background-color);
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
color: var(--text-main);
|
||||||
color: var(--text-color);
|
overflow-x: hidden;
|
||||||
display: flex;
|
margin: 0;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
/* Sidebar Styles */
|
||||||
text-align: center;
|
.sidebar {
|
||||||
overflow: hidden;
|
width: 260px;
|
||||||
position: relative;
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user