970 lines
27 KiB
HTML
970 lines
27 KiB
HTML
{% extends "base_authenticated.html" %}
|
|
|
|
{% block title %}Photo Verification Review{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
<!-- Page Header -->
|
|
<div class="page-header">
|
|
<h1>
|
|
<i class="fas fa-camera-retro"></i>
|
|
Photo Verification Review
|
|
</h1>
|
|
<p>Review and approve/reject check-ins requiring photo verification</p>
|
|
</div>
|
|
|
|
<!-- Status Summary Cards -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-icon pending">
|
|
<i class="fas fa-clock"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>{{ pending_count }}</h3>
|
|
<p>Pending Review</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon approved">
|
|
<i class="fas fa-check-circle"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>{{ approved_count }}</h3>
|
|
<p>Approved</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon rejected">
|
|
<i class="fas fa-times-circle"></i>
|
|
</div>
|
|
<div class="stat-info">
|
|
<h3>{{ rejected_count }}</h3>
|
|
<p>Rejected</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters Section -->
|
|
<div class="filter-section">
|
|
<form method="GET" action="{{ url_for('attendance.verification_review') }}">
|
|
<div class="filter-grid">
|
|
<div class="filter-group">
|
|
<label for="status">Status</label>
|
|
<select name="status" id="status" class="filter-select">
|
|
<option value="all" {% if status_filter == 'all' %}selected{% endif %}>All Status</option>
|
|
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Pending</option>
|
|
<option value="approved" {% if status_filter == 'approved' %}selected{% endif %}>Approved</option>
|
|
<option value="rejected" {% if status_filter == 'rejected' %}selected{% endif %}>Rejected</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label for="project">Project</label>
|
|
<select name="project" id="project" class="filter-select">
|
|
<option value="">All Projects</option>
|
|
{% for project in projects %}
|
|
<option value="{{ project.id }}" {% if project_filter == project.id|string %}selected{% endif %}>
|
|
{{ project.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label for="location">Location</label>
|
|
<select name="location" id="location" class="filter-select">
|
|
<option value="">All Locations</option>
|
|
{% for location in locations %}
|
|
<option value="{{ location }}" {% if location_filter == location %}selected{% endif %}>
|
|
{{ location }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label for="employee">Employee ID</label>
|
|
<input type="text" name="employee" id="employee" value="{{ employee_filter }}"
|
|
class="filter-input" placeholder="Search by ID...">
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label for="date_from">From Date</label>
|
|
<input type="date" name="date_from" id="date_from" value="{{ date_from }}" class="filter-input">
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label for="date_to">To Date</label>
|
|
<input type="date" name="date_to" id="date_to" value="{{ date_to }}" class="filter-input">
|
|
</div>
|
|
|
|
<div class="filter-group filter-buttons">
|
|
<button type="submit" class="btn-filter">
|
|
<i class="fas fa-filter"></i> Apply Filters
|
|
</button>
|
|
<a href="{{ url_for('attendance.verification_review') }}" class="btn-reset">
|
|
<i class="fas fa-undo"></i> Reset
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Verifications List -->
|
|
<div class="verifications-section">
|
|
{% if verifications %}
|
|
<div class="verifications-grid">
|
|
{% for record in verifications %}
|
|
<div class="verification-card" data-record-id="{{ record.id }}">
|
|
<div class="verification-header">
|
|
<div class="verification-info">
|
|
<h3>
|
|
<i class="fas fa-user"></i>
|
|
{{ record.employee_id }}
|
|
{% if employee_names.get(record.employee_id) %}
|
|
<span class="employee-name">- {{ employee_names.get(record.employee_id) }}</span>
|
|
{% endif %}
|
|
</h3>
|
|
<div class="verification-meta">
|
|
<span class="meta-item">
|
|
<i class="fas fa-calendar"></i>
|
|
{{ record.check_in_date.strftime('%b %d, %Y') }}
|
|
</span>
|
|
<span class="meta-item">
|
|
<i class="fas fa-clock"></i>
|
|
{{ record.check_in_time.strftime('%I:%M %p') }}
|
|
</span>
|
|
<span class="meta-item">
|
|
<i class="fas fa-map-marker-alt"></i>
|
|
{{ record.location_name }}
|
|
</span>
|
|
{% if record.qr_code and record.qr_code.location_event %}
|
|
<span class="meta-item event-badge {{ 'check-in' if record.qr_code.location_event == 'Check In' else 'check-out' }}">
|
|
<i class="fas fa-{{ 'sign-in-alt' if record.qr_code.location_event == 'Check In' else 'sign-out-alt' }}"></i>
|
|
{{ record.qr_code.location_event }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if record.qr_code and record.qr_code.project_id and project_names.get(record.qr_code.project_id) %}
|
|
<div class="verification-project">
|
|
<span class="project-badge">
|
|
<i class="fas fa-folder"></i>
|
|
{{ project_names.get(record.qr_code.project_id) }}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="verification-status">
|
|
{% if record.verification_status == 'pending' %}
|
|
<span class="status-badge pending">
|
|
<i class="fas fa-clock"></i> Pending
|
|
</span>
|
|
{% elif record.verification_status == 'approved' %}
|
|
<span class="status-badge approved">
|
|
<i class="fas fa-check-circle"></i> Approved
|
|
</span>
|
|
{% elif record.verification_status == 'rejected' %}
|
|
<span class="status-badge rejected">
|
|
<i class="fas fa-times-circle"></i> Rejected
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="verification-body">
|
|
<div class="photo-section">
|
|
<div class="photo-container">
|
|
{% if record.verification_photo %}
|
|
<img src="{{ record.verification_photo }}" alt="Verification Photo" class="verification-photo" data-record-id="{{ record.id }}" />
|
|
{% else %}
|
|
<div class="no-photo">
|
|
<i class="fas fa-image"></i>
|
|
<p>No photo available</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if record.verification_photo %}
|
|
<div class="photo-actions">
|
|
<button type="button" class="btn-photo download" onclick="downloadPhoto({{ record.id }}, '{{ record.employee_id }}', '{{ record.check_in_date.strftime('%Y-%m-%d') }}')" title="Download Photo">
|
|
<i class="fas fa-download"></i>
|
|
</button>
|
|
<button type="button" class="btn-photo email" onclick="sendByEmail({{ record.id }}, '{{ record.employee_id }}', '{{ employee_names.get(record.employee_id) or 'Unknown' }}', '{{ record.check_in_date.strftime('%b %d, %Y') }}', '{{ record.check_in_time.strftime('%I:%M %p') }}', '{{ record.location_name }}', '{{ record.qr_code.location_event if record.qr_code and record.qr_code.location_event else 'N/A' }}', '{{ record.verification_status }}', '{{ record.qr_code.location_address if record.qr_code and record.qr_code.location_address else 'N/A' }}', '{{ record.address or 'N/A' }}', '{{ '%.3f'|format(record.location_accuracy) if record.location_accuracy else 'N/A' }}', '{{ record.device_info or 'Unknown' }}')" title="Send by Email">
|
|
<i class="fas fa-envelope"></i>
|
|
</button>
|
|
<a href="{{ url_for('attendance.verification_review_detail', record_id=record.id) }}" class="btn-photo view" title="View Details">
|
|
<i class="fas fa-expand"></i>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="details-section">
|
|
<div class="detail-row">
|
|
<span class="detail-label">
|
|
<i class="fas fa-ruler"></i> Distance from Location:
|
|
</span>
|
|
<span class="detail-value distance-value">
|
|
{% if record.location_accuracy %}
|
|
{{ "%.3f"|format(record.location_accuracy) }} miles
|
|
{% else %}
|
|
N/A
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="detail-row">
|
|
<span class="detail-label">
|
|
<i class="fas fa-map-pin"></i> QR Location:
|
|
</span>
|
|
<span class="detail-value">
|
|
{{ record.qr_code.location_address if record.qr_code.location_address else 'N/A' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="detail-row">
|
|
<span class="detail-label">
|
|
<i class="fas fa-location-arrow"></i> Check-in Address:
|
|
</span>
|
|
<span class="detail-value">
|
|
{{ record.address if record.address else 'N/A' }}
|
|
</span>
|
|
</div>
|
|
|
|
{% if record.latitude and record.longitude %}
|
|
<div class="detail-row">
|
|
<span class="detail-label">
|
|
<i class="fas fa-globe"></i> GPS Coordinates:
|
|
</span>
|
|
<span class="detail-value">
|
|
<a href="https://www.google.com/maps?q={{ record.latitude }},{{ record.longitude }}" target="_blank" class="coordinates-link">
|
|
{{ "%.6f"|format(record.latitude) }}, {{ "%.6f"|format(record.longitude) }}
|
|
<i class="fas fa-external-link-alt"></i>
|
|
</a>
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="detail-row">
|
|
<span class="detail-label">
|
|
<i class="fas fa-clock"></i> Verification Submitted:
|
|
</span>
|
|
<span class="detail-value">
|
|
{{ record.verification_timestamp.strftime('%b %d, %Y %I:%M %p') if record.verification_timestamp else 'N/A' }}
|
|
</span>
|
|
</div>
|
|
|
|
{% if record.edit_note %}
|
|
<div class="detail-row">
|
|
<span class="detail-label">
|
|
<i class="fas fa-sticky-note"></i> Note:
|
|
</span>
|
|
<span class="detail-value">
|
|
{{ record.edit_note }}
|
|
</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if record.verification_status == 'pending' %}
|
|
<div class="verification-actions">
|
|
<button type="button" class="btn-action approve" onclick="updateVerificationStatus({{ record.id }}, 'approved')">
|
|
<i class="fas fa-check"></i> Approve
|
|
</button>
|
|
<button type="button" class="btn-action reject" onclick="updateVerificationStatus({{ record.id }}, 'rejected')">
|
|
<i class="fas fa-times"></i> Reject
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<i class="fas fa-inbox"></i>
|
|
<h3>No Verifications Found</h3>
|
|
<p>There are no photo verifications matching your current filters.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
padding: 1.5rem;
|
|
border-radius: 0.75rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
color: white;
|
|
}
|
|
|
|
.stat-icon.pending {
|
|
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
}
|
|
|
|
.stat-icon.approved {
|
|
background: linear-gradient(135deg, #10b981, #059669);
|
|
}
|
|
|
|
.stat-icon.rejected {
|
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
|
}
|
|
|
|
.stat-info h3 {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
margin: 0;
|
|
}
|
|
|
|
.stat-info p {
|
|
color: #64748b;
|
|
font-size: 0.875rem;
|
|
margin: 0;
|
|
}
|
|
|
|
.filter-section {
|
|
background: white;
|
|
padding: 1.5rem;
|
|
border-radius: 0.75rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.filter-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
align-items: end;
|
|
}
|
|
|
|
.filter-group label {
|
|
display: block;
|
|
font-weight: 600;
|
|
color: #374151;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.filter-select,
|
|
.filter-input {
|
|
width: 100%;
|
|
padding: 0.625rem;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.btn-filter {
|
|
width: 100%;
|
|
padding: 0.625rem 1.5rem;
|
|
background: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 0.375rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.btn-filter:hover {
|
|
background: #2563eb;
|
|
}
|
|
|
|
.btn-reset {
|
|
width: 100%;
|
|
padding: 0.625rem 1.5rem;
|
|
background: #6b7280;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 0.375rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
text-decoration: none;
|
|
text-align: center;
|
|
display: inline-block;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.btn-reset:hover {
|
|
background: #4b5563;
|
|
color: white;
|
|
}
|
|
|
|
.filter-buttons {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.employee-name {
|
|
font-weight: 500;
|
|
color: #4b5563;
|
|
font-size: 0.95em;
|
|
}
|
|
|
|
.verification-project {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.project-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
padding: 0.25rem 0.75rem;
|
|
background: #e0e7ff;
|
|
color: #3730a3;
|
|
border-radius: 9999px;
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.event-badge {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-weight: 600;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.event-badge.check-in {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.event-badge.check-out {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.verifications-grid {
|
|
display: grid;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.verification-card {
|
|
background: white;
|
|
border-radius: 0.75rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.verification-header {
|
|
padding: 1.5rem;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: start;
|
|
}
|
|
|
|
.verification-info h3 {
|
|
margin: 0 0 0.5rem 0;
|
|
color: #0f172a;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.verification-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
color: #64748b;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.meta-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-badge.pending {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.status-badge.approved {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.status-badge.rejected {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.verification-body {
|
|
display: grid;
|
|
grid-template-columns: 400px 1fr;
|
|
gap: 2rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.verification-body {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.photo-container {
|
|
background: #f9fafb;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
aspect-ratio: 4/3;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.verification-photo {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.verification-photo:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.no-photo {
|
|
text-align: center;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.no-photo i {
|
|
font-size: 3rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.photo-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
justify-content: center;
|
|
margin-top: 0.75rem;
|
|
}
|
|
|
|
.btn-photo {
|
|
width: 36px;
|
|
height: 36px;
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-photo.download {
|
|
background: #3b82f6;
|
|
color: white;
|
|
}
|
|
|
|
.btn-photo.download:hover {
|
|
background: #2563eb;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.btn-photo.email {
|
|
background: #8b5cf6;
|
|
color: white;
|
|
}
|
|
|
|
.btn-photo.email:hover {
|
|
background: #7c3aed;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.btn-photo.view {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.btn-photo.view:hover {
|
|
background: #059669;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.details-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.detail-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.detail-label {
|
|
font-size: 0.875rem;
|
|
color: #6b7280;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.detail-value {
|
|
color: #0f172a;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.distance-value {
|
|
font-weight: 700;
|
|
color: #dc2626;
|
|
font-size: 1.125rem;
|
|
}
|
|
|
|
.coordinates-link {
|
|
color: #3b82f6;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.coordinates-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.verification-actions {
|
|
padding: 1.5rem;
|
|
border-top: 1px solid #e5e7eb;
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-action {
|
|
padding: 0.75rem 2rem;
|
|
border: none;
|
|
border-radius: 0.5rem;
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.btn-action.approve {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.btn-action.approve:hover {
|
|
background: #059669;
|
|
}
|
|
|
|
.btn-action.reject {
|
|
background: #ef4444;
|
|
color: white;
|
|
}
|
|
|
|
.btn-action.reject:hover {
|
|
background: #dc2626;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 4rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
color: #6b7280;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.empty-state p {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
/* ── Photo Lightbox ── */
|
|
.photo-lightbox {
|
|
display: none;
|
|
position: fixed;
|
|
z-index: 2000;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0, 0, 0, 0.9);
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
}
|
|
.photo-lightbox.active {
|
|
display: flex;
|
|
}
|
|
.photo-lightbox-content {
|
|
position: relative;
|
|
max-width: 90vw;
|
|
max-height: 90vh;
|
|
text-align: center;
|
|
}
|
|
.photo-lightbox-image {
|
|
max-width: 100%;
|
|
max-height: 80vh;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
background: #fff;
|
|
padding: 0.5rem;
|
|
cursor: default;
|
|
}
|
|
.photo-lightbox-close {
|
|
position: absolute;
|
|
top: -48px;
|
|
right: 0;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border: none;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
font-size: 1.2rem;
|
|
color: #374151;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.photo-lightbox-close:hover {
|
|
background: #fff;
|
|
color: #111827;
|
|
}
|
|
.photo-lightbox-info {
|
|
color: #fff;
|
|
margin-top: 1rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
}
|
|
.photo-lightbox-hint {
|
|
color: rgba(255, 255, 255, 0.65);
|
|
margin-top: 0.4rem;
|
|
font-size: 0.8rem;
|
|
}
|
|
.verification-photo {
|
|
cursor: zoom-in;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function updateVerificationStatus(recordId, status) {
|
|
const card = document.querySelector(`[data-record-id="${recordId}"]`);
|
|
const statusText = status.charAt(0).toUpperCase() + status.slice(1);
|
|
|
|
if (!confirm(`Are you sure you want to ${status} this verification?`)) {
|
|
return;
|
|
}
|
|
|
|
// Prompt for reason
|
|
const reasonPrompt = status === 'approved'
|
|
? 'Optional: Enter a reason for approval:'
|
|
: 'Please enter a reason for rejection:';
|
|
|
|
let note = prompt(reasonPrompt);
|
|
|
|
// For rejection, reason is required
|
|
if (status === 'rejected' && (!note || note.trim() === '')) {
|
|
alert('A reason is required when rejecting a verification.');
|
|
return;
|
|
}
|
|
|
|
// User cancelled the prompt
|
|
if (note === null) {
|
|
return;
|
|
}
|
|
|
|
// Disable buttons
|
|
const buttons = card.querySelectorAll('.btn-action');
|
|
buttons.forEach(btn => btn.disabled = true);
|
|
|
|
fetch(`/verification-review/${recordId}/update`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
status: status,
|
|
note: note
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert(`Verification ${status} successfully!`);
|
|
window.location.reload();
|
|
} else {
|
|
alert(`Error: ${data.message}`);
|
|
buttons.forEach(btn => btn.disabled = false);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Network error. Please try again.');
|
|
buttons.forEach(btn => btn.disabled = false);
|
|
});
|
|
}
|
|
|
|
// Click photo to view full size — lightbox (mirrors dashboard behaviour)
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const photos = document.querySelectorAll('.verification-photo');
|
|
photos.forEach(photo => {
|
|
photo.addEventListener('click', function() {
|
|
openPhotoLightbox(this.src, this.alt || 'Verification Photo');
|
|
});
|
|
});
|
|
});
|
|
|
|
function openPhotoLightbox(src, caption) {
|
|
const lightbox = document.getElementById('photoLightbox');
|
|
const img = document.getElementById('photoLightboxImage');
|
|
const info = document.getElementById('photoLightboxInfo');
|
|
img.src = src;
|
|
img.alt = caption;
|
|
info.textContent = caption;
|
|
lightbox.classList.add('active');
|
|
document.addEventListener('keydown', _photoLightboxKeydown);
|
|
}
|
|
|
|
function closePhotoLightbox() {
|
|
const lightbox = document.getElementById('photoLightbox');
|
|
lightbox.classList.remove('active');
|
|
document.getElementById('photoLightboxImage').src = '';
|
|
document.removeEventListener('keydown', _photoLightboxKeydown);
|
|
}
|
|
|
|
function _photoLightboxKeydown(e) {
|
|
if (e.key === 'Escape') closePhotoLightbox();
|
|
}
|
|
|
|
// Download verification photo
|
|
function downloadPhoto(recordId, employeeId, checkInDate) {
|
|
const card = document.querySelector(`[data-record-id="${recordId}"]`);
|
|
const photoElement = card.querySelector('.verification-photo');
|
|
|
|
if (!photoElement) {
|
|
alert('No photo available to download.');
|
|
return;
|
|
}
|
|
|
|
const photoSrc = photoElement.src;
|
|
const fileName = `verification_photo_${employeeId}_${checkInDate}.jpg`;
|
|
|
|
// Handle base64 data URL
|
|
if (photoSrc.startsWith('data:')) {
|
|
const link = document.createElement('a');
|
|
link.href = photoSrc;
|
|
link.download = fileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
console.log('[LOG] Photo downloaded:', fileName);
|
|
} else {
|
|
// Handle regular URL - fetch and download
|
|
fetch(photoSrc)
|
|
.then(response => response.blob())
|
|
.then(blob => {
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = fileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
console.log('[LOG] Photo downloaded:', fileName);
|
|
})
|
|
.catch(error => {
|
|
console.error('[ERROR] Failed to download photo:', error);
|
|
alert('Failed to download photo. Please try again.');
|
|
});
|
|
}
|
|
}
|
|
|
|
// Send verification details by email
|
|
function sendByEmail(recordId, employeeId, employeeName, checkInDate, checkInTime, locationName, locationEvent, verificationStatus, qrAddress, checkInAddress, distance, deviceInfo) {
|
|
// Build the verification record URL
|
|
const recordUrl = window.location.origin + `/verification-review/${recordId}`;
|
|
|
|
// Build email subject
|
|
const subject = encodeURIComponent(`Verification Review - Employee ${employeeId} (${employeeName}) - ${checkInDate}`);
|
|
|
|
// Build email body with verification details
|
|
let body = `VERIFICATION PHOTO REVIEW DETAILS\n`;
|
|
body += `================================\n\n`;
|
|
body += `VIEW VERIFICATION RECORD ONLINE:\n`;
|
|
body += `${recordUrl}\n\n`;
|
|
body += `EMPLOYEE INFORMATION\n`;
|
|
body += `--------------------\n`;
|
|
body += `Employee ID: ${employeeId}\n`;
|
|
body += `Employee Name: ${employeeName}\n`;
|
|
body += `Check-in Date: ${checkInDate}\n`;
|
|
body += `Check-in Time: ${checkInTime}\n`;
|
|
body += `Verification Status: ${verificationStatus.toUpperCase()}\n\n`;
|
|
body += `LOCATION INFORMATION\n`;
|
|
body += `--------------------\n`;
|
|
body += `Location Name: ${locationName}\n`;
|
|
body += `Event Type: ${locationEvent}\n`;
|
|
body += `Distance from QR: ${distance} miles\n`;
|
|
body += `QR Code Address: ${qrAddress}\n`;
|
|
body += `Check-in Address: ${checkInAddress}\n`;
|
|
body += `Device: ${deviceInfo}\n\n`;
|
|
body += `--------------------\n`;
|
|
body += `Record ID: ${recordId}\n\n`;
|
|
body += `Note: Click the link above to view the verification photo and full details.\n`;
|
|
|
|
const encodedBody = encodeURIComponent(body);
|
|
|
|
// Open default email client
|
|
const mailtoLink = `mailto:?subject=${subject}&body=${encodedBody}`;
|
|
window.location.href = mailtoLink;
|
|
|
|
console.log('[LOG] Email client opened for verification record:', recordId);
|
|
}
|
|
</script>
|
|
|
|
<!-- Photo Lightbox Overlay -->
|
|
<div id="photoLightbox" class="photo-lightbox" onclick="closePhotoLightbox()">
|
|
<div class="photo-lightbox-content" onclick="event.stopPropagation()">
|
|
<button class="photo-lightbox-close" onclick="closePhotoLightbox()" title="Close">×</button>
|
|
<img id="photoLightboxImage" src="" alt="" class="photo-lightbox-image" />
|
|
<div class="photo-lightbox-info" id="photoLightboxInfo"></div>
|
|
<div class="photo-lightbox-hint">Click anywhere outside the image to close · ESC to dismiss</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %} |