update on the models and forms
This commit is contained in:
parent
579cc085e2
commit
a23c96cc17
Binary file not shown.
Binary file not shown.
@ -67,7 +67,7 @@ MIDDLEWARE = [
|
|||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
@ -196,7 +196,6 @@ SOCIALACCOUNT_PROVIDERS = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
||||||
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
||||||
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
|
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
|
||||||
@ -215,7 +214,6 @@ CELERY_TASK_SERIALIZER = 'json'
|
|||||||
CELERY_RESULT_SERIALIZER = 'json'
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
CELERY_TIMEZONE = 'UTC'
|
CELERY_TIMEZONE = 'UTC'
|
||||||
|
|
||||||
|
|
||||||
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
|
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
|
||||||
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
|
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
|
||||||
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
|
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
|
||||||
BIN
db.sqlite3
BIN
db.sqlite3
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.
@ -469,4 +469,18 @@ class InterviewScheduleForm(forms.ModelForm):
|
|||||||
def clean_working_days(self):
|
def clean_working_days(self):
|
||||||
working_days = self.cleaned_data.get('working_days')
|
working_days = self.cleaned_data.get('working_days')
|
||||||
# Convert string values to integers
|
# Convert string values to integers
|
||||||
return [int(day) for day in working_days]
|
return [int(day) for day in working_days]
|
||||||
|
|
||||||
|
|
||||||
|
class JobPostingCancelReasonForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = JobPosting
|
||||||
|
fields = ['cancel_reason']
|
||||||
|
class JobPostingStatusForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = JobPosting
|
||||||
|
fields = ['status']
|
||||||
|
class FormTemplateIsActiveForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = FormTemplate
|
||||||
|
fields = ['is_active']
|
||||||
@ -2,12 +2,13 @@ import requests
|
|||||||
|
|
||||||
LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
|
LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
|
||||||
|
|
||||||
|
|
||||||
class LinkedInService:
|
class LinkedInService:
|
||||||
def __init__(self, access_token):
|
def __init__(self, access_token):
|
||||||
self.headers = {
|
self.headers = {
|
||||||
'Authorization': f'Bearer {access_token}',
|
"Authorization": f"Bearer {access_token}",
|
||||||
'X-Restli-Protocol-Version': '2.0.0',
|
"X-Restli-Protocol-Version": "2.0.0",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
def post_job(self, organization_id, job_data):
|
def post_job(self, organization_id, job_data):
|
||||||
@ -17,10 +18,10 @@ class LinkedInService:
|
|||||||
"lifecycleState": "PUBLISHED",
|
"lifecycleState": "PUBLISHED",
|
||||||
"specificContent": {
|
"specificContent": {
|
||||||
"com.linkedin.ugc.ShareContent": {
|
"com.linkedin.ugc.ShareContent": {
|
||||||
"shareCommentary": {"text": job_data['text']},
|
"shareCommentary": {"text": job_data["text"]},
|
||||||
"shareMediaCategory": "NONE"
|
"shareMediaCategory": "NONE",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}
|
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"},
|
||||||
}
|
}
|
||||||
return requests.post(url, json=data, headers=self.headers)
|
return requests.post(url, json=data, headers=self.headers)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-10-08 15:48
|
# Generated by Django 5.2.6 on 2025-10-09 10:10
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
@ -213,6 +213,7 @@ class Migration(migrations.Migration):
|
|||||||
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||||
('phone', models.CharField(max_length=20, verbose_name='Phone')),
|
('phone', models.CharField(max_length=20, verbose_name='Phone')),
|
||||||
|
('address', models.TextField(max_length=200, verbose_name='Address')),
|
||||||
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
|
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
|
||||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
||||||
@ -311,6 +312,14 @@ class Migration(migrations.Migration):
|
|||||||
name='job',
|
name='job',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Profile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='SharedFormTemplate',
|
name='SharedFormTemplate',
|
||||||
fields=[
|
fields=[
|
||||||
@ -374,6 +383,7 @@ class Migration(migrations.Migration):
|
|||||||
name='ScheduledInterview',
|
name='ScheduledInterview',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||||
('interview_date', models.DateField(verbose_name='Interview Date')),
|
('interview_date', models.DateField(verbose_name='Interview Date')),
|
||||||
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
||||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)),
|
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)),
|
||||||
@ -384,5 +394,8 @@ class Migration(migrations.Migration):
|
|||||||
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
||||||
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
||||||
],
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-10-08 17:46
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('recruitment', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='candidate',
|
|
||||||
name='address',
|
|
||||||
field=models.TextField(default='', max_length=200, verbose_name='Address'),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-09 10:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobposting',
|
||||||
|
name='cancel_reason',
|
||||||
|
field=models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobposting',
|
||||||
|
name='cancelled_at',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobposting',
|
||||||
|
name='cancelled_by',
|
||||||
|
field=models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='jobposting',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.6 on 2025-10-09 12:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('recruitment', '0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='candidate',
|
||||||
|
name='is_resume_parsed',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Resume Parsed'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='formtemplate',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=False, help_text='Whether this template is active'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.2.6 on 2025-10-08 17:47
|
|
||||||
|
|
||||||
import django_extensions.db.fields
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('recruitment', '0002_candidate_address'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='scheduledinterview',
|
|
||||||
name='slug',
|
|
||||||
field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-10-08 13:01
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('recruitment', '0026_interviewschedule_scheduledinterview'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Profile',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')),
|
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
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.
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.
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.
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.
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.
@ -11,8 +11,10 @@ from django.urls import reverse
|
|||||||
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
class Profile(models.Model):
|
||||||
profile_image=models.ImageField(null=True,blank=True,upload_to='profile_pic/')
|
profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/")
|
||||||
user=models.OneToOneField(User,on_delete=models.CASCADE,related_name='profile')
|
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
|
||||||
|
|
||||||
|
|
||||||
class Base(models.Model):
|
class Base(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
|
||||||
@ -105,8 +107,9 @@ class JobPosting(Base):
|
|||||||
# Status Fields
|
# Status Fields
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("DRAFT", "Draft"),
|
("DRAFT", "Draft"),
|
||||||
("PUBLISHED", "Published"),
|
("ACTIVE", "Active"),
|
||||||
("CLOSED", "Closed"),
|
("CLOSED", "Closed"),
|
||||||
|
("CANCELLED", "Cancelled"),
|
||||||
("ARCHIVED", "Archived"),
|
("ARCHIVED", "Archived"),
|
||||||
]
|
]
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
@ -165,6 +168,18 @@ class JobPosting(Base):
|
|||||||
"External agency responsible for sourcing candidates for this role"
|
"External agency responsible for sourcing candidates for this role"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
cancel_reason = models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Reason for canceling the job posting"),
|
||||||
|
verbose_name=_("Cancel Reason"),
|
||||||
|
)
|
||||||
|
cancelled_by = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
blank=True,
|
||||||
|
help_text=_("Name of person who cancelled this job"),
|
||||||
|
verbose_name=_("Cancelled By"),
|
||||||
|
)
|
||||||
|
cancelled_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-created_at"]
|
ordering = ["-created_at"]
|
||||||
@ -197,7 +212,7 @@ class JobPosting(Base):
|
|||||||
else:
|
else:
|
||||||
next_num = 1
|
next_num = 1
|
||||||
|
|
||||||
self.internal_job_id = f"{prefix}-{year}-{next_num:04d}"
|
self.internal_job_id = f"{prefix}-{year}-{next_num:06d}"
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@ -260,8 +275,11 @@ class Candidate(Base):
|
|||||||
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
|
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
|
||||||
email = models.EmailField(verbose_name=_("Email"))
|
email = models.EmailField(verbose_name=_("Email"))
|
||||||
phone = models.CharField(max_length=20, verbose_name=_("Phone"))
|
phone = models.CharField(max_length=20, verbose_name=_("Phone"))
|
||||||
address = models.TextField(max_length=200,verbose_name=_("Address"))
|
address = models.TextField(max_length=200, verbose_name=_("Address"))
|
||||||
resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume"))
|
resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume"))
|
||||||
|
is_resume_parsed = models.BooleanField(
|
||||||
|
default=False, verbose_name=_("Resume Parsed")
|
||||||
|
)
|
||||||
parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary"))
|
parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary"))
|
||||||
applied = models.BooleanField(default=False, verbose_name=_("Applied"))
|
applied = models.BooleanField(default=False, verbose_name=_("Applied"))
|
||||||
stage = models.CharField(
|
stage = models.CharField(
|
||||||
@ -331,6 +349,7 @@ class Candidate(Base):
|
|||||||
if self.resume:
|
if self.resume:
|
||||||
return self.resume.size
|
return self.resume.size
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Validate stage transitions"""
|
"""Validate stage transitions"""
|
||||||
# Only validate if this is an existing record (not being created)
|
# Only validate if this is an existing record (not being created)
|
||||||
@ -376,6 +395,14 @@ class Candidate(Base):
|
|||||||
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
||||||
return self.STAGE_SEQUENCE.get(old_stage, [])
|
return self.STAGE_SEQUENCE.get(old_stage, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def submission(self):
|
||||||
|
return FormSubmission.objects.filter(template__job=self.job).first()
|
||||||
|
@property
|
||||||
|
def responses(self):
|
||||||
|
if self.submission:
|
||||||
|
return self.submission.responses.all()
|
||||||
|
return []
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.full_name
|
return self.full_name
|
||||||
|
|
||||||
@ -449,7 +476,7 @@ class FormTemplate(Base):
|
|||||||
User, on_delete=models.CASCADE, related_name="form_templates"
|
User, on_delete=models.CASCADE, related_name="form_templates"
|
||||||
)
|
)
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
default=True, help_text="Whether this template is active"
|
default=False, help_text="Whether this template is active"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -595,6 +622,9 @@ class FormField(Base):
|
|||||||
if self.order < 0:
|
if self.order < 0:
|
||||||
raise ValidationError("Order must be a positive integer")
|
raise ValidationError("Order must be a positive integer")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.stage.template.name} - {self.stage.name} - {self.label}"
|
||||||
|
|
||||||
|
|
||||||
class FormSubmission(Base):
|
class FormSubmission(Base):
|
||||||
"""
|
"""
|
||||||
@ -658,16 +688,19 @@ class FieldResponse(Base):
|
|||||||
if self.uploaded_file:
|
if self.uploaded_file:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_file(self):
|
def get_file(self):
|
||||||
if self.is_file:
|
if self.is_file:
|
||||||
return self.uploaded_file
|
return self.uploaded_file
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_file_size(self):
|
def get_file_size(self):
|
||||||
if self.is_file:
|
if self.is_file:
|
||||||
return self.uploaded_file.size
|
return self.uploaded_file.size
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_value(self):
|
def display_value(self):
|
||||||
"""Return a human-readable representation of the response value"""
|
"""Return a human-readable representation of the response value"""
|
||||||
@ -885,9 +918,7 @@ class InterviewSchedule(Base):
|
|||||||
job = models.ForeignKey(
|
job = models.ForeignKey(
|
||||||
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules"
|
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules"
|
||||||
)
|
)
|
||||||
candidates = models.ManyToManyField(
|
candidates = models.ManyToManyField(Candidate, related_name="interview_schedules")
|
||||||
Candidate, related_name="interview_schedules"
|
|
||||||
)
|
|
||||||
start_date = models.DateField(verbose_name=_("Start Date"))
|
start_date = models.DateField(verbose_name=_("Start Date"))
|
||||||
end_date = models.DateField(verbose_name=_("End Date"))
|
end_date = models.DateField(verbose_name=_("End Date"))
|
||||||
working_days = models.JSONField(
|
working_days = models.JSONField(
|
||||||
@ -895,9 +926,7 @@ class InterviewSchedule(Base):
|
|||||||
) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
||||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||||
breaks = models.ManyToManyField(
|
breaks = models.ManyToManyField(BreakTime, blank=True, related_name="schedules")
|
||||||
BreakTime, blank=True, related_name="schedules"
|
|
||||||
)
|
|
||||||
interview_duration = models.PositiveIntegerField(
|
interview_duration = models.PositiveIntegerField(
|
||||||
verbose_name=_("Interview Duration (minutes)")
|
verbose_name=_("Interview Duration (minutes)")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import asyncio
|
|||||||
|
|
||||||
@receiver(post_save, sender=models.Candidate)
|
@receiver(post_save, sender=models.Candidate)
|
||||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
def score_candidate_resume(sender, instance, created, **kwargs):
|
||||||
|
if instance.is_resume_parsed:
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
# Get absolute file path
|
# Get absolute file path
|
||||||
file_path = instance.resume.path
|
file_path = instance.resume.path
|
||||||
@ -108,12 +110,12 @@ def score_candidate_resume(sender, instance, created, **kwargs):
|
|||||||
instance.weaknesses = result1.get('weaknesses', '')
|
instance.weaknesses = result1.get('weaknesses', '')
|
||||||
instance.criteria_checklist = result1.get('criteria_checklist', {})
|
instance.criteria_checklist = result1.get('criteria_checklist', {})
|
||||||
|
|
||||||
|
instance.is_resume_parsed = True
|
||||||
|
|
||||||
# Save only scoring-related fields to avoid recursion
|
# Save only scoring-related fields to avoid recursion
|
||||||
instance.save(update_fields=[
|
instance.save(update_fields=[
|
||||||
'match_score', 'strengths', 'weaknesses',
|
'match_score', 'strengths', 'weaknesses',
|
||||||
'criteria_checklist','parsed_summary'
|
'criteria_checklist','parsed_summary', 'is_resume_parsed'
|
||||||
])
|
])
|
||||||
|
|
||||||
logger.info(f"Successfully scored resume for candidate {instance.id}")
|
logger.info(f"Successfully scored resume for candidate {instance.id}")
|
||||||
|
|||||||
@ -535,4 +535,19 @@ def get_available_time_slots(schedule, breaks=None):
|
|||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
|
|
||||||
print(f"Total slots generated: {len(slots)}")
|
print(f"Total slots generated: {len(slots)}")
|
||||||
return slots
|
return slots
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def json_to_markdown_table(data_list):
|
||||||
|
if not data_list:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
headers = data_list[0].keys()
|
||||||
|
markdown = "| " + " | ".join(headers) + " |\n"
|
||||||
|
markdown += "| " + " | ".join(["---"] * len(headers)) + " |\n"
|
||||||
|
|
||||||
|
for row in data_list:
|
||||||
|
values = [str(row.get(header, "")) for header in headers]
|
||||||
|
markdown += "| " + " | ".join(values) + " |\n"
|
||||||
|
return markdown
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
|||||||
|
import json
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
from recruitment.utils import json_to_markdown_table
|
||||||
from . import models
|
from . import models
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from . import forms
|
from . import forms
|
||||||
@ -19,6 +22,8 @@ from datastar_py.django import (
|
|||||||
ServerSentEventGenerator as SSE,
|
ServerSentEventGenerator as SSE,
|
||||||
read_signals,
|
read_signals,
|
||||||
)
|
)
|
||||||
|
# from rich import print
|
||||||
|
from rich.markdown import CodeBlock
|
||||||
|
|
||||||
class JobListView(LoginRequiredMixin, ListView):
|
class JobListView(LoginRequiredMixin, ListView):
|
||||||
model = models.JobPosting
|
model = models.JobPosting
|
||||||
@ -41,7 +46,7 @@ class JobListView(LoginRequiredMixin, ListView):
|
|||||||
# Filter for non-staff users
|
# Filter for non-staff users
|
||||||
if not self.request.user.is_staff:
|
if not self.request.user.is_staff:
|
||||||
queryset = queryset.filter(status='Published')
|
queryset = queryset.filter(status='Published')
|
||||||
|
|
||||||
status=self.request.GET.get('status')
|
status=self.request.GET.get('status')
|
||||||
if status:
|
if status:
|
||||||
queryset=queryset.filter(status=status)
|
queryset=queryset.filter(status=status)
|
||||||
@ -49,7 +54,7 @@ class JobListView(LoginRequiredMixin, ListView):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['search_query'] = self.request.GET.get('search', '')
|
context['search_query'] = self.request.GET.get('search', '')
|
||||||
context['lang'] = get_language()
|
context['lang'] = get_language()
|
||||||
@ -201,6 +206,7 @@ def training_list(request):
|
|||||||
|
|
||||||
|
|
||||||
def candidate_detail(request, slug):
|
def candidate_detail(request, slug):
|
||||||
|
from rich.json import JSON
|
||||||
candidate = get_object_or_404(models.Candidate, slug=slug)
|
candidate = get_object_or_404(models.Candidate, slug=slug)
|
||||||
try:
|
try:
|
||||||
parsed = ast.literal_eval(candidate.parsed_summary)
|
parsed = ast.literal_eval(candidate.parsed_summary)
|
||||||
@ -212,6 +218,8 @@ def candidate_detail(request, slug):
|
|||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
stage_form = forms.CandidateStageForm(candidate=candidate)
|
stage_form = forms.CandidateStageForm(candidate=candidate)
|
||||||
|
|
||||||
|
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
|
||||||
|
parsed = json_to_markdown_table([parsed])
|
||||||
return render(request, 'recruitment/candidate_detail.html', {
|
return render(request, 'recruitment/candidate_detail.html', {
|
||||||
'candidate': candidate,
|
'candidate': candidate,
|
||||||
'parsed': parsed,
|
'parsed': parsed,
|
||||||
@ -219,7 +227,7 @@ def candidate_detail(request, slug):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def candidate_update_stage(request, slug):
|
def candidate_update_stage(request, slug):
|
||||||
"""Handle HTMX stage update requests"""
|
"""Handle HTMX stage update requests"""
|
||||||
try:
|
try:
|
||||||
if not request.user.is_staff:
|
if not request.user.is_staff:
|
||||||
return render(request, 'recruitment/partials/error.html', {'error': 'Permission denied'}, status=403)
|
return render(request, 'recruitment/partials/error.html', {'error': 'Permission denied'}, status=403)
|
||||||
@ -293,7 +301,7 @@ class TrainingListView(LoginRequiredMixin, ListView):
|
|||||||
template_name = 'recruitment/training_list.html'
|
template_name = 'recruitment/training_list.html'
|
||||||
context_object_name = 'materials'
|
context_object_name = 'materials'
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
|
|
||||||
|
|||||||
@ -1,100 +1,331 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
|
{% load static i18n crispy_forms_tags %}
|
||||||
|
{% load partials %}
|
||||||
|
|
||||||
{% block title %}Submissions for {{ template.name }}{% endblock %}
|
{% block title %}Submissions for {{ template.name }} - ATS{% endblock %}
|
||||||
|
|
||||||
|
{% block customCSS %}
|
||||||
|
<style>
|
||||||
|
/* ================================================= */
|
||||||
|
/* UI Variables (Matching Form Templates List) */
|
||||||
|
/* ================================================= */
|
||||||
|
:root {
|
||||||
|
--kaauh-teal: #00636e;
|
||||||
|
--kaauh-teal-dark: #004a53;
|
||||||
|
--kaauh-border: #eaeff3;
|
||||||
|
--kaauh-primary-text: #343a40;
|
||||||
|
--kaauh-gray-light: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Typography and Color Overrides --- */
|
||||||
|
.text-primary { color: var(--kaauh-teal) !important; }
|
||||||
|
|
||||||
|
/* --- Button Base Styles (Matching Form Templates List) --- */
|
||||||
|
.btn-main-action {
|
||||||
|
background-color: var(--kaauh-teal);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.btn-main-action:hover {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-teal-dark);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary Button Style (for Edit/Preview) */
|
||||||
|
.btn-outline-secondary {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
}
|
||||||
|
.btn-outline-secondary:hover {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--kaauh-teal-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Size Utilities (matching Bootstrap convention) */
|
||||||
|
.btn-lg {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Card and Layout Styles (Matching Form Templates List) --- */
|
||||||
|
.card {
|
||||||
|
border: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
background-color: white;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background-color: var(--kaauh-teal-dark) !important;
|
||||||
|
border-bottom: 1px solid var(--kaauh-border);
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem 0.75rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h1 {
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.card-header .fas {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.card-header .small {
|
||||||
|
color: rgba(255, 255, 255, 0.7) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Table Styles --- */
|
||||||
|
.table-responsive {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.table thead th {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.table tbody td {
|
||||||
|
padding: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
}
|
||||||
|
.table tbody tr {
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: var(--kaauh-gray-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Pagination Styling (Matching Form Templates List) --- */
|
||||||
|
.pagination .page-item .page-link {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
}
|
||||||
|
.pagination .page-item.active .page-link {
|
||||||
|
background-color: var(--kaauh-teal);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.pagination .page-item:hover .page-link:not(.active) {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
.pagination-info {
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Empty State Theming --- */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
border: 2px dashed var(--kaauh-border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background-color: var(--kaauh-gray-light);
|
||||||
|
}
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
}
|
||||||
|
.empty-state .btn-main-action .fas {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Breadcrumb --- */
|
||||||
|
.breadcrumb {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.breadcrumb-item a {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.breadcrumb-item a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container py-4">
|
||||||
<nav class="mb-6">
|
<nav aria-label="breadcrumb">
|
||||||
<a href="{% url 'form_templates_list' %}" class="text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white flex items-center">
|
<ol class="breadcrumb">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}">{% trans "Dashboard" %}</a></li>
|
||||||
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
|
<li class="breadcrumb-item"><a href="{% url 'form_templates_list' %}">{% trans "Form Templates" %}</a></li>
|
||||||
</svg>
|
<li class="breadcrumb-item active">{% trans "Submissions" %}</li>
|
||||||
Back to Form Templates
|
</ol>
|
||||||
</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
|
<div class="card shadow-sm">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Submissions for: <span class="text-blue-600 dark:text-blue-400">{{ template.name }}</span></h1>
|
<div>
|
||||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Template ID: {{ template.id }}</p>
|
<h1 class="h3 mb-1 d-flex align-items-center">
|
||||||
|
<i class="fas fa-file-alt me-2"></i>
|
||||||
|
{% trans "Submissions for" %}: <span class="text-white ms-2">{{ template.name }}</span>
|
||||||
|
</h1>
|
||||||
|
<small class="text-white-50">Template ID: #{{ template.id }}</small>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'form_templates_list' %}" class="btn btn-outline-light btn-sm">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<div class="p-6">
|
|
||||||
{% if page_obj.object_list %}
|
{% if page_obj.object_list %}
|
||||||
<div class="overflow-x-auto">
|
<div id="form-template-submissions-list">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
{# View Switcher #}
|
||||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
{% include "includes/_list_view_switcher.html" with list_id="form-template-submissions-list" %}
|
||||||
<tr>
|
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Submission ID</th>
|
{# Table View (Default) #}
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Applicant Name</th>
|
<div class="table-view active">
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Applicant Email</th>
|
<div class="table-responsive mb-4">
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Submitted At</th>
|
<table class="table table-hover">
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
|
<thead>
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
<th scope="col">{% trans "Submission ID" %}</th>
|
||||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<th scope="col">{% trans "Applicant Name" %}</th>
|
||||||
|
<th scope="col">{% trans "Applicant Email" %}</th>
|
||||||
|
<th scope="col">{% trans "Submitted At" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for submission in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-medium">{{ submission.id }}</td>
|
||||||
|
<td>{{ submission.applicant_name|default:"N/A" }}</td>
|
||||||
|
<td>{{ submission.applicant_email|default:"N/A" }}</td>
|
||||||
|
<td>{{ submission.submitted_at|date:"M d, Y H:i" }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Card View #}
|
||||||
|
<div class="card-view">
|
||||||
|
<div class="row g-4">
|
||||||
{% for submission in page_obj %}
|
{% for submission in page_obj %}
|
||||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-750">
|
<div class="col-lg-4 col-md-6">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ submission.id }}</td>
|
<div class="card h-100">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.applicant_name|default:"N/A" }}</td>
|
<div class="card-header">
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.applicant_email|default:"N/A" }}</td>
|
<h3 class="h5 mb-2">{% trans "Submission" %} #{{ submission.id }}</h3>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.submitted_at|date:"Y-m-d H:i:s" }}</td>
|
<small class="text-white-50">{{ template.name }}</small>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
</div>
|
||||||
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 inline-flex items-center">
|
<div class="card-body">
|
||||||
View Details
|
<p class="card-text">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<strong>{% trans "Applicant Name" %}:</strong> {{ submission.applicant_name|default:"N/A" }}<br>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<strong>{% trans "Applicant Email" %}:</strong> {{ submission.applicant_email|default:"N/A" }}<br>
|
||||||
</svg>
|
<strong>{% trans "Submitted At" %}:</strong> {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||||
</a>
|
</p>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
<div class="card-footer">
|
||||||
|
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{% if page_obj.has_other_pages %}
|
{% if page_obj.has_other_pages %}
|
||||||
<nav class="mt-6 flex items-center justify-between" aria-label="Pagination">
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center">
|
||||||
<div class="hidden sm:block">
|
<div class="pagination-info mb-3 mb-md-0">
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
{% blocktrans with start=page_obj.start_index end=page_obj.end_index total=page_obj.paginator.count %}
|
||||||
Showing <span class="font-medium">{{ page_obj.start_index }}</span> to <span class="font-medium">{{ page_obj.end_index }}</span> of <span class="font-medium">{{ page_obj.paginator.count }}</span> results.
|
Showing {{ start }} to {{ end }} of {{ total }} results.
|
||||||
</p>
|
{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex justify-between sm:justify-end mt-4 sm:mt-0">
|
<nav aria-label="Page navigation">
|
||||||
{% if page_obj.has_previous %}
|
<ul class="pagination pagination-sm mb-0">
|
||||||
<a href="?page={{ page_obj.previous_page_number }}" class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
{% if page_obj.has_previous %}
|
||||||
Previous
|
<li class="page-item">
|
||||||
</a>
|
<a class="page-link" href="?page=1" aria-label="First">
|
||||||
{% endif %}
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
|
||||||
|
<span aria-hidden="true">‹</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300">
|
<li class="page-item active">
|
||||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
<span class="page-link">
|
||||||
</span>
|
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<a href="?page={{ page_obj.next_page_number }}" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
<li class="page-item">
|
||||||
Next
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||||
</a>
|
<span aria-hidden="true">›</span>
|
||||||
{% endif %}
|
</a>
|
||||||
</div>
|
</li>
|
||||||
</nav>
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-12">
|
<div class="empty-state">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<i class="fas fa-inbox"></i>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<h3 class="h5 mb-3">{% trans "No Submissions Found" %}</h3>
|
||||||
</svg>
|
<p class="text-muted mb-4">
|
||||||
<h3 class="mt-2 text-lg font-medium text-gray-900 dark:text-white">No submissions found</h3>
|
{% trans "There are no submissions for this form template yet." %}
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">There are no submissions for this form template yet.</p>
|
</p>
|
||||||
<div class="mt-6">
|
<a href="{% url 'form_templates_list' %}" class="btn btn-main-action">
|
||||||
<a href="{% url 'form_templates_list' %}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="mr-2 -ml-1 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
</a>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
|
||||||
</svg>
|
|
||||||
Back to Templates
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -231,65 +231,118 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if templates %}
|
{% if templates %}
|
||||||
<div class="row g-4">
|
<div id="form-templates-list">
|
||||||
{% for template in templates %}
|
{# View Switcher #}
|
||||||
<div class="col-lg-4 col-md-6">
|
{% include "includes/_list_view_switcher.html" with list_id="form-templates-list" %}
|
||||||
<div class="card template-card h-100">
|
|
||||||
<div class="card-header ">
|
|
||||||
<h3 class="h5 mb-2">{{ template.name }}</h3>
|
|
||||||
<span><i class="fas fa-sync-alt me-1"></i> {{ template.job }}</span>
|
|
||||||
<div class="d-flex justify-content-between text-muted small">
|
|
||||||
<span><i class="fas fa-calendar me-1"></i> {{ template.created_at|date:"M d, Y" }}</span>
|
|
||||||
<span><i class="fas fa-sync-alt me-1"></i> {{ template.updated_at|timesince }} {% trans "ago" %}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body d-flex flex-column">
|
|
||||||
|
|
||||||
{# Content area - includes stats and description #}
|
{# Card View (Default) #}
|
||||||
<div class="flex-grow-1">
|
<div class="card-view active row g-4">
|
||||||
<div class="row text-center mb-3">
|
{% for template in templates %}
|
||||||
<div class="col-6">
|
<div class="col-lg-4 col-md-6">
|
||||||
<div class="stat-value">{{ template.get_stage_count }}</div>
|
<div class="card template-card h-100">
|
||||||
<div class="stat-label">{% trans "Stages" %}</div>
|
<div class="card-header ">
|
||||||
</div>
|
<h3 class="h5 mb-2">{{ template.name }}</h3>
|
||||||
<div class="col-6">
|
<span><i class="fas fa-sync-alt me-1"></i> {{ template.job }}</span>
|
||||||
<div class="stat-value">{{ template.get_field_count }}</div>
|
<div class="d-flex justify-content-between text-muted small">
|
||||||
<div class="stat-label">{% trans "Fields" %}</div>
|
<span><i class="fas fa-calendar me-1"></i> {{ template.created_at|date:"M d, Y" }}</span>
|
||||||
</div>
|
<span><i class="fas fa-sync-alt me-1"></i> {{ template.updated_at|timesince }} {% trans "ago" %}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="card-text card-description small">
|
|
||||||
{% if template.description %}
|
|
||||||
{{ template.description|truncatewords:20 }}
|
|
||||||
{% else %}
|
|
||||||
<em class="text-muted">{% trans "No description provided" %}</em>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
|
||||||
{# Action area - visually separated with pt-2 border-top #}
|
{# Content area - includes stats and description #}
|
||||||
<div class="mt-auto pt-2 border-top">
|
<div class="flex-grow-1">
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="row text-center mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="stat-value">{{ template.get_stage_count }}</div>
|
||||||
|
<div class="stat-label">{% trans "Stages" %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="stat-value">{{ template.get_field_count }}</div>
|
||||||
|
<div class="stat-label">{% trans "Fields" %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="card-text card-description small">
|
||||||
|
{% if template.description %}
|
||||||
|
{{ template.description|truncatewords:20 }}
|
||||||
|
{% else %}
|
||||||
|
<em class="text-muted">{% trans "No description provided" %}</em>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
{# Action area - visually separated with pt-2 border-top #}
|
||||||
<i class="fas fa-eye me-1"></i> {% trans "Preview" %}
|
<div class="mt-auto pt-2 border-top">
|
||||||
</a>
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
|
||||||
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||||
</a>
|
<i class="fas fa-eye me-1"></i> {% trans "Preview" %}
|
||||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary btn-sm action-btn">
|
</a>
|
||||||
<i class="fas fa-file-alt me-1"></i> {% trans "Submissions" %}
|
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||||
</a>
|
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
||||||
<button class="btn btn-outline-danger btn-sm action-btn delete"
|
</a>
|
||||||
data-template-id="{{ template.id }}"
|
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||||
data-template-name="{{ template.name }}">
|
<i class="fas fa-file-alt me-1"></i> {% trans "Submissions" %}
|
||||||
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
|
</a>
|
||||||
</button>
|
<button class="btn btn-outline-danger btn-sm action-btn delete"
|
||||||
|
data-template-id="{{ template.id }}"
|
||||||
|
data-template-name="{{ template.name }}">
|
||||||
|
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Table View #}
|
||||||
|
<div class="table-view">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Template Name" %}</th>
|
||||||
|
<th scope="col">{% trans "Job" %}</th>
|
||||||
|
<th scope="col">{% trans "Stages" %}</th>
|
||||||
|
<th scope="col">{% trans "Fields" %}</th>
|
||||||
|
<th scope="col">{% trans "Created" %}</th>
|
||||||
|
<th scope="col">{% trans "Last Updated" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for template in templates %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-medium">{{ template.name }}</td>
|
||||||
|
<td>{{ template.job }}</td>
|
||||||
|
<td>{{ template.get_stage_count }}</td>
|
||||||
|
<td>{{ template.get_field_count }}</td>
|
||||||
|
<td>{{ template.created_at|date:"M d, Y" }}</td>
|
||||||
|
<td>{{ template.updated_at|date:"M d, Y" }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-info" title="{% trans 'Submissions' %}">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-outline-danger delete-btn" data-bs-toggle="modal" data-bs-target="#deleteModal" data-template-id="{{ template.id }}" data-template-name="{{ template.name }}" title="{% trans 'Delete' %}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if templates.has_other_pages %}
|
{% if templates.has_other_pages %}
|
||||||
@ -371,4 +424,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
159
templates/includes/_list_view_switcher.html
Normal file
159
templates/includes/_list_view_switcher.html
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm view-toggle active" data-view="table" data-list-id="{{ list_id }}">
|
||||||
|
<i class="fas fa-table me-1"></i> Table
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm view-toggle" data-view="card" data-list-id="{{ list_id }}">
|
||||||
|
<i class="fas fa-th me-1"></i> Card
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* View Toggle Styles */
|
||||||
|
.view-toggle {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
.view-toggle.active {
|
||||||
|
background-color: var(--kaauh-teal);
|
||||||
|
border-color: var(--kaauh-teal);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.view-toggle.active:hover {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
border-color: var(--kaauh-teal-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide elements by default */
|
||||||
|
.table-view,
|
||||||
|
.card-view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show active view */
|
||||||
|
.table-view.active,
|
||||||
|
.card-view.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card View Styles */
|
||||||
|
.card-view .card {
|
||||||
|
border: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
|
background-color: white;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.card-view .card-header {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-radius: 0.75rem 0.75rem 0 0;
|
||||||
|
}
|
||||||
|
.card-view .card-body {
|
||||||
|
padding: 1.25rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.card-view .card-title {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.card-view .card-text {
|
||||||
|
color: var(--kaauh-primary-text);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.card-view .card-footer {
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-top: 1px solid var(--kaauh-border);
|
||||||
|
border-radius: 0 0 0.75rem 0.75rem;
|
||||||
|
}
|
||||||
|
.card-view .btn-sm {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table View Styles */
|
||||||
|
.table-view .table-responsive {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table-view .table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.table-view .table thead th {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.table-view .table tbody td {
|
||||||
|
padding: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
}
|
||||||
|
.table-view .table tbody tr {
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.table-view .table tbody tr:hover {
|
||||||
|
background-color: var(--kaauh-gray-light);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Get the list ID from the data attribute
|
||||||
|
const listId = document.querySelector('.view-toggle').getAttribute('data-list-id');
|
||||||
|
const listContainer = document.getElementById(listId);
|
||||||
|
|
||||||
|
// Get saved view preference from localStorage
|
||||||
|
const savedView = localStorage.getItem(`list_view_${listId}`) || 'table';
|
||||||
|
|
||||||
|
// Set initial view
|
||||||
|
setView(savedView);
|
||||||
|
|
||||||
|
// Add click event listeners to view toggle buttons
|
||||||
|
document.querySelectorAll('.view-toggle').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const view = this.getAttribute('data-view');
|
||||||
|
setView(view);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setView(view) {
|
||||||
|
// Update button states
|
||||||
|
document.querySelectorAll('.view-toggle').forEach(button => {
|
||||||
|
if (button.getAttribute('data-view') === view) {
|
||||||
|
button.classList.add('active');
|
||||||
|
} else {
|
||||||
|
button.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update view visibility
|
||||||
|
const tableView = listContainer.querySelector('.table-view');
|
||||||
|
const cardView = listContainer.querySelector('.card-view');
|
||||||
|
|
||||||
|
if (view === 'table') {
|
||||||
|
tableView.classList.add('active');
|
||||||
|
cardView.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
tableView.classList.remove('active');
|
||||||
|
cardView.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save preference to localStorage
|
||||||
|
localStorage.setItem(`list_view_${listId}`, view);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -97,126 +97,177 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Candidates Table -->
|
<!-- Candidates -->
|
||||||
{% if candidates %}
|
{% if candidates %}
|
||||||
<div class="card">
|
<div id="job-candidates-list">
|
||||||
<div class="card-header">
|
{# View Switcher #}
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
{% include "includes/_list_view_switcher.html" with list_id="job-candidates-list" %}
|
||||||
<h5 class="mb-0">Applicants ({{ candidates.count }})</h5>
|
|
||||||
<div class="d-flex gap-2">
|
{# Table View (Default) #}
|
||||||
<select class="form-select form-select-sm" style="width: auto;" onchange="window.location.href='?stage='+this.value+'&search={{ search_query }}'">
|
<div class="table-view active">
|
||||||
<option value="">All Stages</option>
|
<div class="card">
|
||||||
<option value="Applied" {% if request.GET.stage == 'Applied' %}selected{% endif %}>Applied</option>
|
<div class="card-header">
|
||||||
<option value="Interview" {% if request.GET.stage == 'Interview' %}selected{% endif %}>Interview</option>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<option value="Offer" {% if request.GET.stage == 'Offer' %}selected{% endif %}>Offer</option>
|
<h5 class="mb-0">Applicants ({{ candidates.count }})</h5>
|
||||||
</select>
|
<div class="d-flex gap-2">
|
||||||
|
<select class="form-select form-select-sm" style="width: auto;" onchange="window.location.href='?stage='+this.value+'&search={{ search_query }}'">
|
||||||
|
<option value="">All Stages</option>
|
||||||
|
<option value="Applied" {% if request.GET.stage == 'Applied' %}selected{% endif %}>Applied</option>
|
||||||
|
<option value="Interview" {% if request.GET.stage == 'Interview' %}selected{% endif %}>Interview</option>
|
||||||
|
<option value="Offer" {% if request.GET.stage == 'Offer' %}selected{% endif %}>Offer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">
|
||||||
|
<input type="checkbox" class="form-check-input" id="selectAll">
|
||||||
|
</th>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Email</th>
|
||||||
|
<th scope="col">Phone</th>
|
||||||
|
<th scope="col">Stage</th>
|
||||||
|
<th scope="col">Applied Date</th>
|
||||||
|
<th scope="col" class="text-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for candidate in candidates %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" class="form-check-input candidate-checkbox" value="{{ candidate.slug }}">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<strong>{{ candidate.first_name }} {{ candidate.last_name }}</strong>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ candidate.email }}</td>
|
||||||
|
<td>{{ candidate.phone|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Interview' %}info{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">
|
||||||
|
{{ candidate.stage }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary btn-sm" title="View">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||||
|
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bulk Actions -->
|
||||||
|
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="me-3">Selected: <span id="selectedCount">0</span></span>
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-2" onclick="bulkAction('interview')"
|
||||||
|
{% if not user.is_staff %}disabled{% endif %}>
|
||||||
|
<i class="fas fa-comments"></i> Mark as Interview
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-success" onclick="bulkAction('offer')"
|
||||||
|
{% if not user.is_staff %}disabled{% endif %}>
|
||||||
|
<i class="fas fa-handshake"></i> Mark as Offer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if is_paginated %}
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination pagination-sm mb-0">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Previous">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Next">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover">
|
{# Card View #}
|
||||||
<thead>
|
<div class="card-view">
|
||||||
<tr>
|
<div class="row g-4">
|
||||||
<th scope="col">
|
{% for candidate in candidates %}
|
||||||
<input type="checkbox" class="form-check-input" id="selectAll">
|
<div class="col-md-6 col-lg-4">
|
||||||
</th>
|
<div class="card h-100">
|
||||||
<th scope="col">Name</th>
|
<div class="card-header">
|
||||||
<th scope="col">Email</th>
|
<h5 class="h5 mb-1">{{ candidate.first_name }} {{ candidate.last_name }}</h5>
|
||||||
<th scope="col">Phone</th>
|
<small class="text-white-50">{{ candidate.email }}</small>
|
||||||
<th scope="col">Stage</th>
|
</div>
|
||||||
<th scope="col">Applied Date</th>
|
<div class="card-body">
|
||||||
<th scope="col" class="text-center">Actions</th>
|
<p class="card-text">
|
||||||
</tr>
|
<strong>{% trans "Phone" %}:</strong> {{ candidate.phone|default:"N/A" }}<br>
|
||||||
</thead>
|
<strong>{% trans "Stage" %}:</strong> <span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Interview' %}info{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">{{ candidate.stage }}</span><br>
|
||||||
<tbody>
|
<strong>{% trans "Applied Date" %}:</strong> {{ candidate.created_at|date:"M d, Y" }}
|
||||||
{% for candidate in candidates %}
|
</p>
|
||||||
<tr>
|
</div>
|
||||||
<td>
|
<div class="card-footer">
|
||||||
<input type="checkbox" class="form-check-input candidate-checkbox" value="{{ candidate.slug }}">
|
<div class="d-flex gap-2">
|
||||||
</td>
|
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary w-100">
|
||||||
<td>
|
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||||
<div>
|
|
||||||
<strong>{{ candidate.first_name }} {{ candidate.last_name }}</strong>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{{ candidate.email }}</td>
|
|
||||||
<td>{{ candidate.phone|default:"-" }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Interview' %}info{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">
|
|
||||||
{{ candidate.stage }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary btn-sm" title="View">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
</a>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="Edit">
|
<div class="btn-group w-100" role="group">
|
||||||
<i class="fas fa-edit"></i>
|
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||||
</a>
|
<i class="fas fa-edit"></i>
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete"
|
</a>
|
||||||
data-bs-toggle="deleteModal"
|
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete"
|
||||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
data-bs-toggle="deleteModal"
|
||||||
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||||
<i class="fas fa-trash"></i>
|
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||||
</button>
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
</tbody>
|
{% endfor %}
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bulk Actions -->
|
|
||||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span class="me-3">Selected: <span id="selectedCount">0</span></span>
|
|
||||||
<button class="btn btn-sm btn-outline-primary me-2" onclick="bulkAction('interview')"
|
|
||||||
{% if not user.is_staff %}disabled{% endif %}>
|
|
||||||
<i class="fas fa-comments"></i> Mark as Interview
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-success" onclick="bulkAction('offer')"
|
|
||||||
{% if not user.is_staff %}disabled{% endif %}>
|
|
||||||
<i class="fas fa-handshake"></i> Mark as Offer
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
{% if is_paginated %}
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<ul class="pagination pagination-sm mb-0">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Previous">
|
|
||||||
<span aria-hidden="true">«</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
|
||||||
{% if page_obj.number == num %}
|
|
||||||
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Next">
|
|
||||||
<span aria-hidden="true">»</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.7px;
|
letter-spacing: 0.7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mapped color classes for status badges */
|
/* Mapped color classes for status badges */
|
||||||
.bg-success { background-color: var(--kaauh-teal) !important; }
|
.bg-success { background-color: var(--kaauh-teal) !important; }
|
||||||
.bg-warning { background-color: #ffc107 !important; }
|
.bg-warning { background-color: #ffc107 !important; }
|
||||||
@ -58,7 +58,7 @@
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Standard Card Header (used for single cards or fallback) */
|
/* Standard Card Header (used for single cards or fallback) */
|
||||||
.card-header {
|
.card-header {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -70,7 +70,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--kaauh-primary-text);
|
color: var(--kaauh-primary-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@ -103,7 +103,7 @@
|
|||||||
border-bottom: 3px solid var(--kaauh-teal);
|
border-bottom: 3px solid var(--kaauh-teal);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================================== */
|
/* ==================================== */
|
||||||
/* RIGHT COLUMN TABS STYLING (IMPROVED) */
|
/* RIGHT COLUMN TABS STYLING (IMPROVED) */
|
||||||
/* ==================================== */
|
/* ==================================== */
|
||||||
@ -122,13 +122,13 @@
|
|||||||
display: flex; /* Ensure the nav-items take up equal space */
|
display: flex; /* Ensure the nav-items take up equal space */
|
||||||
}
|
}
|
||||||
.right-column-tabs .nav-item {
|
.right-column-tabs .nav-item {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.right-column-tabs .nav-link {
|
.right-column-tabs .nav-link {
|
||||||
/* Base style for all right column tabs */
|
/* Base style for all right column tabs */
|
||||||
padding: 0.9rem 1rem; /* Slightly larger padding for better spacing */
|
padding: 0.9rem 1rem; /* Slightly larger padding for better spacing */
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--kaauh-primary-text);
|
color: var(--kaauh-primary-text);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -182,7 +182,7 @@
|
|||||||
.applicant-stats .stat-item small {
|
.applicant-stats .stat-item small {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Primary Color Overrides */
|
/* Primary Color Overrides */
|
||||||
.text-primary { color: var(--kaauh-teal) !important; }
|
.text-primary { color: var(--kaauh-teal) !important; }
|
||||||
.text-info { color: #17a2b8 !important; }
|
.text-info { color: #17a2b8 !important; }
|
||||||
@ -219,14 +219,14 @@
|
|||||||
color: white;
|
color: white;
|
||||||
border-color: var(--kaauh-teal-dark);
|
border-color: var(--kaauh-teal-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Specific styling for the deadline box */
|
/* Specific styling for the deadline box */
|
||||||
.deadline-box {
|
.deadline-box {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table styling for the Applicant preview */
|
/* Table styling for the Applicant preview */
|
||||||
.table-applicants tbody tr:hover {
|
.table-applicants tbody tr:hover {
|
||||||
background-color: #f3f9f9; /* Light teal hover for rows */
|
background-color: #f3f9f9; /* Light teal hover for rows */
|
||||||
@ -242,19 +242,20 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
|
|
||||||
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
|
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card shadow-sm no-hover">
|
<div class="card shadow-sm no-hover">
|
||||||
|
|
||||||
{# HEADER SECTION #}
|
{# HEADER SECTION #}
|
||||||
<div class="job-header-card d-flex justify-content-between align-items-center flex-wrap">
|
<div class="job-header-card d-flex justify-content-between align-items-center flex-wrap">
|
||||||
<h2>{{job}}</h2>
|
<h2>{{job}}</h2>
|
||||||
<span class="badge bg-{{ job.status|lower|striptags|yesno:'success,warning,secondary,danger' }} status-badge">
|
<button class="badge bg-success status-badge">
|
||||||
|
{% include "icons/edit.html" %}
|
||||||
{{ job.get_status_display }}
|
{{ job.get_status_display }}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# LEFT TABS NAVIGATION #}
|
{# LEFT TABS NAVIGATION #}
|
||||||
<ul class="nav nav-tabs" id="jobTabs" role="tablist">
|
<ul class="nav nav-tabs" id="jobTabs" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
@ -278,7 +279,7 @@
|
|||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="tab-content" id="jobTabsContent">
|
<div class="tab-content" id="jobTabsContent">
|
||||||
|
|
||||||
{# TAB 1 CONTENT: CORE DETAILS #}
|
{# TAB 1 CONTENT: CORE DETAILS #}
|
||||||
<div class="tab-pane fade show active" id="details" role="tabpanel" aria-labelledby="details-tab">
|
<div class="tab-pane fade show active" id="details" role="tabpanel" aria-labelledby="details-tab">
|
||||||
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}</h5>
|
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}</h5>
|
||||||
@ -365,16 +366,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# FOOTER ACTIONS #}
|
{# FOOTER ACTIONS #}
|
||||||
<div class="card-footer d-flex flex-wrap gap-2">
|
<div class="card-footer d-flex flex-wrap gap-2">
|
||||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action">
|
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action">
|
||||||
<i class="fas fa-edit"></i> {% trans "Edit Job" %}
|
<i class="fas fa-edit"></i> {% trans "Edit Job" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if job.application_url %}
|
{% if job.application_url %}
|
||||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#myModalForm">
|
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#myModalForm">
|
||||||
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
|
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
|
||||||
@ -387,7 +388,7 @@
|
|||||||
{# RIGHT COLUMN: TABBED CARDS #}
|
{# RIGHT COLUMN: TABBED CARDS #}
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="card shadow-sm no-hover">
|
<div class="card shadow-sm no-hover">
|
||||||
|
|
||||||
{# RIGHT TABS NAVIGATION #}
|
{# RIGHT TABS NAVIGATION #}
|
||||||
<ul class="nav nav-tabs right-column-tabs" id="rightJobTabs" role="tablist">
|
<ul class="nav nav-tabs right-column-tabs" id="rightJobTabs" role="tablist">
|
||||||
<li class="nav-item flex-fill" role="presentation">
|
<li class="nav-item flex-fill" role="presentation">
|
||||||
@ -408,7 +409,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content mx-2 my-3" id="rightJobTabsContent">
|
<div class="tab-content mx-2 my-3" id="rightJobTabsContent">
|
||||||
|
|
||||||
{# TAB 1: APPLICANTS CONTENT #}
|
{# TAB 1: APPLICANTS CONTENT #}
|
||||||
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
|
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
|
||||||
<h5 class="mb-3">{% trans "Candidates" %} (<span id="total_candidates">{{ total_candidates }}</span>)</h5>
|
<h5 class="mb-3">{% trans "Candidates" %} (<span id="total_candidates">{{ total_candidates }}</span>)</h5>
|
||||||
@ -476,7 +477,7 @@
|
|||||||
|
|
||||||
{# TAB 2: MANAGEMENT (LinkedIn & Forms) CONTENT #}
|
{# TAB 2: MANAGEMENT (LinkedIn & Forms) CONTENT #}
|
||||||
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
|
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
|
||||||
|
|
||||||
{# LinkedIn Integration (Content from old card) #}
|
{# LinkedIn Integration (Content from old card) #}
|
||||||
<h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5>
|
<h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@ -533,13 +534,13 @@
|
|||||||
<a href="" class="btn btn-outline-secondary">
|
<a href="" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
|
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||||
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
|
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# TAB 3: INTERNAL INFO CONTENT #}
|
{# TAB 3: INTERNAL INFO CONTENT #}
|
||||||
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
|
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
|
||||||
<h5 class="mb-3"><i class="fas fa-info-circle me-2 text-secondary"></i>{% trans "Internal Information" %}</h5>
|
<h5 class="mb-3"><i class="fas fa-info-circle me-2 text-secondary"></i>{% trans "Internal Information" %}</h5>
|
||||||
@ -551,7 +552,7 @@
|
|||||||
<p class="mb-0"><strong>{% trans "Reports To:" %}</strong> {{ job.reporting_to }}</p>
|
<p class="mb-0"><strong>{% trans "Reports To:" %}</strong> {{ job.reporting_to }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary w-100">
|
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary w-100">
|
||||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Jobs" %}
|
<i class="fas fa-arrow-left"></i> {% trans "Back to Jobs" %}
|
||||||
@ -560,7 +561,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static i18n %}
|
||||||
|
|
||||||
{% block title %}Job Postings - University ATS{% endblock %}
|
{% block title %}Job Postings - University ATS{% endblock %}
|
||||||
|
|
||||||
@ -134,7 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
{% url 'job_list' as job_list_url %}
|
{% url 'job_list' as job_list_url %}
|
||||||
|
|
||||||
<form method="GET" class="row g-3 align-items-end" >
|
<form method="GET" class="row g-3 align-items-end" >
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@ -159,50 +159,97 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if page_obj %}
|
{% if page_obj %}
|
||||||
<div class="row">
|
<div id="job-list">
|
||||||
{% for job in page_obj %}
|
{# View Switcher #}
|
||||||
<div class="col-md-6 col-lg-4 mb-4">
|
{% include "includes/_list_view_switcher.html" with list_id="job-list" %}
|
||||||
<div class="card job-card h-100">
|
|
||||||
<div class="card-body d-flex flex-column">
|
|
||||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
||||||
<h5 class="card-title flex-grow-1 me-3">{{ job.title }}</h5>
|
|
||||||
<span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">
|
|
||||||
{{ job.get_status_display }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="card-text text-muted small">
|
{# Card View (Default) #}
|
||||||
<i class="fas fa-building"></i> {{ job.department|default:"No Department" }}<br>
|
<div class="card-view active row">
|
||||||
<i class="fas fa-map-marker-alt"></i> {{ job.get_location_display }}<br>
|
{% for job in page_obj %}
|
||||||
<i class="fas fa-clock"></i> {{ job.get_job_type_display }}<br>
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
<i class="fas fa-briefcase"></i> {{ job.get_source }}
|
<div class="card job-card h-100">
|
||||||
</p>
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
<div class="mt-auto pt-2 border-top">
|
<h5 class="card-title flex-grow-1 me-3">{{ job.title }}</h5>
|
||||||
{% if job.posted_to_linkedin %}
|
<span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">
|
||||||
<span class="badge bg-info mb-2">
|
{{ job.get_status_display }}
|
||||||
<i class="fab fa-linkedin me-1"></i> Posted to LinkedIn
|
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<p class="card-text text-muted small">
|
||||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-sm btn-main-action">
|
<i class="fas fa-building"></i> {{ job.department|default:"No Department" }}<br>
|
||||||
<i class="fas fa-eye"></i> View
|
<i class="fas fa-map-marker-alt"></i> {{ job.get_location_display }}<br>
|
||||||
</a>
|
<i class="fas fa-clock"></i> {{ job.get_job_type_display }}<br>
|
||||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-sm btn-outline-secondary">
|
<i class="fas fa-briefcase"></i> {{ job.get_source }}
|
||||||
<i class="fas fa-edit"></i> Edit
|
</p>
|
||||||
</a>
|
|
||||||
|
<div class="mt-auto pt-2 border-top">
|
||||||
|
{% if job.posted_to_linkedin %}
|
||||||
|
<span class="badge bg-info mb-2">
|
||||||
|
<i class="fab fa-linkedin me-1"></i> Posted to LinkedIn
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'job_detail' job.slug %}" class="btn btn-sm btn-main-action">
|
||||||
|
<i class="fas fa-eye"></i> View
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'job_update' job.slug %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-edit"></i> Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Table View #}
|
||||||
|
<div class="table-view">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Job Title" %}</th>
|
||||||
|
<th scope="col">{% trans "Department" %}</th>
|
||||||
|
<th scope="col">{% trans "Location" %}</th>
|
||||||
|
<th scope="col">{% trans "Job Type" %}</th>
|
||||||
|
<th scope="col">{% trans "Status" %}</th>
|
||||||
|
<th scope="col">{% trans "Source" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for job in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-medium">{{ job.title }}</td>
|
||||||
|
<td>{{ job.department|default:"N/A" }}</td>
|
||||||
|
<td>{{ job.get_location_display }}</td>
|
||||||
|
<td>{{ job.get_job_type_display }}</td>
|
||||||
|
<td><span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">{{ job.get_status_display }}</span></td>
|
||||||
|
<td>{{ job.get_source }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-primary" title="View">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'job_update' job.slug %}" class="btn btn-outline-secondary" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if page_obj.has_other_pages %}
|
{% if page_obj.has_other_pages %}
|
||||||
@ -245,4 +292,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -33,19 +33,19 @@
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
/* FIX: Remove link underline for anchor tags used as buttons */
|
/* FIX: Remove link underline for anchor tags used as buttons */
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Small Button Size */
|
/* Small Button Size */
|
||||||
.btn-sm {
|
.btn-sm {
|
||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main Action Button (Create Meeting) */
|
/* Main Action Button (Create Meeting) */
|
||||||
.btn-main-action {
|
.btn-main-action {
|
||||||
background-color: var(--kaauh-teal);
|
background-color: var(--kaauh-teal);
|
||||||
border-color: var(--kaauh-teal);
|
border-color: var(--kaauh-teal);
|
||||||
color: white;
|
color: white;
|
||||||
@ -57,7 +57,7 @@
|
|||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
text-decoration: none; /* Ensure no hover underline if base fails */
|
text-decoration: none; /* Ensure no hover underline if base fails */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Outline Primary (View/Join buttons) */
|
/* Outline Primary (View/Join buttons) */
|
||||||
.btn-kaats-outline-primary {
|
.btn-kaats-outline-primary {
|
||||||
color: var(--kaauh-teal);
|
color: var(--kaauh-teal);
|
||||||
@ -118,7 +118,7 @@
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TOPIC AND DETAILS STYLES */
|
/* TOPIC AND DETAILS STYLES */
|
||||||
.meeting-topic {
|
.meeting-topic {
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
@ -169,7 +169,7 @@
|
|||||||
background: var(--kaauh-danger) !important;
|
background: var(--kaauh-danger) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ACTION AREA STYLES */
|
/* ACTION AREA STYLES */
|
||||||
.actions {
|
.actions {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
@ -177,7 +177,7 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header Styling */
|
/* Header Styling */
|
||||||
.card-header h1 {
|
.card-header h1 {
|
||||||
color: var(--kaauh-teal-dark);
|
color: var(--kaauh-teal-dark);
|
||||||
@ -193,7 +193,7 @@
|
|||||||
.text-muted.mb-3 {
|
.text-muted.mb-3 {
|
||||||
color: var(--kaauh-teal-dark) !important;
|
color: var(--kaauh-teal-dark) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pagination Link Styling */
|
/* Pagination Link Styling */
|
||||||
.pagination .page-item .page-link {
|
.pagination .page-item .page-link {
|
||||||
color: var(--kaauh-teal-dark);
|
color: var(--kaauh-teal-dark);
|
||||||
@ -246,75 +246,138 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if meetings %}
|
{% if meetings %}
|
||||||
<div class="meetings-grid">
|
<div id="meetings-list">
|
||||||
{% for meeting in meetings %}
|
{# View Switcher #}
|
||||||
<div class="meeting-card">
|
{% include "includes/_list_view_switcher.html" with list_id="meetings-list" %}
|
||||||
<div class="meeting-topic">{{ meeting.topic }}</div>
|
|
||||||
|
|
||||||
<div class="meeting-detail">
|
{# Card View (Default) #}
|
||||||
<div class="detail-label">{% trans "ID" %}:</div>
|
<div class="card-view active">
|
||||||
<div class="detail-value">{{ meeting.meeting_id|default:meeting.id }}</div>
|
<div class="meetings-grid">
|
||||||
</div>
|
{% for meeting in meetings %}
|
||||||
|
<div class="meeting-card">
|
||||||
|
<div class="meeting-topic">{{ meeting.topic }}</div>
|
||||||
|
|
||||||
<div class="meeting-detail">
|
<div class="meeting-detail">
|
||||||
<div class="detail-label">{% trans "Start Time" %}:</div>
|
<div class="detail-label">{% trans "ID" %}:</div>
|
||||||
<div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div>
|
<div class="detail-value">{{ meeting.meeting_id|default:meeting.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="meeting-detail">
|
<div class="meeting-detail">
|
||||||
<div class="detail-label">{% trans "Duration" %}:</div>
|
<div class="detail-label">{% trans "Start Time" %}:</div>
|
||||||
<div class="detail-value">{{ meeting.duration }} minutes</div>
|
<div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="meeting-detail">
|
<div class="meeting-detail">
|
||||||
<div class="detail-label">{% trans "Status" %}:</div>
|
<div class="detail-label">{% trans "Duration" %}:</div>
|
||||||
<div class="detail-value">
|
<div class="detail-value">{{ meeting.duration }} minutes</div>
|
||||||
<span class="status-badge {% if meeting.status == 'waiting' %}bg-warning{% elif meeting.status == 'started' %}bg-success{% elif meeting.status == 'ended' %}bg-danger{% endif %}">
|
</div>
|
||||||
{% if meeting.status == 'waiting' %}
|
|
||||||
{% trans "Waiting" %}
|
|
||||||
{% elif meeting.status == 'started' %}
|
|
||||||
{% trans "Started" %}
|
|
||||||
{% elif meeting.status == 'ended' %}
|
|
||||||
{% trans "Ended" %}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if meeting.join_url %}
|
<div class="meeting-detail">
|
||||||
<div class="meeting-detail">
|
<div class="detail-label">{% trans "Status" %}:</div>
|
||||||
<div class="detail-label">{% trans "Join URL" %}:</div>
|
<div class="detail-value">
|
||||||
<div class="detail-value">
|
<span class="status-badge {% if meeting.status == 'waiting' %}bg-warning{% elif meeting.status == 'started' %}bg-success{% elif meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||||
<a href="{{ meeting.join_url }}" target="_blank" class="btn-base btn-kaats-outline-primary btn-sm">
|
{% if meeting.status == 'waiting' %}
|
||||||
{% trans "Join Meeting" %}
|
{% trans "Waiting" %}
|
||||||
|
{% elif meeting.status == 'started' %}
|
||||||
|
{% trans "Started" %}
|
||||||
|
{% elif meeting.status == 'ended' %}
|
||||||
|
{% trans "Ended" %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if meeting.join_url %}
|
||||||
|
<div class="meeting-detail">
|
||||||
|
<div class="detail-label">{% trans "Join URL" %}:</div>
|
||||||
|
<div class="detail-value">
|
||||||
|
<a href="{{ meeting.join_url }}" target="_blank" class="btn-base btn-kaats-outline-primary btn-sm">
|
||||||
|
{% trans "Join Meeting" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<a href="{% url 'meeting_details' meeting.pk %}" class="btn-base btn-kaats-outline-primary btn-sm" title="{% trans 'View' %}">
|
||||||
|
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url 'update_meeting' meeting.pk %}" class="btn-base btn-kaats-outline-secondary btn-sm" title="{% trans 'Update' %}">
|
||||||
|
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn-base btn-kaats-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
||||||
|
data-item-name="{{ meeting.topic }}">
|
||||||
|
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<a href="{% url 'meeting_details' meeting.pk %}" class="btn-base btn-kaats-outline-primary btn-sm" title="{% trans 'View' %}">
|
|
||||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'update_meeting' meeting.pk %}" class="btn-base btn-kaats-outline-secondary btn-sm" title="{% trans 'Update' %}">
|
|
||||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<button type="button" class="btn-base btn-kaats-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
|
||||||
data-bs-toggle="deleteModal"
|
|
||||||
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
|
||||||
data-item-name="{{ meeting.topic }}">
|
|
||||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
|
||||||
|
{# Table View #}
|
||||||
|
<div class="table-view">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{% trans "Topic" %}</th>
|
||||||
|
<th scope="col">{% trans "ID" %}</th>
|
||||||
|
<th scope="col">{% trans "Start Time" %}</th>
|
||||||
|
<th scope="col">{% trans "Duration" %}</th>
|
||||||
|
<th scope="col">{% trans "Status" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for meeting in meetings %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ meeting.topic }}</strong></td>
|
||||||
|
<td>{{ meeting.meeting_id|default:meeting.id }}</td>
|
||||||
|
<td>{{ meeting.start_time|date:"M d, Y H:i" }}</td>
|
||||||
|
<td>{{ meeting.duration }} minutes</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge {% if meeting.status == 'waiting' %}bg-warning{% elif meeting.status == 'started' %}bg-success{% elif meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||||
|
{% if meeting.status == 'waiting' %}
|
||||||
|
{% trans "Waiting" %}
|
||||||
|
{% elif meeting.status == 'started' %}
|
||||||
|
{% trans "Started" %}
|
||||||
|
{% elif meeting.status == 'ended' %}
|
||||||
|
{% trans "Ended" %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'meeting_details' meeting.pk %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'update_meeting' meeting.pk %}" class="btn btn-outline-secondary" title="{% trans 'Update' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
||||||
|
data-item-name="{{ meeting.topic }}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
@ -365,4 +428,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -143,54 +143,106 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if candidates %}
|
{% if candidates %}
|
||||||
<div class="table-responsive">
|
<div id="candidate-list">
|
||||||
<table class="table table-hover mb-0">
|
{# View Switcher #}
|
||||||
<thead>
|
{% include "includes/_list_view_switcher.html" with list_id="candidate-list" %}
|
||||||
<tr>
|
|
||||||
<th scope="col" style="width: 20%;">{% trans "Name" %}</th>
|
{# Table View (Default) #}
|
||||||
<th scope="col" style="width: 20%;">{% trans "Email" %}</th>
|
<div class="table-view active">
|
||||||
<th scope="col" style="width: 15%;">{% trans "Phone" %}</th>
|
<div class="table-responsive">
|
||||||
<th scope="col" style="width: 15%;">{% trans "Job" %}</th>
|
<table class="table table-hover mb-0">
|
||||||
<th scope="col" style="width: 10%;">{% trans "Stage" %}</th>
|
<thead>
|
||||||
<th scope="col" style="width: 10%;">{% trans "Created" %}</th>
|
<tr>
|
||||||
<th scope="col" style="width: 10%;" class="text-center">{% trans "Actions" %}</th>
|
<th scope="col" style="width: 20%;">{% trans "Name" %}</th>
|
||||||
</tr>
|
<th scope="col" style="width: 20%;">{% trans "Email" %}</th>
|
||||||
</thead>
|
<th scope="col" style="width: 15%;">{% trans "Phone" %}</th>
|
||||||
<tbody>
|
<th scope="col" style="width: 15%;">{% trans "Job" %}</th>
|
||||||
|
<th scope="col" style="width: 10%;">{% trans "Stage" %}</th>
|
||||||
|
<th scope="col" style="width: 10%;">{% trans "Created" %}</th>
|
||||||
|
<th scope="col" style="width: 10%;" class="text-center">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for candidate in candidates %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ candidate.name }}</strong></td>
|
||||||
|
<td>{{ candidate.email }}</td>
|
||||||
|
<td>{{ candidate.phone }}</td>
|
||||||
|
<td> <span class="badge bg-primary">{{ candidate.job.title }}</span></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-primary">
|
||||||
|
{{ candidate.stage }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||||
|
data-item-name="{{ candidate.name }}">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Card View #}
|
||||||
|
<div class="card-view">
|
||||||
|
<div class="row g-4">
|
||||||
{% for candidate in candidates %}
|
{% for candidate in candidates %}
|
||||||
<tr>
|
<div class="col-md-6 col-lg-4">
|
||||||
<td><strong>{{ candidate.name }}</strong></td>
|
<div class="card h-100">
|
||||||
<td>{{ candidate.email }}</td>
|
<div class="card-header">
|
||||||
<td>{{ candidate.phone }}</td>
|
<h5 class="h5 mb-1">{{ candidate.name }}</h5>
|
||||||
<td> <span class="badge bg-primary">{{ candidate.job.title }}</span></td>
|
<small class="text-white-50">{{ candidate.email }}</small>
|
||||||
<td>
|
|
||||||
<span class="badge bg-primary">
|
|
||||||
{{ candidate.stage }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
|
||||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
|
||||||
{% if user.is_staff %}
|
|
||||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'Edit' %}">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
|
||||||
data-bs-toggle="deleteModal"
|
|
||||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
|
||||||
data-item-name="{{ candidate.name }}">
|
|
||||||
<i class="fas fa-trash-alt"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="card-body">
|
||||||
</tr>
|
<p class="card-text">
|
||||||
|
<strong>{% trans "Phone" %}:</strong> {{ candidate.phone|default:"N/A" }}<br>
|
||||||
|
<strong>{% trans "Job" %}:</strong> <span class="badge bg-primary">{{ candidate.job.title }}</span><br>
|
||||||
|
<strong>{% trans "Stage" %}:</strong> <span class="badge bg-primary">{{ candidate.stage }}</span><br>
|
||||||
|
<strong>{% trans "Created" %}:</strong> {{ candidate.created_at|date:"M d, Y" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||||
|
</a>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||||
|
data-item-name="{{ candidate.name }}">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
@ -242,4 +294,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Outlined Button Styles for Table Actions */
|
/* Outlined Button Styles for Table Actions */
|
||||||
.btn-outline-primary {
|
.btn-outline-primary {
|
||||||
color: var(--kaauh-teal);
|
color: var(--kaauh-teal);
|
||||||
@ -66,7 +66,7 @@
|
|||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colored Header Card */
|
/* Colored Header Card */
|
||||||
.list-header-card {
|
.list-header-card {
|
||||||
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
|
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
|
||||||
@ -101,7 +101,7 @@
|
|||||||
.btn-group .btn-sm {
|
.btn-group .btn-sm {
|
||||||
padding: 0.35rem 0.6rem;
|
padding: 0.35rem 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pagination Styling */
|
/* Pagination Styling */
|
||||||
.pagination .page-link {
|
.pagination .page-link {
|
||||||
color: var(--kaauh-teal-dark);
|
color: var(--kaauh-teal-dark);
|
||||||
@ -118,7 +118,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
|
|
||||||
<div class="list-header-card">
|
<div class="list-header-card">
|
||||||
<div class="d-flex justify-content-between align-items-center flex-wrap">
|
<div class="d-flex justify-content-between align-items-center flex-wrap">
|
||||||
<h1 class="h3 mb-0">
|
<h1 class="h3 mb-0">
|
||||||
@ -126,7 +126,7 @@
|
|||||||
{% trans "Training Materials" %}
|
{% trans "Training Materials" %}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="d-flex gap-3 align-items-center mt-2 mt-md-0">
|
<div class="d-flex gap-3 align-items-center mt-2 mt-md-0">
|
||||||
|
|
||||||
<div class="order-3 order-md-1">
|
<div class="order-3 order-md-1">
|
||||||
{% include "includes/search_form.html" with search_query=search_query %}
|
{% include "includes/search_form.html" with search_query=search_query %}
|
||||||
</div>
|
</div>
|
||||||
@ -140,47 +140,96 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% if materials %}
|
{% if materials %}
|
||||||
<div class="table-responsive">
|
<div id="training-materials-list">
|
||||||
<table class="table table-hover align-middle mb-0">
|
{# View Switcher #}
|
||||||
<thead>
|
{% include "includes/_list_view_switcher.html" with list_id="training-materials-list" %}
|
||||||
<tr>
|
|
||||||
<th scope="col">{% trans "Title" %}</th>
|
{# Table View (Default) #}
|
||||||
<th scope="col">{% trans "Created By" %}</th>
|
<div class="table-view active">
|
||||||
<th scope="col">{% trans "Created" %}</th>
|
<div class="table-responsive">
|
||||||
<th scope="col" class="text-center">{% trans "Actions" %}</th>
|
<table class="table table-hover align-middle mb-0">
|
||||||
</tr>
|
<thead>
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>
|
<th scope="col">{% trans "Title" %}</th>
|
||||||
|
<th scope="col">{% trans "Created By" %}</th>
|
||||||
|
<th scope="col">{% trans "Created" %}</th>
|
||||||
|
<th scope="col" class="text-center">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for material in materials %}
|
||||||
|
<tr>
|
||||||
|
<td><strong class="text-primary">{{ material.title }}</strong></td>
|
||||||
|
<td>{{ material.created_by.username|default:"Anonymous" }}</td>
|
||||||
|
<td>{{ material.created_at|date:"M d, Y" }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'training_detail' material.pk %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
{% if user.is_authenticated and material.created_by == user %}
|
||||||
|
<a href="{% url 'training_update' material.pk %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'training_delete' material.pk %}"
|
||||||
|
data-item-name="{{ material.title }}">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Card View #}
|
||||||
|
<div class="card-view">
|
||||||
|
<div class="row g-4">
|
||||||
{% for material in materials %}
|
{% for material in materials %}
|
||||||
<tr>
|
<div class="col-md-6 col-lg-4">
|
||||||
<td><strong class="text-primary">{{ material.title }}</strong></td>
|
<div class="card h-100">
|
||||||
<td>{{ material.created_by.username|default:"Anonymous" }}</td>
|
<div class="card-header">
|
||||||
<td>{{ material.created_at|date:"M d, Y" }}</td>
|
<h5 class="h5 mb-1">{{ material.title }}</h5>
|
||||||
<td class="text-center">
|
<small class="text-white-50">{{ material.created_by.username|default:"Anonymous" }}</small>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
|
||||||
<a href="{% url 'training_detail' material.pk %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
|
||||||
{% if user.is_authenticated and material.created_by == user %}
|
|
||||||
<a href="{% url 'training_update' material.pk %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
|
||||||
data-bs-toggle="deleteModal"
|
|
||||||
data-delete-url="{% url 'training_delete' material.pk %}"
|
|
||||||
data-item-name="{{ material.title }}">
|
|
||||||
<i class="fas fa-trash-alt"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="card-body">
|
||||||
</tr>
|
<p class="card-text">
|
||||||
|
<strong>{% trans "Created" %}:</strong> {{ material.created_at|date:"M d, Y" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'training_detail' material.pk %}" class="btn btn-sm btn-outline-primary w-100">
|
||||||
|
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||||
|
</a>
|
||||||
|
{% if user.is_authenticated and material.created_by == user %}
|
||||||
|
<div class="btn-group w-100" role="group">
|
||||||
|
<a href="{% url 'training_update' material.pk %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||||
|
data-bs-toggle="deleteModal"
|
||||||
|
data-delete-url="{% url 'training_delete' material.pk %}"
|
||||||
|
data-item-name="{{ material.title }}">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
@ -194,7 +243,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
{% for num in page_obj.paginator.page_range %}
|
||||||
{% if page_obj.number == num %}
|
{% if page_obj.number == num %}
|
||||||
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
||||||
@ -204,7 +253,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||||
@ -216,7 +265,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
<i class="fas fa-book-open fa-3x text-muted mb-3"></i>
|
<i class="fas fa-book-open fa-3x text-muted mb-3"></i>
|
||||||
@ -232,4 +281,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user