Initial commit: Quality App v2 - FG Scan Module with Reports
This commit is contained in:
866
app/templates/modules/quality/fg_reports.html
Normal file
866
app/templates/modules/quality/fg_reports.html
Normal file
@@ -0,0 +1,866 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}FG Scan Reports - {{ app_name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* FG Reports Page Styles */
|
||||
.fg-reports-container {
|
||||
max-width: 1400px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Query Section */
|
||||
.query-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.query-card h3 {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.query-card h3 i {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.reports-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.report-option {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.report-option:hover {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--bg-secondary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.report-option.active {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.report-option.active .report-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.report-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.report-option-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.report-option.active .report-option-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.report-option-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.report-option.active .report-option-desc {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* Filter Section */
|
||||
.filter-section {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filter-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.filter-group input,
|
||||
.filter-group select {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.filter-group input:focus,
|
||||
.filter-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-query {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.6rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-query:hover {
|
||||
background: var(--primary-color-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-query:disabled {
|
||||
background: var(--text-disabled);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-reset:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Data Display Section */
|
||||
.data-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.data-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.data-card h3 {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.data-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
background: var(--bg-primary);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.export-section {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-export:hover {
|
||||
background: var(--success-color-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-export:disabled {
|
||||
background: var(--text-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.report-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.report-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.report-table thead {
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.report-table th {
|
||||
color: var(--text-primary);
|
||||
padding: 1rem 0.8rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.report-table td {
|
||||
color: var(--text-primary);
|
||||
padding: 0.8rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.report-table tbody tr:hover {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state h4 {
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.loading-spinner {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.loading-spinner.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid var(--bg-primary);
|
||||
border-top: 4px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner-text {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Success Message */
|
||||
.success-message {
|
||||
display: none;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
color: #4CAF50;
|
||||
padding: 0.8rem 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.success-message.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.success-message i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.reports-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.data-stats {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-query, .btn-reset {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.data-stats {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.report-table {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.report-table th,
|
||||
.report-table td {
|
||||
padding: 0.6rem 0.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="fg-reports-container">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1><i class="fas fa-file-alt"></i> FG Scan Reports</h1>
|
||||
<p>Generate and export quality reports for finished goods scans</p>
|
||||
</div>
|
||||
|
||||
<!-- Query Section -->
|
||||
<div class="query-card">
|
||||
<h3><i class="fas fa-filter"></i> Select Report Type</h3>
|
||||
|
||||
<div class="reports-grid">
|
||||
<!-- Daily Report -->
|
||||
<div class="report-option" data-report="daily">
|
||||
<div class="report-icon"><i class="fas fa-calendar-day"></i></div>
|
||||
<div class="report-option-title">Today's Report</div>
|
||||
<div class="report-option-desc">All scans from today</div>
|
||||
</div>
|
||||
|
||||
<!-- Select Day Report -->
|
||||
<div class="report-option" data-report="select-day">
|
||||
<div class="report-icon"><i class="fas fa-calendar"></i></div>
|
||||
<div class="report-option-title">Select Day</div>
|
||||
<div class="report-option-desc">Choose a specific date</div>
|
||||
</div>
|
||||
|
||||
<!-- Date Range Report -->
|
||||
<div class="report-option" data-report="date-range">
|
||||
<div class="report-icon"><i class="fas fa-calendar-alt"></i></div>
|
||||
<div class="report-option-title">Date Range</div>
|
||||
<div class="report-option-desc">Custom date range</div>
|
||||
</div>
|
||||
|
||||
<!-- 5-Day Report -->
|
||||
<div class="report-option" data-report="5-day">
|
||||
<div class="report-icon"><i class="fas fa-chart-line"></i></div>
|
||||
<div class="report-option-title">Last 5 Days</div>
|
||||
<div class="report-option-desc">Last 5 days of scans</div>
|
||||
</div>
|
||||
|
||||
<!-- Defects Today -->
|
||||
<div class="report-option" data-report="defects-today">
|
||||
<div class="report-icon"><i class="fas fa-exclamation-circle"></i></div>
|
||||
<div class="report-option-title">Defects Today</div>
|
||||
<div class="report-option-desc">Rejected scans today</div>
|
||||
</div>
|
||||
|
||||
<!-- Defects by Date -->
|
||||
<div class="report-option" data-report="defects-date">
|
||||
<div class="report-icon"><i class="fas fa-search"></i></div>
|
||||
<div class="report-option-title">Defects by Date</div>
|
||||
<div class="report-option-desc">Select date for defects</div>
|
||||
</div>
|
||||
|
||||
<!-- Defects by Range -->
|
||||
<div class="report-option" data-report="defects-range">
|
||||
<div class="report-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
||||
<div class="report-option-title">Defects Range</div>
|
||||
<div class="report-option-desc">Date range for defects</div>
|
||||
</div>
|
||||
|
||||
<!-- Defects 5-Day -->
|
||||
<div class="report-option" data-report="defects-5day">
|
||||
<div class="report-icon"><i class="fas fa-ban"></i></div>
|
||||
<div class="report-option-title">Defects 5 Days</div>
|
||||
<div class="report-option-desc">Last 5 days defects</div>
|
||||
</div>
|
||||
|
||||
<!-- Complete Database -->
|
||||
<div class="report-option" data-report="all">
|
||||
<div class="report-icon"><i class="fas fa-database"></i></div>
|
||||
<div class="report-option-title">All Data</div>
|
||||
<div class="report-option-desc">Complete database</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Section (appears based on report type) -->
|
||||
<div class="filter-section" id="filterSection">
|
||||
<div id="filterContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Display Section -->
|
||||
<div class="data-card">
|
||||
<div class="data-card-header">
|
||||
<h3 id="reportTitle">Select a report to view data</h3>
|
||||
<div class="data-stats" id="dataStats" style="display: none;">
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">Total Scans</div>
|
||||
<div class="stat-value" id="statTotal">0</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">Approved</div>
|
||||
<div class="stat-value" id="statApproved" style="border-left-color: #4CAF50;">0</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-label">Rejected</div>
|
||||
<div class="stat-value" id="statRejected" style="border-left-color: #F44336;">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="export-section" id="exportSection" style="display: none;">
|
||||
<button class="btn-export" id="exportExcelBtn">
|
||||
<i class="fas fa-file-excel"></i> Export Excel
|
||||
</button>
|
||||
<button class="btn-export" id="exportCsvBtn">
|
||||
<i class="fas fa-file-csv"></i> Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="success-message" id="successMessage">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span id="successText">Report generated successfully</span>
|
||||
</div>
|
||||
|
||||
<div class="loading-spinner" id="loadingSpinner">
|
||||
<div class="spinner"></div>
|
||||
<div class="spinner-text">Generating report...</div>
|
||||
</div>
|
||||
|
||||
<div class="report-table-container">
|
||||
<table class="report-table" id="reportTable" style="display: none;">
|
||||
<thead>
|
||||
<tr id="tableHead"></tr>
|
||||
</thead>
|
||||
<tbody id="tableBody"></tbody>
|
||||
</table>
|
||||
<div class="empty-state" id="emptyState">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<h4>No data to display</h4>
|
||||
<p>Select a report type and filters above to view data</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import SheetJS for Excel export -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
||||
|
||||
<!-- Report Script -->
|
||||
<script>
|
||||
// Report generation logic
|
||||
class FGReportManager {
|
||||
constructor() {
|
||||
this.currentReport = null;
|
||||
this.currentData = [];
|
||||
this.initializeEventListeners();
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
// Report option selection
|
||||
document.querySelectorAll('.report-option').forEach(option => {
|
||||
option.addEventListener('click', () => this.selectReport(option.dataset.report));
|
||||
});
|
||||
|
||||
// Export buttons
|
||||
document.getElementById('exportExcelBtn').addEventListener('click', () => this.exportExcel());
|
||||
document.getElementById('exportCsvBtn').addEventListener('click', () => this.exportCSV());
|
||||
}
|
||||
|
||||
selectReport(reportType) {
|
||||
this.currentReport = reportType;
|
||||
|
||||
// Update UI
|
||||
document.querySelectorAll('.report-option').forEach(opt => {
|
||||
opt.classList.toggle('active', opt.dataset.report === reportType);
|
||||
});
|
||||
|
||||
// Show/hide filters based on report type
|
||||
this.updateFilters(reportType);
|
||||
}
|
||||
|
||||
updateFilters(reportType) {
|
||||
const filterSection = document.getElementById('filterSection');
|
||||
const filterContent = document.getElementById('filterContent');
|
||||
|
||||
let html = '';
|
||||
let needsFilter = false;
|
||||
|
||||
switch(reportType) {
|
||||
case 'select-day':
|
||||
case 'defects-date':
|
||||
html = `
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<label for="filterDate">Select Date:</label>
|
||||
<input type="date" id="filterDate" max="${new Date().toISOString().split('T')[0]}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="btn-query" id="queryBtn"><i class="fas fa-search"></i> Generate Report</button>
|
||||
<button class="btn-reset" id="resetBtn"><i class="fas fa-redo"></i> Reset</button>
|
||||
</div>
|
||||
`;
|
||||
needsFilter = true;
|
||||
break;
|
||||
|
||||
case 'date-range':
|
||||
case 'defects-range':
|
||||
html = `
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<label for="filterStartDate">Start Date:</label>
|
||||
<input type="date" id="filterStartDate" max="${new Date().toISOString().split('T')[0]}">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="filterEndDate">End Date:</label>
|
||||
<input type="date" id="filterEndDate" max="${new Date().toISOString().split('T')[0]}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="btn-query" id="queryBtn"><i class="fas fa-search"></i> Generate Report</button>
|
||||
<button class="btn-reset" id="resetBtn"><i class="fas fa-redo"></i> Reset</button>
|
||||
</div>
|
||||
`;
|
||||
needsFilter = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Auto-generate for daily, 5-day, defects-today, defects-5day, all
|
||||
html = `
|
||||
<div class="button-row">
|
||||
<button class="btn-query" id="queryBtn"><i class="fas fa-search"></i> Generate Report</button>
|
||||
<button class="btn-reset" id="resetBtn"><i class="fas fa-redo"></i> Reset</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
filterContent.innerHTML = html;
|
||||
filterSection.classList.toggle('active', needsFilter || ['daily', '5-day', 'defects-today', 'defects-5day', 'all'].includes(reportType));
|
||||
|
||||
// Attach button listeners
|
||||
document.getElementById('queryBtn')?.addEventListener('click', () => this.generateReport());
|
||||
document.getElementById('resetBtn')?.addEventListener('click', () => this.resetReport());
|
||||
}
|
||||
|
||||
async generateReport() {
|
||||
const filterDate = document.getElementById('filterDate')?.value;
|
||||
const startDate = document.getElementById('filterStartDate')?.value;
|
||||
const endDate = document.getElementById('filterEndDate')?.value;
|
||||
|
||||
// Show loading
|
||||
this.showLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/quality/api/fg_report', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
report_type: this.currentReport,
|
||||
filter_date: filterDate,
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.currentData = data.data;
|
||||
this.displayReport(data);
|
||||
this.showSuccess('Report generated successfully');
|
||||
} else {
|
||||
this.showError(data.message || 'Failed to generate report');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
this.showError('Error generating report: ' + error.message);
|
||||
} finally {
|
||||
this.showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
displayReport(data) {
|
||||
const reportTable = document.getElementById('reportTable');
|
||||
const tableHead = document.getElementById('tableHead');
|
||||
const tableBody = document.getElementById('tableBody');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const reportTitle = document.getElementById('reportTitle');
|
||||
const dataStats = document.getElementById('dataStats');
|
||||
const exportSection = document.getElementById('exportSection');
|
||||
|
||||
if (!data.data || data.data.length === 0) {
|
||||
reportTable.style.display = 'none';
|
||||
emptyState.style.display = 'block';
|
||||
dataStats.style.display = 'none';
|
||||
exportSection.style.display = 'none';
|
||||
reportTitle.textContent = 'No data found for this report';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build table headers
|
||||
const headers = Object.keys(data.data[0]);
|
||||
tableHead.innerHTML = headers.map(h => `<th>${this.formatHeader(h)}</th>`).join('');
|
||||
|
||||
// Build table body
|
||||
tableBody.innerHTML = data.data.map(row => {
|
||||
return `<tr>${headers.map(h => {
|
||||
let value = row[h];
|
||||
let cellClass = '';
|
||||
|
||||
if (h === 'quality_code' || h === 'defect_code') {
|
||||
const isApproved = value === 0 || value === '0' || value === '000';
|
||||
cellClass = isApproved ? 'status-approved' : 'status-rejected';
|
||||
const statusText = isApproved ? 'APPROVED' : 'REJECTED';
|
||||
return `<td><span class="status-badge ${cellClass}">${statusText}</span></td>`;
|
||||
}
|
||||
|
||||
return `<td>${value}</td>`;
|
||||
}).join('')}</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Update stats
|
||||
const total = data.data.length;
|
||||
const approved = data.summary.approved_count;
|
||||
const rejected = data.summary.rejected_count;
|
||||
|
||||
document.getElementById('statTotal').textContent = total;
|
||||
document.getElementById('statApproved').textContent = approved;
|
||||
document.getElementById('statRejected').textContent = rejected;
|
||||
|
||||
// Update title and show sections
|
||||
reportTitle.textContent = data.title;
|
||||
reportTable.style.display = 'table';
|
||||
emptyState.style.display = 'none';
|
||||
dataStats.style.display = 'flex';
|
||||
exportSection.style.display = 'flex';
|
||||
}
|
||||
|
||||
formatHeader(header) {
|
||||
return header
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
exportExcel() {
|
||||
if (this.currentData.length === 0) {
|
||||
this.showError('No data to export');
|
||||
return;
|
||||
}
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(this.currentData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'FG Scans');
|
||||
XLSX.writeFile(workbook, `fg_report_${new Date().toISOString().split('T')[0]}.xlsx`);
|
||||
this.showSuccess('Report exported to Excel');
|
||||
}
|
||||
|
||||
exportCSV() {
|
||||
if (this.currentData.length === 0) {
|
||||
this.showError('No data to export');
|
||||
return;
|
||||
}
|
||||
|
||||
const csv = [
|
||||
Object.keys(this.currentData[0]).join(','),
|
||||
...this.currentData.map(row =>
|
||||
Object.values(row).map(v => `"${v}"`).join(',')
|
||||
)
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `fg_report_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
this.showSuccess('Report exported to CSV');
|
||||
}
|
||||
|
||||
resetReport() {
|
||||
document.getElementById('filterDate').value = '';
|
||||
document.getElementById('filterStartDate').value = '';
|
||||
document.getElementById('filterEndDate').value = '';
|
||||
document.getElementById('reportTable').style.display = 'none';
|
||||
document.getElementById('emptyState').style.display = 'block';
|
||||
document.getElementById('dataStats').style.display = 'none';
|
||||
document.getElementById('exportSection').style.display = 'none';
|
||||
document.getElementById('reportTitle').textContent = 'Select a report to view data';
|
||||
}
|
||||
|
||||
showLoading(show) {
|
||||
document.getElementById('loadingSpinner').classList.toggle('active', show);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
const successMsg = document.getElementById('successMessage');
|
||||
document.getElementById('successText').textContent = message;
|
||||
successMsg.classList.add('active');
|
||||
setTimeout(() => successMsg.classList.remove('active'), 3000);
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
console.error(message);
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize report manager when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new FGReportManager();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
910
app/templates/modules/quality/fg_scan.html
Normal file
910
app/templates/modules/quality/fg_scan.html
Normal file
@@ -0,0 +1,910 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}FG Scan - Quality App{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fg_scan.css') }}">
|
||||
<script src="https://cdn.jsdelivr.net/npm/qz-tray@2.1.0/qz-tray.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="scan-container">
|
||||
<!-- Form Card -->
|
||||
<div class="scan-form-card">
|
||||
<h3 class="form-title">FG Scan Entry</h3>
|
||||
<form id="scanForm" method="POST" class="scan-form">
|
||||
<label for="operator_code">Operator Code:</label>
|
||||
<input type="text" id="operator_code" name="operator_code" required>
|
||||
<div class="error-message" id="error_operator_code"></div>
|
||||
|
||||
<label for="cp_code">CP Code:</label>
|
||||
<input type="text" id="cp_code" name="cp_code" required>
|
||||
<div class="error-message" id="error_cp_code"></div>
|
||||
|
||||
<label for="oc1_code">OC1 Code:</label>
|
||||
<input type="text" id="oc1_code" name="oc1_code">
|
||||
<div class="error-message" id="error_oc1_code"></div>
|
||||
|
||||
<label for="oc2_code">OC2 Code:</label>
|
||||
<input type="text" id="oc2_code" name="oc2_code">
|
||||
<div class="error-message" id="error_oc2_code"></div>
|
||||
|
||||
<label for="defect_code">Defect Code:</label>
|
||||
<input type="text" id="defect_code" name="defect_code" maxlength="3">
|
||||
<div class="error-message" id="error_defect_code"></div>
|
||||
|
||||
<label for="date_time">Date/Time:</label>
|
||||
<input type="text" id="date_time" name="date_time" readonly>
|
||||
|
||||
<div class="form-buttons">
|
||||
<button type="submit" class="btn-submit">Submit Scan</button>
|
||||
<button type="button" class="btn-clear" id="clearOperator">Clear Quality Operator</button>
|
||||
</div>
|
||||
|
||||
<div class="form-options">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="scanToBoxes" name="scan_to_boxes">
|
||||
Scan To Boxes
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="quickBoxSection" style="display: none;" class="quick-box-section">
|
||||
<button type="button" class="btn-secondary" id="quickBoxLabel">Quick Box Label Creation</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Latest Scans Table -->
|
||||
<div class="scan-table-card">
|
||||
<h3 class="table-title">Latest Scans</h3>
|
||||
<div class="table-wrapper">
|
||||
<table class="scan-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Op Code</th>
|
||||
<th>CP Code</th>
|
||||
<th>OC1</th>
|
||||
<th>OC2</th>
|
||||
<th>Defect Code</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Approved Qty</th>
|
||||
<th>Rejected Qty</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="scansTableBody">
|
||||
{% if scan_groups %}
|
||||
{% for scan_group in scan_groups %}
|
||||
<tr>
|
||||
<td>{{ scan_group.id }}</td>
|
||||
<td>{{ scan_group.operator_code }}</td>
|
||||
<td>{{ scan_group.cp_code }}</td>
|
||||
<td>{{ scan_group.oc1_code or '-' }}</td>
|
||||
<td>{{ scan_group.oc2_code or '-' }}</td>
|
||||
<td>{{ scan_group.defect_code or '-' }}</td>
|
||||
<td>{{ scan_group.date }}</td>
|
||||
<td>{{ scan_group.time }}</td>
|
||||
<td>{{ scan_group.approved_qty }}</td>
|
||||
<td>{{ scan_group.rejected_qty }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="10" style="text-align: center; padding: 20px; color: var(--text-secondary);">
|
||||
<i class="fas fa-inbox"></i> No scans recorded yet. Submit a scan to see results here.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Box Assignment Modal -->
|
||||
<div id="boxAssignmentModal" class="box-modal" style="display: none;">
|
||||
<div class="box-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Assign to Box</h2>
|
||||
<button type="button" class="modal-close" id="closeModal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="boxNumber">Box Number:</label>
|
||||
<input type="text" id="boxNumber" placeholder="Enter box number">
|
||||
<label for="boxQty">Quantity:</label>
|
||||
<input type="number" id="boxQty" placeholder="Enter quantity" min="1">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" id="cancelModal">Cancel</button>
|
||||
<button type="button" class="btn-submit" id="assignToBox">Assign</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global variables
|
||||
let scanToBoxesEnabled = false;
|
||||
let currentCpCode = '';
|
||||
let qzTrayReady = false;
|
||||
let cpCodeLastInputTime = null;
|
||||
|
||||
// Get form input references FIRST (before using them) - with null safety check
|
||||
const operatorCodeInput = document.getElementById('operator_code');
|
||||
const cpCodeInput = document.getElementById('cp_code');
|
||||
const oc1CodeInput = document.getElementById('oc1_code');
|
||||
const oc2CodeInput = document.getElementById('oc2_code');
|
||||
const defectCodeInput = document.getElementById('defect_code');
|
||||
|
||||
// Safety check - ensure all form inputs are available
|
||||
if (!operatorCodeInput || !cpCodeInput || !oc1CodeInput || !oc2CodeInput || !defectCodeInput) {
|
||||
console.error('❌ Error: Required form inputs not found in DOM');
|
||||
console.log('operatorCodeInput:', operatorCodeInput);
|
||||
console.log('cpCodeInput:', cpCodeInput);
|
||||
console.log('oc1CodeInput:', oc1CodeInput);
|
||||
console.log('oc2CodeInput:', oc2CodeInput);
|
||||
console.log('defectCodeInput:', defectCodeInput);
|
||||
}
|
||||
|
||||
// Initialize QZ Tray only when needed (lazy loading)
|
||||
// Don't connect on page load - only connect when user enables "Scan To Boxes"
|
||||
function initializeQzTray() {
|
||||
if (typeof qz === 'undefined') {
|
||||
console.log('ℹ️ QZ Tray library not loaded');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
qz.security.setSignaturePromise(function(toSign) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
// For development, we'll allow unsigned requests
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
qz.websocket.connect().then(function() {
|
||||
qzTrayReady = true;
|
||||
console.log('✅ QZ Tray connected successfully');
|
||||
}).catch(function(err) {
|
||||
console.log('ℹ️ QZ Tray connection failed:', err);
|
||||
qzTrayReady = false;
|
||||
});
|
||||
return true;
|
||||
} catch(err) {
|
||||
console.log('ℹ️ QZ Tray initialization error:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update date/time display
|
||||
function updateDateTime() {
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString('en-US');
|
||||
const timeStr = now.toLocaleTimeString('en-US', { hour12: true });
|
||||
const dateTimeInput = document.getElementById('date_time');
|
||||
if (dateTimeInput) {
|
||||
dateTimeInput.value = dateStr + ' ' + timeStr;
|
||||
}
|
||||
}
|
||||
|
||||
updateDateTime();
|
||||
setInterval(updateDateTime, 1000);
|
||||
|
||||
// Load operator code from localStorage
|
||||
function loadOperatorCode() {
|
||||
if (!operatorCodeInput) return;
|
||||
const saved = localStorage.getItem('quality_operator_code');
|
||||
if (saved) {
|
||||
operatorCodeInput.value = saved;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to clear fields after a successful submission
|
||||
const shouldClearAfterSubmit = localStorage.getItem('fg_scan_clear_after_submit');
|
||||
if (shouldClearAfterSubmit === 'true' && cpCodeInput && oc1CodeInput && oc2CodeInput && defectCodeInput) {
|
||||
// Clear the flag
|
||||
localStorage.removeItem('fg_scan_clear_after_submit');
|
||||
localStorage.removeItem('fg_scan_last_cp');
|
||||
localStorage.removeItem('fg_scan_last_defect');
|
||||
|
||||
// Clear CP code, OC1, OC2, and defect code for next scan (NOT operator code)
|
||||
cpCodeInput.value = '';
|
||||
oc1CodeInput.value = '';
|
||||
oc2CodeInput.value = '';
|
||||
defectCodeInput.value = '';
|
||||
|
||||
// Show success indicator
|
||||
setTimeout(function() {
|
||||
// Focus on CP code field for next scan
|
||||
if (cpCodeInput) {
|
||||
cpCodeInput.focus();
|
||||
}
|
||||
|
||||
// Add visual feedback
|
||||
const successIndicator = document.createElement('div');
|
||||
successIndicator.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
font-weight: bold;
|
||||
`;
|
||||
successIndicator.textContent = '✅ Scan recorded! Ready for next scan';
|
||||
document.body.appendChild(successIndicator);
|
||||
|
||||
// Remove success indicator after 3 seconds
|
||||
setTimeout(function() {
|
||||
if (successIndicator.parentNode) {
|
||||
successIndicator.parentNode.removeChild(successIndicator);
|
||||
}
|
||||
}, 3000);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Focus on the first empty required field (only if not clearing after submit)
|
||||
if (shouldClearAfterSubmit !== 'true' && operatorCodeInput && cpCodeInput && oc1CodeInput && oc2CodeInput && defectCodeInput) {
|
||||
if (!operatorCodeInput.value) {
|
||||
operatorCodeInput.focus();
|
||||
} else if (!cpCodeInput.value) {
|
||||
cpCodeInput.focus();
|
||||
} else if (!oc1CodeInput.value) {
|
||||
oc1CodeInput.focus();
|
||||
} else if (!oc2CodeInput.value) {
|
||||
oc2CodeInput.focus();
|
||||
} else {
|
||||
defectCodeInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
loadOperatorCode();
|
||||
|
||||
// Create error message elements
|
||||
const operatorErrorMessage = document.createElement('div');
|
||||
operatorErrorMessage.className = 'error-message';
|
||||
operatorErrorMessage.id = 'operator-error';
|
||||
operatorErrorMessage.textContent = 'Operator code must start with OP and be 4 characters';
|
||||
operatorCodeInput.parentNode.insertBefore(operatorErrorMessage, operatorCodeInput.nextSibling);
|
||||
|
||||
const cpErrorMessage = document.createElement('div');
|
||||
cpErrorMessage.className = 'error-message';
|
||||
cpErrorMessage.id = 'cp-error';
|
||||
cpErrorMessage.textContent = 'CP code must start with CP and be 15 characters';
|
||||
cpCodeInput.parentNode.insertBefore(cpErrorMessage, cpCodeInput.nextSibling);
|
||||
|
||||
const oc1ErrorMessage = document.createElement('div');
|
||||
oc1ErrorMessage.className = 'error-message';
|
||||
oc1ErrorMessage.id = 'oc1-error';
|
||||
oc1ErrorMessage.textContent = 'OC1 code must start with OC and be 4 characters';
|
||||
oc1CodeInput.parentNode.insertBefore(oc1ErrorMessage, oc1CodeInput.nextSibling);
|
||||
|
||||
const oc2ErrorMessage = document.createElement('div');
|
||||
oc2ErrorMessage.className = 'error-message';
|
||||
oc2ErrorMessage.id = 'oc2-error';
|
||||
oc2ErrorMessage.textContent = 'OC2 code must start with OC and be 4 characters';
|
||||
oc2CodeInput.parentNode.insertBefore(oc2ErrorMessage, oc2CodeInput.nextSibling);
|
||||
|
||||
const defectErrorMessage = document.createElement('div');
|
||||
defectErrorMessage.className = 'error-message';
|
||||
defectErrorMessage.id = 'defect-error';
|
||||
defectErrorMessage.textContent = 'Defect code must be a 3-digit number (e.g., 000, 001, 123)';
|
||||
defectCodeInput.parentNode.insertBefore(defectErrorMessage, defectCodeInput.nextSibling);
|
||||
|
||||
// ===== CP CODE AUTO-COMPLETE LOGIC =====
|
||||
let cpCodeAutoCompleteTimeout = null;
|
||||
|
||||
function autoCompleteCpCode() {
|
||||
const value = cpCodeInput.value.trim().toUpperCase();
|
||||
|
||||
// Only process if it starts with "CP" but is not 15 characters
|
||||
if (value.startsWith('CP') && value.length < 15 && value.length > 2) {
|
||||
console.log('Auto-completing CP code:', value);
|
||||
|
||||
// Check if there's a hyphen in the value
|
||||
if (value.includes('-')) {
|
||||
// Split by hyphen: CP[base]-[suffix]
|
||||
const parts = value.split('-');
|
||||
if (parts.length === 2) {
|
||||
const cpPrefix = parts[0]; // e.g., "CP00002042"
|
||||
const suffix = parts[1]; // e.g., "1" or "12" or "123" or "3"
|
||||
|
||||
console.log('CP prefix:', cpPrefix, 'Suffix:', suffix);
|
||||
|
||||
// Always pad the suffix to exactly 4 digits
|
||||
const paddedSuffix = suffix.padStart(4, '0');
|
||||
|
||||
// Construct the complete CP code
|
||||
const completedCpCode = `${cpPrefix}-${paddedSuffix}`;
|
||||
|
||||
console.log('Completed CP code length:', completedCpCode.length, 'Code:', completedCpCode);
|
||||
|
||||
// Ensure it's exactly 15 characters
|
||||
if (completedCpCode.length === 15) {
|
||||
console.log('✅ Completed CP code:', completedCpCode);
|
||||
cpCodeInput.value = completedCpCode;
|
||||
|
||||
// Show visual feedback
|
||||
cpCodeInput.style.backgroundColor = '#e8f5e9';
|
||||
setTimeout(() => {
|
||||
cpCodeInput.style.backgroundColor = '';
|
||||
}, 500);
|
||||
|
||||
// Move focus to next field (OC1 code)
|
||||
setTimeout(() => {
|
||||
oc1CodeInput.focus();
|
||||
console.log('✅ Auto-completed CP Code and advanced to OC1');
|
||||
}, 50);
|
||||
|
||||
// Show completion notification
|
||||
showNotification(`✅ CP Code auto-completed: ${completedCpCode}`, 'success');
|
||||
} else {
|
||||
console.log('⚠️ Completed code length is not 15:', completedCpCode.length);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('⏳ Waiting for hyphen to be entered before auto-completing');
|
||||
}
|
||||
} else {
|
||||
if (value.length >= 15) {
|
||||
console.log('ℹ️ CP code is already complete (15 characters)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cpCodeInput.addEventListener('input', function() {
|
||||
cpCodeLastInputTime = Date.now();
|
||||
const currentValue = this.value.trim().toUpperCase();
|
||||
this.value = currentValue; // Convert to uppercase
|
||||
|
||||
// Clear existing timeout
|
||||
if (cpCodeAutoCompleteTimeout) {
|
||||
clearTimeout(cpCodeAutoCompleteTimeout);
|
||||
}
|
||||
|
||||
console.log('CP Code input changed:', currentValue);
|
||||
|
||||
// Validate CP code prefix
|
||||
if (currentValue.length >= 2 && !currentValue.startsWith('CP')) {
|
||||
cpErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with CP');
|
||||
} else {
|
||||
cpErrorMessage.classList.remove('show');
|
||||
this.setCustomValidity('');
|
||||
|
||||
// Auto-advance when field is complete and valid
|
||||
if (currentValue.length === 15 && currentValue.startsWith('CP')) {
|
||||
setTimeout(() => {
|
||||
oc1CodeInput.focus();
|
||||
console.log('✅ Auto-advanced from CP Code to OC1 Code');
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// If hyphen is present and value is less than 15 chars, process immediately
|
||||
if (currentValue.includes('-') && currentValue.length < 15) {
|
||||
console.log('Hyphen detected, checking for auto-complete');
|
||||
cpCodeAutoCompleteTimeout = setTimeout(() => {
|
||||
console.log('Processing auto-complete after hyphen');
|
||||
autoCompleteCpCode();
|
||||
}, 500);
|
||||
} else if (currentValue.length < 15 && currentValue.startsWith('CP')) {
|
||||
// Set normal 2-second timeout only when no hyphen yet
|
||||
cpCodeAutoCompleteTimeout = setTimeout(() => {
|
||||
console.log('2-second timeout triggered for CP code');
|
||||
autoCompleteCpCode();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Also trigger auto-complete when focus leaves the field (blur event)
|
||||
cpCodeInput.addEventListener('blur', function() {
|
||||
console.log('CP Code blur event triggered with value:', this.value);
|
||||
if (cpCodeAutoCompleteTimeout) {
|
||||
clearTimeout(cpCodeAutoCompleteTimeout);
|
||||
}
|
||||
autoCompleteCpCode();
|
||||
});
|
||||
|
||||
// Prevent leaving CP code field if invalid
|
||||
cpCodeInput.addEventListener('blur', function(e) {
|
||||
if (this.value.length > 0 && !this.value.startsWith('CP')) {
|
||||
cpErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with CP');
|
||||
// Return focus to this field
|
||||
setTimeout(() => {
|
||||
this.focus();
|
||||
this.select();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent Tab/Enter from moving to next field if CP code is invalid
|
||||
cpCodeInput.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('CP')) {
|
||||
e.preventDefault();
|
||||
cpErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with CP');
|
||||
this.select();
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent focusing on CP code if operator code is invalid
|
||||
cpCodeInput.addEventListener('focus', function(e) {
|
||||
if (operatorCodeInput.value.length > 0 && !operatorCodeInput.value.startsWith('OP')) {
|
||||
e.preventDefault();
|
||||
operatorErrorMessage.classList.add('show');
|
||||
operatorCodeInput.focus();
|
||||
operatorCodeInput.select();
|
||||
}
|
||||
});
|
||||
|
||||
// ===== OPERATOR CODE VALIDATION =====
|
||||
operatorCodeInput.addEventListener('input', function() {
|
||||
const value = this.value.toUpperCase();
|
||||
this.value = value; // Convert to uppercase
|
||||
|
||||
if (value.length >= 2 && !value.startsWith('OP')) {
|
||||
operatorErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OP');
|
||||
} else {
|
||||
operatorErrorMessage.classList.remove('show');
|
||||
this.setCustomValidity('');
|
||||
|
||||
// Auto-advance when field is complete and valid
|
||||
if (value.length === 4 && value.startsWith('OP')) {
|
||||
setTimeout(() => {
|
||||
cpCodeInput.focus();
|
||||
console.log('✅ Auto-advanced from Operator Code to CP Code');
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
operatorCodeInput.addEventListener('blur', function(e) {
|
||||
if (this.value.length > 0 && !this.value.startsWith('OP')) {
|
||||
operatorErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OP');
|
||||
setTimeout(() => {
|
||||
this.focus();
|
||||
this.select();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
operatorCodeInput.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OP')) {
|
||||
e.preventDefault();
|
||||
operatorErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OP');
|
||||
this.select();
|
||||
}
|
||||
});
|
||||
|
||||
// ===== OC1 CODE VALIDATION =====
|
||||
oc1CodeInput.addEventListener('input', function() {
|
||||
const value = this.value.toUpperCase();
|
||||
this.value = value; // Convert to uppercase
|
||||
|
||||
if (value.length >= 2 && !value.startsWith('OC')) {
|
||||
oc1ErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OC');
|
||||
} else {
|
||||
oc1ErrorMessage.classList.remove('show');
|
||||
this.setCustomValidity('');
|
||||
|
||||
// Auto-advance when field is complete and valid
|
||||
if (value.length === 4 && value.startsWith('OC')) {
|
||||
setTimeout(() => {
|
||||
oc2CodeInput.focus();
|
||||
console.log('✅ Auto-advanced from OC1 Code to OC2 Code');
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
oc1CodeInput.addEventListener('blur', function(e) {
|
||||
if (this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||
oc1ErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OC');
|
||||
setTimeout(() => {
|
||||
this.focus();
|
||||
this.select();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
oc1CodeInput.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||
e.preventDefault();
|
||||
oc1ErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OC');
|
||||
this.select();
|
||||
}
|
||||
});
|
||||
|
||||
oc1CodeInput.addEventListener('focus', function(e) {
|
||||
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||
e.preventDefault();
|
||||
cpErrorMessage.classList.add('show');
|
||||
cpCodeInput.focus();
|
||||
cpCodeInput.select();
|
||||
}
|
||||
});
|
||||
|
||||
// ===== OC2 CODE VALIDATION =====
|
||||
oc2CodeInput.addEventListener('input', function() {
|
||||
const value = this.value.toUpperCase();
|
||||
this.value = value; // Convert to uppercase
|
||||
|
||||
if (value.length >= 2 && !value.startsWith('OC')) {
|
||||
oc2ErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OC');
|
||||
} else {
|
||||
oc2ErrorMessage.classList.remove('show');
|
||||
this.setCustomValidity('');
|
||||
|
||||
// Auto-advance when field is complete and valid
|
||||
if (value.length === 4 && value.startsWith('OC')) {
|
||||
setTimeout(() => {
|
||||
defectCodeInput.focus();
|
||||
console.log('✅ Auto-advanced from OC2 Code to Defect Code');
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
oc2CodeInput.addEventListener('blur', function(e) {
|
||||
if (this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||
oc2ErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OC');
|
||||
setTimeout(() => {
|
||||
this.focus();
|
||||
this.select();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
oc2CodeInput.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||
e.preventDefault();
|
||||
oc2ErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must start with OC');
|
||||
this.select();
|
||||
}
|
||||
});
|
||||
|
||||
oc2CodeInput.addEventListener('focus', function(e) {
|
||||
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||
e.preventDefault();
|
||||
cpErrorMessage.classList.add('show');
|
||||
cpCodeInput.focus();
|
||||
cpCodeInput.select();
|
||||
}
|
||||
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
|
||||
e.preventDefault();
|
||||
oc1ErrorMessage.classList.add('show');
|
||||
oc1CodeInput.focus();
|
||||
oc1CodeInput.select();
|
||||
}
|
||||
});
|
||||
|
||||
// ===== DEFECT CODE VALIDATION =====
|
||||
defectCodeInput.addEventListener('input', function() {
|
||||
// Remove any non-digit characters
|
||||
this.value = this.value.replace(/\D/g, '');
|
||||
|
||||
// Validate if it's a valid 3-digit number when length is 3
|
||||
if (this.value.length === 3) {
|
||||
const isValid = /^\d{3}$/.test(this.value);
|
||||
if (!isValid) {
|
||||
defectErrorMessage.classList.add('show');
|
||||
this.setCustomValidity('Must be a 3-digit number');
|
||||
} else {
|
||||
defectErrorMessage.classList.remove('show');
|
||||
this.setCustomValidity('');
|
||||
}
|
||||
} else {
|
||||
defectErrorMessage.classList.remove('show');
|
||||
this.setCustomValidity('');
|
||||
}
|
||||
|
||||
// Auto-submit when 3 characters are entered and all validations pass
|
||||
if (this.value.length === 3) {
|
||||
// Validate operator code before submitting
|
||||
if (!operatorCodeInput.value.startsWith('OP')) {
|
||||
operatorErrorMessage.classList.add('show');
|
||||
operatorCodeInput.focus();
|
||||
operatorCodeInput.setCustomValidity('Must start with OP');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate CP code before submitting
|
||||
if (!cpCodeInput.value.startsWith('CP') || cpCodeInput.value.length !== 15) {
|
||||
cpErrorMessage.classList.add('show');
|
||||
cpCodeInput.focus();
|
||||
cpCodeInput.setCustomValidity('Must start with CP and be complete');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate OC1 code before submitting
|
||||
if (!oc1CodeInput.value.startsWith('OC')) {
|
||||
oc1ErrorMessage.classList.add('show');
|
||||
oc1CodeInput.focus();
|
||||
oc1CodeInput.setCustomValidity('Must start with OC');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate OC2 code before submitting
|
||||
if (!oc2CodeInput.value.startsWith('OC')) {
|
||||
oc2ErrorMessage.classList.add('show');
|
||||
oc2CodeInput.focus();
|
||||
oc2CodeInput.setCustomValidity('Must start with OC');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate defect code is a valid 3-digit number
|
||||
const isValidDefectCode = /^\d{3}$/.test(this.value);
|
||||
if (!isValidDefectCode) {
|
||||
defectErrorMessage.classList.add('show');
|
||||
this.focus();
|
||||
this.setCustomValidity('Must be a 3-digit number');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear all custom validity states before submitting
|
||||
operatorCodeInput.setCustomValidity('');
|
||||
cpCodeInput.setCustomValidity('');
|
||||
oc1CodeInput.setCustomValidity('');
|
||||
oc2CodeInput.setCustomValidity('');
|
||||
this.setCustomValidity('');
|
||||
|
||||
// ===== TIME FIELD AUTO-UPDATE (CRITICAL) =====
|
||||
// Update time field to current time before submitting
|
||||
const timeInput = document.getElementById('date_time');
|
||||
const now = new Date();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
const timeValue = `${hours}:${minutes}:${seconds}`;
|
||||
|
||||
// Parse the current datetime display and update just the time part
|
||||
const dateStr = timeInput.value.split(' ').slice(0, -1).join(' '); // Get date part
|
||||
timeInput.value = dateStr + ' ' + timeValue;
|
||||
|
||||
console.log('✅ Time field updated to:', timeValue);
|
||||
|
||||
// Save current scan data to localStorage for clearing after reload
|
||||
localStorage.setItem('fg_scan_clear_after_submit', 'true');
|
||||
localStorage.setItem('fg_scan_last_cp', cpCodeInput.value);
|
||||
localStorage.setItem('fg_scan_last_defect', defectCodeInput.value);
|
||||
|
||||
// Auto-submit the form
|
||||
console.log('Auto-submitting form on 3-digit defect code');
|
||||
document.getElementById('scanForm').submit();
|
||||
}
|
||||
});
|
||||
|
||||
defectCodeInput.addEventListener('focus', function(e) {
|
||||
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||
e.preventDefault();
|
||||
cpErrorMessage.classList.add('show');
|
||||
cpCodeInput.focus();
|
||||
cpCodeInput.select();
|
||||
}
|
||||
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
|
||||
e.preventDefault();
|
||||
oc1ErrorMessage.classList.add('show');
|
||||
oc1CodeInput.focus();
|
||||
oc1CodeInput.select();
|
||||
}
|
||||
if (oc2CodeInput.value.length > 0 && !oc2CodeInput.value.startsWith('OC')) {
|
||||
e.preventDefault();
|
||||
oc2ErrorMessage.classList.add('show');
|
||||
oc2CodeInput.focus();
|
||||
oc2CodeInput.select();
|
||||
}
|
||||
});
|
||||
|
||||
// ===== CLEAR OPERATOR CODE BUTTON =====
|
||||
document.getElementById('clearOperator').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
operatorCodeInput.value = '';
|
||||
localStorage.removeItem('quality_operator_code');
|
||||
operatorCodeInput.focus();
|
||||
showNotification('Quality Operator Code cleared', 'info');
|
||||
});
|
||||
|
||||
// ===== SAVE OPERATOR CODE ON INPUT =====
|
||||
operatorCodeInput.addEventListener('input', function() {
|
||||
if (this.value.startsWith('OP') && this.value.length >= 3) {
|
||||
localStorage.setItem('quality_operator_code', this.value);
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document.getElementById('scanForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save operator code
|
||||
localStorage.setItem('quality_operator_code', document.getElementById('operator_code').value.trim());
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
// If AJAX is needed (scan to boxes)
|
||||
if (scanToBoxesEnabled) {
|
||||
fetch('{{ url_for("quality.fg_scan") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification('Scan saved successfully!', 'success');
|
||||
resetForm();
|
||||
document.getElementById('boxAssignmentModal').style.display = 'flex';
|
||||
} else {
|
||||
showNotification('Error saving scan', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Error saving scan', 'error');
|
||||
});
|
||||
} else {
|
||||
// Regular form submission
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
|
||||
// Scan to boxes functionality
|
||||
document.getElementById('scanToBoxes').addEventListener('change', function() {
|
||||
scanToBoxesEnabled = this.checked;
|
||||
document.getElementById('quickBoxSection').style.display = this.checked ? 'block' : 'none';
|
||||
|
||||
if (this.checked) {
|
||||
// Initialize QZ Tray when user enables the feature
|
||||
console.log('Scan To Boxes enabled - initializing QZ Tray...');
|
||||
initializeQzTray();
|
||||
}
|
||||
});
|
||||
|
||||
// Quick box label creation
|
||||
document.getElementById('quickBoxLabel').addEventListener('click', function() {
|
||||
if (!qzTrayReady) {
|
||||
alert('QZ Tray is not connected. Please ensure QZ Tray is running.');
|
||||
return;
|
||||
}
|
||||
|
||||
const cpCode = document.getElementById('cp_code').value.trim();
|
||||
if (!cpCode) {
|
||||
alert('Please enter a CP code first');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create label configuration for QZ Tray
|
||||
const label = {
|
||||
type: 'label',
|
||||
cpCode: cpCode,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Send to printer via QZ Tray
|
||||
qz.print({
|
||||
type: 'label',
|
||||
format: cpCode
|
||||
}).catch(function(err) {
|
||||
console.error('Print error:', err);
|
||||
alert('Error printing label');
|
||||
});
|
||||
});
|
||||
|
||||
// Modal functionality
|
||||
document.getElementById('closeModal').addEventListener('click', function() {
|
||||
document.getElementById('boxAssignmentModal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('cancelModal').addEventListener('click', function() {
|
||||
document.getElementById('boxAssignmentModal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('assignToBox').addEventListener('click', function() {
|
||||
const boxNumber = document.getElementById('boxNumber').value.trim();
|
||||
const boxQty = document.getElementById('boxQty').value.trim();
|
||||
|
||||
if (!boxNumber) {
|
||||
alert('Please enter a box number');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!boxQty || isNaN(boxQty) || parseInt(boxQty) < 1) {
|
||||
alert('Please enter a valid quantity');
|
||||
return;
|
||||
}
|
||||
|
||||
// Submit box assignment
|
||||
const data = {
|
||||
box_number: boxNumber,
|
||||
quantity: boxQty,
|
||||
cp_code: currentCpCode
|
||||
};
|
||||
|
||||
fetch('{{ url_for("quality.fg_scan") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification('Box assigned successfully!', 'success');
|
||||
document.getElementById('boxAssignmentModal').style.display = 'none';
|
||||
document.getElementById('boxNumber').value = '';
|
||||
document.getElementById('boxQty').value = '';
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
});
|
||||
|
||||
// Utility functions
|
||||
function resetForm() {
|
||||
document.getElementById('cp_code').value = '';
|
||||
document.getElementById('oc1_code').value = '';
|
||||
document.getElementById('oc2_code').value = '';
|
||||
document.getElementById('defect_code').value = '';
|
||||
currentCpCode = '';
|
||||
document.getElementById('cp_code').focus();
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
notification.style.opacity = '1';
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
function applyDarkModeStyles() {
|
||||
const isDarkMode = document.body.classList.contains('dark-mode');
|
||||
if (isDarkMode) {
|
||||
document.documentElement.style.setProperty('--bg-color', '#1e1e1e');
|
||||
document.documentElement.style.setProperty('--text-color', '#e0e0e0');
|
||||
}
|
||||
}
|
||||
|
||||
// Check dark mode on page load
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
applyDarkModeStyles();
|
||||
}
|
||||
|
||||
// Listen for dark mode changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class') {
|
||||
applyDarkModeStyles();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, { attributes: true });
|
||||
</script>
|
||||
{% endblock %}
|
||||
52
app/templates/modules/quality/index.html
Normal file
52
app/templates/modules/quality/index.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Quality Module - Quality App v2{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-check-circle"></i> Quality Module
|
||||
</h1>
|
||||
<p class="text-muted">Manage quality checks and inspections</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<a href="{{ url_for('quality.fg_scan') }}" class="btn btn-success btn-lg w-100">
|
||||
<i class="fas fa-barcode"></i><br>
|
||||
FG Scan
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="{{ url_for('quality.inspections') }}" class="btn btn-primary btn-lg w-100">
|
||||
<i class="fas fa-clipboard-list"></i><br>
|
||||
Inspections
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="{{ url_for('quality.quality_reports') }}" class="btn btn-info btn-lg w-100">
|
||||
<i class="fas fa-chart-bar"></i><br>
|
||||
Reports
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Quality Overview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted text-center py-5">
|
||||
<i class="fas fa-inbox"></i> No data available yet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
88
app/templates/modules/quality/inspections.html
Normal file
88
app/templates/modules/quality/inspections.html
Normal file
@@ -0,0 +1,88 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Inspections - Quality Module{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-clipboard-list"></i> Quality Inspections
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#newInspectionModal">
|
||||
<i class="fas fa-plus"></i> New Inspection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Inspection Records</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted py-4">
|
||||
No inspections found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Inspection Modal -->
|
||||
<div class="modal fade" id="newInspectionModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Quality Inspection</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="newInspectionForm">
|
||||
<div class="mb-3">
|
||||
<label for="inspectionType" class="form-label">Inspection Type</label>
|
||||
<select class="form-select" id="inspectionType" required>
|
||||
<option value="">Select type...</option>
|
||||
<option value="visual">Visual Check</option>
|
||||
<option value="functional">Functional Test</option>
|
||||
<option value="measurement">Measurement</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="inspectionNote" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="inspectionNote" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary">Create Inspection</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
57
app/templates/modules/quality/reports.html
Normal file
57
app/templates/modules/quality/reports.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Reports - Quality Module{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-chart-bar"></i> Quality Reports
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Total Inspections</h5>
|
||||
<h2 class="text-primary">0</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Pass Rate</h5>
|
||||
<h2 class="text-success">0%</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Issues Found</h5>
|
||||
<h2 class="text-warning">0</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Report Data</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted text-center py-5">
|
||||
<i class="fas fa-inbox"></i> No report data available
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user