diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index b4e2d8a..b3fa580 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index 9a77a66..6b6b9ff 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index 8f09ef6..4b4d2ca 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-312.pyc and b/recruitment/__pycache__/views_frontend.cpython-312.pyc differ diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index d6c2da6..48af4fe 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-23 14:08 +# Generated by Django 5.2.7 on 2025-10-29 11:45 import django.core.validators import django.db.models.deletion @@ -66,6 +66,22 @@ class Migration(migrations.Migration): 'ordering': ['name'], }, ), + migrations.CreateModel( + name='Participants', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Participant Name')), + ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), + ('designation', models.CharField(blank=True, max_length=100, null=True, verbose_name='Designation')), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='Source', fields=[ @@ -211,6 +227,7 @@ class Migration(migrations.Migration): ('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')), ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), ('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')), + ('retry', models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry')), ('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')), ], options={ @@ -248,6 +265,7 @@ class Migration(migrations.Migration): ('posted_to_linkedin', models.BooleanField(default=False)), ('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)), ('linkedin_posted_at', models.DateTimeField(blank=True, null=True)), + ('linkedin_post_formated_data', models.TextField(blank=True, null=True)), ('published_at', models.DateTimeField(blank=True, db_index=True, null=True)), ('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)), ('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)), @@ -257,6 +275,8 @@ class Migration(migrations.Migration): ('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')), ('cancelled_at', models.DateTimeField(blank=True, null=True)), ('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')), + ('users', models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant')), + ('participants', models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs_participating', to='recruitment.participants', verbose_name='External Participant')), ('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')), ], options={ @@ -308,9 +328,17 @@ class Migration(migrations.Migration): name='Profile', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size])), + ('designation', models.CharField(blank=True, max_length=100, null=True)), + ('phone', models.CharField(blank=True, max_length=12, null=True, verbose_name='Phone Number')), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='SharedFormTemplate', diff --git a/recruitment/migrations/0002_candidate_retry.py b/recruitment/migrations/0002_candidate_retry.py deleted file mode 100644 index 0f405fc..0000000 --- a/recruitment/migrations/0002_candidate_retry.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-26 11:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='retry', - field=models.SmallIntegerField(default=3, verbose_name='Resume Parsing Retry'), - ), - ] diff --git a/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py b/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py new file mode 100644 index 0000000..00aaa1e --- /dev/null +++ b/recruitment/migrations/0002_remove_profile_created_at_remove_profile_slug_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.7 on 2025-10-29 12:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('recruitment', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='profile', + name='created_at', + ), + migrations.RemoveField( + model_name='profile', + name='slug', + ), + migrations.RemoveField( + model_name='profile', + name='updated_at', + ), + ] diff --git a/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py deleted file mode 100644 index 5e92015..0000000 --- a/recruitment/migrations/0003_jobposting_linkedin_post_formated_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-27 10:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0002_candidate_retry'), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='linkedin_post_formated_data', - field=models.JSONField(blank=True, null=True), - ), - ] diff --git a/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py deleted file mode 100644 index d821d60..0000000 --- a/recruitment/migrations/0004_alter_jobposting_linkedin_post_formated_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-27 11:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0003_jobposting_linkedin_post_formated_data'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='linkedin_post_formated_data', - field=models.CharField(blank=True, null=True), - ), - ] diff --git a/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py b/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py deleted file mode 100644 index 6bd9476..0000000 --- a/recruitment/migrations/0005_alter_jobposting_linkedin_post_formated_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-27 11:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0004_alter_jobposting_linkedin_post_formated_data'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='linkedin_post_formated_data', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/recruitment/migrations/0006_participants.py b/recruitment/migrations/0006_participants.py deleted file mode 100644 index db585d3..0000000 --- a/recruitment/migrations/0006_participants.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 12:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0005_alter_jobposting_linkedin_post_formated_data'), - ] - - operations = [ - migrations.CreateModel( - name='Participants', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, verbose_name='Participant Name')), - ('email', models.EmailField(max_length=254, verbose_name='Email')), - ('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')), - ('designation', models.CharField(blank=True, max_length=100, verbose_name='Designation')), - ('job', models.ManyToManyField(blank=True, related_name='participants', to='recruitment.jobposting')), - ], - ), - ] diff --git a/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py b/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py deleted file mode 100644 index fc89f39..0000000 --- a/recruitment/migrations/0007_participants_created_at_participants_slug_and_more.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 12:14 - -import django_extensions.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0006_participants'), - ] - - operations = [ - migrations.AddField( - model_name='participants', - name='created_at', - field=models.DateTimeField(auto_now_add=True, default=None, verbose_name='Created at'), - preserve_default=False, - ), - migrations.AddField( - model_name='participants', - name='slug', - field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'), - ), - migrations.AddField( - model_name='participants', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - ] diff --git a/recruitment/migrations/0008_rename_job_participants_jobs.py b/recruitment/migrations/0008_rename_job_participants_jobs.py deleted file mode 100644 index 4bea33f..0000000 --- a/recruitment/migrations/0008_rename_job_participants_jobs.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 13:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0007_participants_created_at_participants_slug_and_more'), - ] - - operations = [ - migrations.RenameField( - model_name='participants', - old_name='job', - new_name='jobs', - ), - ] diff --git a/recruitment/migrations/0009_jobposting_assigned_users.py b/recruitment/migrations/0009_jobposting_assigned_users.py deleted file mode 100644 index 1ec5f0b..0000000 --- a/recruitment/migrations/0009_jobposting_assigned_users.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 16:41 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0008_rename_job_participants_jobs'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='assigned_users', - field=models.ManyToManyField(blank=True, related_name='assigned_jobs', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/recruitment/migrations/0010_remove_jobposting_assigned_users.py b/recruitment/migrations/0010_remove_jobposting_assigned_users.py deleted file mode 100644 index 3b7548d..0000000 --- a/recruitment/migrations/0010_remove_jobposting_assigned_users.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 17:12 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0009_jobposting_assigned_users'), - ] - - operations = [ - migrations.RemoveField( - model_name='jobposting', - name='assigned_users', - ), - ] diff --git a/recruitment/migrations/0011_jobposting_internal_participant.py b/recruitment/migrations/0011_jobposting_internal_participant.py deleted file mode 100644 index e051508..0000000 --- a/recruitment/migrations/0011_jobposting_internal_participant.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 20:42 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0010_remove_jobposting_assigned_users'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='jobposting', - name='internal_participant', - field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='internal_participant_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), - ), - ] diff --git a/recruitment/migrations/0012_remove_participants_jobs_and_more.py b/recruitment/migrations/0012_remove_participants_jobs_and_more.py deleted file mode 100644 index 799ee52..0000000 --- a/recruitment/migrations/0012_remove_participants_jobs_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 21:30 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0011_jobposting_internal_participant'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name='participants', - name='jobs', - ), - migrations.AddField( - model_name='jobposting', - name='external_participant', - field=models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs', to='recruitment.participants', verbose_name='External Participant'), - ), - migrations.AlterField( - model_name='jobposting', - name='internal_participant', - field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), - ), - ] diff --git a/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py b/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py deleted file mode 100644 index e830392..0000000 --- a/recruitment/migrations/0013_remove_jobposting_external_participant_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 22:20 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0012_remove_participants_jobs_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.RemoveField( - model_name='jobposting', - name='external_participant', - ), - migrations.RemoveField( - model_name='jobposting', - name='internal_participant', - ), - migrations.AddField( - model_name='jobposting', - name='participants', - field=models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs_participating', to='recruitment.participants', verbose_name='External Participant'), - ), - migrations.AddField( - model_name='jobposting', - name='users', - field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'), - ), - ] diff --git a/recruitment/models.py b/recruitment/models.py index a258a42..7d7f08a 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -28,6 +28,8 @@ class Base(models.Model): class Profile(models.Model): profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/",validators=[validate_image_size]) + designation = models.CharField(max_length=100, blank=True,null=True) + phone=models.CharField(blank=True,null=True,verbose_name=_("Phone Number"),max_length=12) user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") def __str__(self): @@ -735,6 +737,41 @@ class ZoomMeeting(Base): def __str__(self): return self.topic + + @property + def get_job(self): + try: + job=self.interview.job.first() + return job + except: + return None + @property + def get_candidate(self): + try: + candidate=self.interview.candidate.first() + return candidate + except: + return None + + @property + def get_external_participants(self): + try: + interview=self.interview.first() + if interview: + return interview.job.participants.all() + return None + except: + return None + @property + def get_users_participants(self): + try: + interview=self.interview.first() + if interview: + return interview.job.users.all() + return None + except: + return None + class MeetingComment(Base): @@ -1267,7 +1304,8 @@ class InterviewSchedule(Base): models.Index(fields=['end_date']), models.Index(fields=['created_by']), ] - + + class ScheduledInterview(Base): """Stores individual scheduled interviews""" @@ -1319,11 +1357,11 @@ class ScheduledInterview(Base): class Participants(Base): """Model to store Participants details""" - name = models.CharField(max_length=255, verbose_name=_("Participant Name")) + name = models.CharField(max_length=255, verbose_name=_("Participant Name"),null=True,blank=True) email= models.EmailField(verbose_name=_("Email")) - phone = models.CharField(max_length=20, blank=True, verbose_name=_("Phone")) + phone = models.CharField(max_length=12,verbose_name=_("Phone Number"),null=True,blank=True) designation = models.CharField( - max_length=100, blank=True, verbose_name=_("Designation") + max_length=100, blank=True, verbose_name=_("Designation"),null=True ) def __str__(self): diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index a7b0b8c..938e150 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -336,7 +336,8 @@ class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView): success_url = reverse_lazy('training_list') success_message = 'Training material deleted successfully.' - +from django.db.models import F, IntegerField, Count, Avg +from django.db.models.functions import Cast, Coalesce @login_required def dashboard_view(request): all_candidates_count=0 @@ -362,39 +363,39 @@ def dashboard_view(request): # Assuming 'match_score' is a direct IntegerField/FloatField on the Candidate model # (based on the final, optimized version of handle_reume_parsing_and_scoring) + # The path to your score: ai_analysis_data['analysis_data']['match_score'] + SCORE_PATH = 'ai_analysis_data__analysis_data__match_score' - # Average Match Score (Overall Quality) - candidates_with_score = models.Candidate.objects.filter( - # Filter only candidates that have been parsed/scored + # --- The Annotate Step --- + candidates_with_score_query = models.Candidate.objects.filter( is_resume_parsed=True ).annotate( - score_as_text=KeyTextTransform( - 'match_score', - KeyTextTransform('scoring_data', F('ai_analysis_data')) + # 1. Use Coalesce to handle cases where the score might be missing or NULL + # (It defaults the value to 0 if missing). + # 2. Use Cast to convert the JSON value (which is often returned as text/string by the DB) + # into a proper IntegerField so we can perform math on it. + annotated_match_score=Coalesce( + Cast(SCORE_PATH, output_field=IntegerField()), + 0 ) - ).annotate( - # Cast the extracted text score to a FloatField so AVG() can operate on it. - sortable_score=Cast('score_as_text', output_field=FloatField()) ) + # Now calculate the average match score + avg_match_score_result = candidates_with_score_query.aggregate( + avg_score=Avg('annotated_match_score') + )['avg_score'] + avg_match_score = round(avg_match_score_result or 0, 1) - # 2b. AGGREGATE using the newly created 'sortable_score' field - avg_match_score_result = candidates_with_score.aggregate( - avg_score=Avg('sortable_score') - )['avg_score'] + hight_potential_count=0 + # --- The Filter Step for High Potential Candidates --- + candidates_with_score_gte_75 = candidates_with_score_query.filter( + annotated_match_score__gte=75 + ) + high_potential_count=candidates_with_score_gte_75.count() + high_potential_ratio = round((hight_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 - avg_match_score = round(avg_match_score_result or 0, 1) - - # 2c. Use the annotated QuerySet for other metrics - - # Scored Candidates Ratio (Now simpler, as we filtered the QuerySet) - total_scored = candidates_with_score.count() - scored_ratio = round((total_scored / total_candidates) * 100, 1) if total_candidates > 0 else 0 - - # High Potential Candidates (Filter the annotated QuerySet) - high_potential_count = candidates_with_score.filter( - sortable_score__gte=75 - ).count() - high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 + # Scored Candidates Ratio + total_scored_candidates = candidates_with_score_query.count() + scored_ratio = round((total_scored_candidates / total_candidates) * 100, 1) if total_candidates > 0 else 0 jobs=models.JobPosting.objects.all().order_by('internal_job_id') selected_job_pk=request.GET.get('selected_job_pk','') diff --git a/templates/meetings/meeting_details.html b/templates/meetings/meeting_details.html index 2d3761a..8df9080 100644 --- a/templates/meetings/meeting_details.html +++ b/templates/meetings/meeting_details.html @@ -3,199 +3,199 @@ {% block customCSS %} {% endblock %} @@ -203,251 +203,263 @@ body { {% block content %}
- {# --- TOP BAR / BACK BUTTON --- #} -
+ {# --- TOP BAR / BACK BUTTON & ACTIONS (EDIT/DELETE) --- #} +
+ {# Back Button #} {% trans "Back to Meetings" %} + + {# Edit and Delete Buttons #} +
+ + {% trans "Edit Meeting" %} + + {# DELETE MEETING FORM #} +
+ {% csrf_token %} + +
+
-
+ {# ========================================================= #} + {# --- SECTION 1: PROMINENT TOP DETAILS & JOIN INFO --- #} + {# ========================================================= #} +
- {# --- LEFT COLUMN (COMMENTS & INTERNAL CONTEXT) - Takes 50% of the screen #} -
- - {# --- 1. INTERNAL NOTES / DESCRIPTION CARD (New CRM Feature) --- #} - {% if meeting.description %} -
-
-

{% trans "Internal Context" %}

-

{% trans "Meeting agenda, purpose, or interview details for internal team use." %}

-
-

{{ meeting.description|safe }}

-
+ {# --- LEFT HALF: MAIN TOPIC & JOB CONTEXT --- #} +
+
+

+ + {{ meeting.topic|default:"[Meeting Topic N/A]" }} + + {{ meeting.status|title|default:'N/A' }} + +

+
+ + {# JOB CONTEXT DETAILS (Simple Divs) #} +
+

{% trans "Interview Detail" %}

+
+
{% trans "Job Title" %}:
{{ meeting.get_job.title|default:"N/A" }}
+ + +
{% trans "Job Type" %}:
{{ meeting.get_job.job_type|default:"N/A" }}
- {% endif %} +
- {# --- 2. Comments Section (Now in the Left Column) --- #} -
+ {# --- RIGHT HALF: ZOOM LINK / ACTIONS --- #} +
+
+

{% trans "Connection Details" %}

+
+
{% trans "Date & Time" %}:
{{ meeting.start_time|date:"M d, Y H:i"|default:"N/A" }}
+
{% trans "Duration" %}:
{{ meeting.duration|default:"N/A" }} {% trans "minutes" %}
+
{% trans "Meeting ID" %}:
{{ meeting.meeting_id|default:"N/A" }}
+
{% trans "Host Email" %}:
{{ meeting.host_email|default:"N/A" }}
+ {% if meeting.join_url %} +
+
{% trans "Copied!" %}
+ +
+
+ {% trans "Join URL" %}: + {{ meeting.join_url }} +
+ +
+
+ {% endif %} +
+
+
+ +
+ + {# ========================================================= #} + {# --- SECTION 2: PERSONNEL TABLES --- #} + {# ========================================================= #} +
+ + + {# --- PARTICIPANTS TABLE --- #} +
+
+

{% trans "Assigned Participants" %}

+ + + + + + + + + + + + + {% for participant in meeting.get_external_participants %} + + + + + + + + {% endfor %} + {% for participant in meeting.get_users_participants %} + + + + + + + + {% endfor %} + + +
{% trans "Name" %}{% trans "Role/Designation" %}{% trans "Email" %}{% trans "Phone Number" %}{% trans "Source Type" %}
{{participant.name}}{{participant.designation}}{{participant.email}}{{participant.phone}}{% trans "External Participants" %}
{{participant.name}}{{participant.designation}}{{participant.email}}{{participant.phone}}{% trans "System User" %}
+
+
+
+ + {# ========================================================= #} + {# --- SECTION 3: COMMENTS (CORRECTED) --- #} + {# ========================================================= #} +
+ +
+
- {% trans "Comments" %} ({{ meeting.comments.count }}) + {% trans "Comments" %} ({% if meeting.comments %}{{ meeting.comments.count }}{% else %}0{% endif %})
- {% if user.is_authenticated %} - - {% endif %}
-
+ + {# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #} +
{% if meeting.comments.all %} {% for comment in meeting.comments.all|dictsortreversed:"created_at" %} -
-
-
- {{ comment.author.get_full_name|default:comment.author.username }} - {% if comment.author != user %} - {% trans "Comment" %} - {% endif %} -
- {{ comment.created_at|date:"M d, Y P" }} -
-
-

{{ comment.content|safe }}

-
- -
-
+ +
- {# --- RIGHT COLUMN (MAIN DETAILS & JOIN INFO) - Takes 50% of the screen #} -
-
- - {# --- CRM ASSOCIATED RECORD CARD (Elevated Importance) --- #} - {% if meeting.interview %} -
-
-
- -
-
{% trans "Associated Record" %}
- - - {{ meeting.interview.candidate.name }} - - - {{ meeting.interview.job_position }} + {# 2. NEW COMMENT SUBMISSION (Remains the same) #} +
{% trans "Add a New Comment" %}
+ {% if user.is_authenticated %} +
+ {% csrf_token %} + {% if comment_form %} + {{ comment_form.as_p }} + {% else %} +
+ +
-
-
-
- {% endif %} - - {# --- 1. MAIN DETAILS CARD --- #} -
- {# --- CONSOLIDATED HEADER --- #} -
-
-
-

- - - - - {{ meeting.topic }} -

-
- - {{ meeting.status|title }} - -
-
-
-
- - {# --- CONNECTION DETAIL BODY (Renamed from Core Details) --- #} -
-

{% trans "Connection Details" %}

-
-
{% trans "Meeting ID" %}:
{{ meeting.meeting_id }}
-
{% trans "Start Time" %}:
{{ meeting.start_time|date:"M d, Y H:i" }}
-
{% trans "Duration" %}:
{{ meeting.duration }} {% trans "minutes" %}
-
{% trans "Timezone" %}:
{{ meeting.timezone|default:"UTC" }}
-
{% trans "Host Email" %}:
{{ meeting.host_email|default:"N/A" }}
-
-
- - {# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #} - + + {% else %} +

{% trans "You must be logged in to add a comment." %}

+ {% endif %}
- - {# --- 2. JOIN INFO CARD (Separate from details, but in the same column) --- #} - {% if meeting.join_url %} -
-
-

{% trans "Join Information" %}

- - - {% trans "Join Meeting Now" %} - - -
- {# Message should not be display: none; but opacity: 0; for smooth transition #} -
{% trans "Copied!" %}
- -
-
- {% trans "Join URL" %}: - {{ meeting.join_url }} -
- -
-
- - {% if meeting.password %} -
-
{% trans "Password" %}:
-
{{ meeting.password }}
-
- {% endif %} -
-
- {% endif %} -
-
-
- - {# --- API RESPONSE CARD (Full width, hidden by default) --- #} - {% if meeting.zoom_gateway_response %} - - {% endif %} - -
- -{# MODALS (KEEP OUTSIDE OF THE MAIN LAYOUT ROWS) #} -{% comment %} {% include 'modals/delete_modal.html' with item_name="Meeting" delete_url_name='delete_meeting' %} {% endcomment %} - + {% endblock %} {% block customJS %}