abdullahalioo commited on
Commit
b8dbc9f
·
verified ·
1 Parent(s): 747dd99

Upload 9 files

Browse files
Files changed (9) hide show
  1. Dockerfile +40 -0
  2. app.py +275 -0
  3. data/tickets.json +12 -0
  4. main.py +4 -0
  5. requirements.txt +3 -0
  6. static/css/style.css +535 -0
  7. static/js/main.js +320 -0
  8. templates/admin.html +251 -0
  9. templates/index.html +200 -0
Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ curl \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first for better caching
12
+ COPY app_requirements.txt ./
13
+
14
+ # Install Python dependencies
15
+ RUN pip install --no-cache-dir -r app_requirements.txt
16
+
17
+ # Copy application code
18
+ COPY . .
19
+
20
+ # Create data directory with proper permissions
21
+ RUN mkdir -p data && chmod 755 data
22
+
23
+ # Create non-root user for security
24
+ RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
25
+ USER appuser
26
+
27
+ # Expose port
28
+ EXPOSE 5000
29
+
30
+ # Set environment variables
31
+ ENV PYTHONPATH=/app
32
+ ENV FLASK_APP=main.py
33
+ ENV FLASK_ENV=production
34
+
35
+ # Health check
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
37
+ CMD curl -f http://localhost:5000/ || exit 1
38
+
39
+ # Start application
40
+ CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "1", "--timeout", "120", "--keep-alive", "5", "--max-requests", "1000", "--max-requests-jitter", "100", "main:app"]
app.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ from datetime import datetime
5
+ from flask import Flask, render_template, request, jsonify, send_file, flash, redirect, url_for
6
+ from openpyxl import Workbook
7
+ from openpyxl.styles import Font, PatternFill, Alignment
8
+ import tempfile
9
+ import re
10
+
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.DEBUG)
13
+
14
+ app = Flask(__name__)
15
+ app.secret_key = os.environ.get("SESSION_SECRET", "your-secret-key-for-development")
16
+
17
+ # Ensure data directory exists
18
+ DATA_DIR = "data"
19
+ TICKETS_FILE = os.path.join(DATA_DIR, "tickets.json")
20
+
21
+ if not os.path.exists(DATA_DIR):
22
+ os.makedirs(DATA_DIR)
23
+
24
+ def load_tickets():
25
+ """Load tickets from JSON file"""
26
+ try:
27
+ if os.path.exists(TICKETS_FILE):
28
+ with open(TICKETS_FILE, 'r', encoding='utf-8') as f:
29
+ return json.load(f)
30
+ return []
31
+ except Exception as e:
32
+ logging.error(f"Error loading tickets: {e}")
33
+ return []
34
+
35
+ def save_tickets(tickets):
36
+ """Save tickets to JSON file"""
37
+ try:
38
+ with open(TICKETS_FILE, 'w', encoding='utf-8') as f:
39
+ json.dump(tickets, f, indent=2, ensure_ascii=False)
40
+ return True
41
+ except Exception as e:
42
+ logging.error(f"Error saving tickets: {e}")
43
+ return False
44
+
45
+ def validate_email(email):
46
+ """Validate email format"""
47
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
48
+ return re.match(pattern, email) is not None
49
+
50
+ def validate_phone(phone):
51
+ """Validate phone number format"""
52
+ # Allow digits, spaces, hyphens, parentheses, and plus sign
53
+ pattern = r'^[\+\(\)\-\s\d]{7,20}$'
54
+ return re.match(pattern, phone) is not None
55
+
56
+ @app.route('/')
57
+ def index():
58
+ """Main page with API documentation"""
59
+ return render_template('index.html')
60
+
61
+ @app.route('/admin')
62
+ def admin():
63
+ """Admin dashboard to view all tickets"""
64
+ tickets = load_tickets()
65
+ return render_template('admin.html', tickets=tickets, total_tickets=len(tickets))
66
+
67
+ @app.route('/add_data/<email>/<phone>/<name>/<int:tickets>/<ticket_number>/<country>/<region>')
68
+ def add_ticket_data(email, phone, name, tickets, ticket_number, country, region):
69
+ """Add ticket data via URL parameters"""
70
+ try:
71
+ # Validate input data
72
+ errors = []
73
+
74
+ if not validate_email(email):
75
+ errors.append("Invalid email format")
76
+
77
+ if not validate_phone(phone):
78
+ errors.append("Invalid phone number format")
79
+
80
+ if not name.strip():
81
+ errors.append("Name cannot be empty")
82
+
83
+ if tickets <= 0:
84
+ errors.append("Number of tickets must be greater than 0")
85
+
86
+ if not ticket_number.strip():
87
+ errors.append("Ticket number cannot be empty")
88
+
89
+ if not country.strip():
90
+ errors.append("Country cannot be empty")
91
+
92
+ if not region.strip():
93
+ errors.append("Region cannot be empty")
94
+
95
+ if errors:
96
+ return jsonify({
97
+ "success": False,
98
+ "message": "Validation errors",
99
+ "errors": errors
100
+ }), 400
101
+
102
+ # Create ticket record
103
+ ticket_data = {
104
+ "email": email.lower().strip(),
105
+ "phone": phone.strip(),
106
+ "name": name.strip(),
107
+ "tickets": tickets,
108
+ "ticket_number": ticket_number.strip(),
109
+ "country": country.strip(),
110
+ "region": region.strip(),
111
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
112
+ }
113
+
114
+ # Load existing tickets
115
+ all_tickets = load_tickets()
116
+
117
+ # Check for duplicate ticket numbers
118
+ existing_ticket_numbers = [t.get('ticket_number') for t in all_tickets]
119
+ if ticket_number in existing_ticket_numbers:
120
+ return jsonify({
121
+ "success": False,
122
+ "message": "Ticket number already exists"
123
+ }), 409
124
+
125
+ # Add new ticket
126
+ all_tickets.append(ticket_data)
127
+
128
+ # Save to file
129
+ if save_tickets(all_tickets):
130
+ logging.info(f"New ticket added: {ticket_number} for {name}")
131
+ return jsonify({
132
+ "success": True,
133
+ "message": "Ticket data added successfully",
134
+ "ticket_id": ticket_number,
135
+ "total_tickets": len(all_tickets)
136
+ })
137
+ else:
138
+ return jsonify({
139
+ "success": False,
140
+ "message": "Failed to save ticket data"
141
+ }), 500
142
+
143
+ except Exception as e:
144
+ logging.error(f"Error adding ticket data: {e}")
145
+ return jsonify({
146
+ "success": False,
147
+ "message": "Internal server error"
148
+ }), 500
149
+
150
+ @app.route('/api/tickets')
151
+ def get_tickets_api():
152
+ """API endpoint to get all tickets"""
153
+ tickets = load_tickets()
154
+ return jsonify({
155
+ "success": True,
156
+ "tickets": tickets,
157
+ "total": len(tickets)
158
+ })
159
+
160
+ @app.route('/export/excel')
161
+ def export_excel():
162
+ """Export tickets data to Excel file"""
163
+ try:
164
+ tickets = load_tickets()
165
+
166
+ if not tickets:
167
+ flash("No ticket data available to export", "warning")
168
+ return redirect(url_for('admin'))
169
+
170
+ # Create workbook and worksheet
171
+ wb = Workbook()
172
+ ws = wb.active
173
+ ws.title = "Ticket Data"
174
+
175
+ # Define headers
176
+ headers = ['Email', 'Phone', 'Name', 'Tickets', 'Ticket Number', 'Country', 'Region', 'Timestamp']
177
+
178
+ # Style for headers
179
+ header_font = Font(bold=True, color='FFFFFF')
180
+ header_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid')
181
+ header_alignment = Alignment(horizontal='center', vertical='center')
182
+
183
+ # Add headers
184
+ for col, header in enumerate(headers, 1):
185
+ cell = ws.cell(row=1, column=col, value=header)
186
+ cell.font = header_font
187
+ cell.fill = header_fill
188
+ cell.alignment = header_alignment
189
+
190
+ # Add data
191
+ for row, ticket in enumerate(tickets, 2):
192
+ ws.cell(row=row, column=1, value=ticket.get('email', ''))
193
+ ws.cell(row=row, column=2, value=ticket.get('phone', ''))
194
+ ws.cell(row=row, column=3, value=ticket.get('name', ''))
195
+ ws.cell(row=row, column=4, value=ticket.get('tickets', ''))
196
+ ws.cell(row=row, column=5, value=ticket.get('ticket_number', ''))
197
+ ws.cell(row=row, column=6, value=ticket.get('country', ''))
198
+ ws.cell(row=row, column=7, value=ticket.get('region', ''))
199
+ ws.cell(row=row, column=8, value=ticket.get('timestamp', ''))
200
+
201
+ # Auto-adjust column widths
202
+ for col in ws.columns:
203
+ max_length = 0
204
+ column = col[0].column_letter
205
+ for cell in col:
206
+ try:
207
+ if len(str(cell.value)) > max_length:
208
+ max_length = len(str(cell.value))
209
+ except:
210
+ pass
211
+ adjusted_width = min(max_length + 2, 50)
212
+ ws.column_dimensions[column].width = adjusted_width
213
+
214
+ # Create temporary file
215
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx')
216
+ wb.save(temp_file.name)
217
+ temp_file.close()
218
+
219
+ # Generate filename with timestamp
220
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
221
+ filename = f"ticket_data_{timestamp}.xlsx"
222
+
223
+ return send_file(
224
+ temp_file.name,
225
+ as_attachment=True,
226
+ download_name=filename,
227
+ mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
228
+ )
229
+
230
+ except Exception as e:
231
+ logging.error(f"Error exporting Excel: {e}")
232
+ flash("Error exporting data to Excel", "error")
233
+ return redirect(url_for('admin'))
234
+
235
+ @app.route('/delete_ticket/<ticket_number>', methods=['POST'])
236
+ def delete_ticket(ticket_number):
237
+ """Delete a specific ticket"""
238
+ try:
239
+ tickets = load_tickets()
240
+ original_count = len(tickets)
241
+
242
+ # Filter out the ticket to delete
243
+ tickets = [t for t in tickets if t.get('ticket_number') != ticket_number]
244
+
245
+ if len(tickets) < original_count:
246
+ if save_tickets(tickets):
247
+ flash(f"Ticket {ticket_number} deleted successfully", "success")
248
+ else:
249
+ flash("Error deleting ticket", "error")
250
+ else:
251
+ flash("Ticket not found", "warning")
252
+
253
+ return redirect(url_for('admin'))
254
+
255
+ except Exception as e:
256
+ logging.error(f"Error deleting ticket: {e}")
257
+ flash("Error deleting ticket", "error")
258
+ return redirect(url_for('admin'))
259
+
260
+ @app.errorhandler(404)
261
+ def not_found(error):
262
+ return jsonify({
263
+ "success": False,
264
+ "message": "Endpoint not found"
265
+ }), 404
266
+
267
+ @app.errorhandler(500)
268
+ def internal_error(error):
269
+ return jsonify({
270
+ "success": False,
271
+ "message": "Internal server error"
272
+ }), 500
273
+
274
+ if __name__ == "__main__":
275
+ app.run(debug=True, host="0.0.0.0", port=5000)
data/tickets.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "email": "[email protected]",
4
+ "phone": "03037191639",
5
+ "name": "Abdullah Ali",
6
+ "tickets": 3,
7
+ "ticket_number": "283598",
8
+ "country": "pakistan",
9
+ "region": "punjab",
10
+ "timestamp": "2025-08-10 15:04:34"
11
+ }
12
+ ]
main.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from app import app
2
+
3
+ if __name__ == "__main__":
4
+ app.run(debug=True, host="0.0.0.0", port=5000)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Flask==3.0.0
2
+ gunicorn==21.2.0
3
+ openpyxl==3.1.2
static/css/style.css ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Premium Minimal Ticket Collection System */
2
+
3
+ :root {
4
+ --primary-color: #1a1a1a;
5
+ --secondary-color: #6b7280;
6
+ --accent-color: #3b82f6;
7
+ --success-color: #10b981;
8
+ --warning-color: #f59e0b;
9
+ --danger-color: #ef4444;
10
+ --background-color: #fafbfc;
11
+ --card-background: #ffffff;
12
+ --border-color: #e5e7eb;
13
+ --text-primary: #111827;
14
+ --text-secondary: #6b7280;
15
+ --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
16
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
17
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ background-color: var(--background-color);
26
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
27
+ font-size: 14px;
28
+ line-height: 1.6;
29
+ color: var(--text-primary);
30
+ margin: 0;
31
+ padding: 0;
32
+ }
33
+
34
+ /* Navigation */
35
+ .navbar {
36
+ background-color: var(--card-background) !important;
37
+ border-bottom: 1px solid var(--border-color);
38
+ box-shadow: none;
39
+ padding: 1rem 0;
40
+ }
41
+
42
+ .navbar-brand {
43
+ font-weight: 600;
44
+ font-size: 18px;
45
+ color: var(--text-primary) !important;
46
+ display: flex;
47
+ align-items: center;
48
+ }
49
+
50
+ .navbar-brand i {
51
+ font-size: 20px;
52
+ margin-right: 8px;
53
+ color: var(--accent-color);
54
+ }
55
+
56
+ .navbar-nav .nav-link {
57
+ color: var(--text-secondary) !important;
58
+ font-weight: 500;
59
+ transition: color 0.2s ease;
60
+ }
61
+
62
+ .navbar-nav .nav-link:hover {
63
+ color: var(--text-primary) !important;
64
+ }
65
+
66
+ /* Hero Section - Minimal */
67
+ .hero-section {
68
+ background: var(--card-background);
69
+ border: 1px solid var(--border-color);
70
+ border-radius: 12px;
71
+ padding: 3rem 2rem;
72
+ margin-bottom: 2rem;
73
+ text-align: center;
74
+ box-shadow: var(--shadow-sm);
75
+ }
76
+
77
+ .hero-section h1 {
78
+ font-size: 2.5rem;
79
+ font-weight: 600;
80
+ color: var(--text-primary);
81
+ margin-bottom: 1rem;
82
+ letter-spacing: -0.025em;
83
+ }
84
+
85
+ .hero-section .lead {
86
+ color: var(--text-secondary);
87
+ font-size: 1.1rem;
88
+ font-weight: 400;
89
+ max-width: 600px;
90
+ margin: 0 auto;
91
+ }
92
+
93
+ /* Feature Cards - Minimal */
94
+ .feature-card {
95
+ background: var(--card-background);
96
+ border: 1px solid var(--border-color);
97
+ border-radius: 12px;
98
+ padding: 2rem 1.5rem;
99
+ transition: all 0.2s ease;
100
+ height: 100%;
101
+ text-align: center;
102
+ }
103
+
104
+ .feature-card:hover {
105
+ border-color: var(--accent-color);
106
+ transform: translateY(-2px);
107
+ box-shadow: var(--shadow-md);
108
+ }
109
+
110
+ .feature-icon i {
111
+ font-size: 2.5rem;
112
+ color: var(--accent-color);
113
+ margin-bottom: 1rem;
114
+ }
115
+
116
+ .feature-card h5 {
117
+ color: var(--text-primary);
118
+ font-weight: 600;
119
+ font-size: 1.1rem;
120
+ margin-bottom: 0.75rem;
121
+ }
122
+
123
+ .feature-card p {
124
+ color: var(--text-secondary);
125
+ font-size: 0.9rem;
126
+ margin: 0;
127
+ }
128
+
129
+ /* API Documentation */
130
+ .api-documentation .card {
131
+ border: none;
132
+ border-radius: 15px;
133
+ }
134
+
135
+ .code-block {
136
+ font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
137
+ font-size: 0.9rem;
138
+ line-height: 1.4;
139
+ overflow-x: auto;
140
+ }
141
+
142
+ .code-block code {
143
+ background: none !important;
144
+ color: inherit !important;
145
+ }
146
+
147
+ /* Stats Cards - Premium Minimal */
148
+ .stats-card {
149
+ background: var(--card-background);
150
+ border: 1px solid var(--border-color);
151
+ border-radius: 12px;
152
+ padding: 1.5rem;
153
+ margin-bottom: 1rem;
154
+ box-shadow: var(--shadow-sm);
155
+ transition: all 0.2s ease;
156
+ display: flex;
157
+ flex-direction: column;
158
+ align-items: center;
159
+ text-align: center;
160
+ }
161
+
162
+ .stats-card:hover {
163
+ box-shadow: var(--shadow-md);
164
+ transform: translateY(-1px);
165
+ }
166
+
167
+ .stats-card .stats-icon {
168
+ font-size: 1.75rem;
169
+ color: var(--accent-color);
170
+ margin-bottom: 0.75rem;
171
+ opacity: 1;
172
+ }
173
+
174
+ .stats-card .stats-content h3 {
175
+ font-size: 2rem;
176
+ font-weight: 700;
177
+ margin: 0 0 0.25rem 0;
178
+ color: var(--text-primary);
179
+ }
180
+
181
+ .stats-card .stats-content p {
182
+ margin: 0;
183
+ font-size: 0.85rem;
184
+ color: var(--text-secondary);
185
+ font-weight: 500;
186
+ }
187
+
188
+ /* Table Styling - Premium Minimal */
189
+ .table-responsive {
190
+ border-radius: 12px;
191
+ overflow: hidden;
192
+ border: 1px solid var(--border-color);
193
+ background: var(--card-background);
194
+ }
195
+
196
+ .table {
197
+ margin-bottom: 0;
198
+ font-size: 13px;
199
+ }
200
+
201
+ .table thead {
202
+ background-color: #f8fafc;
203
+ }
204
+
205
+ .table thead th {
206
+ border: none;
207
+ font-weight: 600;
208
+ cursor: pointer;
209
+ user-select: none;
210
+ padding: 1rem 0.75rem;
211
+ color: var(--text-primary);
212
+ font-size: 12px;
213
+ text-transform: uppercase;
214
+ letter-spacing: 0.025em;
215
+ border-bottom: 1px solid var(--border-color);
216
+ }
217
+
218
+ .table thead th:hover {
219
+ background-color: #f1f5f9;
220
+ }
221
+
222
+ .table thead th i.fas.fa-sort {
223
+ opacity: 0.4;
224
+ margin-left: 0.5rem;
225
+ transition: opacity 0.2s ease;
226
+ font-size: 10px;
227
+ }
228
+
229
+ .table thead th:hover i.fas.fa-sort {
230
+ opacity: 1;
231
+ }
232
+
233
+ .table tbody tr {
234
+ transition: background-color 0.2s ease;
235
+ border: none;
236
+ }
237
+
238
+ .table tbody tr:hover {
239
+ background-color: #f8fafc;
240
+ }
241
+
242
+ .table tbody td {
243
+ vertical-align: middle;
244
+ border-color: var(--border-color);
245
+ padding: 0.875rem 0.75rem;
246
+ color: var(--text-primary);
247
+ border-top: 1px solid #f1f5f9;
248
+ }
249
+
250
+ .table tbody td:first-child {
251
+ font-weight: 500;
252
+ }
253
+
254
+ /* Form Controls - Minimal */
255
+ .table-controls {
256
+ background: var(--card-background);
257
+ padding: 1.5rem;
258
+ border-radius: 12px;
259
+ margin-bottom: 1.5rem;
260
+ border: 1px solid var(--border-color);
261
+ box-shadow: var(--shadow-sm);
262
+ }
263
+
264
+ .form-control, .form-select {
265
+ border-radius: 8px;
266
+ border: 1px solid var(--border-color);
267
+ transition: all 0.2s ease;
268
+ font-size: 14px;
269
+ padding: 0.625rem 0.875rem;
270
+ background-color: var(--card-background);
271
+ color: var(--text-primary);
272
+ }
273
+
274
+ .form-control:focus, .form-select:focus {
275
+ border-color: var(--accent-color);
276
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
277
+ outline: none;
278
+ }
279
+
280
+ .form-control::placeholder {
281
+ color: var(--text-secondary);
282
+ opacity: 0.7;
283
+ }
284
+
285
+ /* Buttons - Premium Minimal */
286
+ .btn {
287
+ border-radius: 8px;
288
+ font-weight: 500;
289
+ transition: all 0.2s ease;
290
+ border: 1px solid transparent;
291
+ padding: 0.625rem 1.25rem;
292
+ font-size: 14px;
293
+ display: inline-flex;
294
+ align-items: center;
295
+ gap: 0.5rem;
296
+ }
297
+
298
+ .btn:hover {
299
+ transform: translateY(-1px);
300
+ box-shadow: var(--shadow-md);
301
+ }
302
+
303
+ .btn-primary {
304
+ background: var(--accent-color);
305
+ border-color: var(--accent-color);
306
+ color: white;
307
+ }
308
+
309
+ .btn-primary:hover {
310
+ background: #2563eb;
311
+ border-color: #2563eb;
312
+ }
313
+
314
+ .btn-success {
315
+ background: var(--success-color);
316
+ border-color: var(--success-color);
317
+ color: white;
318
+ }
319
+
320
+ .btn-success:hover {
321
+ background: #059669;
322
+ border-color: #059669;
323
+ }
324
+
325
+ .btn-outline-primary {
326
+ background: transparent;
327
+ border-color: var(--accent-color);
328
+ color: var(--accent-color);
329
+ }
330
+
331
+ .btn-outline-primary:hover {
332
+ background: var(--accent-color);
333
+ color: white;
334
+ }
335
+
336
+ .btn-outline-secondary {
337
+ background: transparent;
338
+ border-color: var(--border-color);
339
+ color: var(--text-secondary);
340
+ }
341
+
342
+ .btn-outline-secondary:hover {
343
+ background: var(--secondary-color);
344
+ border-color: var(--secondary-color);
345
+ color: white;
346
+ }
347
+
348
+ .btn-outline-danger {
349
+ background: transparent;
350
+ border-color: var(--danger-color);
351
+ color: var(--danger-color);
352
+ }
353
+
354
+ .btn-outline-danger:hover {
355
+ background: var(--danger-color);
356
+ border-color: var(--danger-color);
357
+ color: white;
358
+ transform: none;
359
+ }
360
+
361
+
362
+
363
+ /* Cards - Premium Minimal */
364
+ .card {
365
+ border: 1px solid var(--border-color);
366
+ border-radius: 12px;
367
+ box-shadow: var(--shadow-sm);
368
+ background: var(--card-background);
369
+ }
370
+
371
+ .card-header {
372
+ border: none;
373
+ border-bottom: 1px solid var(--border-color);
374
+ font-weight: 600;
375
+ background: var(--card-background);
376
+ padding: 1.5rem;
377
+ color: var(--text-primary);
378
+ }
379
+
380
+ .card-body {
381
+ padding: 1.5rem;
382
+ }
383
+
384
+ /* Badges */
385
+ .badge {
386
+ border-radius: 6px;
387
+ font-weight: 500;
388
+ font-size: 12px;
389
+ }
390
+
391
+ /* Footer */
392
+ footer {
393
+ margin-top: auto;
394
+ background: var(--card-background) !important;
395
+ border-top: 1px solid var(--border-color);
396
+ color: var(--text-secondary) !important;
397
+ }
398
+
399
+ footer p {
400
+ font-size: 13px;
401
+ color: var(--text-secondary) !important;
402
+ }
403
+
404
+ footer i {
405
+ color: var(--accent-color);
406
+ }
407
+
408
+ /* Empty State */
409
+ .text-center.py-5 {
410
+ padding: 3rem 1rem !important;
411
+ }
412
+
413
+ .text-center.py-5 i {
414
+ opacity: 0.5;
415
+ }
416
+
417
+ /* Alerts */
418
+ .alert {
419
+ border: none;
420
+ border-radius: 10px;
421
+ font-weight: 500;
422
+ }
423
+
424
+ .alert-dismissible .btn-close {
425
+ padding: 0.75rem 1rem;
426
+ }
427
+
428
+ /* Responsive Design */
429
+ @media (max-width: 768px) {
430
+ .hero-section {
431
+ padding: 2rem 1rem;
432
+ text-align: center;
433
+ }
434
+
435
+ .hero-section h1 {
436
+ font-size: 2rem;
437
+ }
438
+
439
+ .stats-card {
440
+ text-align: center;
441
+ }
442
+
443
+ .table-responsive {
444
+ font-size: 0.875rem;
445
+ }
446
+
447
+ .d-flex.justify-content-between {
448
+ flex-direction: column;
449
+ gap: 1rem;
450
+ }
451
+
452
+ .d-flex.justify-content-between > div {
453
+ text-align: center;
454
+ }
455
+ }
456
+
457
+ @media (max-width: 576px) {
458
+ .container-fluid {
459
+ padding-left: 1rem;
460
+ padding-right: 1rem;
461
+ }
462
+
463
+ .btn-group-sm > .btn, .btn-sm {
464
+ font-size: 0.75rem;
465
+ }
466
+
467
+ .code-block {
468
+ font-size: 0.8rem;
469
+ padding: 0.75rem !important;
470
+ }
471
+ }
472
+
473
+ /* Loading Animation */
474
+ .loading {
475
+ opacity: 0.6;
476
+ pointer-events: none;
477
+ }
478
+
479
+ /* Success/Error States */
480
+ .success-highlight {
481
+ background-color: rgba(40, 167, 69, 0.1) !important;
482
+ border-left: 4px solid var(--success-color);
483
+ animation: highlight 2s ease-out;
484
+ }
485
+
486
+ .error-highlight {
487
+ background-color: rgba(220, 53, 69, 0.1) !important;
488
+ border-left: 4px solid var(--danger-color);
489
+ animation: highlight 2s ease-out;
490
+ }
491
+
492
+ @keyframes highlight {
493
+ 0% {
494
+ background-color: rgba(13, 110, 253, 0.2);
495
+ }
496
+ 100% {
497
+ background-color: transparent;
498
+ }
499
+ }
500
+
501
+ /* Scrollbar Styling */
502
+ ::-webkit-scrollbar {
503
+ width: 6px;
504
+ height: 6px;
505
+ }
506
+
507
+ ::-webkit-scrollbar-track {
508
+ background: #f1f1f1;
509
+ border-radius: 10px;
510
+ }
511
+
512
+ ::-webkit-scrollbar-thumb {
513
+ background: #c1c1c1;
514
+ border-radius: 10px;
515
+ }
516
+
517
+ ::-webkit-scrollbar-thumb:hover {
518
+ background: #a8a8a8;
519
+ }
520
+
521
+ /* Print Styles */
522
+ @media print {
523
+ .navbar, .table-controls, .btn, footer {
524
+ display: none !important;
525
+ }
526
+
527
+ .card {
528
+ box-shadow: none !important;
529
+ border: 1px solid #dee2e6 !important;
530
+ }
531
+
532
+ .table {
533
+ font-size: 0.8rem;
534
+ }
535
+ }
static/js/main.js ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Main JavaScript functionality for Ticket Collection System
2
+
3
+ document.addEventListener('DOMContentLoaded', function() {
4
+ initializeAdmin();
5
+ });
6
+
7
+ // Initialize admin dashboard functionality
8
+ function initializeAdmin() {
9
+ if (window.location.pathname === '/admin') {
10
+ initializeTableFeatures();
11
+ initializeSearch();
12
+ initializeFilters();
13
+ updateLastUpdated();
14
+ }
15
+ }
16
+
17
+ // Initialize table sorting and filtering features
18
+ function initializeTableFeatures() {
19
+ const table = document.getElementById('ticketsTable');
20
+ if (!table) return;
21
+
22
+ // Store original table data for filtering
23
+ window.originalTableData = Array.from(table.querySelectorAll('tbody tr'));
24
+ updateShowingCount();
25
+ }
26
+
27
+ // Table sorting functionality
28
+ let sortDirection = {};
29
+ function sortTable(columnIndex) {
30
+ const table = document.getElementById('ticketsTable');
31
+ const tbody = table.querySelector('tbody');
32
+ const rows = Array.from(tbody.querySelectorAll('tr'));
33
+
34
+ // Determine sort direction
35
+ const currentDir = sortDirection[columnIndex] || 'asc';
36
+ const newDir = currentDir === 'asc' ? 'desc' : 'asc';
37
+ sortDirection[columnIndex] = newDir;
38
+
39
+ // Update sort icons
40
+ updateSortIcons(columnIndex, newDir);
41
+
42
+ // Sort rows
43
+ rows.sort((a, b) => {
44
+ const aText = a.cells[columnIndex].textContent.trim();
45
+ const bText = b.cells[columnIndex].textContent.trim();
46
+
47
+ // Handle numeric columns
48
+ if (columnIndex === 3) { // Tickets column
49
+ const aNum = parseInt(aText) || 0;
50
+ const bNum = parseInt(bText) || 0;
51
+ return newDir === 'asc' ? aNum - bNum : bNum - aNum;
52
+ }
53
+
54
+ // Handle date columns
55
+ if (columnIndex === 7) { // Timestamp column
56
+ const aDate = new Date(aText);
57
+ const bDate = new Date(bText);
58
+ return newDir === 'asc' ? aDate - bDate : bDate - aDate;
59
+ }
60
+
61
+ // Handle text columns
62
+ const comparison = aText.localeCompare(bText);
63
+ return newDir === 'asc' ? comparison : -comparison;
64
+ });
65
+
66
+ // Rebuild table
67
+ rows.forEach(row => tbody.appendChild(row));
68
+ updateShowingCount();
69
+ }
70
+
71
+ // Update sort direction icons
72
+ function updateSortIcons(activeColumn, direction) {
73
+ const headers = document.querySelectorAll('#ticketsTable thead th');
74
+ headers.forEach((header, index) => {
75
+ const icon = header.querySelector('i.fa-sort, i.fa-sort-up, i.fa-sort-down');
76
+ if (icon) {
77
+ if (index === activeColumn) {
78
+ icon.className = direction === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down';
79
+ } else {
80
+ icon.className = 'fas fa-sort';
81
+ }
82
+ }
83
+ });
84
+ }
85
+
86
+ // Search functionality
87
+ function initializeSearch() {
88
+ const searchInput = document.getElementById('searchInput');
89
+ if (!searchInput) return;
90
+
91
+ searchInput.addEventListener('input', function() {
92
+ const searchTerm = this.value.toLowerCase();
93
+ filterTable();
94
+ });
95
+ }
96
+
97
+ // Filter functionality
98
+ function initializeFilters() {
99
+ const countryFilter = document.getElementById('countryFilter');
100
+ if (!countryFilter) return;
101
+
102
+ countryFilter.addEventListener('change', function() {
103
+ filterTable();
104
+ });
105
+ }
106
+
107
+ // Combined filter and search function
108
+ function filterTable() {
109
+ if (!window.originalTableData) return;
110
+
111
+ const searchTerm = document.getElementById('searchInput')?.value.toLowerCase() || '';
112
+ const countryFilter = document.getElementById('countryFilter')?.value || '';
113
+ const tbody = document.querySelector('#ticketsTable tbody');
114
+
115
+ // Clear current table
116
+ tbody.innerHTML = '';
117
+
118
+ let visibleCount = 0;
119
+
120
+ window.originalTableData.forEach(row => {
121
+ const rowText = row.textContent.toLowerCase();
122
+ const countryCell = row.cells[5].textContent.trim();
123
+
124
+ const matchesSearch = searchTerm === '' || rowText.includes(searchTerm);
125
+ const matchesCountry = countryFilter === '' || countryCell === countryFilter;
126
+
127
+ if (matchesSearch && matchesCountry) {
128
+ tbody.appendChild(row.cloneNode(true));
129
+ visibleCount++;
130
+ }
131
+ });
132
+
133
+ // Update showing count
134
+ document.getElementById('showingCount').textContent = visibleCount;
135
+
136
+ // Show empty state if no results
137
+ if (visibleCount === 0) {
138
+ showEmptySearchResults();
139
+ }
140
+ }
141
+
142
+ // Show empty search results
143
+ function showEmptySearchResults() {
144
+ const tbody = document.querySelector('#ticketsTable tbody');
145
+ const emptyRow = document.createElement('tr');
146
+ emptyRow.innerHTML = `
147
+ <td colspan="9" class="text-center py-4">
148
+ <i class="fas fa-search fa-2x text-muted mb-2"></i>
149
+ <p class="text-muted mb-0">No tickets match your search criteria</p>
150
+ </td>
151
+ `;
152
+ tbody.appendChild(emptyRow);
153
+ }
154
+
155
+ // Update showing count
156
+ function updateShowingCount() {
157
+ const tbody = document.querySelector('#ticketsTable tbody');
158
+ if (!tbody) return;
159
+
160
+ const visibleRows = tbody.querySelectorAll('tr').length;
161
+ const showingElement = document.getElementById('showingCount');
162
+ if (showingElement) {
163
+ showingElement.textContent = visibleRows;
164
+ }
165
+ }
166
+
167
+ // Refresh data function
168
+ function refreshData() {
169
+ const refreshBtn = document.querySelector('button[onclick="refreshData()"]');
170
+ if (refreshBtn) {
171
+ refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Refreshing...';
172
+ refreshBtn.disabled = true;
173
+ }
174
+
175
+ // Simulate refresh delay
176
+ setTimeout(() => {
177
+ location.reload();
178
+ }, 500);
179
+ }
180
+
181
+ // Export visible data function
182
+ function exportVisible() {
183
+ const table = document.getElementById('ticketsTable');
184
+ if (!table) return;
185
+
186
+ const visibleRows = Array.from(table.querySelectorAll('tbody tr'));
187
+ if (visibleRows.length === 0) {
188
+ alert('No data to export');
189
+ return;
190
+ }
191
+
192
+ // Create CSV content
193
+ let csvContent = 'Email,Phone,Name,Tickets,Ticket Number,Country,Region,Timestamp\n';
194
+
195
+ visibleRows.forEach(row => {
196
+ const cells = Array.from(row.cells).slice(0, 8); // Exclude actions column
197
+ const rowData = cells.map(cell => {
198
+ let text = cell.textContent.trim();
199
+ // Clean up badge text
200
+ if (cell.querySelector('.badge')) {
201
+ text = cell.querySelector('.badge').textContent.trim();
202
+ }
203
+ // Clean up code text
204
+ if (cell.querySelector('code')) {
205
+ text = cell.querySelector('code').textContent.trim();
206
+ }
207
+ // Escape quotes and commas
208
+ if (text.includes(',') || text.includes('"')) {
209
+ text = '"' + text.replace(/"/g, '""') + '"';
210
+ }
211
+ return text;
212
+ });
213
+ csvContent += rowData.join(',') + '\n';
214
+ });
215
+
216
+ // Download CSV
217
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
218
+ const link = document.createElement('a');
219
+ if (link.download !== undefined) {
220
+ const url = URL.createObjectURL(blob);
221
+ link.setAttribute('href', url);
222
+ link.setAttribute('download', `ticket_data_filtered_${new Date().toISOString().split('T')[0]}.csv`);
223
+ link.style.visibility = 'hidden';
224
+ document.body.appendChild(link);
225
+ link.click();
226
+ document.body.removeChild(link);
227
+ }
228
+ }
229
+
230
+ // Update last updated timestamp
231
+ function updateLastUpdated() {
232
+ const element = document.getElementById('lastUpdated');
233
+ if (element) {
234
+ const now = new Date();
235
+ element.textContent = now.toISOString().slice(0, 19).replace('T', ' ');
236
+ }
237
+ }
238
+
239
+ // Utility function to show notifications
240
+ function showNotification(message, type = 'info') {
241
+ const alertDiv = document.createElement('div');
242
+ alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
243
+ alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
244
+ alertDiv.innerHTML = `
245
+ ${message}
246
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
247
+ `;
248
+
249
+ document.body.appendChild(alertDiv);
250
+
251
+ // Auto-remove after 5 seconds
252
+ setTimeout(() => {
253
+ if (alertDiv.parentNode) {
254
+ alertDiv.parentNode.removeChild(alertDiv);
255
+ }
256
+ }, 5000);
257
+ }
258
+
259
+ // Handle form submissions with loading states
260
+ document.addEventListener('submit', function(e) {
261
+ const form = e.target;
262
+ if (form.tagName === 'FORM') {
263
+ const submitBtn = form.querySelector('button[type="submit"]');
264
+ if (submitBtn) {
265
+ submitBtn.disabled = true;
266
+ const originalHtml = submitBtn.innerHTML;
267
+ submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Processing...';
268
+
269
+ // Re-enable after 3 seconds (fallback)
270
+ setTimeout(() => {
271
+ submitBtn.disabled = false;
272
+ submitBtn.innerHTML = originalHtml;
273
+ }, 3000);
274
+ }
275
+ }
276
+ });
277
+
278
+ // Add keyboard shortcuts
279
+ document.addEventListener('keydown', function(e) {
280
+ // Ctrl+R or F5 for refresh
281
+ if ((e.ctrlKey && e.key === 'r') || e.key === 'F5') {
282
+ if (window.location.pathname === '/admin') {
283
+ e.preventDefault();
284
+ refreshData();
285
+ }
286
+ }
287
+
288
+ // Ctrl+F for search focus
289
+ if (e.ctrlKey && e.key === 'f') {
290
+ const searchInput = document.getElementById('searchInput');
291
+ if (searchInput) {
292
+ e.preventDefault();
293
+ searchInput.focus();
294
+ searchInput.select();
295
+ }
296
+ }
297
+
298
+ // Escape to clear search
299
+ if (e.key === 'Escape') {
300
+ const searchInput = document.getElementById('searchInput');
301
+ if (searchInput && searchInput.value) {
302
+ searchInput.value = '';
303
+ filterTable();
304
+ }
305
+ }
306
+ });
307
+
308
+ // Add smooth scrolling for anchor links
309
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
310
+ anchor.addEventListener('click', function (e) {
311
+ e.preventDefault();
312
+ const target = document.querySelector(this.getAttribute('href'));
313
+ if (target) {
314
+ target.scrollIntoView({
315
+ behavior: 'smooth',
316
+ block: 'start'
317
+ });
318
+ }
319
+ });
320
+ });
templates/admin.html ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Admin Dashboard - Ticket Collection System</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
+ <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <nav class="navbar navbar-expand-lg">
13
+ <div class="container">
14
+ <a class="navbar-brand" href="/">
15
+ <i class="fas fa-ticket-alt"></i>
16
+ Ticket Collection System
17
+ </a>
18
+ <div class="navbar-nav ms-auto">
19
+ <a class="nav-link" href="{{ url_for('index') }}">
20
+ <i class="fas fa-home me-1"></i>
21
+ Home
22
+ </a>
23
+ </div>
24
+ </div>
25
+ </nav>
26
+
27
+ <div class="container-fluid mt-4">
28
+ {% with messages = get_flashed_messages(with_categories=true) %}
29
+ {% if messages %}
30
+ {% for category, message in messages %}
31
+ <div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
32
+ {{ message }}
33
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
34
+ </div>
35
+ {% endfor %}
36
+ {% endif %}
37
+ {% endwith %}
38
+
39
+ <div class="row mb-4">
40
+ <div class="col-12">
41
+ <div class="d-flex justify-content-between align-items-center">
42
+ <div>
43
+ <h1 style="font-size: 1.75rem; font-weight: 600; margin: 0; color: var(--text-primary);">
44
+ Admin Dashboard
45
+ </h1>
46
+ <p style="color: var(--text-secondary); margin: 0.25rem 0 0 0; font-size: 14px;">
47
+ Manage and monitor ticket submissions
48
+ </p>
49
+ </div>
50
+ <div class="d-flex gap-2">
51
+ <button class="btn btn-outline-secondary btn-sm" onclick="refreshData()">
52
+ <i class="fas fa-sync-alt"></i>
53
+ Refresh
54
+ </button>
55
+ {% if tickets %}
56
+ <a href="{{ url_for('export_excel') }}" class="btn btn-primary btn-sm">
57
+ <i class="fas fa-file-excel"></i>
58
+ Export Excel
59
+ </a>
60
+ {% endif %}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+
66
+ <div class="row mb-4">
67
+ <div class="col-md-3">
68
+ <div class="stats-card">
69
+ <div class="stats-icon">
70
+ <i class="fas fa-ticket-alt"></i>
71
+ </div>
72
+ <div class="stats-content">
73
+ <h3>{{ total_tickets }}</h3>
74
+ <p>Total Tickets</p>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <div class="col-md-3">
79
+ <div class="stats-card">
80
+ <div class="stats-icon">
81
+ <i class="fas fa-users"></i>
82
+ </div>
83
+ <div class="stats-content">
84
+ <h3>{{ tickets|map(attribute='email')|unique|list|length }}</h3>
85
+ <p>Unique Customers</p>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <div class="col-md-3">
90
+ <div class="stats-card">
91
+ <div class="stats-icon">
92
+ <i class="fas fa-globe"></i>
93
+ </div>
94
+ <div class="stats-content">
95
+ <h3>{{ tickets|map(attribute='country')|unique|list|length }}</h3>
96
+ <p>Countries</p>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <div class="col-md-3">
101
+ <div class="stats-card">
102
+ <div class="stats-icon">
103
+ <i class="fas fa-calendar"></i>
104
+ </div>
105
+ <div class="stats-content">
106
+ <h3>{{ tickets|selectattr('timestamp')|list|length }}</h3>
107
+ <p>Records Today</p>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ <div class="row">
114
+ <div class="col-12">
115
+ <div class="card">
116
+ <div class="card-header">
117
+ <h5 class="card-title mb-0" style="font-size: 16px; font-weight: 600;">
118
+ <i class="fas fa-table me-2" style="color: var(--accent-color);"></i>
119
+ Ticket Data Records
120
+ </h5>
121
+ </div>
122
+ <div class="card-body">
123
+ {% if tickets %}
124
+ <div class="table-controls mb-3">
125
+ <div class="row">
126
+ <div class="col-md-6">
127
+ <input type="text" id="searchInput" class="form-control" placeholder="Search tickets...">
128
+ </div>
129
+ <div class="col-md-6">
130
+ <select id="countryFilter" class="form-select">
131
+ <option value="">All Countries</option>
132
+ {% for country in tickets|map(attribute='country')|unique|sort %}
133
+ <option value="{{ country }}">{{ country }}</option>
134
+ {% endfor %}
135
+ </select>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="table-responsive">
141
+ <table class="table" id="ticketsTable">
142
+ <thead>
143
+ <tr>
144
+ <th onclick="sortTable(0)">
145
+ Email <i class="fas fa-sort"></i>
146
+ </th>
147
+ <th onclick="sortTable(1)">
148
+ Phone <i class="fas fa-sort"></i>
149
+ </th>
150
+ <th onclick="sortTable(2)">
151
+ Name <i class="fas fa-sort"></i>
152
+ </th>
153
+ <th onclick="sortTable(3)">
154
+ Tickets <i class="fas fa-sort"></i>
155
+ </th>
156
+ <th onclick="sortTable(4)">
157
+ Ticket Number <i class="fas fa-sort"></i>
158
+ </th>
159
+ <th onclick="sortTable(5)">
160
+ Country <i class="fas fa-sort"></i>
161
+ </th>
162
+ <th onclick="sortTable(6)">
163
+ Region <i class="fas fa-sort"></i>
164
+ </th>
165
+ <th onclick="sortTable(7)">
166
+ Timestamp <i class="fas fa-sort"></i>
167
+ </th>
168
+ <th>Actions</th>
169
+ </tr>
170
+ </thead>
171
+ <tbody id="ticketsTableBody">
172
+ {% for ticket in tickets %}
173
+ <tr>
174
+ <td>{{ ticket.email }}</td>
175
+ <td>{{ ticket.phone }}</td>
176
+ <td>{{ ticket.name }}</td>
177
+ <td>
178
+ <span style="background: #f0f9ff; color: #0369a1; padding: 0.25rem 0.5rem; border-radius: 6px; font-size: 12px; font-weight: 500;">{{ ticket.tickets }}</span>
179
+ </td>
180
+ <td>
181
+ <span style="background: #f8fafc; color: #475569; padding: 0.25rem 0.5rem; border-radius: 4px; font-family: 'Monaco', monospace; font-size: 12px;">{{ ticket.ticket_number }}</span>
182
+ </td>
183
+ <td>{{ ticket.country }}</td>
184
+ <td>{{ ticket.region }}</td>
185
+ <td>
186
+ <span style="color: var(--text-secondary); font-size: 12px;">{{ ticket.timestamp }}</span>
187
+ </td>
188
+ <td>
189
+ <form method="POST" action="{{ url_for('delete_ticket', ticket_number=ticket.ticket_number) }}"
190
+ style="display: inline;"
191
+ onsubmit="return confirm('Are you sure you want to delete this ticket?')">
192
+ <button type="submit" class="btn btn-outline-danger" style="padding: 0.375rem 0.5rem; font-size: 12px;">
193
+ <i class="fas fa-trash" style="font-size: 11px;"></i>
194
+ </button>
195
+ </form>
196
+ </td>
197
+ </tr>
198
+ {% endfor %}
199
+ </tbody>
200
+ </table>
201
+ </div>
202
+
203
+ <div class="d-flex justify-content-between align-items-center mt-3">
204
+ <div>
205
+ <small class="text-muted">
206
+ Showing <span id="showingCount">{{ tickets|length }}</span> of {{ total_tickets }} records
207
+ </small>
208
+ </div>
209
+ <div>
210
+ <button class="btn btn-outline-primary btn-sm" onclick="exportVisible()">
211
+ <i class="fas fa-download me-1"></i>
212
+ Export Visible
213
+ </button>
214
+ </div>
215
+ </div>
216
+ {% else %}
217
+ <div class="text-center py-5">
218
+ <i class="fas fa-inbox fa-4x text-muted mb-3"></i>
219
+ <h4 class="text-muted">No Ticket Data Available</h4>
220
+ <p class="text-muted">No tickets have been submitted yet. Use the API endpoint to add ticket data.</p>
221
+ <a href="{{ url_for('index') }}" class="btn btn-primary">
222
+ <i class="fas fa-plus me-1"></i>
223
+ View API Documentation
224
+ </a>
225
+ </div>
226
+ {% endif %}
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+
233
+ <footer class="bg-dark text-light mt-5 py-4">
234
+ <div class="container text-center">
235
+ <p class="mb-0">
236
+ <i class="fas fa-shield-alt me-2"></i>
237
+ Secure Admin Dashboard - Last updated: <span id="lastUpdated"></span>
238
+ </p>
239
+ </div>
240
+ </footer>
241
+
242
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
243
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
244
+ <script>
245
+ // Auto-refresh every 30 seconds
246
+ setInterval(function() {
247
+ document.getElementById('lastUpdated').textContent = new Date().toISOString().slice(0, 19).replace('T', ' ');
248
+ }, 30000);
249
+ </script>
250
+ </body>
251
+ </html>
templates/index.html ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Ticket Data Collection System</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
+ <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
10
+ </head>
11
+ <body>
12
+ <nav class="navbar navbar-expand-lg">
13
+ <div class="container">
14
+ <a class="navbar-brand" href="/">
15
+ <i class="fas fa-ticket-alt"></i>
16
+ Ticket Collection System
17
+ </a>
18
+ <div class="navbar-nav ms-auto">
19
+ <a class="nav-link" href="{{ url_for('admin') }}">
20
+ <i class="fas fa-chart-bar me-1"></i>
21
+ Admin Dashboard
22
+ </a>
23
+ </div>
24
+ </div>
25
+ </nav>
26
+
27
+ <div class="container mt-5">
28
+ <div class="row justify-content-center">
29
+ <div class="col-lg-10">
30
+ <div class="hero-section mb-5">
31
+ <h1>
32
+ Ticket Data Collection API
33
+ </h1>
34
+ <p class="lead">Professional ticket data collection system with real-time storage and Excel export capabilities</p>
35
+ </div>
36
+
37
+ <div class="row g-4 mb-5">
38
+ <div class="col-md-4">
39
+ <div class="feature-card text-center p-4">
40
+ <div class="feature-icon mb-3">
41
+ <i class="fas fa-link fa-3x text-primary"></i>
42
+ </div>
43
+ <h5>URL-based Submission</h5>
44
+ <p class="text-muted">Submit ticket data directly through URL parameters</p>
45
+ </div>
46
+ </div>
47
+ <div class="col-md-4">
48
+ <div class="feature-card text-center p-4">
49
+ <div class="feature-icon mb-3">
50
+ <i class="fas fa-database fa-3x text-success"></i>
51
+ </div>
52
+ <h5>JSON Storage</h5>
53
+ <p class="text-muted">Persistent data storage using JSON files</p>
54
+ </div>
55
+ </div>
56
+ <div class="col-md-4">
57
+ <div class="feature-card text-center p-4">
58
+ <div class="feature-icon mb-3">
59
+ <i class="fas fa-file-excel fa-3x text-warning"></i>
60
+ </div>
61
+ <h5>Excel Export</h5>
62
+ <p class="text-muted">Download complete dataset in Excel format</p>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="api-documentation">
68
+ <div class="card shadow-lg">
69
+ <div class="card-header bg-primary text-white">
70
+ <h3 class="card-title mb-0">
71
+ <i class="fas fa-code me-2"></i>
72
+ API Documentation
73
+ </h3>
74
+ </div>
75
+ <div class="card-body">
76
+ <h5 class="text-primary mb-3">
77
+ <i class="fas fa-plus-circle me-2"></i>
78
+ Add Ticket Data
79
+ </h5>
80
+ <div class="api-endpoint mb-4">
81
+ <h6>Endpoint:</h6>
82
+ <div class="code-block p-3 bg-dark text-light rounded">
83
+ <code>GET /add_data/&lt;email&gt;/&lt;phone&gt;/&lt;name&gt;/&lt;tickets&gt;/&lt;ticket_number&gt;/&lt;country&gt;/&lt;region&gt;</code>
84
+ </div>
85
+ </div>
86
+
87
+ <h6>Parameters:</h6>
88
+ <div class="table-responsive">
89
+ <table class="table table-striped">
90
+ <thead class="table-dark">
91
+ <tr>
92
+ <th>Parameter</th>
93
+ <th>Type</th>
94
+ <th>Description</th>
95
+ <th>Example</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ <tr>
100
+ <td><code>email</code></td>
101
+ <td>string</td>
102
+ <td>Customer email address</td>
103
+ <td>[email protected]</td>
104
+ </tr>
105
+ <tr>
106
+ <td><code>phone</code></td>
107
+ <td>string</td>
108
+ <td>Customer phone number</td>
109
+ <td>+1234567890</td>
110
+ </tr>
111
+ <tr>
112
+ <td><code>name</code></td>
113
+ <td>string</td>
114
+ <td>Customer full name</td>
115
+ <td>John Doe</td>
116
+ </tr>
117
+ <tr>
118
+ <td><code>tickets</code></td>
119
+ <td>integer</td>
120
+ <td>Number of tickets purchased</td>
121
+ <td>2</td>
122
+ </tr>
123
+ <tr>
124
+ <td><code>ticket_number</code></td>
125
+ <td>string</td>
126
+ <td>Unique ticket identifier</td>
127
+ <td>TKT123456</td>
128
+ </tr>
129
+ <tr>
130
+ <td><code>country</code></td>
131
+ <td>string</td>
132
+ <td>Customer country</td>
133
+ <td>USA</td>
134
+ </tr>
135
+ <tr>
136
+ <td><code>region</code></td>
137
+ <td>string</td>
138
+ <td>Customer region/state</td>
139
+ <td>California</td>
140
+ </tr>
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+
145
+ <h6>Example Request:</h6>
146
+ <div class="code-block p-3 bg-light border rounded mb-4">
147
+ <code class="text-dark">
148
+ https://yourserver.com/add_data/[email protected]/+1234567890/John%20Doe/2/TKT123456/USA/California
149
+ </code>
150
+ </div>
151
+
152
+ <h6>Success Response:</h6>
153
+ <div class="code-block p-3 bg-success text-light rounded mb-4">
154
+ <pre><code>{
155
+ "success": true,
156
+ "message": "Ticket data added successfully",
157
+ "ticket_id": "TKT123456",
158
+ "total_tickets": 1
159
+ }</code></pre>
160
+ </div>
161
+
162
+ <h6>Error Response:</h6>
163
+ <div class="code-block p-3 bg-danger text-light rounded">
164
+ <pre><code>{
165
+ "success": false,
166
+ "message": "Validation errors",
167
+ "errors": ["Invalid email format"]
168
+ }</code></pre>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+
174
+ <div class="text-center mt-5">
175
+ <a href="{{ url_for('admin') }}" class="btn btn-primary btn-lg me-3">
176
+ <i class="fas fa-chart-bar me-2"></i>
177
+ View Admin Dashboard
178
+ </a>
179
+ <a href="/api/tickets" class="btn btn-outline-secondary btn-lg" target="_blank">
180
+ <i class="fas fa-code me-2"></i>
181
+ View API Data
182
+ </a>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <footer class="bg-dark text-light mt-5 py-4">
189
+ <div class="container text-center">
190
+ <p class="mb-0">
191
+ <i class="fas fa-ticket-alt me-2"></i>
192
+ Professional Ticket Data Collection System
193
+ </p>
194
+ </div>
195
+ </footer>
196
+
197
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
198
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
199
+ </body>
200
+ </html>