diff --git a/app/modules/labels/pdf_generator.py b/app/modules/labels/pdf_generator.py new file mode 100644 index 0000000..fd769dc --- /dev/null +++ b/app/modules/labels/pdf_generator.py @@ -0,0 +1,452 @@ +""" +PDF Label Generator for Print Module +Generates 80x110mm labels with sequential numbering based on quantity +""" +from reportlab.lib.pagesizes import letter, A4 +from reportlab.lib.units import mm +from reportlab.lib import colors +from reportlab.pdfgen import canvas +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus import Paragraph +from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.graphics.barcode import code128 +from reportlab.graphics import renderPDF +from reportlab.graphics.shapes import Drawing +import os +from flask import current_app +import io + + +def mm_to_points(mm_value): + """Convert millimeters to points (ReportLab uses points)""" + return mm_value * mm + + +class LabelPDFGenerator: + def __init__(self, paper_saving_mode=True): + # Label dimensions: 80mm x 105mm (reduced from 110mm by cutting 5mm from bottom) + self.label_width = mm_to_points(80) + self.label_height = mm_to_points(105) + + # Paper-saving mode: positions content at top of label to minimize waste + self.paper_saving_mode = paper_saving_mode + + # Match the HTML preview dimensions exactly + # Preview: 227.4px width x 321.3px height + # Convert to proportional dimensions for 80x110mm + self.content_width = mm_to_points(60) # ~227px scaled to 80mm + self.content_height = mm_to_points(68) # Reduced by 20% from 85mm to 68mm + + # Position content in label - rectangle positioned 15mm from top + # Label height: 105mm, Rectangle height: 68mm + # content_y = 105mm - 15mm - 68mm = 22mm from bottom + if self.paper_saving_mode: + # Start content from top of label with 15mm top margin + self.content_x = mm_to_points(4) # 4mm from left edge + self.content_y = mm_to_points(22) # 22mm from bottom (15mm from top) + self.top_margin = mm_to_points(15) # 15mm top margin + else: + # Original positioning + self.content_x = mm_to_points(5) # 5mm from left edge + self.content_y = mm_to_points(22) # 22mm from bottom (15mm from top) + self.top_margin = mm_to_points(15) # 15mm top margin + + # Row dimensions (9 rows total, row 6 is double height) + self.row_height = self.content_height / 10 # 8.5mm per standard row + self.double_row_height = self.row_height * 2 + + # Column split at 40% (90.96px / 227.4px = 40%) + self.left_column_width = self.content_width * 0.4 + self.right_column_width = self.content_width * 0.6 + + # Vertical divider starts from row 3 + self.vertical_divider_start_y = self.content_y + self.content_height - (2 * self.row_height) + + def generate_labels_pdf(self, order_data, quantity, printer_optimized=True): + """ + Generate PDF with multiple labels based on quantity + Creates sequential labels: CP00000711-001 to CP00000711-XXX + Optimized for thermal label printers (Epson TM-T20, Citizen CTS-310) + """ + buffer = io.BytesIO() + + # Create canvas with label dimensions + c = canvas.Canvas(buffer, pagesize=(self.label_width, self.label_height)) + + # Optimize PDF for label printers + if printer_optimized: + self._optimize_for_label_printer(c) + + # Extract base production order number for sequential numbering + prod_order = order_data.get('comanda_productie', 'CP00000000') + + # Generate labels for each quantity + for i in range(1, quantity + 1): + if i > 1: # Add new page for each label except first + c.showPage() + if printer_optimized: + self._optimize_for_label_printer(c) + + # Create sequential label number: CP00000711/001, CP00000711/002, etc. + sequential_number = f"{prod_order}/{i:03d}" + + # Draw single label + self._draw_label(c, order_data, sequential_number, i, quantity) + + c.save() + buffer.seek(0) + return buffer + + def generate_single_label_pdf(self, order_data, piece_number, total_pieces, printer_optimized=True): + """ + Generate PDF with single label for specific piece number + Creates sequential label: CP00000711-001, CP00000711-002, etc. + Optimized for thermal label printers via QZ Tray + """ + buffer = io.BytesIO() + + # Create canvas with label dimensions + c = canvas.Canvas(buffer, pagesize=(self.label_width, self.label_height)) + + # Optimize PDF for label printers + if printer_optimized: + self._optimize_for_label_printer(c) + + # Extract base production order number for sequential numbering + prod_order = order_data.get('comanda_productie', 'CP00000000') + + # Create sequential label number with specific piece number + sequential_number = f"{prod_order}/{piece_number:03d}" + + print(f"DEBUG: Generating label {sequential_number} (piece {piece_number} of {total_pieces})") + + # Draw single label with specific piece number + self._draw_label(c, order_data, sequential_number, piece_number, total_pieces) + + c.save() + buffer.seek(0) + return buffer + + def _optimize_for_label_printer(self, canvas): + """ + Optimize PDF settings for thermal label printers + - Sets high resolution for crisp text + - Minimizes margins to save paper + - Optimizes for monochrome printing + """ + # Set high resolution for thermal printers (300 DPI) + canvas.setPageCompression(1) # Enable compression + + # Add PDF metadata for printer optimization + canvas.setCreator("Recticel Label System") + canvas.setTitle("Thermal Label - Optimized for Label Printers") + canvas.setSubject("Production Label") + + # Set print scaling to none (100%) to maintain exact dimensions + canvas.setPageRotation(0) + + # Add custom PDF properties for label printers + canvas._doc.info.producer = "Optimized for Epson TM-T20 / Citizen CTS-310" + + def _draw_label(self, canvas, order_data, sequential_number, current_num, total_qty): + """Draw a single label matching the HTML preview layout exactly""" + + # Draw main content border (like the HTML preview rectangle) + canvas.setStrokeColor(colors.black) + canvas.setLineWidth(2) + canvas.rect(self.content_x, self.content_y, self.content_width, self.content_height) + + # Calculate row positions from top + current_y = self.content_y + self.content_height + + # Row 1: Company Header - (removed) + row_y = current_y - self.row_height + canvas.setFont("Helvetica-Bold", 10) + text = "" + text_width = canvas.stringWidth(text, "Helvetica-Bold", 10) + x_centered = self.content_x + (self.content_width - text_width) / 2 + canvas.drawString(x_centered, row_y + self.row_height/3, text) + current_y = row_y + + # Row 2: Customer Name + row_y = current_y - self.row_height + canvas.setFont("Helvetica-Bold", 9) + customer_name = str(order_data.get('customer_name', ''))[:30] + text_width = canvas.stringWidth(customer_name, "Helvetica-Bold", 9) + x_centered = self.content_x + (self.content_width - text_width) / 2 + canvas.drawString(x_centered, row_y + self.row_height/3, customer_name) + current_y = row_y + + # Draw horizontal lines after rows 1 and 2 + canvas.setLineWidth(1) + canvas.line(self.content_x, current_y, self.content_x + self.content_width, current_y) + canvas.line(self.content_x, current_y + self.row_height, self.content_x + self.content_width, current_y + self.row_height) + + # Draw vertical divider line (starts from row 3, goes to bottom) + vertical_x = self.content_x + self.left_column_width + canvas.line(vertical_x, current_y, vertical_x, self.content_y) + + # Row 3: Quantity ordered + row_y = current_y - self.row_height + canvas.setFont("Helvetica", 8) + canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Quantity ordered") + canvas.setFont("Helvetica-Bold", 11) + quantity = str(order_data.get('cantitate', '0')) + q_text_width = canvas.stringWidth(quantity, "Helvetica-Bold", 11) + q_x_centered = vertical_x + (self.right_column_width - q_text_width) / 2 + canvas.drawString(q_x_centered, row_y + self.row_height/3, quantity) + current_y = row_y + + # Row 4: Customer order - CORRECTED to match HTML preview + row_y = current_y - self.row_height + canvas.setFont("Helvetica", 8) + canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Customer order") + canvas.setFont("Helvetica-Bold", 10) + # Match HTML: com_achiz_client + "-" + nr_linie_com_client + com_achiz_client = str(order_data.get('com_achiz_client', '')) + nr_linie = str(order_data.get('nr_linie_com_client', '')) + if com_achiz_client and nr_linie: + customer_order = f"{com_achiz_client}-{nr_linie}"[:18] + else: + customer_order = "N/A" + co_text_width = canvas.stringWidth(customer_order, "Helvetica-Bold", 10) + co_x_centered = vertical_x + (self.right_column_width - co_text_width) / 2 + canvas.drawString(co_x_centered, row_y + self.row_height/3, customer_order) + current_y = row_y + + # Row 5: Delivery date + row_y = current_y - self.row_height + canvas.setFont("Helvetica", 8) + canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Delivery date") + canvas.setFont("Helvetica-Bold", 10) + delivery_date = str(order_data.get('data_livrare', 'N/A')) + if delivery_date != 'N/A' and delivery_date: + try: + # Format date if it's a valid date + from datetime import datetime + if isinstance(delivery_date, str) and len(delivery_date) > 8: + delivery_date = delivery_date[:10] # Take first 10 chars for date + except: + pass + dd_text_width = canvas.stringWidth(delivery_date, "Helvetica-Bold", 10) + dd_x_centered = vertical_x + (self.right_column_width - dd_text_width) / 2 + canvas.drawString(dd_x_centered, row_y + self.row_height/3, delivery_date) + current_y = row_y + + # Row 6: Description (double height) + row_y = current_y - self.double_row_height + canvas.setFont("Helvetica", 8) + canvas.drawString(self.content_x + mm_to_points(2), row_y + self.double_row_height/2, "Description") + + # Handle description text wrapping for double height area + canvas.setFont("Helvetica-Bold", 9) + description = str(order_data.get('descr_com_prod', 'N/A')) + max_chars_per_line = 18 + lines = [] + words = description.split() + current_line = "" + + for word in words: + if len(current_line + word + " ") <= max_chars_per_line: + current_line += word + " " + else: + if current_line: + lines.append(current_line.strip()) + current_line = word + " " + if current_line: + lines.append(current_line.strip()) + + # Draw up to 3 lines in the double height area + line_spacing = self.double_row_height / 4 + start_y = row_y + 3 * line_spacing + for i, line in enumerate(lines[:3]): + line_y = start_y - (i * line_spacing) + l_text_width = canvas.stringWidth(line, "Helvetica-Bold", 9) + l_x_centered = vertical_x + (self.right_column_width - l_text_width) / 2 + canvas.drawString(l_x_centered, line_y, line) + + current_y = row_y + + # Row 7: Size + row_y = current_y - self.row_height + canvas.setFont("Helvetica", 8) + canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Size") + canvas.setFont("Helvetica-Bold", 10) + size = str(order_data.get('dimensiune', 'N/A'))[:12] + s_text_width = canvas.stringWidth(size, "Helvetica-Bold", 10) + s_x_centered = vertical_x + (self.right_column_width - s_text_width) / 2 + canvas.drawString(s_x_centered, row_y + self.row_height/3, size) + current_y = row_y + + # Row 8: Article Code - CORRECTED to use customer_article_number like HTML preview + row_y = current_y - self.row_height + canvas.setFont("Helvetica", 8) + canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Article Code") + canvas.setFont("Helvetica-Bold", 10) + # Match HTML: uses customer_article_number, not cod_articol + article_code = str(order_data.get('customer_article_number', 'N/A'))[:12] + ac_text_width = canvas.stringWidth(article_code, "Helvetica-Bold", 10) + ac_x_centered = vertical_x + (self.right_column_width - ac_text_width) / 2 + canvas.drawString(ac_x_centered, row_y + self.row_height/3, article_code) + current_y = row_y + + # Draw horizontal line between Article Code and Prod Order + canvas.setLineWidth(1) + canvas.line(self.content_x, current_y, self.content_x + self.content_width, current_y) + + # Row 9: Prod Order - CORRECTED to match HTML preview with sequential numbering + row_y = current_y - self.row_height + canvas.setFont("Helvetica", 8) + canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Prod Order") + canvas.setFont("Helvetica-Bold", 10) + # Match HTML: comanda_productie + "-" + sequential number (not quantity!) + comanda_productie = str(order_data.get('comanda_productie', 'N/A')) + prod_order = f"{comanda_productie}-{current_num:03d}" # Sequential number for this specific label + po_text_width = canvas.stringWidth(prod_order, "Helvetica-Bold", 10) + po_x_centered = vertical_x + (self.right_column_width - po_text_width) / 2 + canvas.drawString(po_x_centered, row_y + self.row_height/3, prod_order) + + # Draw all horizontal lines between rows (from row 3 onwards) + for i in range(6): # 6 lines between 7 rows (3-9) + line_y = self.content_y + self.content_height - (2 + i + 1) * self.row_height + if i == 3: # Account for double height description row + line_y -= self.row_height + canvas.line(self.content_x, line_y, self.content_x + self.content_width, line_y) + + # Bottom horizontal barcode - positioned 1.5mm below the main rectangle + barcode_area_height = mm_to_points(12) # Reserve space for barcode + barcode_y = self.content_y - mm_to_points(1.5) - barcode_area_height # 1.5mm gap below rectangle + barcode_width = self.content_width # Use full content width + barcode_x = self.content_x + + try: + # Create barcode for sequential number + barcode = code128.Code128(sequential_number, + barWidth=0.25*mm, # Adjust bar width for better fit + barHeight=mm_to_points(10)) # Increase height to 10mm + + # Always scale to fit the full allocated width + scale_factor = barcode_width / barcode.width + canvas.saveState() + canvas.translate(barcode_x, barcode_y) + canvas.scale(scale_factor, 1) + barcode.drawOn(canvas, 0, 0) + canvas.restoreState() + + # NO TEXT BELOW BARCODE - Remove all text rendering for horizontal barcode + + except Exception as e: + # Fallback: Simple barcode pattern that fills the width + canvas.setStrokeColor(colors.black) + canvas.setFillColor(colors.black) + bar_width = barcode_width / 50 # 50 bars across width + for i in range(50): + if i % 3 < 2: # Create barcode-like pattern + x_pos = barcode_x + (i * bar_width) + canvas.rect(x_pos, barcode_y, bar_width * 0.8, mm_to_points(8), fill=1) + + # Right side vertical barcode - positioned 3mm further right and fill frame + vertical_barcode_x = self.content_x + self.content_width + mm_to_points(4) # Moved 3mm right (1mm + 3mm = 4mm) + vertical_barcode_y = self.content_y + vertical_barcode_height = self.content_height + vertical_barcode_width = mm_to_points(12) # Increased width for better fill + + try: + # Create vertical barcode code - CORRECTED to match HTML preview + # Use same format as customer order: com_achiz_client + "/" + nr_linie_com_client + com_achiz_client = str(order_data.get('com_achiz_client', '')) + nr_linie = str(order_data.get('nr_linie_com_client', '')) + if com_achiz_client and nr_linie: + vertical_code = f"{com_achiz_client}/{nr_linie}" + else: + vertical_code = "000000/00" + + # Create a vertical barcode using Code128 + v_barcode = code128.Code128(vertical_code, + barWidth=0.15*mm, # Thinner bars for better fit + barHeight=mm_to_points(8)) # Increased bar height + + # Draw rotated barcode - fill the entire frame height + canvas.saveState() + canvas.translate(vertical_barcode_x + mm_to_points(6), vertical_barcode_y) + canvas.rotate(90) + + # Always scale to fill the frame height + scale_factor = vertical_barcode_height / v_barcode.width + canvas.scale(scale_factor, 1) + + v_barcode.drawOn(canvas, 0, 0) + canvas.restoreState() + + # NO TEXT FOR VERTICAL BARCODE - Remove all text rendering + + except Exception as e: + # Fallback: Vertical barcode pattern that fills the frame + canvas.setStrokeColor(colors.black) + canvas.setFillColor(colors.black) + bar_height = vertical_barcode_height / 60 # 60 bars across height + for i in range(60): + if i % 3 < 2: # Create barcode pattern + y_pos = vertical_barcode_y + (i * bar_height) + canvas.rect(vertical_barcode_x, y_pos, mm_to_points(8), bar_height * 0.8, fill=1) + + +def generate_order_labels_pdf(order_id, order_data, paper_saving_mode=True): + """ + Main function to generate PDF for an order with multiple labels + Optimized for thermal label printers (Epson TM-T20, Citizen CTS-310) + + Args: + order_id: Order identifier + order_data: Order information dictionary + paper_saving_mode: If True, positions content at top to save paper + """ + try: + generator = LabelPDFGenerator(paper_saving_mode=paper_saving_mode) + + # Get quantity from order data + quantity = int(order_data.get('cantitate', 1)) + + # Generate PDF with printer optimization + pdf_buffer = generator.generate_labels_pdf(order_data, quantity, printer_optimized=True) + + return pdf_buffer + + except Exception as e: + print(f"Error generating PDF labels: {e}") + raise e + + +def update_order_printed_status(order_id): + """ + Update the order status to printed in the database + """ + try: + from .print_module import get_db_connection + + conn = get_db_connection() + cursor = conn.cursor() + + # Check if printed_labels column exists + cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'") + column_exists = cursor.fetchone() + + if column_exists: + # Update printed status + cursor.execute(""" + UPDATE order_for_labels + SET printed_labels = 1, updated_at = NOW() + WHERE id = %s + """, (order_id,)) + else: + # If column doesn't exist, we could add it or use another method + print(f"Warning: printed_labels column doesn't exist for order {order_id}") + + conn.commit() + conn.close() + + return True + + except Exception as e: + print(f"Error updating printed status for order {order_id}: {e}") + return False \ No newline at end of file diff --git a/app/modules/labels/routes.py b/app/modules/labels/routes.py index 09f910b..ed2d17e 100644 --- a/app/modules/labels/routes.py +++ b/app/modules/labels/routes.py @@ -5,6 +5,7 @@ Handles label printing pages and API endpoints from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request import logging +from app.database import get_db from .print_module import ( get_unprinted_orders_data, get_printed_orders_data, @@ -35,6 +36,15 @@ def print_module(): return render_template('modules/labels/print_module.html') +@labels_bp.route('/print-labels', methods=['GET']) +def print_labels(): + """Original print labels interface - complete copy from quality app""" + if 'user_id' not in session: + return redirect(url_for('main.login')) + + return render_template('modules/labels/print_labels.html') + + @labels_bp.route('/print-lost-labels', methods=['GET']) def print_lost_labels(): """Print lost/missing labels interface""" @@ -74,6 +84,28 @@ def help(page='index'): ''' }, + 'print_labels': { + 'title': 'Print Labels Help', + 'content': ''' +

Print Labels - Thermal Printer Guide

+

This module helps you print labels directly to thermal printers.

+

Features:

+ +

How to use:

+
    +
  1. Select orders from the list
  2. +
  3. Preview labels in the preview pane
  4. +
  5. Select your printer
  6. +
  7. Click "Print Labels" to send to printer
  8. +
+ ''' + }, 'print_lost_labels': { 'title': 'Print Lost Labels Help', 'content': ''' @@ -198,3 +230,110 @@ def api_update_printed_status(order_id): except Exception as e: logger.error(f"Error updating order status: {e}") return jsonify({'success': False, 'error': str(e)}), 500 + + +@labels_bp.route('/api/generate-pdf', methods=['POST'], endpoint='api_generate_pdf') +def api_generate_pdf(): + """Generate single label PDF for thermal printing via QZ Tray""" + if 'user_id' not in session: + return jsonify({'error': 'Unauthorized'}), 401 + + try: + from .pdf_generator import LabelPDFGenerator + + # Get order data from request + order_data = request.get_json() + + if not order_data: + return jsonify({'error': 'No order data provided'}), 400 + + # Extract piece number and total pieces for sequential numbering + piece_number = order_data.get('piece_number', 1) + total_pieces = order_data.get('total_pieces', 1) + + logger.info(f"Generating single label PDF for piece {piece_number} of {total_pieces}") + + # Initialize PDF generator in thermal printer optimized mode + pdf_generator = LabelPDFGenerator(paper_saving_mode=True) + + # Generate single label PDF with specific piece number for sequential CP numbering + pdf_buffer = pdf_generator.generate_single_label_pdf(order_data, piece_number, total_pieces, printer_optimized=True) + + # Create response with PDF data + from flask import make_response + response = make_response(pdf_buffer.getvalue()) + response.headers['Content-Type'] = 'application/pdf' + response.headers['Content-Disposition'] = f'inline; filename="label_{piece_number:03d}.pdf"' + + return response + + except Exception as e: + logger.error(f"Error generating PDF: {e}") + return jsonify({'error': str(e)}), 500 + + +@labels_bp.route('/api/generate-pdf//true', methods=['POST'], endpoint='api_generate_batch_pdf') +def api_generate_batch_pdf(order_id): + """Generate all label PDFs for an order and mark as printed""" + if 'user_id' not in session: + return jsonify({'error': 'Unauthorized'}), 401 + + try: + from .pdf_generator import LabelPDFGenerator + + # Get order data from database + conn = get_db() + cursor = conn.cursor() + + cursor.execute(""" + SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate, + com_achiz_client, nr_linie_com_client, customer_name, + customer_article_number, open_for_order, line_number, + created_at, updated_at, printed_labels, data_livrare, dimensiune + FROM order_for_labels + WHERE id = %s + """, (order_id,)) + + row = cursor.fetchone() + + if not row: + cursor.close() + return jsonify({'error': 'Order not found'}), 404 + + # Create order data dictionary + columns = [col[0] for col in cursor.description] + order_data = {columns[i]: row[i] for i in range(len(columns))} + + # Ensure date fields are strings + if order_data.get('data_livrare'): + order_data['data_livrare'] = str(order_data['data_livrare']) + + cursor.close() + + logger.info(f"Generating batch PDF for order {order_id} with {order_data.get('cantitate', 0)} labels") + + # Initialize PDF generator + pdf_generator = LabelPDFGenerator(paper_saving_mode=True) + + # Get quantity from order data + quantity = int(order_data.get('cantitate', 1)) + + # Generate PDF with all labels + pdf_buffer = pdf_generator.generate_labels_pdf(order_data, quantity, printer_optimized=True) + + # Mark order as printed + success = update_order_printed_status(order_id, True) + if not success: + logger.warning(f"Failed to mark order {order_id} as printed, but PDF was generated") + + # Create response with PDF data + from flask import make_response + response = make_response(pdf_buffer.getvalue()) + response.headers['Content-Type'] = 'application/pdf' + response.headers['Content-Disposition'] = f'attachment; filename="labels_{order_data.get("comanda_productie", "unknown")}.pdf"' + + return response + + except Exception as e: + logger.error(f"Error generating batch PDF: {e}") + return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/app/static/css/print_module.css b/app/static/css/print_module.css new file mode 100644 index 0000000..71ecdf2 --- /dev/null +++ b/app/static/css/print_module.css @@ -0,0 +1,807 @@ +/* ========================================================================== + PRINT MODULE CSS - Dedicated styles for Labels/Printing Module + ========================================================================== + + This file contains all CSS for the printing module pages: + - print_module.html (main printing interface) + - print_lost_labels.html (lost labels printing) + - main_page_etichete.html (labels main page) + - upload_data.html (upload orders) + - view_orders.html (view orders) + + ========================================================================== */ + +/* ========================================================================== + LABEL PREVIEW STYLES + ========================================================================== */ + +#label-preview { + background: #fafafa; + position: relative; + overflow: visible; +} + +/* Label content rectangle styling */ +#label-content { + position: absolute; + top: 65.7px; + left: 11.34px; + width: 227.4px; + height: 321.3px; + border: 1px solid #ddd; + background: white; +} + +/* Barcode frame styling */ +#barcode-frame { + position: absolute; + top: 387px; + left: 50%; + transform: translateX(calc(-50% - 20px)); + width: 220px; + max-width: 220px; + height: 50px; + background: white; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; + border: none; +} + +#barcode-display { + width: 100%; + height: 40px; + max-width: 220px; +} + +#barcode-text { + font-size: 8px; + font-family: 'Courier New', monospace; + margin-top: 2px; + text-align: center; + font-weight: bold; + display: none; +} + +/* Vertical barcode frame styling */ +#vertical-barcode-frame { + position: absolute; + top: 50px; + left: 270px; + width: 321.3px; + height: 40px; + background: white; + display: flex; + align-items: center; + justify-content: center; + transform: rotate(90deg); + transform-origin: left center; + border: none; +} + +#vertical-barcode-display { + width: 100%; + height: 35px; +} + +#vertical-barcode-text { + position: absolute; + bottom: -15px; + font-size: 7px; + font-family: 'Courier New', monospace; + text-align: center; + font-weight: bold; + width: 100%; + display: none; +} + +/* Allow JsBarcode to control SVG colors naturally - removed forced black styling */ + +/* ========================================================================== + PRINT MODULE TABLE STYLES + ========================================================================== */ + +/* Enhanced table styling for print module tables */ +.card.scan-table-card table.print-module-table.scan-table thead th { + border-bottom: 2px solid var(--print-table-border) !important; + background-color: var(--print-table-header-bg) !important; + color: var(--print-table-header-text) !important; + padding: 0.25rem 0.4rem !important; + text-align: left !important; + font-weight: 600 !important; + font-size: 10px !important; + line-height: 1.2 !important; +} + +.card.scan-table-card table.print-module-table.scan-table { + width: 100% !important; + border-collapse: collapse !important; + background-color: var(--print-table-body-bg) !important; +} + +.card.scan-table-card table.print-module-table.scan-table tbody tr:hover td { + background-color: var(--print-table-hover) !important; + cursor: pointer !important; +} + +.card.scan-table-card table.print-module-table.scan-table tbody td { + background-color: var(--print-table-body-bg) !important; + color: var(--print-table-body-text) !important; + border: 1px solid var(--print-table-border) !important; + padding: 0.25rem 0.4rem !important; +} + +.card.scan-table-card table.print-module-table.scan-table tbody tr.selected td { + background-color: var(--print-table-selected) !important; + color: white !important; +} + +/* ========================================================================== + VIEW ORDERS TABLE STYLES (for print_lost_labels.html) + ========================================================================== */ + +table.view-orders-table.scan-table { + margin: 0 !important; + border-spacing: 0 !important; + border-collapse: collapse !important; + width: 100% !important; + table-layout: fixed !important; + font-size: 11px !important; +} + +table.view-orders-table.scan-table thead th { + height: 85px !important; + min-height: 85px !important; + max-height: 85px !important; + vertical-align: middle !important; + text-align: center !important; + white-space: normal !important; + word-wrap: break-word !important; + line-height: 1.3 !important; + padding: 6px 3px !important; + font-size: 11px !important; + background-color: var(--print-table-header-bg) !important; + color: var(--print-table-header-text) !important; + font-weight: bold !important; + text-transform: none !important; + letter-spacing: 0 !important; + overflow: visible !important; + box-sizing: border-box !important; + border: 1px solid var(--print-table-border) !important; + text-overflow: clip !important; + position: relative !important; +} + +table.view-orders-table.scan-table tbody td { + padding: 4px 2px !important; + font-size: 10px !important; + text-align: center !important; + border: 1px solid var(--print-table-border) !important; + background-color: var(--print-table-body-bg) !important; + color: var(--print-table-body-text) !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + vertical-align: middle !important; +} + +/* Column width definitions for view orders table */ +table.view-orders-table.scan-table td:nth-child(1) { width: 50px !important; } +table.view-orders-table.scan-table td:nth-child(2) { width: 80px !important; } +table.view-orders-table.scan-table td:nth-child(3) { width: 80px !important; } +table.view-orders-table.scan-table td:nth-child(4) { width: 150px !important; } +table.view-orders-table.scan-table td:nth-child(5) { width: 70px !important; } +table.view-orders-table.scan-table td:nth-child(6) { width: 80px !important; } +table.view-orders-table.scan-table td:nth-child(7) { width: 75px !important; } +table.view-orders-table.scan-table td:nth-child(8) { width: 90px !important; } +table.view-orders-table.scan-table td:nth-child(9) { width: 70px !important; } +table.view-orders-table.scan-table td:nth-child(10) { width: 100px !important; } +table.view-orders-table.scan-table td:nth-child(11) { width: 90px !important; } +table.view-orders-table.scan-table td:nth-child(12) { width: 70px !important; } +table.view-orders-table.scan-table td:nth-child(13) { width: 50px !important; } +table.view-orders-table.scan-table td:nth-child(14) { width: 70px !important; } +table.view-orders-table.scan-table td:nth-child(15) { width: 100px !important; } + +table.view-orders-table.scan-table tbody tr:hover td { + background-color: var(--print-table-hover) !important; +} + +table.view-orders-table.scan-table tbody tr.selected td { + background-color: var(--print-table-selected) !important; + color: white !important; +} + +/* Remove unwanted spacing */ +.report-table-card > * { + margin-top: 0 !important; +} + +.report-table-container { + margin-top: 0 !important; +} + +/* ========================================================================== + PRINT MODULE LAYOUT STYLES + ========================================================================== */ + +/* Scan container layout */ +.scan-container { + display: flex; + flex-direction: row; + gap: 20px; + width: 100%; + align-items: flex-start; +} + +/* Label preview card styling */ +.card.scan-form-card { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + min-height: 700px; + width: 330px; + flex-shrink: 0; + position: relative; + padding: 15px; +} + +/* Data preview card styling */ +.card.scan-table-card { + min-height: 700px; + width: calc(100% - 350px); + margin: 0; +} + +/* View Orders and Upload Orders page specific layout - 25/75 split */ +.card.report-form-card, +.card.scan-form-card { + min-height: 700px; + width: 25%; + flex-shrink: 0; + padding: 15px; + margin-bottom: 0; /* Remove bottom margin for horizontal layout */ +} + +.card.report-table-card, +.card.scan-table-card { + min-height: 700px; + width: 75%; + margin: 0; + padding: 15px; +} + +/* Upload Orders specific table styling */ +.card.scan-table-card table { + margin-bottom: 0; +} + +/* Ensure proper scroll behavior for upload preview */ +.card.scan-table-card[style*="overflow-y: auto"] { + /* Maintain scroll functionality while keeping consistent height */ + max-height: 700px; +} + +/* Label view title */ +.label-view-title { + width: 100%; + text-align: center; + padding: 0 0 15px 0; + font-size: 18px; + font-weight: bold; + letter-spacing: 0.5px; +} + +/* ========================================================================== + SEARCH AND FORM STYLES + ========================================================================== */ + +/* Search card styling */ +.search-card { + margin-bottom: 20px; + padding: 20px; +} + +.search-field { + width: 100%; + max-width: 400px; + padding: 8px; + font-size: 14px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.quantity-field { + width: 100px; + padding: 8px; + font-size: 14px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.search-result-table { + margin-top: 15px; + margin-bottom: 15px; +} + +/* ========================================================================== + BUTTON STYLES + ========================================================================== */ + +.print-btn { + background-color: #28a745; + color: white; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.print-btn:hover { + background-color: #218838; +} + +.print-btn:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +/* ========================================================================== + REPORT TABLE CONTAINER STYLES + ========================================================================== */ + +.report-table-card h3 { + margin: 0 0 15px 0 !important; + padding: 0 !important; +} + +.report-table-card { + padding: 15px !important; +} + +/* ========================================================================== + PRINT MODULE SPECIFIC LAYOUT ADJUSTMENTS + ========================================================================== */ + +/* For print_lost_labels.html - Two-column layout */ +.scan-container.lost-labels { + display: flex; + flex-direction: column; + gap: 0; + width: 100%; +} + +.scan-container.lost-labels .search-card { + width: 100%; + max-height: 100px; + min-height: 70px; + display: flex; + align-items: center; + flex-wrap: wrap; + margin-bottom: 24px; +} + +.scan-container.lost-labels .row-container { + display: flex; + flex-direction: row; + gap: 24px; + width: 100%; + align-items: flex-start; +} + +/* ========================================================================== + PRINT OPTIONS STYLES + ========================================================================== */ + +/* Print method selection */ +.print-method-container { + margin-bottom: 15px; +} + +.print-method-label { + font-size: 12px; + font-weight: 600; + color: #495057; + margin-bottom: 8px; +} + +.form-check { + margin-bottom: 6px; +} + +.form-check-label { + font-size: 11px; + line-height: 1.2; +} + +/* Printer selection styling */ +#qztray-printer-selection { + margin-bottom: 10px; +} + +#qztray-printer-selection label { + font-size: 11px; + font-weight: 600; + color: #495057; + margin-bottom: 3px; + display: block; +} + +#qztray-printer-select { + font-size: 11px; + padding: 3px 6px; +} + +/* Print button styling */ +#print-label-btn { + font-size: 13px; + padding: 8px 24px; + border-radius: 5px; + font-weight: 600; +} + +/* QZ Tray info section */ +#qztray-info { + width: 100%; + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #e9ecef; +} + +#qztray-info .info-box { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + padding: 10px; + text-align: center; +} + +#qztray-info .info-text { + font-size: 10px; + color: #495057; + margin-bottom: 8px; +} + +#qztray-info .download-link { + font-size: 10px; + padding: 4px 16px; +} + +/* ========================================================================== + BADGE AND STATUS STYLES + ========================================================================== */ + +.badge { + font-size: 9px; + padding: 2px 6px; +} + +.badge-success { + background-color: #28a745; + color: white; +} + +.badge-danger { + background-color: #dc3545; + color: white; +} + +.badge-warning { + background-color: #ffc107; + color: #212529; +} + +/* Status indicators */ +#qztray-status { + font-size: 9px; + padding: 2px 6px; +} + +/* ========================================================================== + RESPONSIVE DESIGN + ========================================================================== */ + +@media (max-width: 1200px) { + .scan-container { + flex-direction: column; + gap: 15px; + } + + .card.scan-form-card { + width: 100%; + } + + .card.scan-table-card { + width: 100%; + } + + /* View Orders and Upload Orders page responsive */ + .card.report-form-card, + .card.scan-form-card { + width: 100%; + margin-bottom: 24px; /* Restore bottom margin for stacked layout */ + } + + .card.report-table-card, + .card.scan-table-card { + width: 100%; + } +} + +@media (max-width: 992px) and (min-width: 769px) { + /* Tablet view - adjust proportions for better fit */ + .card.report-form-card, + .card.scan-form-card { + width: 30%; + } + + .card.report-table-card, + .card.scan-table-card { + width: 70%; + } +} + +@media (max-width: 768px) { + .label-view-title { + font-size: 16px; + } + + #label-preview { + width: 280px; + height: 400px; + } + + #label-content { + width: 200px; + height: 290px; + } + + .search-field { + max-width: 300px; + } +} + +/* ========================================================================== + THEME SUPPORT (Light/Dark Mode) + ========================================================================== */ + +/* CSS Custom Properties for Theme Support */ +:root { + /* Light mode colors (default) */ + --print-table-header-bg: #e9ecef; + --print-table-header-text: #000; + --print-table-body-bg: #fff; + --print-table-body-text: #000; + --print-table-border: #ddd; + --print-table-hover: #f8f9fa; + --print-table-selected: #007bff; + --print-card-bg: #fff; + --print-card-border: #ddd; + --print-search-field-bg: #fff; + --print-search-field-text: #000; + --print-search-field-border: #ddd; +} + +/* Light mode theme variables */ +body.light-mode { + --print-table-header-bg: #e9ecef; + --print-table-header-text: #000; + --print-table-body-bg: #fff; + --print-table-body-text: #000; + --print-table-border: #ddd; + --print-table-hover: #f8f9fa; + --print-table-selected: #007bff; + --print-card-bg: #fff; + --print-card-border: #ddd; + --print-search-field-bg: #fff; + --print-search-field-text: #000; + --print-search-field-border: #ddd; +} + +/* Dark mode theme variables */ +body.dark-mode { + --print-table-header-bg: #2a3441; + --print-table-header-text: #ffffff; + --print-table-body-bg: #2a3441; + --print-table-body-text: #ffffff; + --print-table-border: #495057; + --print-table-hover: #3a4451; + --print-table-selected: #007bff; + --print-card-bg: #2a2a2a; + --print-card-border: #555; + --print-search-field-bg: #333; + --print-search-field-text: #fff; + --print-search-field-border: #555; +} + +/* Label Preview Theme Support */ +body.light-mode #label-preview { + background: #fafafa; + border: none; +} + +body.light-mode #label-content { + background: white; + border: 1px solid #ddd; +} + +body.light-mode #barcode-frame, +body.light-mode #vertical-barcode-frame { + background: #ffffff; + border: 1px solid var(--print-card-border); +} + +body.dark-mode #label-preview { + background: #2a2a2a; + border: none; +} + +body.dark-mode #label-content { + background: #f8f9fa; + border: 1px solid #555; +} + +body.dark-mode #barcode-frame, +body.dark-mode #vertical-barcode-frame { + background: #ffffff; + border: 1px solid var(--print-card-border); +} + +/* Card Theme Support */ +body.dark-mode .search-card, +body.dark-mode .card { + background-color: var(--print-card-bg); + border: 1px solid var(--print-card-border); + color: var(--print-table-body-text); +} + +/* Search Field Theme Support */ +body.dark-mode .search-field, +body.dark-mode .quantity-field { + background-color: var(--print-search-field-bg); + border: 1px solid var(--print-search-field-border); + color: var(--print-search-field-text); +} + +/* Button Theme Support */ +body.dark-mode .print-btn { + background-color: #28a745; +} + +body.dark-mode .print-btn:hover { + background-color: #218838; +} + +/* ========================================================================== + UTILITY CLASSES + ========================================================================== */ + +.text-center { + text-align: center; +} + +.font-weight-bold { + font-weight: bold; +} + +.margin-bottom-15 { + margin-bottom: 15px; +} + +.padding-10 { + padding: 10px; +} + +.full-width { + width: 100%; +} + +.flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +/* ========================================================================== + DEBUG STYLES (can be removed in production) + ========================================================================== */ + +.debug-border { + border: 2px solid red !important; +} + +.debug-bg { + background-color: rgba(255, 0, 0, 0.1) !important; +} + +/* ========================================================================== + PRINT MODULE SPECIFIC STYLES + ========================================================================== */ + +/* Label preview container styling for print_module page */ +.scan-form-card { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + min-height: 700px; + position: relative; + padding: 15px; +} + +/* Label preview section */ +#label-preview { + border: 1px solid #ddd; + padding: 10px; + position: relative; + background: #fafafa; + width: 100%; + max-width: 301px; + height: 434.7px; + margin: 0 auto; +} + +/* Ensure label content scales properly in responsive layout */ +@media (max-width: 1024px) { + #label-preview { + max-width: 280px; + height: 404px; + } + + .scan-form-card { + padding: 10px; + } +} + +@media (max-width: 768px) { + #label-preview { + max-width: 100%; + height: 350px; + } + + .scan-form-card { + padding: 8px; + } +} + +/* ========================================================================== + FORM CONTROLS FIX + ========================================================================== */ + +/* Fix radio button styling to prevent oval display issues */ +.form-check-input[type="radio"] { + width: 1rem !important; + height: 1rem !important; + margin-top: 0.25rem !important; + border: 1px solid #dee2e6 !important; + border-radius: 50% !important; + background-color: #fff !important; + appearance: none !important; + -webkit-appearance: none !important; + -moz-appearance: none !important; +} + +.form-check-input[type="radio"]:checked { + background-color: #007bff !important; + border-color: #007bff !important; + background-image: radial-gradient(circle, #fff 30%, transparent 32%) !important; +} + +.form-check-input[type="radio"]:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; + outline: none !important; +} + +.form-check { + display: flex !important; + align-items: flex-start !important; + margin-bottom: 0.5rem !important; +} + +.form-check-label { + margin-left: 0.5rem !important; + cursor: pointer !important; +} \ No newline at end of file diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..d6f8391 --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,1236 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f4f4f9; +} + +.container { + width: 100%; /* Ensure it spans the full width */ + margin: 0 auto; /* Center the container */ + padding: 0; /* Remove padding */ + background: none; /* Remove white background */ + border-radius: 0; /* Remove rounded corners */ + box-shadow: none; /* Remove shadow */ +} + +.login-page { + display: flex; + align-items: center; /* Vertically center the content */ + justify-content: space-between; /* Space between the logo and the form container */ + height: 100vh; /* Full height of the viewport */ + background-color: #f4f4f9; + padding: 0 20px; /* Add padding to the sides */ +} + +header { + padding: 10px 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #ccc; +} + +.header-content { + display: flex; + justify-content: space-between; + width: 100%; + align-items: center; +} + +.left-header { + display: flex; + align-items: center; +} + +.left-header .logo { + width: 60px; /* Increased from 50px to 60px (20% larger) */ + height: auto; + margin-right: 10px; +} + +.left-header .page-title { + font-size: 1.5em; + font-weight: bold; +} + +.right-header { + display: flex; + align-items: center; +} + +.right-header .user-info { + margin-right: 15px; + font-size: 1em; + color: #333; +} + +.right-header .logout-button { + padding: 5px 10px; + font-size: 1em; + background-color: #007bff; + color: #fff; + text-decoration: none; + border-radius: 5px; + border: none; + cursor: pointer; +} + +.right-header .logout-button:hover { + background-color: #0056b3; +} + +.logo-container { + width: 100%; /* Full width */ + text-align: center; + margin-bottom: 10px; /* Reduce padding between logo and login container */ +} + +.login-logo { + max-height: 90vh; /* Scale the logo to fit the page height */ + width: auto; /* Maintain aspect ratio */ +} + +.form-container { + width: 600px; /* Fixed width for the login container */ + background: #fff; + padding: 15px 30px; /* Add padding inside the container */ + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + margin: 0; /* Remove any extra margin */ + align-self: center; /* Vertically center the form container */ +} + +.form-container h2 { + text-align: center; + margin-bottom: 15px; /* Reduce spacing below the title */ +} + +.form-container form { + display: flex; + flex-direction: column; +} + +.form-container label { + margin-bottom: 5px; + font-weight: bold; +} + +.form-container input { + margin-bottom: 10px; /* Reduce spacing between inputs */ + padding: 10px; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 5px; +} + +.form-container button { + padding: 10px; + font-size: 1em; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.form-container button:hover { + background-color: #0056b3; +} + +/* Light mode styles */ +body.light-mode { + background-color: #f4f4f9; + color: #000; +} + +header.light-mode { + background-color: #f4f4f9; + color: #000; +} + +/* Light mode styles for user info */ +body.light-mode .user-info { + color: #333; /* Darker text for light mode */ +} + +/* Dark mode styles */ +body.dark-mode { + background-color: #121212; + color: #fff; +} + +header.dark-mode { + background-color: #1e1e1e; + color: #fff; +} + +/* Dark mode styles for user info */ +body.dark-mode .user-info { + color: #ccc; /* Lighter text for dark mode */ +} + +/* Hide the header only on the login page */ +body.light-mode.login-page header, +body.dark-mode.login-page header { + display: none; +} + +/* Ensure the header is displayed on other pages */ +body.light-mode header, +body.dark-mode header { + display: flex; +} + +/* Common styles */ +.theme-toggle { + margin-right: 15px; + padding: 5px 10px; + font-size: 1em; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.theme-toggle:hover { + background-color: #0056b3; +} + +.logout-button { + padding: 5px 10px; + font-size: 1em; + background-color: #007bff; + color: #fff; + text-decoration: none; + border-radius: 5px; + border: none; + cursor: pointer; +} + +.logout-button:hover { + background-color: #0056b3; +} + +/* Card styles for light mode */ +body.light-mode .card { + background: #fff; + color: #000; + border: 1px solid #ddd; +} + +/* Card styles for dark mode */ +body.dark-mode .card { + background: #1e1e1e; + color: #fff; + border: 1px solid #444; +} + +/* Common card styles */ +.card { + flex: 1 1 auto; /* Allow cards to grow and shrink */ + max-width: none; /* Remove any fixed width */ + background: #fff; + border-radius: 5px; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + text-align: center; +} + +.card h3 { + margin-bottom: 20px; + font-size: 1.5em; +} + +.card label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +.card input { + width: 100%; + padding: 10px; + margin-bottom: 15px; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 5px; +} + +.card button { + padding: 10px 20px; + font-size: 1em; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.card button:hover { + background-color: #0056b3; +} + +.card p { + margin-bottom: 20px; + color: inherit; /* Inherit text color from the card */ +} + +.card .btn { + display: inline-block; + padding: 10px 20px; + font-size: 1em; + color: #fff; + background-color: #007bff; + text-decoration: none; + border-radius: 5px; + transition: background-color 0.3s ease; +} + +.card .btn:hover { + background-color: #0056b3; +} + +.user-list { + list-style: none; + padding: 0; + margin: 0 0 20px 0; +} + +.user-list li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid #ddd; +} + +.user-name { + font-size: 1em; + font-weight: bold; +} + +.user-role { + font-size: 0.9em; + color: #555; + margin-left: 10px; +} + +.btn { + padding: 5px 10px; + font-size: 1em; + color: #fff; + background-color: #007bff; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.btn:hover { + background-color: #0056b3; +} + +.create-btn { + margin-top: 20px; + background-color: #28a745; +} + +.create-btn:hover { + background-color: #218838; +} + +.popup { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + justify-content: center; + align-items: center; +} + +/* Light mode styles for pop-ups */ +body.light-mode .popup-content { + background: #fff; + color: #000; + border: 1px solid #ddd; +} + +/* Dark mode styles for pop-ups */ +body.dark-mode .popup-content { + background: #1e1e1e; + color: #fff; + border: 1px solid #444; +} + +/* Common styles for pop-ups */ +.popup-content { + padding: 20px; + border-radius: 5px; + width: 400px; + text-align: center; + transition: background-color 0.3s ease, color 0.3s ease, border 0.3s ease; +} + +.popup-content h3 { + margin-bottom: 20px; + font-size: 1.2em; +} + +.popup-content form { + display: flex; + flex-direction: column; +} + +.popup-content label { + margin-bottom: 5px; + font-weight: bold; +} + +.popup-content input, +.popup-content select { + margin-bottom: 15px; + padding: 10px; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 5px; + width: 100%; +} + +.popup-content input[readonly] { + background-color: #e9ecef; + cursor: not-allowed; +} + +.cancel-btn { + background-color: #dc3545; +} + +.cancel-btn:hover { + background-color: #c82333; +} + +.delete-confirm-btn { + background-color: #dc3545; +} + +.delete-confirm-btn:hover { + background-color: #c82333; +} + +.go-to-dashboard-btn { + margin-left: 10px; + padding: 5px 10px; + font-size: 1em; + background-color: #007bff; + color: #fff; + text-decoration: none; + border-radius: 5px; + border: none; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.go-to-dashboard-btn:hover { + background-color: #0056b3; +} + +.form-centered { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 20px; +} + +.form-centered label { + width: 100%; + text-align: left; + margin-bottom: 5px; + font-weight: bold; +} + +.form-centered input { + width: 80%; + padding: 10px; + margin-bottom: 15px; + font-size: 1em; + border: 1px solid #ccc; + border-radius: 5px; +} + +.scan-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +.scan-table th, .scan-table td { + border: 1px solid #ddd; + padding: 8px; + text-align: center; +} + +.scan-table th { + background-color: #f4f4f4; + font-weight: bold; +} + +/* Dark mode styles for the scan table */ +body.dark-mode .scan-table { + background-color: #1e1e1e; /* Dark background for the table */ + color: #fff; /* Light text for better contrast */ +} + +body.dark-mode .scan-table th { + background-color: #333; /* Darker background for table headers */ + color: #fff; /* Light text for headers */ +} + +body.dark-mode .scan-table td { + color: #ddd; /* Slightly lighter text for table cells */ +} + +body.dark-mode .scan-table tr:nth-child(even) { + background-color: #2a2a2a; /* Slightly lighter background for even rows */ +} + +body.dark-mode .scan-table tr:nth-child(odd) { + background-color: #1e1e1e; /* Match the table background for odd rows */ +} + +/* Container for the cards */ +.card-container { + display: flex; + flex-wrap: wrap; /* Allow wrapping to the next row if needed */ + justify-content: center; /* Center the cards horizontally */ + gap: 20px; /* Add spacing between the cards */ + margin: 20px auto; /* Center the container */ + max-width: 1200px; /* Optional: Limit the maximum width of the container */ +} + +/* Container for the scan page */ +.scan-container { + display: grid; + grid-template-columns: 1fr 2fr; /* 1/3 and 2/3 layout */ + gap: 20px; /* Add spacing between the columns */ + margin: 20px; /* Add 20px space around the container */ + max-width: calc(100% - 40px); /* Ensure it spans the full width minus the margin */ + width: 100%; /* Ensure it takes the full width */ + align-items: flex-start; /* Align items at the top */ + overflow-x: hidden; /* Prevent horizontal overflow */ + box-sizing: border-box; /* Include padding and borders in width calculations */ + border-radius: 5px; /* Optional: Add rounded corners */ + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Optional: Add a subtle shadow */ +} + +/* Input Form Card */ +.scan-form-card { + background: #fff; + border-radius: 5px; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +/* Latest Scans Card */ +.scan-table-card { + background: #fff; + border-radius: 5px; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + overflow-x: auto; /* Allow horizontal scrolling for the table */ + width: 100%; /* Ensure it takes the full width of its grid column */ + max-width: 100%; /* Prevent it from exceeding the container width */ + box-sizing: border-box; /* Include padding and borders in width calculations */ +} + +/* Media query for smaller screens */ +@media (max-width: 768px) { + .scan-container { + grid-template-columns: 1fr; /* Stack cards vertically on smaller screens */ + } + + .scan-form-card, .scan-table-card { + width: 100%; /* Make both cards take full width */ + } +} + +/* Dashboard container */ +.dashboard-container { + display: flex; + flex-wrap: wrap; /* Allow cards to wrap to the next row if needed */ + justify-content: space-between; /* Distribute cards evenly */ + gap: 20px; /* Add spacing between the cards */ + margin: 20px auto; /* Center the container */ + max-width: 1200px; /* Optional: Limit the maximum width of the container */ +} + +/* Individual cards for the dashboard */ +.dashboard-card { + flex: 1 1 calc(33.333% - 20px); /* Each card takes 1/3 of the row, minus the gap */ + border: 1px solid #ddd; + border-radius: 8px; + padding: 16px; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background-color: #fff; +} + +/* Ensure cards have consistent height */ +.dashboard-card h3 { + margin-bottom: 15px; + font-size: 1.5em; +} + +.dashboard-card p { + margin-bottom: 20px; + color: inherit; /* Inherit text color from the card */ +} + +.dashboard-card .btn { + display: inline-block; + padding: 10px 20px; + font-size: 1em; + color: #fff; + background-color: #007bff; + text-decoration: none; + border-radius: 5px; + transition: background-color 0.3s ease; +} + +.dashboard-card .btn:hover { + background-color: #0056b3; +} + +/* Dark mode styles for dashboard cards */ +body.dark-mode .dashboard-card { + background-color: #333; /* Dark background color */ + color: #fff; /* Light text color */ + border: 1px solid #444; /* Subtle border for dark mode */ +} + +body.dark-mode .dashboard-card .btn { + background-color: #555; /* Darker button background */ + color: #fff; /* Light button text */ +} + +body.dark-mode .dashboard-card .btn:hover { + background-color: #777; /* Slightly lighter button background on hover */ +} + +/* Responsive design for smaller screens */ +@media (max-width: 768px) { + .dashboard-card { + flex: 1 1 100%; /* Stack cards vertically on smaller screens */ + } +} + +/* Style for the export description label */ +.export-description { + display: block; + margin-bottom: 10px; + font-size: 1em; + font-weight: bold; + color: #333; /* Default color for light mode */ +} + +/* Dark mode styles for the export description label */ +body.dark-mode .export-description { + color: #fefdfd; /* Light color for dark mode */ +} + +/* Style for the container holding the last two buttons */ +.form-centered.last-buttons .button-row { + display: flex; + justify-content: flex-start; /* Align buttons to the left */ + gap: 10px; /* Add spacing between the buttons */ +} + +/* Style for the buttons */ +.form-centered.last-buttons .btn { + padding: 5px 10px; /* Make the buttons smaller */ + font-size: 0.9em; /* Reduce the font size */ + border-radius: 3px; /* Slightly smaller border radius */ +} + +/* Style for the report-table container */ +.report-table-container { + max-height: 1000px; /* Set the maximum height */ + overflow-y: auto; /* Enable vertical scrolling */ + border: 1px solid #ddd; /* Optional: Add a border for better visibility */ + margin-top: 10px; /* Optional: Add spacing above the table */ +} + +/* Ensure the table takes full width */ +.report-table-container table { + width: 100%; /* Make the table take the full width of the container */ + border-collapse: collapse; /* Optional: Collapse table borders */ +} + +/* Optional: Add styles for table rows and cells */ +.report-table-container th, +.report-table-container td { + padding: 8px; + text-align: left; + border: 1px solid #ddd; /* Add borders to table cells */ +} + +/* Reports Grid Layout - Label and Button Side by Side */ +.reports-grid { + display: flex; + flex-direction: column; + gap: 8px; /* Reduced spacing between report rows */ + margin-bottom: 25px; /* More space before separator */ +} + +.report-column { + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Specific styles for Quality Reports Card ONLY */ +.report-form-card { + padding: 15px; /* Reduced padding for more compact card */ +} + +.report-form-card .form-centered { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 10px; /* Reduced gap between columns */ + align-items: center; + padding: 0; /* No vertical padding for maximum compactness */ +} + +.report-form-card .form-centered .report-description { + margin: 0; + padding-right: 5px; /* Reduced padding */ + font-size: 0.82em; /* Slightly smaller text */ + line-height: 1.2; /* Tighter line spacing */ +} + +.report-form-card .form-centered .btn { + margin: 0; + white-space: nowrap; + font-size: 0.78em; /* Smaller button text */ + padding: 6px 10px; /* Reduced button padding for compactness */ +} + +/* Keep original form-centered styles for other pages */ +.form-centered:not(.report-form-card .form-centered) { + display: flex; + flex-direction: column; + align-items: center; + margin: 20px 0; +} + +/* Separator between reports and export section */ +.report-separator { + width: 100%; + height: 2px; + background: linear-gradient(to right, #ddd, #999, #ddd); + margin: 25px 0 20px 0; /* More space above, good space below */ + border-radius: 1px; +} + +/* Export section styling - specific to quality reports */ +.export-section { + padding-top: 5px; + margin-bottom: 15px; /* Add space at bottom of card */ +} + +.report-form-card .export-section .export-description { + font-size: 0.85em; /* Smaller export label text */ + margin-bottom: 8px; /* Reduced margin */ +} + +.report-form-card .export-section .btn { + font-size: 0.8em; /* Smaller export button text */ + padding: 8px 12px; +} + +.test-db-btn { + background-color: #6c757d !important; /* Gray color for test button */ + border-color: #6c757d !important; +} + +.test-db-btn:hover { + background-color: #5a6268 !important; + border-color: #545b62 !important; +} + +.report-form-card .export-section .form-centered.last-buttons { + padding: 5px 0; /* Reduced padding for export section */ +} + +/* Responsive design for Quality Reports Card only */ +@media (max-width: 768px) { + .report-form-card { + padding: 10px; /* Even more compact on mobile */ + } + + .report-form-card .form-centered { + grid-template-columns: 1fr; + gap: 5px; /* Reduced gap on mobile */ + text-align: center; + padding: 0; /* No vertical padding on mobile */ + } + + .report-form-card .form-centered .report-description { + padding-right: 0; + margin-bottom: 2px; /* Very tight spacing */ + font-size: 0.78em; /* Smaller text on mobile */ + line-height: 1.1; /* Tighter line spacing on mobile */ + } + + .report-form-card .form-centered .btn { + font-size: 0.72em; /* Smaller buttons on mobile */ + padding: 4px 8px; /* Reduced button padding on mobile */ + } +} + +.go-to-main-etichete-btn { + background-color: #28a745; /* Green background */ + color: #fff; /* White text */ + padding: 10px 20px; + border-radius: 5px; + text-decoration: none; + margin-left: 10px; + transition: background-color 0.3s ease; +} + +.go-to-main-etichete-btn:hover { + background-color: #218838; /* Darker green on hover */ +} + +.go-to-main-reports-btn { + background-color: #007bff; /* Blue background */ + color: #fff; /* White text */ + padding: 10px 20px; + border-radius: 5px; + text-decoration: none; + margin-left: 10px; + transition: background-color 0.3s ease; +} + +.go-to-main-reports-btn:hover { + background-color: #0056b3; /* Darker blue on hover */ +} + +.draggable-field { + cursor: move; + padding: 5px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 4px; + text-align: center; + user-select: none; +} + +.draggable-field.dragging { + opacity: 0.5; +} + +.label-preview-container { + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ + height: 100%; /* Full height of the parent container */ + position: relative; /* Ensure proper positioning */ +} + +#label-preview { + border: 1px solid #ddd; + padding: 10px; + min-height: 400px; + position: relative; + background-color: #f8f9fa; /* Light gray background */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Subtle shadow for better visibility */ +} + +/* Calendar Modal Styles */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); +} + +.modal-content { + background-color: #ffffff; + margin: 5% auto; + padding: 0; + border-radius: 8px; + width: 400px; + max-width: 90%; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + border: 1px solid #ddd; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background-color: #f4f4f9; + border-radius: 8px 8px 0 0; + border-bottom: 1px solid #ddd; +} + +.modal-header h4 { + margin: 0; + color: #333; + font-size: 1.2em; +} + +.close-modal { + font-size: 24px; + font-weight: bold; + cursor: pointer; + color: #666; + line-height: 1; + transition: color 0.2s ease; +} + +.close-modal:hover { + color: #333; +} + +.modal-body { + padding: 20px; + background-color: #ffffff; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 15px 20px; + border-top: 1px solid #ddd; + border-radius: 0 0 8px 8px; + background-color: #f4f4f9; +} + +/* Dark Mode Support for Calendar Modal */ +body.dark-mode .modal-content { + background-color: #2c2c2c; + border: 1px solid #555; +} + +body.dark-mode .modal-header { + background-color: #1e1e1e; + border-bottom: 1px solid #555; +} + +body.dark-mode .modal-header h4 { + color: #fff; +} + +body.dark-mode .close-modal { + color: #ccc; +} + +body.dark-mode .close-modal:hover { + color: #fff; +} + +body.dark-mode .modal-body { + background-color: #2c2c2c; +} + +body.dark-mode .modal-footer { + background-color: #1e1e1e; + border-top: 1px solid #555; +} + +/* Modal Button Styling */ +.modal-footer .btn { + padding: 8px 16px; + font-size: 0.9em; + border-radius: 4px; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.modal-footer .btn-primary { + background-color: #007bff; + color: white; +} + +.modal-footer .btn-primary:hover { + background-color: #0056b3; +} + +.modal-footer .btn-primary:disabled { + background-color: #6c757d; + cursor: not-allowed; + opacity: 0.6; +} + +.modal-footer .btn-secondary { + background-color: #6c757d; + color: white; +} + +.modal-footer .btn-secondary:hover { + background-color: #545b62; +} + +/* Dark Mode Modal Buttons */ +body.dark-mode .modal-footer .btn-primary { + background-color: #007bff; +} + +body.dark-mode .modal-footer .btn-primary:hover { + background-color: #0056b3; +} + +body.dark-mode .modal-footer .btn-secondary { + background-color: #6c757d; +} + +body.dark-mode .modal-footer .btn-secondary:hover { + background-color: #545b62; +} + +/* Date Range Modal Styles */ +.date-range-container { + padding: 20px 0; +} + +.date-input-group { + margin-bottom: 20px; +} + +.date-input-group label { + display: block; + font-weight: 600; + margin-bottom: 8px; + color: #333; + font-size: 0.95em; +} + +.date-input { + width: 100%; + padding: 12px 15px; + border: 2px solid #ddd; + border-radius: 8px; + font-size: 1em; + background-color: #fff; + transition: border-color 0.3s ease; + box-sizing: border-box; +} + +.date-input:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); +} + +.date-help { + display: block; + font-size: 0.8em; + color: #666; + margin-top: 5px; + font-style: italic; +} + +/* Dark mode styles for date range modal */ +body.dark-mode .date-input-group label { + color: #e0e0e0; +} + +body.dark-mode .date-input { + background-color: #2d3748; + border-color: #4a5568; + color: #e0e0e0; +} + +body.dark-mode .date-input:focus { + border-color: #63b3ed; + box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.1); +} + +body.dark-mode .date-help { + color: #a0aec0; +} + +/* Calendar Styles */ +.calendar-container { + width: 100%; +} + +.calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.calendar-header h3 { + margin: 0; + color: #333; + font-size: 1.1em; +} + +.calendar-nav { + background-color: #f4f4f9; + border: 1px solid #ddd; + border-radius: 4px; + padding: 8px 12px; + cursor: pointer; + font-size: 14px; + color: #333; + transition: all 0.2s ease; +} + +.calendar-nav:hover { + background-color: #007bff; + color: white; + border-color: #007bff; +} + +.calendar-grid { + width: 100%; +} + +.calendar-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; + margin-bottom: 5px; +} + +.calendar-weekdays div { + padding: 8px 4px; + text-align: center; + font-weight: bold; + font-size: 0.85em; + color: #666; + background-color: #f4f4f9; + border-radius: 4px; +} + +.calendar-days { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 2px; +} + +.calendar-day { + padding: 10px 4px; + text-align: center; + cursor: pointer; + border-radius: 4px; + font-size: 0.9em; + min-height: 35px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + background-color: #ffffff; + border: 1px solid transparent; +} + +.calendar-day:hover { + background-color: #e9ecef; + border-color: #007bff; +} + +.calendar-day.other-month { + color: #ccc; + background-color: #f8f9fa; +} + +.calendar-day.today { + background-color: #007bff; + color: white; + font-weight: bold; +} + +.calendar-day.selected { + background-color: #28a745; + color: white; + font-weight: bold; +} + +.calendar-day.selected:hover { + background-color: #218838; +} + +/* Dark Mode Calendar Styles */ +body.dark-mode .calendar-header h3 { + color: #fff; +} + +body.dark-mode .calendar-nav { + background-color: #3c3c3c; + border-color: #555; + color: #fff; +} + +body.dark-mode .calendar-nav:hover { + background-color: #007bff; + border-color: #007bff; +} + +body.dark-mode .calendar-weekdays div { + background-color: #3c3c3c; + color: #ccc; +} + +body.dark-mode .calendar-day { + background-color: #2c2c2c; + color: #fff; +} + +body.dark-mode .calendar-day:hover { + background-color: #444; + border-color: #007bff; +} + +body.dark-mode .calendar-day.other-month { + color: #666; + background-color: #333; +} + +body.dark-mode .calendar-day.today { + background-color: #007bff; + color: white; +} + +body.dark-mode .calendar-day.selected { + background-color: #28a745; + color: white; +} + +/* Responsive Calendar */ +@media (max-width: 480px) { + .modal-content { + margin: 10% auto; + width: 95%; + } + + .calendar-day { + min-height: 30px; + font-size: 0.8em; + } +} + +/* Selected row styles */ +tr.location-row.selected, .location-row.selected td { + background-color: #ffb300 !important; /* Accent color: Amber */ + color: #222 !important; + font-weight: bold; + box-shadow: 0 2px 8px rgba(255,179,0,0.12); + border-left: 4px solid #ff9800; + transition: background 0.2s, color 0.2s; +} \ No newline at end of file diff --git a/app/templates/base_print.html b/app/templates/base_print.html new file mode 100644 index 0000000..9602f0d --- /dev/null +++ b/app/templates/base_print.html @@ -0,0 +1,35 @@ + + + + + + {% block title %}Print Module - Quality App{% endblock %} + + + + + + + {% block head %}{% endblock %} + + + {% block content %}{% endblock %} + + + + {% block scripts %}{% endblock %} + + diff --git a/app/templates/modules/labels/print_labels.html b/app/templates/modules/labels/print_labels.html new file mode 100644 index 0000000..5be448e --- /dev/null +++ b/app/templates/modules/labels/print_labels.html @@ -0,0 +1,1661 @@ + + + + + + Print Module - Quality App + + + + + + + + + + + + + + + +
+ +
+
Label View
+ + +
+ + + + +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+
+
+
+
+ + +
+ + +
+ Quantity ordered +
+
+ +
+ + +
+ Customer order +
+
+ +
+ + +
+ Delivery date +
+
+ +
+ + +
+ Product description +
+
+ +
+ + +
+ Size +
+
+ +
+ + +
+ Article code +
+
+ +
+ + +
+ Prod. order +
+
+ +
+
+ + +
+ + + + + +
+ + +
+ + + + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ + +
+
+ + +
+ + +
+ + +
+ +
+ + +
+ (e.g., CP00000711-001, 002, ...) +
+ + +
+
+
+ QZ Tray is required for direct printing +
+ + šŸ“„ Download QZ Tray + +
+
+
+
+ + +
+

Data Preview (Unprinted Orders)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/app/templates/modules/labels/print_lost_labels.html b/app/templates/modules/labels/print_lost_labels.html index cbf9ea3..7a67f67 100644 --- a/app/templates/modules/labels/print_lost_labels.html +++ b/app/templates/modules/labels/print_lost_labels.html @@ -1,134 +1,70 @@ -{% extends "base.html" %} - -{% block head %} - - +{% endblock %} + +{% block content %} + +
+ + šŸ“– + +
+ + + + + + + + + + + + +{% endblock %} diff --git a/app/templates/modules/labels/print_module.html b/app/templates/modules/labels/print_module.html index dd1ae4d..4adc960 100644 --- a/app/templates/modules/labels/print_module.html +++ b/app/templates/modules/labels/print_module.html @@ -1,1682 +1,14 @@ {% extends "base.html" %} -{% block head %} - - -{% endblock %} - {% block content %} - -{#
- - šŸ“– - -
#} - -
- -
- -
-
Label View
- - -
- - - - {% if session.role == 'superadmin' %} - šŸ”‘ Manage Keys - {% endif %} -
- - -
- -
- -
- -
- - -
- -
- - -
-
-
-
-
-
-
-
- - -
- - -
- Quantity ordered -
-
- -
- - -
- Customer order -
-
- -
- - -
- Delivery date -
-
- -
- - -
- Product description -
-
- -
- - -
- Size -
-
- -
- - -
- Article code -
-
- -
- - -
- Prod. order -
-
- -
-
- - -
- - - - - -
- - -
- - - - - -
-
- - -
- -
- - - -
-
- - -
- - -
-
- - -
- - -
- - -
- -
- - -
- (e.g., CP00000711-001, 002, ...) -
- - -
-
-
- QZ Tray is required for direct printing -
- - šŸ“„ Download QZ Tray - -
-
-
-
-
- - -
- -
-

Data Preview (Unprinted Orders)

- -
- - - - - - - - - - - - - - - - - - - - - - - - -
-
-
+
+

Redirecting to Print Labels Module...

+

You will be redirected to the full-featured print labels interface.

+

Click here if not automatically redirected

- - - - - - - - - - - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/documentation/PDF_GENERATION_IMPLEMENTATION_PLAN.md b/documentation/PDF_GENERATION_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..b07b4c5 --- /dev/null +++ b/documentation/PDF_GENERATION_IMPLEMENTATION_PLAN.md @@ -0,0 +1,182 @@ +# Implementation Plan: PDF Generation for Print Functionality + +## Current Status + +The new Quality App v2 has: +āœ… Labels module with routes and templates +āœ… print_labels.html and print_lost_labels.html with UI +āœ… Database schema (order_for_labels table) +āœ… API endpoints for fetching orders +āŒ PDF generation functionality (NEEDS IMPLEMENTATION) +āŒ `/labels/api/generate-pdf` endpoint +āŒ `/labels/api/generate-pdf/{order_id}/true` endpoint + +## What Needs to Be Done + +### 1. Create PDF Generator Module +**File**: `/srv/quality_app-v2/app/modules/labels/pdf_generator.py` + +Copy from old app: `/srv/quality_app/py_app/app/pdf_generator.py` + +Key classes: +- `LabelPDFGenerator` - Main PDF generation class +- Methods for single label and batch label generation + +### 2. Create/Update Print Module Functions +**File**: `/srv/quality_app-v2/app/modules/labels/print_module.py` + +Add to existing functions: +- `update_order_printed_status(order_id, status=1)` - Mark order as printed +- Import PDF generator functions + +### 3. Add API Endpoints to Routes +**File**: `/srv/quality_app-v2/app/modules/labels/routes.py` + +Add two new routes: + +#### Endpoint 1: Single Label PDF (for QZ Tray) +```python +@labels_bp.route('/api/generate-pdf', methods=['POST']) +def api_generate_pdf(): + """Generate single label PDF for QZ Tray thermal printing""" + # Accept JSON with: + # - order_data: Complete order information + # - piece_number: Which label number (1, 2, 3, etc.) + # - total_pieces: Total quantity + # Return: PDF binary data +``` + +#### Endpoint 2: Batch PDF (for download) +```python +@labels_bp.route('/api/generate-pdf//true', methods=['POST']) +def api_generate_batch_pdf(order_id): + """Generate all label PDFs for an order and mark as printed""" + # Fetch order from database + # Generate all labels in one PDF + # Mark order as printed (printed_labels = 1) + # Return: PDF binary data +``` + +### 4. Install Required Dependencies + +The new app needs these Python packages: +``` +reportlab>=3.6.0 (for PDF generation) +``` + +Check `/srv/quality_app-v2/requirements.txt` and add if needed. + +--- + +## Implementation Details from Old App + +### LabelPDFGenerator Class Structure + +```python +class LabelPDFGenerator: + def __init__(self, paper_saving_mode=True): + self.label_width = 80mm + self.label_height = 105mm + # ... other dimensions + + def generate_labels_pdf(self, order_data, quantity, printer_optimized=True): + # Generate multiple labels (one per page) + # Return BytesIO buffer + + def generate_single_label_pdf(self, order_data, piece_number, total_pieces): + # Generate one label for QZ Tray + # Return BytesIO buffer + + def _draw_label(self, canvas, order_data, sequential_number, current_num, total_qty): + # Draw all label elements on canvas + + def _optimize_for_label_printer(self, canvas): + # Set printer optimization settings +``` + +### Label Data Structure + +```python +order_data = { + 'id': int, + 'comanda_productie': str, # Production order (CP00000711) + 'cod_articol': str, # Article code + 'descr_com_prod': str, # Product description + 'cantitate': int, # Quantity (total labels) + 'com_achiz_client': str, # Customer PO + 'nr_linie_com_client': str,# Customer PO line + 'customer_name': str, + 'customer_article_number': str, + 'data_livrare': str/date, # Delivery date + 'dimensiune': str, # Product size + 'printed_labels': bool # Print status +} +``` + +### API Request/Response Format + +#### Generate Single Label (QZ Tray) +``` +POST /labels/api/generate-pdf +Content-Type: application/json + +{ + "comanda_productie": "CP00000711", + "cod_articol": "ART001", + "descr_com_prod": "Product Description", + "cantitate": 5, + "com_achiz_client": "PO2026001", + "nr_linie_com_client": "001", + "customer_name": "ACME Corp", + "customer_article_number": "ACME-001", + "data_livrare": "2026-02-11", + "dimensiune": "Standard", + "piece_number": 1, + "total_pieces": 5 +} + +Response: Binary PDF data (Content-Type: application/pdf) +``` + +#### Generate Batch PDF (Download) +``` +POST /labels/api/generate-pdf/711/true + +Response: Binary PDF data with all 5 labels +Side effect: Sets order.printed_labels = 1 +``` + +--- + +## Files to Copy from Old App + +1. **pdf_generator.py** + - Source: `/srv/quality_app/py_app/app/pdf_generator.py` + - Destination: `/srv/quality_app-v2/app/modules/labels/pdf_generator.py` + - Status: READY TO COPY + +2. **Requirements Update** + - Add `reportlab>=3.6.0` to `/srv/quality_app-v2/requirements.txt` + +--- + +## Next Steps + +1. Copy pdf_generator.py from old app +2. Update requirements.txt with reportlab +3. Add two new API routes to routes.py +4. Update print_module.py with update_order_printed_status function +5. Test the endpoints with test data already inserted + +--- + +## Testing Checklist + +- [ ] Single label generation works +- [ ] Batch PDF generation works +- [ ] QZ Tray can print generated labels +- [ ] Order marked as printed after successful generation +- [ ] PDF dimensions correct (80mm x 105mm) +- [ ] Barcodes generate correctly +- [ ] All order data displays correctly on label +- [ ] Thermal printer optimization enabled diff --git a/documentation/PRINTING_MODULE_REFERENCE.md b/documentation/PRINTING_MODULE_REFERENCE.md new file mode 100644 index 0000000..f52864a --- /dev/null +++ b/documentation/PRINTING_MODULE_REFERENCE.md @@ -0,0 +1,189 @@ +# Old Quality App - Printing Module Summary + +## Key Files + +### 1. `/srv/quality_app/py_app/app/pdf_generator.py` +**Main PDF Generation Engine** + +The `LabelPDFGenerator` class handles all label PDF generation: +- **Dimensions**: 80mm x 105mm thermal printer labels +- **Optimized for**: Epson TM-T20, Citizen CTS-310 thermal printers + +#### Key Methods: + +##### `generate_labels_pdf(order_data, quantity, printer_optimized=True)` +- Generates PDF with multiple labels based on quantity +- Creates sequential labels: CP00000711-001, CP00000711-002, etc. +- Returns BytesIO buffer with complete PDF + +##### `generate_single_label_pdf(order_data, piece_number, total_pieces, printer_optimized=True)` +- Generates single label PDF for specific piece number +- Used by QZ Tray for direct thermal printing +- Returns BytesIO buffer with one label + +##### `_draw_label(canvas, order_data, sequential_number, current_num, total_qty)` +**Label Layout:** +- **Row 1**: Company header (empty) +- **Row 2**: Customer name +- **Row 3**: Quantity ordered (left) | Right column split 40/60 +- **Row 4**: Customer order (com_achiz_client - nr_linie_com_client) +- **Row 5**: Delivery date (data_livrare) +- **Row 6**: Product description (double height) +- **Row 7**: Size (dimensiune) +- **Row 8**: Article code (customer_article_number) +- **Row 9**: Production order (comanda_productie - sequential_number) + +**Barcodes:** +- **Bottom**: CODE128 barcode with sequential number (CP00000711/001) +- **Right side**: CODE128 vertical barcode with customer order format + +##### `_optimize_for_label_printer(canvas)` +- Sets 300 DPI resolution +- Enables compression +- Sets print scaling to 100% (no scaling) +- Adds PDF metadata for printer optimization + +--- + +### 2. `/srv/quality_app/py_app/app/print_module.py` +**Database Data Retrieval** + +#### Key Functions: + +##### `get_unprinted_orders_data(limit=100)` +- Returns orders where `printed_labels != 1` +- Retrieves all fields needed for label generation +- Returns list of order dictionaries + +##### `get_printed_orders_data(limit=100)` +- Returns orders where `printed_labels = 1` +- Used for "print lost labels" page +- Returns list of order dictionaries + +##### `update_order_printed_status(order_id)` +- Updates `printed_labels = 1` for order +- Called after successful printing + +--- + +### 3. API Routes in `/srv/quality_app/py_app/app/routes.py` + +#### `/generate_label_pdf` (POST) +**Single Label Generation for QZ Tray** +```python +Accepts JSON: +{ + "comanda_productie": "CP00000711", + "cantitate": 5, + "piece_number": 1, + "total_pieces": 5, + ... (other order fields) +} + +Returns: PDF binary data +Purpose: Generate single label for thermal printer via QZ Tray +``` + +#### `/generate_labels_pdf//true` (POST) +**Batch Label Generation for PDF Download** +``` +Returns: Multi-page PDF with all labels for order +Purpose: Generate all labels for download/preview +Also marks order as printed (printed_labels = 1) +``` + +--- + +### 4. `/srv/quality_app/py_app/app/print_config.py` +**Print Service Configuration** + +```python +WINDOWS_PRINT_SERVICE_URL = "http://192.168.1.XXX:8765" +PRINT_SERVICE_TIMEOUT = 30 +PRINT_SERVICE_ENABLED = True +``` + +Note: Windows print service is configured but QZ Tray is the primary method. + +--- + +## Label Printing Flow + +### For Direct Printing (QZ Tray): +1. User selects order from table +2. Clicks "Print Labels" button +3. Frontend calls `/labels/api/generate-pdf` with order data and piece_number +4. Backend's `LabelPDFGenerator.generate_single_label_pdf()` creates PDF +5. Returns PDF binary to frontend +6. QZ Tray prints directly to thermal printer +7. After successful print, calls `/labels/api/update-printed-status/{id}` + +### For PDF Download: +1. User selects order +2. Clicks "Generate PDF" button +3. Frontend calls `/labels/api/generate-pdf/{orderId}/true` +4. Backend's `LabelPDFGenerator.generate_labels_pdf()` creates multi-page PDF +5. Returns PDF for browser download/print +6. Order marked as printed + +--- + +## Key Configuration Classes + +### `LabelPDFGenerator` Parameters: +```python +# Label dimensions +label_width = 80mm +label_height = 105mm + +# Content area +content_width = 60mm +content_height = 68mm +content_x = 4mm from left +content_y = 22mm from bottom + +# Layout +row_height = 6.8mm (content_height / 10) +left_column_width = 40% (24mm) +right_column_width = 60% (36mm) + +# Barcode areas +bottom_barcode: 80mm width x 12mm height +vertical_barcode: 12mm width x 68mm height (rotated 90°) +``` + +--- + +## Order Data Fields Used + +```python +Required fields for label generation: +- comanda_productie: Production order number +- cod_articol: Article code +- descr_com_prod: Product description +- cantitate: Quantity +- com_achiz_client: Customer purchase order +- nr_linie_com_client: Customer order line +- customer_name: Customer name +- customer_article_number: Customer article number +- data_livrare: Delivery date +- dimensiune: Product size +- printed_labels: Status (0 = unprinted, 1 = printed) +``` + +--- + +## Thermal Printer Optimization + +The PDF generator is specifically optimized for thermal label printers: + +1. **No margins**: Fills entire label area +2. **High resolution**: 300 DPI quality +3. **Monochrome**: Black/white only (no colors) +4. **Compression**: Reduces file size +5. **Scaling**: 100% (no scaling in printer) +6. **Bar width**: Optimized for thermal resolution + +Tested with: +- Epson TM-T20 (80mm thermal printer) +- Citizen CTS-310 (80mm thermal printer) diff --git a/documentation/insert_test_data.py b/documentation/insert_test_data.py new file mode 100644 index 0000000..ded0790 --- /dev/null +++ b/documentation/insert_test_data.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +""" +Insert test data into the order_for_labels table for testing print functionality +""" +import pymysql +from datetime import datetime, timedelta +import sys + +# Database connection parameters +DB_HOST = 'mariadb' +DB_PORT = 3306 +DB_USER = 'quality_user' +DB_PASSWORD = 'quality_secure_password_2026' +DB_NAME = 'quality_db' + +def insert_test_data(): + """Insert test orders into the database""" + + try: + # Connect to database + conn = pymysql.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASSWORD, + database=DB_NAME, + charset='utf8mb4' + ) + + cursor = conn.cursor() + + # Check if table exists + cursor.execute("SHOW TABLES LIKE 'order_for_labels'") + if not cursor.fetchone(): + print("Error: order_for_labels table does not exist") + cursor.close() + conn.close() + return False + + # Check if printed_labels column exists + cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'") + if not cursor.fetchone(): + print("Error: printed_labels column does not exist") + cursor.close() + conn.close() + return False + + # Sample test data + today = datetime.now() + delivery_date = today + timedelta(days=7) + + test_orders = [ + { + 'comanda_productie': 'CP00000711', + 'cod_articol': 'ART001', + 'descr_com_prod': 'Memory Foam Pillow - Premium Quality', + 'cantitate': 5, + 'com_achiz_client': 'PO2026001', + 'nr_linie_com_client': '001', + 'customer_name': 'ACME Corporation', + 'customer_article_number': 'ACME-MFP-001', + 'open_for_order': 1, + 'line_number': 1, + 'data_livrara': delivery_date.date(), + 'dimensiune': 'Standard (50x70cm)', + 'printed_labels': 0 + }, + { + 'comanda_productie': 'CP00000712', + 'cod_articol': 'ART002', + 'descr_com_prod': 'Gel Infused Mattress Topper', + 'cantitate': 10, + 'com_achiz_client': 'PO2026002', + 'nr_linie_com_client': '001', + 'customer_name': 'Sleep Solutions Ltd', + 'customer_article_number': 'SS-GIM-002', + 'open_for_order': 1, + 'line_number': 1, + 'data_livrara': delivery_date.date(), + 'dimensiune': 'Double (140x200cm)', + 'printed_labels': 0 + }, + { + 'comanda_productie': 'CP00000713', + 'cod_articol': 'ART003', + 'descr_com_prod': 'Cooling Gel Pillow - Twin Pack', + 'cantitate': 8, + 'com_achiz_client': 'PO2026003', + 'nr_linie_com_client': '002', + 'customer_name': 'Bedding Warehouse', + 'customer_article_number': 'BW-CGP-003', + 'open_for_order': 1, + 'line_number': 2, + 'data_livrara': delivery_date.date(), + 'dimensiune': 'King (200x200cm)', + 'printed_labels': 1 + }, + { + 'comanda_productie': 'CP00000714', + 'cod_articol': 'ART004', + 'descr_com_prod': 'Hypoallergenic Pillow Insert', + 'cantitate': 15, + 'com_achiz_client': 'PO2026004', + 'nr_linie_com_client': '001', + 'customer_name': 'Health Products Inc', + 'customer_article_number': 'HPI-HYP-004', + 'open_for_order': 0, + 'line_number': 1, + 'data_livrara': delivery_date.date(), + 'dimensiune': 'Standard (50x70cm)', + 'printed_labels': 0 + }, + { + 'comanda_productie': 'CP00000715', + 'cod_articol': 'ART005', + 'descr_com_prod': 'Memory Foam Mattress 10CM', + 'cantitate': 3, + 'com_achiz_client': 'PO2026005', + 'nr_linie_com_client': '001', + 'customer_name': 'Premium Comfort Ltd', + 'customer_article_number': 'PC-MFM-005', + 'open_for_order': 1, + 'line_number': 1, + 'data_livrara': delivery_date.date(), + 'dimensiune': 'Queen (160x200cm)', + 'printed_labels': 1 + }, + { + 'comanda_productie': 'CP00000716', + 'cod_articol': 'ART006', + 'descr_com_prod': 'Latex Pillow - Eco Friendly', + 'cantitate': 12, + 'com_achiz_client': 'PO2026006', + 'nr_linie_com_client': '003', + 'customer_name': 'Sustainable Sleep', + 'customer_article_number': 'SS-LAT-006', + 'open_for_order': 1, + 'line_number': 3, + 'data_livrara': delivery_date.date(), + 'dimensiune': 'Standard (50x70cm)', + 'printed_labels': 0 + }, + { + 'comanda_productie': 'CP00000717', + 'cod_articol': 'ART007', + 'descr_com_prod': 'Body Pillow with Cover', + 'cantitate': 6, + 'com_achiz_client': 'PO2026007', + 'nr_linie_com_client': '001', + 'customer_name': 'Comfort Essentials', + 'customer_article_number': 'CE-BP-007', + 'open_for_order': 1, + 'line_number': 1, + 'data_livrara': delivery_date.date(), + 'dimensiune': 'Long (40x150cm)', + 'printed_labels': 1 + } + ] + + # Insert test data + insert_query = """ + INSERT INTO order_for_labels + (comanda_productie, cod_articol, descr_com_prod, cantitate, + com_achiz_client, nr_linie_com_client, customer_name, customer_article_number, + open_for_order, line_number, data_livrara, dimensiune, printed_labels, + created_at, updated_at) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) + """ + + inserted = 0 + for order in test_orders: + try: + cursor.execute(insert_query, ( + order['comanda_productie'], + order['cod_articol'], + order['descr_com_prod'], + order['cantitate'], + order['com_achiz_client'], + order['nr_linie_com_client'], + order['customer_name'], + order['customer_article_number'], + order['open_for_order'], + order['line_number'], + order['data_livrara'], + order['dimensiune'], + order['printed_labels'] + )) + inserted += 1 + print(f"āœ“ Inserted: {order['comanda_productie']} - {order['descr_com_prod'][:40]}") + except Exception as e: + print(f"āœ— Failed to insert {order['comanda_productie']}: {e}") + + # Commit the transaction + conn.commit() + + # Show summary + cursor.execute("SELECT COUNT(*) FROM order_for_labels") + total_orders = cursor.fetchone()[0] + + print(f"\n{'='*60}") + print(f"Test data insertion completed!") + print(f"Inserted: {inserted} new orders") + print(f"Total orders in database: {total_orders}") + print(f"{'='*60}") + + cursor.close() + conn.close() + return True + + except pymysql.Error as e: + print(f"Database error: {e}") + return False + except Exception as e: + print(f"Unexpected error: {e}") + return False + +if __name__ == '__main__': + success = insert_test_data() + sys.exit(0 if success else 1) diff --git a/test_pdf_generation.py b/test_pdf_generation.py new file mode 100644 index 0000000..0300c59 --- /dev/null +++ b/test_pdf_generation.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +Test script for PDF generation functionality +Tests both single label and batch PDF generation +""" +import json +import requests +import sys + +# Configuration +BASE_URL = "http://localhost:8080" +LOGIN_URL = f"{BASE_URL}/login" +SINGLE_PDF_URL = f"{BASE_URL}/labels/api/generate-pdf" +BATCH_PDF_URL = f"{BASE_URL}/labels/api/generate-pdf/1/true" + +# Test credentials +TEST_USERNAME = "admin" +TEST_PASSWORD = "admin123" + +def test_pdf_generation(): + """Test PDF generation endpoints""" + + session = requests.Session() + + # Step 1: Login + print("=" * 70) + print("Step 1: Logging in...") + print("=" * 70) + + login_data = { + 'username': TEST_USERNAME, + 'password': TEST_PASSWORD + } + + response = session.post(LOGIN_URL, data=login_data) + if response.status_code not in [200, 302]: # Accept both OK and redirect + print(f"āŒ Login failed with status {response.status_code}") + print(f"Response: {response.text[:200]}") + return False + + print(f"āœ“ Login successful (status: {response.status_code})") + + # Step 2: Test Single Label PDF Generation + print("\n" + "=" * 70) + print("Step 2: Testing Single Label PDF Generation...") + print("=" * 70) + + order_data = { + "id": 1, + "comanda_productie": "CP00000711", + "cod_articol": "ART001", + "descr_com_prod": "Memory Foam Pillow - Premium Quality", + "cantitate": 5, + "com_achiz_client": "PO2026001", + "nr_linie_com_client": "001", + "customer_name": "ACME Corporation", + "customer_article_number": "ACME-MFP-001", + "data_livrara": "2026-02-11", + "dimensiune": "Standard (50x70cm)", + "piece_number": 1, + "total_pieces": 5 + } + + response = session.post( + SINGLE_PDF_URL, + json=order_data, + headers={'Content-Type': 'application/json'} + ) + + if response.status_code == 200: + if response.headers.get('Content-Type') == 'application/pdf': + print(f"āœ“ Single label PDF generated successfully") + print(f" Content-Type: {response.headers.get('Content-Type')}") + print(f" PDF Size: {len(response.content)} bytes") + + # Save to file for manual inspection + with open('/tmp/test_label_single.pdf', 'wb') as f: + f.write(response.content) + print(f" Saved to: /tmp/test_label_single.pdf") + else: + print(f"āŒ Invalid content type: {response.headers.get('Content-Type')}") + print(f"Response: {response.text[:200]}") + return False + else: + print(f"āŒ Single PDF generation failed with status {response.status_code}") + print(f"Response: {response.text[:200]}") + return False + + # Step 3: Test Batch PDF Generation + print("\n" + "=" * 70) + print("Step 3: Testing Batch PDF Generation...") + print("=" * 70) + + response = session.post(BATCH_PDF_URL) + + if response.status_code == 200: + if response.headers.get('Content-Type') == 'application/pdf': + print(f"āœ“ Batch PDF generated successfully") + print(f" Content-Type: {response.headers.get('Content-Type')}") + print(f" PDF Size: {len(response.content)} bytes") + + # Save to file for manual inspection + with open('/tmp/test_label_batch.pdf', 'wb') as f: + f.write(response.content) + print(f" Saved to: /tmp/test_label_batch.pdf") + else: + print(f"āŒ Invalid content type: {response.headers.get('Content-Type')}") + print(f"Response: {response.text[:200]}") + return False + else: + print(f"āŒ Batch PDF generation failed with status {response.status_code}") + print(f"Response: {response.text[:200]}") + return False + + # Step 4: Verify order marked as printed + print("\n" + "=" * 70) + print("Step 4: Verifying order status...") + print("=" * 70) + + # Check if order is marked as printed + import pymysql + try: + conn = pymysql.connect( + host='127.0.0.1', + port=3306, + user='quality_user', + password='quality_secure_password_2026', + database='quality_db' + ) + cursor = conn.cursor() + cursor.execute("SELECT id, comanda_productie, printed_labels FROM order_for_labels WHERE id = 1") + row = cursor.fetchone() + cursor.close() + conn.close() + + if row: + order_id, cp_code, printed_status = row + print(f"Order ID: {order_id}, CP Code: {cp_code}") + if printed_status == 1: + print(f"āœ“ Order marked as printed (printed_labels = {printed_status})") + else: + print(f"⚠ Order NOT marked as printed (printed_labels = {printed_status})") + else: + print("⚠ Order not found in database") + except Exception as e: + print(f"⚠ Could not verify order status: {e}") + + print("\n" + "=" * 70) + print("āœ“ All PDF generation tests completed successfully!") + print("=" * 70) + + return True + +if __name__ == '__main__': + try: + success = test_pdf_generation() + sys.exit(0 if success else 1) + except Exception as e: + print(f"\nāŒ Unexpected error: {e}") + import traceback + traceback.print_exc() + sys.exit(1)