Compare commits
7 Commits
213a11e731
...
feat/wicke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa99a167a5 | ||
|
|
9b33b73bb4 | ||
|
|
bc47027c22 | ||
|
|
7ef934d8a8 | ||
|
|
974edce238 | ||
|
|
7009245ab0 | ||
|
|
a984243fe2 |
14
.env.example
14
.env.example
@@ -1,18 +1,18 @@
|
|||||||
# Application data
|
# Application data
|
||||||
RAILS_ENV=production
|
RAILS_ENV=development
|
||||||
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
||||||
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
||||||
APP_NAME=Aperonight
|
APP_NAME=Aperonight
|
||||||
|
|
||||||
# Database Configuration for production and development
|
# Database Configuration for production and development
|
||||||
# DB_HOST=127.0.0.1
|
DB_HOST=localhost
|
||||||
# DB_PORT=3306
|
|
||||||
DB_ROOT_PASSWORD=root
|
DB_ROOT_PASSWORD=root
|
||||||
DB_DATABASE=aperonight
|
DB_DATABASE=aperonight
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=root
|
DB_PASSWORD=root
|
||||||
|
|
||||||
# Test database
|
# Test database
|
||||||
|
DB_TEST_ADAPTER=sqlite3
|
||||||
DB_TEST_DATABASE=aperonight_test
|
DB_TEST_DATABASE=aperonight_test
|
||||||
DB_TEST_USERNAME=root
|
DB_TEST_USERNAME=root
|
||||||
DB_TEST_USERNAME=root
|
DB_TEST_USERNAME=root
|
||||||
@@ -28,6 +28,14 @@ SMTP_PORT=1025
|
|||||||
# SMTP_DOMAIN=localhost
|
# SMTP_DOMAIN=localhost
|
||||||
SMTP_AUTHENTICATION=plain
|
SMTP_AUTHENTICATION=plain
|
||||||
SMTP_ENABLE_STARTTLS=false
|
SMTP_ENABLE_STARTTLS=false
|
||||||
|
|
||||||
|
# Production SMTP Configuration (set these in .env.production)
|
||||||
|
# SMTP_ADDRESS=smtp.example.com
|
||||||
|
# SMTP_PORT=587
|
||||||
|
# SMTP_USERNAME=your_smtp_username
|
||||||
|
# SMTP_PASSWORD=your_smtp_password
|
||||||
|
# SMTP_AUTHENTICATION=plain
|
||||||
|
# SMTP_DOMAIN=example.com
|
||||||
# SMTP_STARTTLS=true
|
# SMTP_STARTTLS=true
|
||||||
|
|
||||||
# Application variables
|
# Application variables
|
||||||
|
|||||||
67
BACKLOG.md
67
BACKLOG.md
@@ -2,50 +2,43 @@
|
|||||||
|
|
||||||
## 📋 Todo
|
## 📋 Todo
|
||||||
|
|
||||||
### High Priority
|
- [ ] Set up project infrastructure
|
||||||
|
- [ ] Design user interface mockups
|
||||||
- [ ] feat: Check-in system with QR code scanning
|
- [ ] Create user dashboard
|
||||||
|
- [ ] Implement data persistence
|
||||||
### Medium Priority
|
- [ ] Add responsive design
|
||||||
|
- [ ] Write unit tests
|
||||||
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display
|
- [ ] Set up CI/CD pipeline
|
||||||
- [ ] feat: Multiple ticket types (early bird, VIP, general admission)
|
- [ ] Add error handling
|
||||||
- [ ] feat: Refund management system
|
- [ ] Implement search functionality
|
||||||
- [ ] feat: Real-time sales analytics dashboard
|
- [ ] Add user profile management
|
||||||
- [ ] feat: Guest checkout without account creation
|
- [ ] Create admin panel
|
||||||
- [ ] feat: Seat selection with interactive venue maps
|
- [ ] Optimize performance
|
||||||
- [ ] feat: Dynamic pricing based on demand
|
- [ ] Add documentation
|
||||||
|
- [ ] Security audit
|
||||||
### Low Priority
|
- [ ] Deploy to production
|
||||||
|
|
||||||
- [ ] feat: SMS integration for ticket delivery and updates
|
|
||||||
- [ ] feat: Mobile wallet integration
|
|
||||||
- [ ] feat: Multi-currency support
|
|
||||||
- [ ] feat: Event updates communication system
|
|
||||||
- [ ] feat: Bulk operations for group bookings
|
|
||||||
- [ ] feat: Fraud prevention and bot protection
|
|
||||||
- [ ] feat: Social login options
|
|
||||||
- [ ] feat: Event recommendations system
|
|
||||||
|
|
||||||
### Design & Infrastructure
|
|
||||||
|
|
||||||
- [ ] style: Rewrite design system
|
|
||||||
- [ ] refactor: Rewrite design mockup
|
|
||||||
|
|
||||||
## 🚧 Doing
|
## 🚧 Doing
|
||||||
|
|
||||||
- [ ] feat: Page to display all tickets for an event
|
- [ ] refactor: Moving checkout to OrdersController
|
||||||
- [ ] feat: Add a link into notification email to order page that display all tickets
|
|
||||||
|
|
||||||
## ✅ Done
|
## ✅ Done
|
||||||
|
|
||||||
|
- [x] Initialize git repository
|
||||||
|
- [x] Set up development environment
|
||||||
|
- [x] Create project structure
|
||||||
|
- [x] Install dependencies
|
||||||
|
- [x] Configure build tools
|
||||||
|
- [x] Set up linting rules
|
||||||
|
- [x] Create initial README
|
||||||
|
- [x] Set up version control
|
||||||
|
- [x] Configure development server
|
||||||
|
- [x] Establish coding standards
|
||||||
|
- [x] Set up package.json
|
||||||
|
- [x] Create .gitignore file
|
||||||
|
- [x] Initialize npm project
|
||||||
|
- [x] Set up basic folder structure
|
||||||
- [x] Configure environment variables
|
- [x] Configure environment variables
|
||||||
- [x] Create authentication system
|
- [x] Create authentication system
|
||||||
- [x] Implement user registration
|
- [x] Implement user registration
|
||||||
- [x] Add login functionality
|
- [x] Add login functionality
|
||||||
- [x] refactor: Moving checkout to OrdersController
|
|
||||||
- [x] feat: Payment gateway integration (Stripe) - PayPal not implemented
|
|
||||||
- [x] feat: Digital tickets with QR codes
|
|
||||||
- [x] feat: Ticket inventory management and capacity limits
|
|
||||||
- [x] feat: Event discovery with search and filtering
|
|
||||||
- [x] feat: Email notifications (purchase confirmations, event reminders)
|
|
||||||
|
|||||||
3
Gemfile
3
Gemfile
@@ -87,8 +87,7 @@ gem "kaminari-tailwind", "~> 0.1.0"
|
|||||||
gem "stripe", "~> 15.5"
|
gem "stripe", "~> 15.5"
|
||||||
|
|
||||||
# PDF generation for tickets
|
# PDF generation for tickets
|
||||||
gem "prawn", "~> 2.5"
|
gem "grover"
|
||||||
gem "prawn-qrcode", "~> 0.5"
|
|
||||||
|
|
||||||
# QR code generation
|
# QR code generation
|
||||||
gem "rqrcode", "~> 3.1"
|
gem "rqrcode", "~> 3.1"
|
||||||
|
|||||||
15
Gemfile.lock
15
Gemfile.lock
@@ -127,6 +127,8 @@ GEM
|
|||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
|
grover (1.2.3)
|
||||||
|
nokogiri (~> 1)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
io-console (0.8.1)
|
io-console (0.8.1)
|
||||||
@@ -221,16 +223,8 @@ GEM
|
|||||||
parser (3.3.9.0)
|
parser (3.3.9.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pdf-core (0.10.0)
|
|
||||||
pp (0.6.2)
|
pp (0.6.2)
|
||||||
prettyprint
|
prettyprint
|
||||||
prawn (2.5.0)
|
|
||||||
matrix (~> 0.4)
|
|
||||||
pdf-core (~> 0.10.0)
|
|
||||||
ttfunk (~> 1.8)
|
|
||||||
prawn-qrcode (0.5.2)
|
|
||||||
prawn (>= 1)
|
|
||||||
rqrcode (>= 1.0.0)
|
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
prism (1.4.0)
|
prism (1.4.0)
|
||||||
propshaft (1.2.1)
|
propshaft (1.2.1)
|
||||||
@@ -378,8 +372,6 @@ GEM
|
|||||||
thruster (0.1.15-aarch64-linux)
|
thruster (0.1.15-aarch64-linux)
|
||||||
thruster (0.1.15-x86_64-linux)
|
thruster (0.1.15-x86_64-linux)
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
ttfunk (1.8.0)
|
|
||||||
bigdecimal (~> 3.1)
|
|
||||||
turbo-rails (2.0.16)
|
turbo-rails (2.0.16)
|
||||||
actionpack (>= 7.1.0)
|
actionpack (>= 7.1.0)
|
||||||
railties (>= 7.1.0)
|
railties (>= 7.1.0)
|
||||||
@@ -423,6 +415,7 @@ DEPENDENCIES
|
|||||||
debug
|
debug
|
||||||
devise (~> 4.9)
|
devise (~> 4.9)
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
|
grover
|
||||||
jbuilder
|
jbuilder
|
||||||
jsbundling-rails
|
jsbundling-rails
|
||||||
kamal
|
kamal
|
||||||
@@ -431,8 +424,6 @@ DEPENDENCIES
|
|||||||
minitest-reporters (~> 1.7)
|
minitest-reporters (~> 1.7)
|
||||||
mocha
|
mocha
|
||||||
mysql2 (~> 0.5)
|
mysql2 (~> 0.5)
|
||||||
prawn (~> 2.5)
|
|
||||||
prawn-qrcode (~> 0.5)
|
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rails (~> 8.0.2, >= 8.0.2.1)
|
rails (~> 8.0.2, >= 8.0.2.1)
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
// Self-contained QR Code Generator
|
|
||||||
// No external dependencies required
|
|
||||||
|
|
||||||
class QRCodeGenerator {
|
|
||||||
constructor() {
|
|
||||||
// QR Code error correction levels
|
|
||||||
this.errorCorrectionLevels = {
|
|
||||||
L: 1, // Low ~7%
|
|
||||||
M: 0, // Medium ~15%
|
|
||||||
Q: 3, // Quartile ~25%
|
|
||||||
H: 2 // High ~30%
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mode indicators
|
|
||||||
this.modes = {
|
|
||||||
NUMERIC: 1,
|
|
||||||
ALPHANUMERIC: 2,
|
|
||||||
BYTE: 4,
|
|
||||||
KANJI: 8
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate QR code as SVG
|
|
||||||
generateSVG(text, options = {}) {
|
|
||||||
const size = options.size || 200;
|
|
||||||
const margin = options.margin || 4;
|
|
||||||
const errorCorrection = options.errorCorrection || 'M';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const qrData = this.createQRData(text, errorCorrection);
|
|
||||||
const moduleSize = (size - 2 * margin) / qrData.length;
|
|
||||||
|
|
||||||
let svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">`;
|
|
||||||
svg += `<rect width="${size}" height="${size}" fill="white"/>`;
|
|
||||||
|
|
||||||
for (let row = 0; row < qrData.length; row++) {
|
|
||||||
for (let col = 0; col < qrData[row].length; col++) {
|
|
||||||
if (qrData[row][col]) {
|
|
||||||
const x = margin + col * moduleSize;
|
|
||||||
const y = margin + row * moduleSize;
|
|
||||||
svg += `<rect x="${x}" y="${y}" width="${moduleSize}" height="${moduleSize}" fill="black"/>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg += '</svg>';
|
|
||||||
return svg;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('QR Code generation failed:', error);
|
|
||||||
return this.createErrorSVG(size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create QR code data matrix (simplified implementation)
|
|
||||||
createQRData(text, errorCorrection) {
|
|
||||||
// For simplicity, we'll create a basic QR code pattern
|
|
||||||
// This is a minimal implementation - real QR codes are much more complex
|
|
||||||
|
|
||||||
const version = this.determineVersion(text.length);
|
|
||||||
const size = 21 + (version - 1) * 4; // QR code size formula
|
|
||||||
|
|
||||||
// Initialize matrix
|
|
||||||
const matrix = Array(size).fill().map(() => Array(size).fill(false));
|
|
||||||
|
|
||||||
// Add finder patterns (corners)
|
|
||||||
this.addFinderPatterns(matrix);
|
|
||||||
|
|
||||||
// Add timing patterns
|
|
||||||
this.addTimingPatterns(matrix);
|
|
||||||
|
|
||||||
// Add data (simplified - just create a pattern based on text)
|
|
||||||
this.addDataPattern(matrix, text);
|
|
||||||
|
|
||||||
return matrix;
|
|
||||||
}
|
|
||||||
|
|
||||||
determineVersion(length) {
|
|
||||||
// Simplified version determination
|
|
||||||
if (length <= 25) return 1;
|
|
||||||
if (length <= 47) return 2;
|
|
||||||
if (length <= 77) return 3;
|
|
||||||
return 4; // Max we'll support in this simple implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
addFinderPatterns(matrix) {
|
|
||||||
const size = matrix.length;
|
|
||||||
const pattern = [
|
|
||||||
[1,1,1,1,1,1,1],
|
|
||||||
[1,0,0,0,0,0,1],
|
|
||||||
[1,0,1,1,1,0,1],
|
|
||||||
[1,0,1,1,1,0,1],
|
|
||||||
[1,0,1,1,1,0,1],
|
|
||||||
[1,0,0,0,0,0,1],
|
|
||||||
[1,1,1,1,1,1,1]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Top-left
|
|
||||||
this.placePattern(matrix, 0, 0, pattern);
|
|
||||||
// Top-right
|
|
||||||
this.placePattern(matrix, 0, size - 7, pattern);
|
|
||||||
// Bottom-left
|
|
||||||
this.placePattern(matrix, size - 7, 0, pattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
addTimingPatterns(matrix) {
|
|
||||||
const size = matrix.length;
|
|
||||||
|
|
||||||
// Horizontal timing pattern
|
|
||||||
for (let i = 8; i < size - 8; i++) {
|
|
||||||
matrix[6][i] = i % 2 === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical timing pattern
|
|
||||||
for (let i = 8; i < size - 8; i++) {
|
|
||||||
matrix[i][6] = i % 2 === 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addDataPattern(matrix, text) {
|
|
||||||
const size = matrix.length;
|
|
||||||
|
|
||||||
// Simple data pattern based on text hash
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
hash = ((hash << 5) - hash + text.charCodeAt(i)) & 0xffffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill available spaces with pattern based on hash
|
|
||||||
for (let row = 0; row < size; row++) {
|
|
||||||
for (let col = 0; col < size; col++) {
|
|
||||||
if (!this.isReserved(row, col, size)) {
|
|
||||||
matrix[row][col] = ((hash >> ((row + col) % 32)) & 1) === 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
placePattern(matrix, startRow, startCol, pattern) {
|
|
||||||
for (let row = 0; row < pattern.length; row++) {
|
|
||||||
for (let col = 0; col < pattern[row].length; col++) {
|
|
||||||
matrix[startRow + row][startCol + col] = pattern[row][col] === 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isReserved(row, col, size) {
|
|
||||||
// Check if position is reserved for finder patterns, timing patterns, etc.
|
|
||||||
|
|
||||||
// Finder patterns
|
|
||||||
if ((row < 9 && col < 9) || // Top-left
|
|
||||||
(row < 9 && col >= size - 8) || // Top-right
|
|
||||||
(row >= size - 8 && col < 9)) { // Bottom-left
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timing patterns
|
|
||||||
if (row === 6 || col === 6) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
createErrorSVG(size) {
|
|
||||||
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="${size}" height="${size}" fill="#f3f4f6"/>
|
|
||||||
<text x="${size/2}" y="${size/2-10}" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#6b7280">QR Code</text>
|
|
||||||
<text x="${size/2}" y="${size/2+10}" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#6b7280">Error</text>
|
|
||||||
</svg>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global function for easy access
|
|
||||||
window.generateQRCode = function(text, containerId, options = {}) {
|
|
||||||
const generator = new QRCodeGenerator();
|
|
||||||
const container = document.getElementById(containerId);
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error('Container not found:', containerId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = generator.generateSVG(text, options);
|
|
||||||
container.innerHTML = svg;
|
|
||||||
};
|
|
||||||
@@ -13,3 +13,16 @@
|
|||||||
|
|
||||||
/* Import pages */
|
/* Import pages */
|
||||||
@import "pages/home";
|
@import "pages/home";
|
||||||
|
|
||||||
|
/* QR Code Styles */
|
||||||
|
.qr-code-container {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-container svg {
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
width: 208px !important;
|
||||||
|
height: 208px !important;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|||||||
141
app/assets/stylesheets/pdf.css
Normal file
141
app/assets/stylesheets/pdf.css
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/* PDF Styles for Ticket Generation */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-container {
|
||||||
|
max-width: 350px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #2D1B69;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event name */
|
||||||
|
.event-name {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-name h2 {
|
||||||
|
color: #000000;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ticket info box */
|
||||||
|
.ticket-info-box {
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
border: 1px solid #E5E7EB;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
display: inline-block;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Venue information */
|
||||||
|
.venue-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-info h3 {
|
||||||
|
color: #374151;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-details {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-address {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QR Code */
|
||||||
|
.qr-code-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-section h3 {
|
||||||
|
color: #000000;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto 10px auto;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-text {
|
||||||
|
font-size: 8px;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid #E5E7EB;
|
||||||
|
padding-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 8px;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer p {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-date {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
@@ -188,8 +188,15 @@ class OrdersController < ApplicationController
|
|||||||
# Don't fail the payment process due to job scheduling issues
|
# Don't fail the payment process due to job scheduling issues
|
||||||
end
|
end
|
||||||
|
|
||||||
# Email confirmation is handled by the order model's mark_as_paid! method
|
# Send confirmation emails
|
||||||
# to avoid duplicate emails
|
@order.tickets.each do |ticket|
|
||||||
|
begin
|
||||||
|
TicketMailer.purchase_confirmation(ticket).deliver_now
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
||||||
|
# Don't fail the entire payment process due to email/PDF generation issues
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Clear session data
|
# Clear session data
|
||||||
session.delete(:pending_cart)
|
session.delete(:pending_cart)
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
# This controller now primarily handles legacy redirects and backward compatibility
|
# This controller now primarily handles legacy redirects and backward compatibility
|
||||||
# Most ticket creation functionality has been moved to OrdersController
|
# Most ticket creation functionality has been moved to OrdersController
|
||||||
class TicketsController < ApplicationController
|
class TicketsController < ApplicationController
|
||||||
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show ]
|
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :ticket_view, :download_ticket ]
|
||||||
before_action :set_event, only: [ :checkout, :retry_payment ]
|
before_action :set_event, only: [ :checkout, :retry_payment ]
|
||||||
|
|
||||||
|
|
||||||
# Redirect to order-based checkout
|
# Redirect to order-based checkout
|
||||||
def checkout
|
def checkout
|
||||||
# Check for draft order
|
# Check for draft order
|
||||||
@@ -48,13 +47,26 @@ class TicketsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Display informations about the event with QR code
|
# Display ticket details
|
||||||
def show
|
def show
|
||||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user)
|
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||||
.find_by(tickets: { qr_code: params[:qr_code] })
|
tickets: { id: params[:ticket_id] },
|
||||||
|
orders: { user_id: current_user.id }
|
||||||
|
)
|
||||||
|
@event = @ticket.event
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Display ticket in PDF-like format
|
||||||
|
def ticket_view
|
||||||
|
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||||
|
tickets: { id: params[:ticket_id] },
|
||||||
|
orders: { user_id: current_user.id }
|
||||||
|
)
|
||||||
|
|
||||||
if @ticket.nil?
|
if @ticket.nil?
|
||||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -63,6 +75,107 @@ class TicketsController < ApplicationController
|
|||||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Download PDF ticket - only accessible by ticket owner
|
||||||
|
# User must be authenticated to download ticket
|
||||||
|
# TODO: change ID to an unique identifier (UUID)
|
||||||
|
def download_ticket
|
||||||
|
# Find ticket and ensure it belongs to current user
|
||||||
|
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by(
|
||||||
|
tickets: { id: params[:ticket_id] },
|
||||||
|
orders: { user_id: current_user.id }
|
||||||
|
)
|
||||||
|
|
||||||
|
if @ticket.nil?
|
||||||
|
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate PDF using Grover
|
||||||
|
begin
|
||||||
|
Rails.logger.info "Starting PDF generation for ticket ID: #{@ticket.id}"
|
||||||
|
|
||||||
|
# Render the HTML template
|
||||||
|
html = render_to_string(
|
||||||
|
partial: "tickets/pdf_ticket",
|
||||||
|
layout: false,
|
||||||
|
locals: { ticket: @ticket }
|
||||||
|
)
|
||||||
|
|
||||||
|
Rails.logger.info "HTML template rendered successfully, length: #{html.length}"
|
||||||
|
|
||||||
|
# Try to load and use Grover
|
||||||
|
begin
|
||||||
|
Rails.logger.info "Attempting to load Grover gem"
|
||||||
|
|
||||||
|
# Try different approaches to load grover
|
||||||
|
begin
|
||||||
|
require "bundler"
|
||||||
|
Bundler.require(:default, Rails.env)
|
||||||
|
Rails.logger.info "Bundler required gems successfully"
|
||||||
|
rescue => bundler_error
|
||||||
|
Rails.logger.warn "Bundler require failed: #{bundler_error.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Direct path approach using bundle show
|
||||||
|
grover_gem_path = `bundle show grover`.strip
|
||||||
|
grover_path = File.join(grover_gem_path, "lib", "grover")
|
||||||
|
|
||||||
|
if File.exist?(grover_path + ".rb")
|
||||||
|
Rails.logger.info "Loading Grover from direct path: #{grover_path}"
|
||||||
|
require grover_path
|
||||||
|
else
|
||||||
|
Rails.logger.error "Grover not found at path: #{grover_path}"
|
||||||
|
raise LoadError, "Grover gem not available at expected path"
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails.logger.info "Creating Grover instance with options"
|
||||||
|
grover = Grover.new(html,
|
||||||
|
format: "A6",
|
||||||
|
margin: {
|
||||||
|
top: "10mm",
|
||||||
|
bottom: "10mm",
|
||||||
|
left: "10mm",
|
||||||
|
right: "10mm"
|
||||||
|
},
|
||||||
|
prefer_css_page_size: true,
|
||||||
|
emulate_media: "print",
|
||||||
|
cache: false,
|
||||||
|
launch_args: [ "--no-sandbox", "--disable-setuid-sandbox" ] # For better compatibility
|
||||||
|
)
|
||||||
|
Rails.logger.info "Grover instance created successfully"
|
||||||
|
|
||||||
|
pdf_content = grover.to_pdf
|
||||||
|
Rails.logger.info "PDF generated successfully, length: #{pdf_content.length}"
|
||||||
|
|
||||||
|
# Send PDF as download
|
||||||
|
send_data pdf_content,
|
||||||
|
filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf",
|
||||||
|
type: "application/pdf",
|
||||||
|
disposition: "attachment"
|
||||||
|
rescue LoadError => grover_error
|
||||||
|
Rails.logger.error "Failed to load Grover: #{grover_error.message}"
|
||||||
|
# Fallback: return HTML instead of PDF
|
||||||
|
send_data html,
|
||||||
|
filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.html",
|
||||||
|
type: "text/html",
|
||||||
|
disposition: "attachment"
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Error generating ticket PDF with Grover:"
|
||||||
|
Rails.logger.error "Message: #{e.message}"
|
||||||
|
Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}"
|
||||||
|
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound => e
|
||||||
|
Rails.logger.error "ActiveRecord::RecordNotFound error: #{e.message}"
|
||||||
|
redirect_to dashboard_path, alert: "Billet non trouvé"
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Unexpected error in download_ticket action:"
|
||||||
|
Rails.logger.error "Message: #{e.message}"
|
||||||
|
Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}"
|
||||||
|
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_event
|
def set_event
|
||||||
|
|||||||
@@ -18,6 +18,3 @@ application.register("ticket-selection", TicketSelectionController);
|
|||||||
|
|
||||||
import HeaderController from "./header_controller";
|
import HeaderController from "./header_controller";
|
||||||
application.register("header", HeaderController);
|
application.register("header", HeaderController);
|
||||||
|
|
||||||
import QrCodeController from "./qr_code_controller";
|
|
||||||
application.register("qr-code", QrCodeController);
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
// QR Code generator controller using qrcode npm package
|
|
||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
import QRCode from "qrcode"
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static values = { data: String }
|
|
||||||
static targets = ["container", "loading"]
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.generateQRCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateQRCode() {
|
|
||||||
try {
|
|
||||||
// Hide loading indicator
|
|
||||||
if (this.hasLoadingTarget) {
|
|
||||||
this.loadingTarget.style.display = 'none'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create canvas element
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
|
|
||||||
// Generate QR code using qrcode library
|
|
||||||
await QRCode.toCanvas(canvas, this.dataValue, {
|
|
||||||
width: 128,
|
|
||||||
height: 128,
|
|
||||||
margin: 1,
|
|
||||||
color: {
|
|
||||||
dark: '#000000',
|
|
||||||
light: '#FFFFFF'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Clear container and add QR code
|
|
||||||
this.containerTarget.innerHTML = ''
|
|
||||||
this.containerTarget.appendChild(canvas)
|
|
||||||
|
|
||||||
console.log('QR code generated successfully')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating QR code:', error)
|
|
||||||
this.showFallback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showFallback() {
|
|
||||||
this.containerTarget.innerHTML = `
|
|
||||||
<div class="w-32 h-32 bg-gray-100 rounded flex items-center justify-center text-gray-500 text-xs border-2 border-dashed border-gray-300">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="text-lg mb-1">📱</div>
|
|
||||||
<div>QR Code</div>
|
|
||||||
<div class="font-mono text-xs mt-1 break-all px-2">${this.dataValue}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -118,7 +118,7 @@ export default class extends Controller {
|
|||||||
await this.storeCartInSession(cartData);
|
await this.storeCartInSession(cartData);
|
||||||
|
|
||||||
// Redirect to event-scoped orders/new page
|
// Redirect to event-scoped orders/new page
|
||||||
const OrderNewUrl = `/events/${this.eventSlugValue}.${this.eventIdValue}/orders/new`;
|
const OrderNewUrl = `/orders/new/events/${this.eventSlugValue}.${this.eventIdValue}`;
|
||||||
window.location.href = OrderNewUrl;
|
window.location.href = OrderNewUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error storing cart:", error);
|
console.error("Error storing cart:", error);
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
class EventReminderJob < ApplicationJob
|
|
||||||
queue_as :default
|
|
||||||
|
|
||||||
def perform(event_id, days_before)
|
|
||||||
event = Event.find(event_id)
|
|
||||||
|
|
||||||
# Find all users with active tickets for this event
|
|
||||||
users_with_tickets = User.joins(orders: { tickets: :ticket_type })
|
|
||||||
.where(ticket_types: { event: event })
|
|
||||||
.where(tickets: { status: "active" })
|
|
||||||
.distinct
|
|
||||||
|
|
||||||
users_with_tickets.find_each do |user|
|
|
||||||
TicketMailer.event_reminder(user, event, days_before).deliver_now
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error "Failed to send event reminder to user #{user.id} for event #{event.id}: #{e.message}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
class EventReminderSchedulerJob < ApplicationJob
|
|
||||||
queue_as :default
|
|
||||||
|
|
||||||
def perform
|
|
||||||
schedule_weekly_reminders
|
|
||||||
schedule_daily_reminders
|
|
||||||
schedule_day_of_reminders
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def schedule_weekly_reminders
|
|
||||||
# Find events starting in exactly 7 days
|
|
||||||
target_date = 7.days.from_now.beginning_of_day
|
|
||||||
events = Event.published
|
|
||||||
.where(start_time: target_date..(target_date + 1.day))
|
|
||||||
|
|
||||||
events.find_each do |event|
|
|
||||||
EventReminderJob.perform_later(event.id, 7)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def schedule_daily_reminders
|
|
||||||
# Find events starting in exactly 1 day (tomorrow)
|
|
||||||
target_date = 1.day.from_now.beginning_of_day
|
|
||||||
events = Event.published
|
|
||||||
.where(start_time: target_date..(target_date + 1.day))
|
|
||||||
|
|
||||||
events.find_each do |event|
|
|
||||||
EventReminderJob.perform_later(event.id, 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def schedule_day_of_reminders
|
|
||||||
# Find events starting today
|
|
||||||
target_date = Time.current.beginning_of_day
|
|
||||||
events = Event.published
|
|
||||||
.where(start_time: target_date..(target_date + 1.day))
|
|
||||||
|
|
||||||
events.find_each do |event|
|
|
||||||
EventReminderJob.perform_later(event.id, 0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: ENV.fetch("MAILER_FROM_EMAIL", "no-reply@aperonight.fr")
|
default from: "from@example.com"
|
||||||
layout "mailer"
|
layout "mailer"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,30 +1,5 @@
|
|||||||
class TicketMailer < ApplicationMailer
|
class TicketMailer < ApplicationMailer
|
||||||
def purchase_confirmation_order(order)
|
default from: "notifications@aperonight.com"
|
||||||
@order = order
|
|
||||||
@user = order.user
|
|
||||||
@event = order.event
|
|
||||||
@tickets = order.tickets
|
|
||||||
|
|
||||||
# Generate PDF attachments for all tickets
|
|
||||||
@tickets.each do |ticket|
|
|
||||||
begin
|
|
||||||
pdf = ticket.to_pdf
|
|
||||||
attachments["ticket-#{@event.name.parameterize}-#{ticket.qr_code[0..7]}.pdf"] = {
|
|
||||||
mime_type: "application/pdf",
|
|
||||||
content: pdf
|
|
||||||
}
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error "Failed to generate PDF for ticket #{ticket.id}: #{e.message}"
|
|
||||||
# Continue without PDF attachment rather than failing the entire email
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
mail(
|
|
||||||
to: @user.email,
|
|
||||||
subject: "Confirmation d'achat - #{@event.name}",
|
|
||||||
template_name: "purchase_confirmation"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def purchase_confirmation(ticket)
|
def purchase_confirmation(ticket)
|
||||||
@ticket = ticket
|
@ticket = ticket
|
||||||
@@ -32,49 +7,15 @@ class TicketMailer < ApplicationMailer
|
|||||||
@event = ticket.event
|
@event = ticket.event
|
||||||
|
|
||||||
# Generate PDF attachment
|
# Generate PDF attachment
|
||||||
begin
|
|
||||||
pdf = @ticket.to_pdf
|
pdf = @ticket.to_pdf
|
||||||
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
||||||
mime_type: "application/pdf",
|
mime_type: "application/pdf",
|
||||||
content: pdf
|
content: pdf
|
||||||
}
|
}
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error "Failed to generate PDF for ticket #{@ticket.id}: #{e.message}"
|
|
||||||
# Continue without PDF attachment rather than failing the entire email
|
|
||||||
end
|
|
||||||
|
|
||||||
mail(
|
mail(
|
||||||
to: @user.email,
|
to: @user.email,
|
||||||
subject: "Confirmation d'achat - #{@event.name}"
|
subject: "Confirmation d'achat - #{@event.name}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def event_reminder(user, event, days_before)
|
|
||||||
@user = user
|
|
||||||
@event = event
|
|
||||||
@days_before = days_before
|
|
||||||
|
|
||||||
# Get user's tickets for this event
|
|
||||||
@tickets = Ticket.joins(:order, :ticket_type)
|
|
||||||
.where(orders: { user: @user }, ticket_types: { event: @event }, status: "active")
|
|
||||||
|
|
||||||
return if @tickets.empty?
|
|
||||||
|
|
||||||
subject = case days_before
|
|
||||||
when 7
|
|
||||||
"Rappel : #{@event.name} dans une semaine"
|
|
||||||
when 1
|
|
||||||
"Rappel : #{@event.name} demain"
|
|
||||||
when 0
|
|
||||||
"C'est aujourd'hui : #{@event.name}"
|
|
||||||
else
|
|
||||||
"Rappel : #{@event.name} dans #{days_before} jours"
|
|
||||||
end
|
|
||||||
|
|
||||||
mail(
|
|
||||||
to: @user.email,
|
|
||||||
subject: subject,
|
|
||||||
template_name: "event_reminder"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -76,16 +76,6 @@ class Order < ApplicationRecord
|
|||||||
update!(status: "paid")
|
update!(status: "paid")
|
||||||
tickets.update_all(status: "active")
|
tickets.update_all(status: "active")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Send purchase confirmation email outside the transaction
|
|
||||||
# so that payment completion isn't affected by email failures
|
|
||||||
begin
|
|
||||||
TicketMailer.purchase_confirmation_order(self).deliver_now
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error "Failed to send purchase confirmation email for order #{id}: #{e.message}"
|
|
||||||
Rails.logger.error e.backtrace.join("\n")
|
|
||||||
# Don't re-raise the error - payment should still succeed
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calculate total from tickets
|
# Calculate total from tickets
|
||||||
|
|||||||
@@ -27,6 +27,29 @@ class Ticket < ApplicationRecord
|
|||||||
TicketPdfGenerator.new(self).generate
|
TicketPdfGenerator.new(self).generate
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Generate QR code data for ticket validation
|
||||||
|
def to_qr_data
|
||||||
|
{
|
||||||
|
ticket_id: id,
|
||||||
|
qr_code: qr_code,
|
||||||
|
event_id: event&.id,
|
||||||
|
user_id: user&.id
|
||||||
|
}.compact.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate QR code as SVG
|
||||||
|
def generate_qr_svg
|
||||||
|
require "rqrcode"
|
||||||
|
qrcode = RQRCode::QRCode.new(to_qr_data)
|
||||||
|
qrcode.as_svg(
|
||||||
|
offset: 0,
|
||||||
|
color: "000",
|
||||||
|
shape_rendering: "crispEdges",
|
||||||
|
module_size: 4,
|
||||||
|
standalone: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
# Price in euros (formatted)
|
# Price in euros (formatted)
|
||||||
def price_euros
|
def price_euros
|
||||||
price_cents / 100.0
|
price_cents / 100.0
|
||||||
@@ -70,6 +93,7 @@ class Ticket < ApplicationRecord
|
|||||||
self.qr_code = "#{id || 'temp'}-#{Time.current.to_i}-#{SecureRandom.hex(4)}"
|
self.qr_code = "#{id || 'temp'}-#{Time.current.to_i}-#{SecureRandom.hex(4)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def draft?
|
def draft?
|
||||||
status == "draft"
|
status == "draft"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
require "prawn"
|
|
||||||
require "prawn/qrcode"
|
|
||||||
require "rqrcode"
|
|
||||||
|
|
||||||
# PDF ticket generator service using Prawn
|
|
||||||
#
|
|
||||||
# Generates PDF tickets with QR codes for event entry validation
|
|
||||||
# Includes event details, venue information, and unique QR code for each ticket
|
|
||||||
class TicketPdfGenerator
|
|
||||||
# Suppress Prawn's internationalization warning for built-in fonts
|
|
||||||
Prawn::Fonts::AFM.hide_m17n_warning = true
|
|
||||||
attr_reader :ticket
|
|
||||||
|
|
||||||
def initialize(ticket)
|
|
||||||
@ticket = ticket
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate
|
|
||||||
Prawn::Document.new(page_size: [ 350, 600 ], margin: 20) do |pdf|
|
|
||||||
# Header
|
|
||||||
pdf.fill_color "2D1B69"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 24
|
|
||||||
pdf.text "ApéroNight", align: :center
|
|
||||||
pdf.move_down 10
|
|
||||||
|
|
||||||
# Event name
|
|
||||||
pdf.fill_color "000000"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 18
|
|
||||||
pdf.text ticket.event.name, align: :center
|
|
||||||
pdf.move_down 20
|
|
||||||
|
|
||||||
# Ticket info box
|
|
||||||
pdf.stroke_color "E5E7EB"
|
|
||||||
pdf.fill_color "F9FAFB"
|
|
||||||
pdf.rounded_rectangle [ 0, pdf.cursor ], 310, 120, 10
|
|
||||||
pdf.fill_and_stroke
|
|
||||||
|
|
||||||
pdf.move_down 10
|
|
||||||
pdf.fill_color "000000"
|
|
||||||
pdf.font "Helvetica", size: 12
|
|
||||||
|
|
||||||
# Ticket details
|
|
||||||
pdf.text "Ticket Type:", style: :bold
|
|
||||||
pdf.text ticket.ticket_type.name
|
|
||||||
pdf.move_down 8
|
|
||||||
|
|
||||||
pdf.text "Price:", style: :bold
|
|
||||||
pdf.text "€#{ticket.price_euros}"
|
|
||||||
pdf.move_down 8
|
|
||||||
|
|
||||||
pdf.text "Date & Time:", style: :bold
|
|
||||||
pdf.text ticket.event.start_time.strftime("%B %d, %Y at %I:%M %p")
|
|
||||||
pdf.move_down 20
|
|
||||||
|
|
||||||
# Venue information
|
|
||||||
pdf.fill_color "374151"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 14
|
|
||||||
pdf.text "Venue Information"
|
|
||||||
pdf.move_down 8
|
|
||||||
|
|
||||||
pdf.font "Helvetica", size: 11
|
|
||||||
pdf.text ticket.event.venue_name, style: :bold
|
|
||||||
pdf.text ticket.event.venue_address
|
|
||||||
pdf.move_down 20
|
|
||||||
|
|
||||||
# QR Code
|
|
||||||
pdf.fill_color "000000"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 14
|
|
||||||
pdf.text "Ticket QR Code", align: :center
|
|
||||||
pdf.move_down 10
|
|
||||||
|
|
||||||
# Ensure all required data is present before generating QR code
|
|
||||||
if ticket.qr_code.blank?
|
|
||||||
raise "Ticket QR code is missing"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Build QR code data with safe association loading
|
|
||||||
qr_code_data = build_qr_code_data(ticket)
|
|
||||||
|
|
||||||
# Validate QR code data before creating QR code
|
|
||||||
if qr_code_data.blank? || qr_code_data == "{}"
|
|
||||||
Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}"
|
|
||||||
raise "QR code data is empty or invalid"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Ensure qr_code_data is a proper string for QR code generation
|
|
||||||
unless qr_code_data.is_a?(String) && qr_code_data.length > 2
|
|
||||||
Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})"
|
|
||||||
raise "QR code data must be a valid string"
|
|
||||||
end
|
|
||||||
|
|
||||||
qrcode = RQRCode::QRCode.new(qr_code_data)
|
|
||||||
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
|
||||||
|
|
||||||
pdf.move_down 15
|
|
||||||
|
|
||||||
# QR code text
|
|
||||||
pdf.font "Helvetica", size: 8
|
|
||||||
pdf.fill_color "6B7280"
|
|
||||||
pdf.text "QR Code: #{ticket.qr_code[0..7]}...", align: :center
|
|
||||||
|
|
||||||
# Footer
|
|
||||||
pdf.move_down 30
|
|
||||||
pdf.stroke_color "E5E7EB"
|
|
||||||
pdf.horizontal_line 0, 310
|
|
||||||
pdf.move_down 10
|
|
||||||
|
|
||||||
pdf.font "Helvetica", size: 8
|
|
||||||
pdf.fill_color "6B7280"
|
|
||||||
pdf.text "This ticket is valid for one entry only.", align: :center
|
|
||||||
pdf.text "Present this ticket at the venue entrance.", align: :center
|
|
||||||
pdf.move_down 5
|
|
||||||
pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}", align: :center
|
|
||||||
end.render
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def build_qr_code_data(ticket)
|
|
||||||
# Try multiple approaches to get valid QR code data
|
|
||||||
begin
|
|
||||||
# Primary approach: full JSON with all data
|
|
||||||
data = {
|
|
||||||
ticket_id: ticket.id,
|
|
||||||
qr_code: ticket.qr_code,
|
|
||||||
event_id: ticket.ticket_type&.event_id,
|
|
||||||
user_id: ticket.order&.user_id
|
|
||||||
}.compact
|
|
||||||
|
|
||||||
# Ensure we have the minimum required data
|
|
||||||
if data[:ticket_id] && data[:qr_code]
|
|
||||||
return data.to_json
|
|
||||||
end
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.warn "Failed to build complex QR data: #{e.message}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Fallback approach: just use the ticket's QR code string
|
|
||||||
begin
|
|
||||||
return ticket.qr_code.to_s if ticket.qr_code.present?
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.warn "Failed to use ticket QR code: #{e.message}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Final fallback: simple ticket identifier
|
|
||||||
"TICKET-#{ticket.id}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
11
app/views/layouts/pdf.html.erb
Normal file
11
app/views/layouts/pdf.html.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title><%= yield :title %></title>
|
||||||
|
<%= stylesheet_link_tag "pdf" %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%= yield %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
|
|
||||||
<div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #e9ecef;">
|
|
||||||
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;">ApéroNight</h1>
|
|
||||||
<p style="color: #6c757d; margin: 10px 0 0;">Rappel d'événement</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background-color: white; border-radius: 8px; padding: 30px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
||||||
<h2 style="color: #212529; margin-top: 0;">Salut <%= @user.email.split('@').first %> ! 🎉</h2>
|
|
||||||
|
|
||||||
<p style="color: #495057; line-height: 1.6; font-size: 18px;">
|
|
||||||
<% case @days_before %>
|
|
||||||
<% when 7 %>
|
|
||||||
Plus qu'une semaine avant <strong><%= @event.name %></strong> !
|
|
||||||
<% when 1 %>
|
|
||||||
C'est demain ! <strong><%= @event.name %></strong> a lieu demain.
|
|
||||||
<% when 0 %>
|
|
||||||
C'est aujourd'hui ! <strong><%= @event.name %></strong> a lieu aujourd'hui.
|
|
||||||
<% else %>
|
|
||||||
Plus que <%= @days_before %> jours avant <strong><%= @event.name %></strong> !
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
|
||||||
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de l'événement</h3>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
|
||||||
<div>
|
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">📅 Date & heure</p>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529; font-size: 16px;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-bottom: 15px;">
|
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">📍 Lieu</p>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.venue_name %></p>
|
|
||||||
<p style="margin: 5px 0 0; color: #495057;"><%= @event.venue_address %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
|
||||||
<h4 style="color: #4c1d95; margin-top: 0; margin-bottom: 15px;">Vos billets pour cet événement :</h4>
|
|
||||||
<% @tickets.each_with_index do |ticket, index| %>
|
|
||||||
<div style="border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; margin-bottom: 10px; background-color: white;">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<div>
|
|
||||||
<p style="margin: 0 0 5px; font-weight: bold; color: #212529;">🎫 Billet #<%= index + 1 %></p>
|
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;"><%= ticket.ticket_type.name %></p>
|
|
||||||
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<span style="background-color: #d4edda; color: #155724; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold;">ACTIF</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
|
||||||
<% if @days_before == 0 %>
|
|
||||||
<p style="color: #495057; margin-bottom: 20px; font-size: 16px;">🚨 N'oubliez pas vos billets ! Ils ont été envoyés par email lors de votre achat.</p>
|
|
||||||
<% else %>
|
|
||||||
<p style="color: #495057; margin-bottom: 20px;">📧 Vos billets ont été envoyés par email lors de votre achat.</p>
|
|
||||||
<% end %>
|
|
||||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-les à l'entrée de l'événement pour y accéder.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if @days_before <= 1 %>
|
|
||||||
<div style="background-color: #d1ecf1; border-radius: 6px; padding: 15px; border-left: 4px solid #17a2b8; margin: 20px 0;">
|
|
||||||
<p style="margin: 0; color: #0c5460; font-size: 14px;">
|
|
||||||
<strong>💡 Conseil :</strong> Arrivez un peu en avance pour éviter les files d'attente à l'entrée !
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<div style="background-color: #d4edda; border-radius: 6px; padding: 15px; border-left: 4px solid #28a745;">
|
|
||||||
<p style="margin: 0; color: #155724; font-size: 14px;">
|
|
||||||
<strong>📅 Ajoutez à votre calendrier :</strong> N'oubliez pas d'ajouter cet événement à votre calendrier pour ne pas le manquer !
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align: center; color: #6c757d; font-size: 14px; padding: 20px 0;">
|
|
||||||
<p style="margin: 0;">Des questions ? Contactez-nous à <a href="mailto:support@aperonight.com" style="color: #4c1d95; text-decoration: none;">support@aperonight.com</a></p>
|
|
||||||
<p style="margin: 10px 0 0;">© <%= Time.current.year %> ApéroNight. Tous droits réservés.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
Salut <%= @user.email.split('@').first %> !
|
|
||||||
|
|
||||||
<% case @days_before %>
|
|
||||||
<% when 7 %>
|
|
||||||
Plus qu'une semaine avant "<%= @event.name %>" !
|
|
||||||
<% when 1 %>
|
|
||||||
C'est demain ! "<%= @event.name %>" a lieu demain.
|
|
||||||
<% when 0 %>
|
|
||||||
C'est aujourd'hui ! "<%= @event.name %>" a lieu aujourd'hui.
|
|
||||||
<% else %>
|
|
||||||
Plus que <%= @days_before %> jours avant "<%= @event.name %>" !
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
DÉTAILS DE L'ÉVÉNEMENT
|
|
||||||
======================
|
|
||||||
|
|
||||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
|
||||||
Lieu : <%= @event.venue_name %>
|
|
||||||
Adresse : <%= @event.venue_address %>
|
|
||||||
|
|
||||||
VOS BILLETS POUR CET ÉVÉNEMENT :
|
|
||||||
<% @tickets.each_with_index do |ticket, index| %>
|
|
||||||
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> (ACTIF)
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if @days_before == 0 %>
|
|
||||||
N'oubliez pas vos billets ! Ils ont été envoyés par email lors de votre achat.
|
|
||||||
<% else %>
|
|
||||||
Vos billets ont été envoyés par email lors de votre achat.
|
|
||||||
<% end %>
|
|
||||||
Présentez-les à l'entrée de l'événement pour y accéder.
|
|
||||||
|
|
||||||
<% if @days_before <= 1 %>
|
|
||||||
Conseil : Arrivez un peu en avance pour éviter les files d'attente à l'entrée !
|
|
||||||
<% else %>
|
|
||||||
N'oubliez pas d'ajouter cet événement à votre calendrier pour ne pas le manquer !
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
Des questions ? Contactez-nous à support@aperonight.com
|
|
||||||
|
|
||||||
© <%= Time.current.year %> ApéroNight. Tous droits réservés.
|
|
||||||
@@ -1,68 +1,17 @@
|
|||||||
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
|
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
|
||||||
<div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #e9ecef;">
|
<div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #e9ecef;">
|
||||||
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;"><%= ENV.fetch("APP_NAME", "Aperonight") %></h1>
|
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;">ApéroNight</h1>
|
||||||
<p style="color: #6c757d; margin: 10px 0 0;">Confirmation de votre achat</p>
|
<p style="color: #6c757d; margin: 10px 0 0;">Confirmation de votre achat</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background-color: white; border-radius: 8px; padding: 30px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
<div style="background-color: white; border-radius: 8px; padding: 30px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||||
<% if user.first_name %>
|
|
||||||
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.first_name %>,</h2>
|
|
||||||
<% else %>
|
|
||||||
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.email.split('@').first %>,</h2>
|
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.email.split('@').first %>,</h2>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<p style="color: #495057; line-height: 1.6;">
|
<p style="color: #495057; line-height: 1.6;">
|
||||||
<% if defined?(@order) && @order.present? %>
|
|
||||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre commande pour l'événement <strong><%= @event.name %></strong>.
|
|
||||||
<% else %>
|
|
||||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement <strong><%= @event.name %></strong>.
|
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement <strong><%= @event.name %></strong>.
|
||||||
<% end %>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
||||||
<% if defined?(@order) && @order.present? %>
|
|
||||||
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre commande</h3>
|
|
||||||
|
|
||||||
<div style="margin-bottom: 20px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
|
||||||
<div>
|
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Événement</p>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.name %></p>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Date & heure</p>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
|
||||||
<div>
|
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Nombre de billets</p>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @tickets.count %></p>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;">Total</p>
|
|
||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@order.total_amount_euros, unit: "€") %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 style="color: #4c1d95; margin: 20px 0 15px;">Billets inclus :</h4>
|
|
||||||
<% @tickets.each_with_index do |ticket, index| %>
|
|
||||||
<div style="border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; margin-bottom: 10px; background-color: white;">
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
|
||||||
<div>
|
|
||||||
<p style="margin: 0 0 5px; font-weight: bold; color: #212529;">Billet #<%= index + 1 %></p>
|
|
||||||
<p style="margin: 0; color: #6c757d; font-size: 14px;"><%= ticket.ticket_type.name %></p>
|
|
||||||
<p style="margin: 5px 0 0;"><a href="<%= ticket_url(ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px;">📱 Voir le détail et le code QR</a></p>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<p style="margin: 0; font-weight: bold; color: #212529;"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% else %>
|
|
||||||
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre billet</h3>
|
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre billet</h3>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
||||||
@@ -86,31 +35,16 @@
|
|||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
|
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 15px; text-align: center;">
|
|
||||||
<a href="<%= ticket_url(@ticket) %>" style="color: #4c1d95; text-decoration: none; font-size: 14px; display: inline-block; padding: 10px 15px; border: 1px solid #4c1d95; border-radius: 6px; background-color: #f8f9fa;">📱 Voir le détail et le code QR</a>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
<% if defined?(@order) && @order.present? %>
|
|
||||||
<p style="color: #495057; margin-bottom: 20px;">Vos billets sont attachés à cet email en format PDF.</p>
|
|
||||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-les à l'entrée de l'événement pour y accéder.</p>
|
|
||||||
<% else %>
|
|
||||||
<p style="color: #495057; margin-bottom: 20px;">Votre billet est attaché à cet email en format PDF.</p>
|
<p style="color: #495057; margin-bottom: 20px;">Votre billet est attaché à cet email en format PDF.</p>
|
||||||
<p style="color: #495057; margin-bottom: 20px;">Présentez-le à l'entrée de l'événement pour y accéder.</p>
|
<p style="color: #495057; margin-bottom: 20px;">Présentez-le à l'entrée de l'événement pour y accéder.</p>
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background-color: #fff3cd; border-radius: 6px; padding: 15px; border-left: 4px solid #ffc107;">
|
<div style="background-color: #fff3cd; border-radius: 6px; padding: 15px; border-left: 4px solid #ffc107;">
|
||||||
<p style="margin: 0; color: #856404; font-size: 14px;">
|
<p style="margin: 0; color: #856404; font-size: 14px;">
|
||||||
<strong>Important :</strong>
|
<strong>Important :</strong> Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
||||||
<% if defined?(@order) && @order.present? %>
|
|
||||||
Ces billets sont valables pour une seule entrée chacun. Conservez-les précieusement.
|
|
||||||
<% else %>
|
|
||||||
Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
|
||||||
<% end %>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,5 @@
|
|||||||
<% if @user.first_name %>
|
|
||||||
Bonjour <%= @user.first_name %>,
|
|
||||||
<% else %>
|
|
||||||
Bonjour <%= @user.email.split('@').first %>,
|
Bonjour <%= @user.email.split('@').first %>,
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if defined?(@order) && @order.present? %>
|
|
||||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre commande pour l'événement "<%= @event.name %>".
|
|
||||||
|
|
||||||
DÉTAILS DE VOTRE COMMANDE
|
|
||||||
=========================
|
|
||||||
|
|
||||||
Événement : <%= @event.name %>
|
|
||||||
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
|
|
||||||
Nombre de billets : <%= @tickets.count %>
|
|
||||||
Total : <%= number_to_currency(@order.total_amount_euros, unit: "€") %>
|
|
||||||
|
|
||||||
BILLETS INCLUS :
|
|
||||||
<% @tickets.each_with_index do |ticket, index| %>
|
|
||||||
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> - <%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
Vos billets sont attachés à cet email en format PDF. Présentez-les à l'entrée de l'événement pour y accéder.
|
|
||||||
|
|
||||||
Important : Ces billets sont valables pour une seule entrée chacun. Conservez-les précieusement.
|
|
||||||
<% else %>
|
|
||||||
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement "<%= @event.name %>".
|
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement "<%= @event.name %>".
|
||||||
|
|
||||||
DÉTAILS DE VOTRE BILLET
|
DÉTAILS DE VOTRE BILLET
|
||||||
@@ -37,8 +13,7 @@ Prix : <%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>
|
|||||||
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
|
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
|
||||||
|
|
||||||
Important : Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
Important : Ce billet est valable pour une seule entrée. Conservez-le précieusement.
|
||||||
<% end %>
|
|
||||||
|
|
||||||
Si vous avez des questions, contactez-nous à support@aperonight.com
|
Si vous avez des questions, contactez-nous à support@aperonight.com
|
||||||
|
|
||||||
© <%= Time.current.year %> <%= ENV.fetch("APP_NAME", "Aperonight") %>. Tous droits réservés.
|
© <%= Time.current.year %> ApéroNight. Tous droits réservés.
|
||||||
98
app/views/tickets/_pdf_ticket.html.erb
Normal file
98
app/views/tickets/_pdf_ticket.html.erb
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Ticket #<%= ticket.id %></title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-container {
|
||||||
|
max-width: 350px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
color: #2D1B69;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-name {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-name h2 {
|
||||||
|
color: #000000;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-container svg {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ticket-container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>ApéroNight</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="event-name">
|
||||||
|
<h2><%= ticket.event.name %></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ticket-info">
|
||||||
|
<div class="info-row">
|
||||||
|
<strong>Ticket Holder:</strong> <%= ticket.first_name %> <%= ticket.last_name %>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<strong>Ticket Type:</strong> <%= ticket.ticket_type.name %>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<strong>Price:</strong> €<%= ticket.price_euros %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qr-code-section">
|
||||||
|
<div class="qr-code-container">
|
||||||
|
<%= raw ticket.generate_qr_svg %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 py-8">
|
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 py-8">
|
||||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<nav class="mb-8" aria-label="Breadcrumb">
|
<nav class="mb-8" aria-label="Breadcrumb">
|
||||||
<ol class="flex items-center space-x-2 text-sm">
|
<ol class="flex items-center space-x-2 text-sm">
|
||||||
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
<%= link_to root_path, class: "text-slate-500 hover:text-purple-600 transition-colors duration-200" do %>
|
||||||
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Accueil
|
Accueil
|
||||||
<% end %>
|
<% end %>
|
||||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= link_to dashboard_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
<%= link_to dashboard_path, class: "text-slate-500 hover:text-purple-600 transition-colors duration-200" do %>
|
||||||
Tableau de bord
|
Tableau de bord
|
||||||
<% end %>
|
<% end %>
|
||||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<li class="font-medium text-gray-900" aria-current="page">Billet #<%= @ticket.id %></li>
|
<li class="font-medium text-slate-900" aria-current="page">Billet #<%= @ticket.id %></li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-xl overflow-hidden border border-slate-200">
|
||||||
<!-- Ticket Header -->
|
<!-- Ticket Header -->
|
||||||
<div class="bg-gradient-to-r from-purple-600 to-indigo-600 px-8 py-6">
|
<div class="bg-gradient-to-r from-purple-600 to-violet-600 px-8 py-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl md:text-3xl font-bold text-white mb-2">Billet Électronique</h1>
|
<h1 class="text-2xl md:text-3xl font-bold text-white mb-2">Billet Électronique</h1>
|
||||||
@@ -33,12 +33,12 @@
|
|||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<div class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium <%=
|
<div class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium <%=
|
||||||
case @ticket.status
|
case @ticket.status
|
||||||
when 'active' then 'bg-green-100 text-green-800'
|
when 'active' then 'bg-emerald-100 text-emerald-800'
|
||||||
when 'draft' then 'bg-yellow-100 text-yellow-800'
|
when 'draft' then 'bg-amber-100 text-amber-800'
|
||||||
when 'used' then 'bg-gray-100 text-gray-800'
|
when 'used' then 'bg-slate-100 text-slate-800'
|
||||||
when 'expired' then 'bg-red-100 text-red-800'
|
when 'expired' then 'bg-red-100 text-red-800'
|
||||||
when 'refunded' then 'bg-blue-100 text-blue-800'
|
when 'refunded' then 'bg-sky-100 text-sky-800'
|
||||||
else 'bg-gray-100 text-gray-800'
|
else 'bg-slate-100 text-slate-800'
|
||||||
end %>">
|
end %>">
|
||||||
<%=
|
<%=
|
||||||
case @ticket.status
|
case @ticket.status
|
||||||
@@ -58,47 +58,49 @@
|
|||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<!-- Event Details -->
|
<!-- Event Details -->
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Détails de l'événement</h2>
|
<h2 class="text-xl font-semibold text-slate-900 mb-6">Détails de l'événement</h2>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Événement</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">Événement</label>
|
||||||
<p class="text-lg font-semibold text-gray-900"><%= @event.name %></p>
|
<p class="text-lg font-semibold text-slate-900"><%= @event.name %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Date et heure</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">Date et heure</label>
|
||||||
<div class="flex items-center text-gray-900">
|
<div class="flex items-start text-slate-900">
|
||||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2 mt-0.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= @event.start_time.strftime("%d %B %Y") %><br>
|
<div>
|
||||||
<small class="text-gray-600"><%= @event.start_time.strftime("%H:%M") %></small>
|
<div class="font-medium"><%= @event.start_time.strftime("%d %B %Y") %></div>
|
||||||
|
<div class="text-sm text-slate-600"><%= @event.start_time.strftime("%H:%M") %></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Lieu</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">Lieu</label>
|
||||||
<div class="flex items-center text-gray-900">
|
<div class="flex items-center text-slate-900">
|
||||||
<svg class="w-4 h-4 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= @event.venue_name %>
|
<span class="font-medium"><%= @event.venue_name %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Type de billet</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">Type de billet</label>
|
||||||
<p class="text-gray-900 font-medium"><%= @ticket.ticket_type.name %></p>
|
<p class="text-slate-900 font-medium mb-1"><%= @ticket.ticket_type.name %></p>
|
||||||
<p class="text-sm text-gray-600"><%= @ticket.ticket_type.description %></p>
|
<p class="text-sm text-slate-600"><%= @ticket.ticket_type.description %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Prix</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">Prix</label>
|
||||||
<p class="text-xl font-bold text-gray-900">
|
<p class="text-2xl font-bold text-slate-900">
|
||||||
<%= number_to_currency(@ticket.price_euros, unit: "€", separator: ",", delimiter: " ", format: "%n %u") %>
|
<%= number_to_currency(@ticket.price_euros, unit: "€", separator: ",", delimiter: " ", format: "%n %u") %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,40 +109,36 @@
|
|||||||
|
|
||||||
<!-- Ticket Details -->
|
<!-- Ticket Details -->
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Informations du billet</h2>
|
<h2 class="text-xl font-semibold text-slate-900 mb-6">Informations du billet</h2>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-6">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Prénom</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">Prénom</label>
|
||||||
<p class="text-gray-900 font-medium"><%= @ticket.first_name %></p>
|
<p class="text-slate-900 font-medium"><%= @ticket.first_name %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Nom</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">Nom</label>
|
||||||
<p class="text-gray-900 font-medium"><%= @ticket.last_name %></p>
|
<p class="text-slate-900 font-medium"><%= @ticket.last_name %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">Date d'achat</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">Date d'achat</label>
|
||||||
<p class="text-gray-900"><%= @ticket.created_at.strftime("%d %B %Y à %H:%M") %></p>
|
<p class="text-slate-900 font-medium"><%= @ticket.created_at.strftime("%d %B %Y à %H:%M") %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-500 mb-1">QR Code</label>
|
<label class="block text-sm font-medium text-slate-500 mb-2">QR Code</label>
|
||||||
<div class="bg-gray-50 rounded-lg p-4 text-center">
|
<div class="bg-slate-50 rounded-xl p-6 text-center border border-slate-200">
|
||||||
<div class="inline-block bg-white p-4 rounded-lg shadow-sm">
|
<div class="inline-block bg-white p-4 rounded-xl shadow-sm border border-slate-200">
|
||||||
<div data-controller="qr-code" data-qr-code-data-value="<%= @ticket.qr_code %>" class="w-32 h-32">
|
<div class="w-64 h-64 flex items-center justify-center">
|
||||||
<!-- Loading indicator -->
|
<%= raw @ticket.generate_qr_svg %>
|
||||||
<div data-qr-code-target="loading" class="w-32 h-32 bg-gray-100 rounded flex items-center justify-center">
|
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
|
||||||
</div>
|
|
||||||
<!-- QR code container -->
|
|
||||||
<div data-qr-code-target="container" class="w-32 h-32"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-2 font-mono"><%= @ticket.qr_code %></p>
|
<p class="text-xs text-slate-500 mt-3 font-mono tracking-wider"><%= @ticket.qr_code[0..7]... %></p>
|
||||||
|
<p class="text-xs text-slate-400 mt-1">Scannez ce code à l'entrée</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,21 +146,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="mt-8 pt-6 border-t border-gray-200">
|
<div class="mt-8 pt-6 border-t border-slate-200">
|
||||||
<div class="flex flex-col sm:flex-row gap-4">
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
<%= link_to dashboard_path,
|
<%= link_to dashboard_path,
|
||||||
class: "px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 text-center font-medium transition-colors duration-200" do %>
|
class: "flex items-center justify-center px-6 py-3 border border-slate-300 text-slate-700 rounded-xl hover:bg-slate-50 hover:border-slate-400 font-medium transition-all duration-200" do %>
|
||||||
<svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16l-4-4m0 0l4-4m-4 4h18"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16l-4-4m0 0l4-4m-4 4h18"/>
|
||||||
</svg>
|
</svg>
|
||||||
Retour au tableau de bord
|
Retour au tableau de bord
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if @ticket.status == 'active' %>
|
<% if @ticket.status == 'active' %>
|
||||||
<%= link_to ticket_download_path(@ticket.qr_code),
|
<%= link_to download_ticket_path(@ticket.id),
|
||||||
class: "flex-1 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5 text-center" do %>
|
class: "flex-1 flex items-center justify-center bg-gradient-to-r from-purple-600 to-violet-600 hover:from-purple-700 hover:to-violet-700 text-white font-medium py-3 px-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5" do %>
|
||||||
<svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Télécharger le PDF
|
Télécharger le PDF
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -171,17 +169,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Important Notice -->
|
<!-- Important Notice -->
|
||||||
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
<div class="mt-6 bg-sky-50 border border-sky-200 rounded-xl p-6">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<svg class="w-5 h-5 text-blue-600 mr-2 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 text-sky-600 mr-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="text-blue-800 font-medium mb-1">Informations importantes</h3>
|
<h3 class="text-sky-800 font-semibold mb-2">Informations importantes</h3>
|
||||||
<ul class="text-blue-700 text-sm space-y-1">
|
<ul class="text-sky-700 text-sm space-y-2">
|
||||||
<li>• Présentez ce billet (ou son code QR) à l'entrée de l'événement</li>
|
<li class="flex items-start">
|
||||||
<li>• Arrivez en avance pour éviter les files d'attente</li>
|
<span class="w-1.5 h-1.5 bg-sky-600 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||||
<li>• En cas de problème, contactez l'organisateur</li>
|
Présentez ce billet (ou son code QR) à l'entrée de l'événement
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="w-1.5 h-1.5 bg-sky-600 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||||
|
Arrivez en avance pour éviter les files d'attente
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<span class="w-1.5 h-1.5 bg-sky-600 rounded-full mt-2 mr-3 flex-shrink-0"></span>
|
||||||
|
En cas de problème, contactez l'organisateur
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
14
app/views/tickets/show.pdf.erb
Normal file
14
app/views/tickets/show.pdf.erb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<% content_for :title, "Ticket ##{ticket.id}" %>
|
||||||
|
|
||||||
|
<div style="font-family: Arial, sans-serif; max-width: 350px; margin: 20px auto; padding: 20px; border: 1px solid #ccc;">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<h1 style="color: #2D1B69;">ApéroNight</h1>
|
||||||
|
</div>
|
||||||
|
<h2><%= ticket.event.name %></h2>
|
||||||
|
<p>Ticket Holder: <%= ticket.first_name %> <%= ticket.last_name %></p>
|
||||||
|
<p>Ticket Type: <%= ticket.ticket_type.name %></p>
|
||||||
|
<p>Price: €<%= ticket.price_euros %></p>
|
||||||
|
<div style="text-align: center; margin-top: 20px;">
|
||||||
|
<%= raw ticket.generate_qr_svg %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
118
app/views/tickets/ticket_view.html.erb
Normal file
118
app/views/tickets/ticket_view.html.erb
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<% content_for :title, "Billet ##{@ticket.id} - #{@ticket.event.name}" %>
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-slate-100 py-8">
|
||||||
|
<div class="max-w-md mx-auto px-4">
|
||||||
|
<!-- Ticket Card -->
|
||||||
|
<div class="max-w-md bg-white rounded-xl shadow-2xl overflow-hidden mx-auto border border-slate-200">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="bg-gradient-to-r from-purple-700 to-violet-600 text-center py-6 px-6">
|
||||||
|
<h1 class="text-2xl font-bold text-white mb-2">ApéroNight</h1>
|
||||||
|
<div class="w-16 h-0.5 bg-purple-200 mx-auto rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Name -->
|
||||||
|
<div class="text-center py-4 px-6 bg-purple-50 border-b border-purple-100">
|
||||||
|
<h2 class="text-xl font-bold text-slate-900 leading-tight"><%= @ticket.event.name %></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ticket Information -->
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<!-- Ticket Holder -->
|
||||||
|
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||||
|
<span class="text-sm font-medium text-slate-600">Porteur du billet:</span>
|
||||||
|
<span class="text-sm font-semibold text-slate-900 text-right"><%= @ticket.first_name %> <%= @ticket.last_name %></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ticket Type -->
|
||||||
|
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||||
|
<span class="text-sm font-medium text-slate-600">Type de billet:</span>
|
||||||
|
<span class="text-sm font-semibold text-slate-900"><%= @ticket.ticket_type.name %></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Price -->
|
||||||
|
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||||
|
<span class="text-sm font-medium text-slate-600">Prix:</span>
|
||||||
|
<span class="text-sm font-semibold text-slate-900">
|
||||||
|
<%= number_to_currency(@ticket.price_euros, unit: "€", separator: ",", delimiter: " ", format: "%n %u") %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date & Time -->
|
||||||
|
<div class="flex justify-between items-center py-2 border-b border-slate-100">
|
||||||
|
<span class="text-sm font-medium text-slate-600">Date & Heure:</span>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-sm font-semibold text-slate-900"><%= @ticket.event.start_time.strftime("%d %B %Y") %></div>
|
||||||
|
<div class="text-xs text-slate-600"><%= @ticket.event.start_time.strftime("%H:%M") %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Venue -->
|
||||||
|
<div class="py-2 border-b border-slate-100">
|
||||||
|
<span class="text-sm font-medium text-slate-600 block mb-1">Lieu :</span>
|
||||||
|
<div class="text-sm font-semibold text-slate-900"><%= @ticket.event.venue_name %></div>
|
||||||
|
<% if @ticket.event.venue_address.present? %>
|
||||||
|
<div class="text-xs text-slate-600 mt-1"><%= @ticket.event.venue_address %></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- QR Code Section -->
|
||||||
|
<div class="bg-slate-50 p-6 text-center border-t border-slate-200">
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900 mb-4">Code QR du billet</h3>
|
||||||
|
<div class="inline-block bg-white p-6 rounded-xl shadow-sm border border-slate-200">
|
||||||
|
<div class="w-52 h-52 flex items-center justify-center qr-code-container">
|
||||||
|
<%= raw @ticket.generate_qr_svg %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-500 mt-3 font-mono tracking-wider">QR: <%= @ticket.qr_code[0..7] %>...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Notice -->
|
||||||
|
<div class="bg-slate-100 px-6 py-4 text-center border-t border-slate-200">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="text-xs text-slate-600">Ce billet est valide pour une seule entrée.</p>
|
||||||
|
<p class="text-xs text-slate-600">Présentez ce billet à l'entrée du lieu.</p>
|
||||||
|
<div class="pt-2 border-t border-slate-200">
|
||||||
|
<p class="text-xs text-slate-500">
|
||||||
|
Généré le <%= Time.current.strftime('%d %B %Y à %H:%M') %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="p-4 bg-white border-t border-slate-200">
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<%= link_to ticket_path(@ticket),
|
||||||
|
class: "flex-1 flex items-center justify-center bg-slate-100 hover:bg-slate-200 text-slate-700 py-2.5 px-3 rounded-lg text-sm font-medium transition-colors duration-200" do %>
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||||
|
</svg>
|
||||||
|
Vue détaillée
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @ticket.status == 'active' %>
|
||||||
|
<%= link_to download_ticket_path(@ticket.id),
|
||||||
|
class: "flex-1 flex items-center justify-center bg-purple-600 hover:bg-purple-700 text-white py-2.5 px-3 rounded-lg text-sm font-medium transition-colors duration-200 shadow-sm hover:shadow-md" do %>
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
|
</svg>
|
||||||
|
PDF
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<div class="text-center mt-6">
|
||||||
|
<%= link_to dashboard_path, class: "inline-flex items-center text-purple-600 hover:text-purple-800 text-sm font-medium transition-colors duration-200" do %>
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7 16l-4-4m0 0l4-4m-4 4h18"/>
|
||||||
|
</svg>
|
||||||
|
Retour au tableau de bord
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Disable view annotations for mailer templates to prevent HTML comments
|
|
||||||
# from breaking email formatting in development mode
|
|
||||||
if Rails.env.development?
|
|
||||||
Rails.application.configure do
|
|
||||||
# Override the annotation setting for ActionMailer specifically
|
|
||||||
config.to_prepare do
|
|
||||||
ActionMailer::Base.prepend(Module.new do
|
|
||||||
def mail(headers = {}, &block)
|
|
||||||
# Temporarily disable view annotations during email rendering
|
|
||||||
original_setting = ActionView::Base.annotate_rendered_view_with_filenames
|
|
||||||
ActionView::Base.annotate_rendered_view_with_filenames = false
|
|
||||||
|
|
||||||
result = super(headers, &block)
|
|
||||||
|
|
||||||
# Restore original setting
|
|
||||||
ActionView::Base.annotate_rendered_view_with_filenames = original_setting
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Schedule event reminder notifications
|
|
||||||
Rails.application.config.after_initialize do
|
|
||||||
# Only schedule in production or when SCHEDULE_REMINDERS is set
|
|
||||||
if Rails.env.production? || ENV["SCHEDULE_REMINDERS"] == "true"
|
|
||||||
# Schedule the reminder scheduler to run daily at 9 AM
|
|
||||||
begin
|
|
||||||
# Use a simple cron-like approach with ActiveJob
|
|
||||||
# This will be handled by solid_queue in production
|
|
||||||
EventReminderSchedulerJob.set(wait_until: next_run_time).perform_later
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.warn "Could not schedule event reminders: #{e.message}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_run_time
|
|
||||||
# Schedule for 9 AM today, or 9 AM tomorrow if it's already past 9 AM
|
|
||||||
target_time = Time.current.beginning_of_day + 9.hours
|
|
||||||
target_time += 1.day if Time.current > target_time
|
|
||||||
target_time
|
|
||||||
end
|
|
||||||
@@ -38,9 +38,9 @@ Rails.application.routes.draw do
|
|||||||
get "events", to: "events#index", as: "events"
|
get "events", to: "events#index", as: "events"
|
||||||
get "events/:slug.:id", to: "events#show", as: "event"
|
get "events/:slug.:id", to: "events#show", as: "event"
|
||||||
|
|
||||||
# === Orders ===
|
# === Orders (scoped to events) ===
|
||||||
get "events/:slug.:id/orders/new", to: "orders#new", as: "event_order_new"
|
get "orders/new/events/:slug.:id", to: "orders#new", as: "event_order_new"
|
||||||
post "events/:slug.:id/orders", to: "orders#create", as: "event_order_create"
|
post "orders/create/events/:slug.:id", to: "orders#create", as: "event_order_create"
|
||||||
|
|
||||||
resources :orders, only: [ :show ] do
|
resources :orders, only: [ :show ] do
|
||||||
member do
|
member do
|
||||||
@@ -53,15 +53,16 @@ Rails.application.routes.draw do
|
|||||||
get "orders/payments/success", to: "orders#payment_success", as: "order_payment_success"
|
get "orders/payments/success", to: "orders#payment_success", as: "order_payment_success"
|
||||||
get "orders/payments/cancel", to: "orders#payment_cancel", as: "order_payment_cancel"
|
get "orders/payments/cancel", to: "orders#payment_cancel", as: "order_payment_cancel"
|
||||||
|
|
||||||
# Legacy ticket routes - redirect to order system
|
# legacy routes
|
||||||
get "events/:slug.:id/tickets/checkout", to: "tickets#checkout", as: "ticket_checkout"
|
|
||||||
post "events/:slug.:id/tickets/retry", to: "tickets#retry_payment", as: "ticket_retry_payment"
|
|
||||||
get "payments/success", to: "tickets#payment_success", as: "payment_success"
|
get "payments/success", to: "tickets#payment_success", as: "payment_success"
|
||||||
get "payments/cancel", to: "tickets#payment_cancel", as: "payment_cancel"
|
get "payments/cancel", to: "tickets#payment_cancel", as: "payment_cancel"
|
||||||
|
|
||||||
# === Tickets ===
|
# === Tickets ===
|
||||||
get "tickets/:qr_code", to: "tickets#show", as: "ticket"
|
get "tickets/checkout/events/:slug.:id", to: "tickets#checkout", as: "ticket_checkout"
|
||||||
get "tickets/:qr_code/download", to: "events#download_ticket", as: "ticket_download"
|
post "tickets/retry/events/:slug.:id", to: "tickets#retry_payment", as: "ticket_retry_payment"
|
||||||
|
get "tickets/:ticket_id", to: "tickets#show", as: "ticket"
|
||||||
|
get "tickets/:ticket_id/view", to: "tickets#ticket_view", as: "ticket_view"
|
||||||
|
get "tickets/:ticket_id/download", to: "tickets#download_ticket", as: "download_ticket"
|
||||||
|
|
||||||
# === Promoter Routes ===
|
# === Promoter Routes ===
|
||||||
namespace :promoter do
|
namespace :promoter do
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ services:
|
|||||||
|
|
||||||
mailhog:
|
mailhog:
|
||||||
image: corpusops/mailhog:v1.0.1
|
image: corpusops/mailhog:v1.0.1
|
||||||
restart: unless-stopped
|
|
||||||
# environment:
|
# environment:
|
||||||
# - "mh_auth_file=/opt/mailhog/passwd.conf"
|
# - "mh_auth_file=/opt/mailhog/passwd.conf"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,162 +0,0 @@
|
|||||||
# Email Notifications System
|
|
||||||
|
|
||||||
This document describes the email notifications system implemented for ApéroNight.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The email notifications system provides two main types of notifications:
|
|
||||||
1. **Purchase Confirmation Emails** - Sent when orders are completed
|
|
||||||
2. **Event Reminder Emails** - Sent at scheduled intervals before events
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
### Purchase Confirmation Emails
|
|
||||||
|
|
||||||
- **Trigger**: Automatically sent when an order is marked as paid
|
|
||||||
- **Content**: Order details, ticket information, PDF attachments for each ticket
|
|
||||||
- **Template**: Supports both single tickets and multi-ticket orders
|
|
||||||
- **Languages**: French (can be extended)
|
|
||||||
|
|
||||||
### Event Reminder Emails
|
|
||||||
|
|
||||||
- **Schedule**: 7 days before, 1 day before, and day of event
|
|
||||||
- **Content**: Event details, user's ticket information, venue information
|
|
||||||
- **Recipients**: Only users with active tickets for the event
|
|
||||||
- **Smart Content**: Different messaging based on time until event
|
|
||||||
|
|
||||||
## Technical Implementation
|
|
||||||
|
|
||||||
### Mailer Classes
|
|
||||||
|
|
||||||
#### TicketMailer
|
|
||||||
- `purchase_confirmation_order(order)` - For complete orders with multiple tickets
|
|
||||||
- `purchase_confirmation(ticket)` - For individual tickets
|
|
||||||
- `event_reminder(user, event, days_before)` - For event reminders
|
|
||||||
|
|
||||||
### Background Jobs
|
|
||||||
|
|
||||||
#### EventReminderJob
|
|
||||||
- Sends reminder emails to all users with active tickets for a specific event
|
|
||||||
- Parameters: `event_id`, `days_before`
|
|
||||||
- Error handling: Logs failures but continues processing other users
|
|
||||||
|
|
||||||
#### EventReminderSchedulerJob
|
|
||||||
- Runs daily to schedule reminder emails
|
|
||||||
- Automatically finds events starting in 7 days, 1 day, or same day
|
|
||||||
- Only processes published events
|
|
||||||
- Configurable via environment variables
|
|
||||||
|
|
||||||
### Email Templates
|
|
||||||
|
|
||||||
Templates are available in both HTML and text formats:
|
|
||||||
|
|
||||||
- `app/views/ticket_mailer/purchase_confirmation.html.erb`
|
|
||||||
- `app/views/ticket_mailer/purchase_confirmation.text.erb`
|
|
||||||
- `app/views/ticket_mailer/event_reminder.html.erb`
|
|
||||||
- `app/views/ticket_mailer/event_reminder.text.erb`
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
#### Environment Variables
|
|
||||||
- `MAILER_FROM_EMAIL` - From address for emails (default: no-reply@aperonight.fr)
|
|
||||||
- `SMTP_*` - SMTP configuration for production
|
|
||||||
- `SCHEDULE_REMINDERS` - Enable automatic reminder scheduling in non-production
|
|
||||||
|
|
||||||
#### Development Setup
|
|
||||||
- Uses localhost:1025 for development (MailCatcher recommended)
|
|
||||||
- Email delivery is configured but won't raise errors in development
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# Test purchase confirmation
|
|
||||||
order = Order.last
|
|
||||||
TicketMailer.purchase_confirmation_order(order).deliver_now
|
|
||||||
|
|
||||||
# Test event reminder
|
|
||||||
user = User.first
|
|
||||||
event = Event.published.first
|
|
||||||
TicketMailer.event_reminder(user, event, 7).deliver_now
|
|
||||||
|
|
||||||
# Test scheduler job
|
|
||||||
EventReminderSchedulerJob.perform_now
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration in Code
|
|
||||||
|
|
||||||
Purchase confirmation emails are automatically sent when orders are marked as paid:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
order.mark_as_paid! # Automatically sends confirmation email
|
|
||||||
```
|
|
||||||
|
|
||||||
Event reminders are automatically scheduled via the initializer, but can be manually triggered:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# Schedule reminders for a specific event
|
|
||||||
EventReminderJob.perform_later(event.id, 7) # 7 days before
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Notes
|
|
||||||
|
|
||||||
### Production Configuration
|
|
||||||
|
|
||||||
1. Configure SMTP settings via environment variables
|
|
||||||
2. Set `MAILER_FROM_EMAIL` to your domain
|
|
||||||
3. Ensure `SCHEDULE_REMINDERS=true` to enable automatic reminders
|
|
||||||
4. Configure solid_queue for background job processing
|
|
||||||
|
|
||||||
### Monitoring
|
|
||||||
|
|
||||||
- Check logs for email delivery failures
|
|
||||||
- Monitor job queue for stuck reminder jobs
|
|
||||||
- Verify SMTP configuration is working
|
|
||||||
|
|
||||||
### Customization
|
|
||||||
|
|
||||||
- Email templates can be customized in `app/views/ticket_mailer/`
|
|
||||||
- Add new reminder intervals by modifying `EventReminderSchedulerJob`
|
|
||||||
- Internationalization can be added using Rails I18n
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
app/
|
|
||||||
├── jobs/
|
|
||||||
│ ├── event_reminder_job.rb
|
|
||||||
│ └── event_reminder_scheduler_job.rb
|
|
||||||
├── mailers/
|
|
||||||
│ ├── application_mailer.rb
|
|
||||||
│ └── ticket_mailer.rb
|
|
||||||
└── views/
|
|
||||||
└── ticket_mailer/
|
|
||||||
├── purchase_confirmation.html.erb
|
|
||||||
├── purchase_confirmation.text.erb
|
|
||||||
├── event_reminder.html.erb
|
|
||||||
└── event_reminder.text.erb
|
|
||||||
|
|
||||||
config/
|
|
||||||
├── environments/
|
|
||||||
│ ├── development.rb (SMTP localhost:1025)
|
|
||||||
│ └── production.rb (ENV-based SMTP)
|
|
||||||
└── initializers/
|
|
||||||
└── event_reminder_scheduler.rb
|
|
||||||
|
|
||||||
test/
|
|
||||||
├── jobs/
|
|
||||||
│ ├── event_reminder_job_test.rb
|
|
||||||
│ └── event_reminder_scheduler_job_test.rb
|
|
||||||
├── mailers/
|
|
||||||
│ └── ticket_mailer_test.rb
|
|
||||||
└── integration/
|
|
||||||
└── email_notifications_integration_test.rb
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- No sensitive information in email templates
|
|
||||||
- User data is properly escaped in templates
|
|
||||||
- QR codes contain only necessary ticket verification data
|
|
||||||
- Email addresses are validated through Devise
|
|
||||||
742
package-lock.json
generated
742
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -11,7 +11,7 @@
|
|||||||
"@hotwired/turbo-rails": "^8.0.13",
|
"@hotwired/turbo-rails": "^8.0.13",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"lucide": "^0.542.0",
|
"lucide": "^0.542.0",
|
||||||
"qrcode": "^1.5.4",
|
"puppeteer": "^24.19.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
@@ -32,5 +32,21 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
}
|
},
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "ecosystem.config.js",
|
||||||
|
"directories": {
|
||||||
|
"doc": "docs",
|
||||||
|
"lib": "lib",
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "ssh://git@gitea.cyanet.fr:2222/kbe/aperonight.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class EmailNotificationsIntegrationTest < ActionDispatch::IntegrationTest
|
|
||||||
include ActiveJob::TestHelper
|
|
||||||
|
|
||||||
def setup
|
|
||||||
@user = User.create!(
|
|
||||||
email: "test@example.com",
|
|
||||||
password: "password123",
|
|
||||||
first_name: "Test",
|
|
||||||
last_name: "User"
|
|
||||||
)
|
|
||||||
|
|
||||||
@event = Event.create!(
|
|
||||||
name: "Test Event",
|
|
||||||
slug: "test-event",
|
|
||||||
description: "A test event for integration testing",
|
|
||||||
state: :published,
|
|
||||||
venue_name: "Test Venue",
|
|
||||||
venue_address: "123 Test Street",
|
|
||||||
latitude: 40.7128,
|
|
||||||
longitude: -74.0060,
|
|
||||||
start_time: 1.week.from_now,
|
|
||||||
end_time: 1.week.from_now + 4.hours,
|
|
||||||
user: @user
|
|
||||||
)
|
|
||||||
|
|
||||||
@ticket_type = TicketType.create!(
|
|
||||||
name: "General Admission",
|
|
||||||
description: "General admission ticket",
|
|
||||||
price_cents: 2500,
|
|
||||||
quantity: 100,
|
|
||||||
sale_start_at: 1.day.ago,
|
|
||||||
sale_end_at: 1.day.from_now,
|
|
||||||
event: @event
|
|
||||||
)
|
|
||||||
|
|
||||||
@order = Order.create!(
|
|
||||||
user: @user,
|
|
||||||
event: @event,
|
|
||||||
status: "draft",
|
|
||||||
total_amount_cents: 2500,
|
|
||||||
payment_attempts: 0
|
|
||||||
)
|
|
||||||
|
|
||||||
@ticket = Ticket.create!(
|
|
||||||
order: @order,
|
|
||||||
ticket_type: @ticket_type,
|
|
||||||
first_name: "Test",
|
|
||||||
last_name: "User",
|
|
||||||
price_cents: 2500,
|
|
||||||
status: "draft"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends purchase confirmation email when order is marked as paid" do
|
|
||||||
# Mock PDF generation to avoid QR code issues
|
|
||||||
@ticket.stubs(:to_pdf).returns("fake_pdf_content")
|
|
||||||
|
|
||||||
assert_emails 1 do
|
|
||||||
@order.mark_as_paid!
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal "paid", @order.status
|
|
||||||
assert_equal "active", @ticket.reload.status
|
|
||||||
end
|
|
||||||
|
|
||||||
test "event reminder email can be sent to users with active tickets" do
|
|
||||||
# Setup: mark order as paid and activate tickets
|
|
||||||
@ticket.stubs(:to_pdf).returns("fake_pdf_content")
|
|
||||||
@order.mark_as_paid!
|
|
||||||
|
|
||||||
# Clear any emails from the setup
|
|
||||||
ActionMailer::Base.deliveries.clear
|
|
||||||
|
|
||||||
assert_emails 1 do
|
|
||||||
TicketMailer.event_reminder(@user, @event, 7).deliver_now
|
|
||||||
end
|
|
||||||
|
|
||||||
email = ActionMailer::Base.deliveries.last
|
|
||||||
assert_equal [ @user.email ], email.to
|
|
||||||
assert_equal "Rappel : #{@event.name} dans une semaine", email.subject
|
|
||||||
end
|
|
||||||
|
|
||||||
test "event reminder job schedules emails for users with tickets" do
|
|
||||||
# Setup: mark order as paid and activate tickets
|
|
||||||
@ticket.stubs(:to_pdf).returns("fake_pdf_content")
|
|
||||||
@order.mark_as_paid!
|
|
||||||
|
|
||||||
# Clear any emails from the setup
|
|
||||||
ActionMailer::Base.deliveries.clear
|
|
||||||
|
|
||||||
# Perform the job
|
|
||||||
EventReminderJob.perform_now(@event.id, 7)
|
|
||||||
|
|
||||||
assert_equal 1, ActionMailer::Base.deliveries.size
|
|
||||||
email = ActionMailer::Base.deliveries.last
|
|
||||||
assert_equal [ @user.email ], email.to
|
|
||||||
assert_match "une semaine", email.subject
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class EventReminderJobTest < ActiveJob::TestCase
|
|
||||||
def setup
|
|
||||||
@event = events(:concert_event)
|
|
||||||
@user = users(:one)
|
|
||||||
@ticket = tickets(:one)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "performs event reminder job for users with tickets" do
|
|
||||||
# Mock the mailer to avoid actual email sending in tests
|
|
||||||
TicketMailer.expects(:event_reminder).with(@user, @event, 7).returns(stub(deliver_now: true))
|
|
||||||
|
|
||||||
EventReminderJob.perform_now(@event.id, 7)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "handles missing event gracefully" do
|
|
||||||
assert_raises(ActiveRecord::RecordNotFound) do
|
|
||||||
EventReminderJob.perform_now(999999, 7)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "logs error when mailer fails" do
|
|
||||||
# Mock a failing mailer
|
|
||||||
TicketMailer.stubs(:event_reminder).raises(StandardError.new("Test error"))
|
|
||||||
|
|
||||||
Rails.logger.expects(:error).with(regexp_matches(/Failed to send event reminder/))
|
|
||||||
|
|
||||||
EventReminderJob.perform_now(@event.id, 7)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class EventReminderSchedulerJobTest < ActiveJob::TestCase
|
|
||||||
def setup
|
|
||||||
@event = events(:concert_event)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "schedules weekly reminders for events starting in 7 days" do
|
|
||||||
# Set event to start in exactly 7 days
|
|
||||||
@event.update(start_time: 7.days.from_now.beginning_of_day + 10.hours)
|
|
||||||
|
|
||||||
assert_enqueued_with(job: EventReminderJob, args: [ @event.id, 7 ]) do
|
|
||||||
EventReminderSchedulerJob.perform_now
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "schedules daily reminders for events starting tomorrow" do
|
|
||||||
# Set event to start tomorrow
|
|
||||||
@event.update(start_time: 1.day.from_now.beginning_of_day + 20.hours)
|
|
||||||
|
|
||||||
assert_enqueued_with(job: EventReminderJob, args: [ @event.id, 1 ]) do
|
|
||||||
EventReminderSchedulerJob.perform_now
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "schedules day-of reminders for events starting today" do
|
|
||||||
# Set event to start today
|
|
||||||
@event.update(start_time: Time.current.beginning_of_day + 21.hours)
|
|
||||||
|
|
||||||
assert_enqueued_with(job: EventReminderJob, args: [ @event.id, 0 ]) do
|
|
||||||
EventReminderSchedulerJob.perform_now
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not schedule reminders for draft events" do
|
|
||||||
@event.update(state: :draft, start_time: 7.days.from_now.beginning_of_day + 10.hours)
|
|
||||||
|
|
||||||
assert_no_enqueued_jobs(only: EventReminderJob) do
|
|
||||||
EventReminderSchedulerJob.perform_now
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not schedule reminders for cancelled events" do
|
|
||||||
@event.update(state: :canceled, start_time: 7.days.from_now.beginning_of_day + 10.hours)
|
|
||||||
|
|
||||||
assert_no_enqueued_jobs(only: EventReminderJob) do
|
|
||||||
EventReminderSchedulerJob.perform_now
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class TicketMailerTest < ActionMailer::TestCase
|
|
||||||
def setup
|
|
||||||
@user = users(:one)
|
|
||||||
@event = events(:concert_event)
|
|
||||||
@ticket_type = ticket_types(:standard)
|
|
||||||
@order = orders(:paid_order)
|
|
||||||
@ticket = tickets(:one)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "purchase confirmation order email" do
|
|
||||||
# Mock PDF generation for all tickets
|
|
||||||
@order.tickets.each do |ticket|
|
|
||||||
ticket.stubs(:to_pdf).returns("fake_pdf_data")
|
|
||||||
end
|
|
||||||
|
|
||||||
email = TicketMailer.purchase_confirmation_order(@order)
|
|
||||||
|
|
||||||
assert_emails 1 do
|
|
||||||
email.deliver_now
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal [ "no-reply@aperonight.fr" ], email.from
|
|
||||||
assert_equal [ @user.email ], email.to
|
|
||||||
assert_equal "Confirmation d'achat - #{@event.name}", email.subject
|
|
||||||
assert_match @event.name, email.body.to_s
|
|
||||||
assert_match @user.email.split("@").first, email.body.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
test "purchase confirmation single ticket email" do
|
|
||||||
# Mock PDF generation
|
|
||||||
@ticket.stubs(:to_pdf).returns("fake_pdf_data")
|
|
||||||
|
|
||||||
email = TicketMailer.purchase_confirmation(@ticket)
|
|
||||||
|
|
||||||
assert_emails 1 do
|
|
||||||
email.deliver_now
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal [ "no-reply@aperonight.fr" ], email.from
|
|
||||||
assert_equal [ @ticket.user.email ], email.to
|
|
||||||
assert_equal "Confirmation d'achat - #{@ticket.event.name}", email.subject
|
|
||||||
assert_match @ticket.event.name, email.body.to_s
|
|
||||||
assert_match @ticket.user.email.split("@").first, email.body.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
test "event reminder email one week before" do
|
|
||||||
# Ensure the user has active tickets for the event by using the existing fixtures
|
|
||||||
# The 'one' ticket fixture is already linked to the 'paid_order' and 'concert_event'
|
|
||||||
email = TicketMailer.event_reminder(@user, @event, 7)
|
|
||||||
|
|
||||||
# Only test delivery if the user has tickets (the method returns early if not)
|
|
||||||
if email
|
|
||||||
assert_emails 1 do
|
|
||||||
email.deliver_now
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal [ "no-reply@aperonight.fr" ], email.from
|
|
||||||
assert_equal [ @user.email ], email.to
|
|
||||||
assert_equal "Rappel : #{@event.name} dans une semaine", email.subject
|
|
||||||
assert_match "une semaine", email.body.to_s
|
|
||||||
assert_match @event.name, email.body.to_s
|
|
||||||
else
|
|
||||||
# If no email is sent, that's expected behavior when user has no active tickets
|
|
||||||
assert_no_emails do
|
|
||||||
TicketMailer.event_reminder(@user, @event, 7)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "event reminder email one day before" do
|
|
||||||
email = TicketMailer.event_reminder(@user, @event, 1)
|
|
||||||
|
|
||||||
assert_emails 1 do
|
|
||||||
email.deliver_now
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal "Rappel : #{@event.name} demain", email.subject
|
|
||||||
assert_match "demain", email.body.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
test "event reminder email day of event" do
|
|
||||||
email = TicketMailer.event_reminder(@user, @event, 0)
|
|
||||||
|
|
||||||
assert_emails 1 do
|
|
||||||
email.deliver_now
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal "C'est aujourd'hui : #{@event.name}", email.subject
|
|
||||||
assert_match "aujourd'hui", email.body.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
test "event reminder email custom days" do
|
|
||||||
email = TicketMailer.event_reminder(@user, @event, 3)
|
|
||||||
|
|
||||||
assert_emails 1 do
|
|
||||||
email.deliver_now
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal "Rappel : #{@event.name} dans 3 jours", email.subject
|
|
||||||
assert_match "3 jours", email.body.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class OrderEmailTest < ActiveSupport::TestCase
|
|
||||||
def setup
|
|
||||||
@order = orders(:draft_order)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends purchase confirmation email when order is marked as paid" do
|
|
||||||
# Mock the mailer to capture the call
|
|
||||||
TicketMailer.expects(:purchase_confirmation_order).with(@order).returns(stub(deliver_now: true))
|
|
||||||
|
|
||||||
@order.mark_as_paid!
|
|
||||||
|
|
||||||
assert_equal "paid", @order.status
|
|
||||||
end
|
|
||||||
|
|
||||||
test "activates all tickets when order is marked as paid" do
|
|
||||||
@order.tickets.update_all(status: "reserved")
|
|
||||||
|
|
||||||
# Mock the mailer to avoid actual email sending
|
|
||||||
TicketMailer.stubs(:purchase_confirmation_order).returns(stub(deliver_now: true))
|
|
||||||
|
|
||||||
@order.mark_as_paid!
|
|
||||||
|
|
||||||
assert @order.tickets.all? { |ticket| ticket.status == "active" }
|
|
||||||
end
|
|
||||||
|
|
||||||
test "email sending failure does not prevent order completion" do
|
|
||||||
# Mock mailer to raise an error
|
|
||||||
TicketMailer.stubs(:purchase_confirmation_order).raises(StandardError.new("Email error"))
|
|
||||||
|
|
||||||
# Should not raise error - email failure is logged but doesn't fail the payment
|
|
||||||
@order.mark_as_paid!
|
|
||||||
|
|
||||||
# Order should still be marked as paid even if email fails
|
|
||||||
assert_equal "paid", @order.reload.status
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class TicketPdfGeneratorTest < ActiveSupport::TestCase
|
|
||||||
def setup
|
|
||||||
# Stub QR code generation to avoid dependency issues
|
|
||||||
mock_qrcode = mock("qrcode")
|
|
||||||
mock_qrcode.stubs(:modules).returns([])
|
|
||||||
RQRCode::QRCode.stubs(:new).returns(mock_qrcode)
|
|
||||||
|
|
||||||
@user = User.create!(
|
|
||||||
email: "test@example.com",
|
|
||||||
password: "password123",
|
|
||||||
password_confirmation: "password123"
|
|
||||||
)
|
|
||||||
|
|
||||||
@event = Event.create!(
|
|
||||||
name: "Test Event",
|
|
||||||
slug: "test-event",
|
|
||||||
description: "A valid description for the test event that is long enough",
|
|
||||||
latitude: 48.8566,
|
|
||||||
longitude: 2.3522,
|
|
||||||
venue_name: "Test Venue",
|
|
||||||
venue_address: "123 Test Street",
|
|
||||||
user: @user,
|
|
||||||
start_time: 1.week.from_now,
|
|
||||||
end_time: 1.week.from_now + 3.hours,
|
|
||||||
state: :published
|
|
||||||
)
|
|
||||||
|
|
||||||
@ticket_type = TicketType.create!(
|
|
||||||
name: "General Admission",
|
|
||||||
description: "General admission tickets with full access to the event",
|
|
||||||
price_cents: 2500,
|
|
||||||
quantity: 100,
|
|
||||||
sale_start_at: Time.current,
|
|
||||||
sale_end_at: @event.start_time - 1.hour,
|
|
||||||
requires_id: false,
|
|
||||||
event: @event
|
|
||||||
)
|
|
||||||
|
|
||||||
@order = Order.create!(
|
|
||||||
user: @user,
|
|
||||||
event: @event,
|
|
||||||
status: "paid",
|
|
||||||
total_amount_cents: 2500
|
|
||||||
)
|
|
||||||
|
|
||||||
@ticket = Ticket.create!(
|
|
||||||
order: @order,
|
|
||||||
ticket_type: @ticket_type,
|
|
||||||
status: "active",
|
|
||||||
first_name: "John",
|
|
||||||
last_name: "Doe",
|
|
||||||
qr_code: "test-qr-code-123"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# === Initialization Tests ===
|
|
||||||
|
|
||||||
test "should initialize with ticket" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
assert_equal @ticket, generator.ticket
|
|
||||||
end
|
|
||||||
|
|
||||||
# === PDF Generation Tests ===
|
|
||||||
|
|
||||||
test "should generate PDF for valid ticket" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
pdf_string = generator.generate
|
|
||||||
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert_kind_of String, pdf_string
|
|
||||||
assert pdf_string.length > 0
|
|
||||||
|
|
||||||
# Check if it starts with PDF header
|
|
||||||
assert pdf_string.start_with?("%PDF")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should include event name in PDF" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
|
|
||||||
# Test that PDF generates successfully
|
|
||||||
pdf_string = generator.generate
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.start_with?("%PDF")
|
|
||||||
assert pdf_string.length > 1000, "PDF should be substantial in size"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should include ticket type information in PDF" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
pdf_string = generator.generate
|
|
||||||
|
|
||||||
# Basic check that PDF was generated - actual content validation
|
|
||||||
# would require parsing the PDF which is complex
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.length > 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should include price information in PDF" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
pdf_string = generator.generate
|
|
||||||
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.length > 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should include venue information in PDF" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
pdf_string = generator.generate
|
|
||||||
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.length > 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should include QR code in PDF" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
|
|
||||||
# Just test that PDF generates successfully
|
|
||||||
pdf_string = generator.generate
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.length > 0
|
|
||||||
assert pdf_string.start_with?("%PDF")
|
|
||||||
end
|
|
||||||
|
|
||||||
# === Error Handling Tests ===
|
|
||||||
|
|
||||||
test "should raise error when QR code is blank" do
|
|
||||||
# Create ticket with blank QR code (skip validations)
|
|
||||||
ticket_with_blank_qr = Ticket.new(
|
|
||||||
order: @order,
|
|
||||||
ticket_type: @ticket_type,
|
|
||||||
status: "active",
|
|
||||||
first_name: "John",
|
|
||||||
last_name: "Doe",
|
|
||||||
price_cents: 2500,
|
|
||||||
qr_code: ""
|
|
||||||
)
|
|
||||||
ticket_with_blank_qr.save(validate: false)
|
|
||||||
|
|
||||||
generator = TicketPdfGenerator.new(ticket_with_blank_qr)
|
|
||||||
|
|
||||||
error = assert_raises(RuntimeError) do
|
|
||||||
generator.generate
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal "Ticket QR code is missing", error.message
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should raise error when QR code is nil" do
|
|
||||||
# Create ticket with nil QR code (skip validations)
|
|
||||||
ticket_with_nil_qr = Ticket.new(
|
|
||||||
order: @order,
|
|
||||||
ticket_type: @ticket_type,
|
|
||||||
status: "active",
|
|
||||||
first_name: "John",
|
|
||||||
last_name: "Doe",
|
|
||||||
price_cents: 2500,
|
|
||||||
qr_code: nil
|
|
||||||
)
|
|
||||||
ticket_with_nil_qr.save(validate: false)
|
|
||||||
|
|
||||||
generator = TicketPdfGenerator.new(ticket_with_nil_qr)
|
|
||||||
|
|
||||||
error = assert_raises(RuntimeError) do
|
|
||||||
generator.generate
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal "Ticket QR code is missing", error.message
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should handle missing event gracefully in QR data" do
|
|
||||||
# Create ticket with minimal data but valid QR code
|
|
||||||
orphaned_ticket = Ticket.new(
|
|
||||||
order: @order,
|
|
||||||
ticket_type: @ticket_type,
|
|
||||||
status: "active",
|
|
||||||
first_name: "John",
|
|
||||||
last_name: "Doe",
|
|
||||||
price_cents: 2500,
|
|
||||||
qr_code: "test-qr-code-orphaned"
|
|
||||||
)
|
|
||||||
orphaned_ticket.save(validate: false)
|
|
||||||
|
|
||||||
generator = TicketPdfGenerator.new(orphaned_ticket)
|
|
||||||
|
|
||||||
# Should still generate PDF
|
|
||||||
pdf_string = generator.generate
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.length > 0
|
|
||||||
assert pdf_string.start_with?("%PDF")
|
|
||||||
end
|
|
||||||
|
|
||||||
# === QR Code Data Tests ===
|
|
||||||
|
|
||||||
test "should generate correct QR code data" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
|
|
||||||
# Just test that PDF generates successfully with QR data
|
|
||||||
pdf_string = generator.generate
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.start_with?("%PDF")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should compact QR code data removing nils" do
|
|
||||||
# Test with a ticket that has unique QR code
|
|
||||||
ticket_with_minimal_data = Ticket.new(
|
|
||||||
order: @order,
|
|
||||||
ticket_type: @ticket_type,
|
|
||||||
status: "active",
|
|
||||||
first_name: "Jane",
|
|
||||||
last_name: "Smith",
|
|
||||||
price_cents: 2500,
|
|
||||||
qr_code: "test-qr-minimal-data"
|
|
||||||
)
|
|
||||||
ticket_with_minimal_data.save(validate: false)
|
|
||||||
|
|
||||||
generator = TicketPdfGenerator.new(ticket_with_minimal_data)
|
|
||||||
|
|
||||||
# Should generate PDF successfully
|
|
||||||
pdf_string = generator.generate
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.start_with?("%PDF")
|
|
||||||
end
|
|
||||||
|
|
||||||
# === Price Display Tests ===
|
|
||||||
|
|
||||||
test "should format price correctly in euros" do
|
|
||||||
# Test different price formats
|
|
||||||
@ticket.update!(price_cents: 1050) # €10.50
|
|
||||||
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
pdf_string = generator.generate
|
|
||||||
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert_equal 10.5, @ticket.price_euros
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should handle low price" do
|
|
||||||
@ticket_type.update!(price_cents: 1)
|
|
||||||
@ticket.update!(price_cents: 1)
|
|
||||||
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
pdf_string = generator.generate
|
|
||||||
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert_equal 0.01, @ticket.price_euros
|
|
||||||
end
|
|
||||||
|
|
||||||
# === Date Formatting Tests ===
|
|
||||||
|
|
||||||
test "should format event date correctly" do
|
|
||||||
specific_time = Time.parse("2024-12-25 19:30:00")
|
|
||||||
@event.update!(start_time: specific_time)
|
|
||||||
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
pdf_string = generator.generate
|
|
||||||
|
|
||||||
# Just verify PDF generates - date formatting is handled by strftime
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.length > 0
|
|
||||||
end
|
|
||||||
|
|
||||||
# === Integration Tests ===
|
|
||||||
|
|
||||||
test "should generate valid PDF with all required elements" do
|
|
||||||
generator = TicketPdfGenerator.new(@ticket)
|
|
||||||
pdf_string = generator.generate
|
|
||||||
|
|
||||||
# Basic PDF structure validation
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.start_with?("%PDF")
|
|
||||||
assert pdf_string.end_with?("%%EOF\n")
|
|
||||||
assert pdf_string.length > 1000, "PDF should be substantial in size"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "should be callable from ticket model" do
|
|
||||||
# Test the integration with the Ticket model's to_pdf method
|
|
||||||
pdf_string = @ticket.to_pdf
|
|
||||||
|
|
||||||
assert_not_nil pdf_string
|
|
||||||
assert pdf_string.start_with?("%PDF")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
469
yarn.lock
469
yarn.lock
@@ -7,6 +7,20 @@
|
|||||||
resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz"
|
resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz"
|
||||||
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
|
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
|
||||||
|
|
||||||
|
"@babel/code-frame@^7.0.0":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz"
|
||||||
|
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-validator-identifier" "^7.27.1"
|
||||||
|
js-tokens "^4.0.0"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz"
|
||||||
|
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
|
||||||
|
|
||||||
"@csstools/selector-resolve-nested@^3.1.0":
|
"@csstools/selector-resolve-nested@^3.1.0":
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz"
|
||||||
@@ -131,6 +145,19 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.3.1"
|
debug "^4.3.1"
|
||||||
|
|
||||||
|
"@puppeteer/browsers@2.10.8":
|
||||||
|
version "2.10.8"
|
||||||
|
resolved "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.8.tgz"
|
||||||
|
integrity sha512-f02QYEnBDE0p8cteNoPYHHjbDuwyfbe4cCIVlNi8/MRicIxFW4w4CfgU0LNgWEID6s06P+hRJ1qjpBLMhPRCiQ==
|
||||||
|
dependencies:
|
||||||
|
debug "^4.4.1"
|
||||||
|
extract-zip "^2.0.1"
|
||||||
|
progress "^2.0.3"
|
||||||
|
proxy-agent "^6.5.0"
|
||||||
|
semver "^7.7.2"
|
||||||
|
tar-fs "^3.1.0"
|
||||||
|
yargs "^17.7.2"
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs@1.1.2":
|
"@radix-ui/react-compose-refs@1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz"
|
resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz"
|
||||||
@@ -265,6 +292,20 @@
|
|||||||
resolved "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz"
|
resolved "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz"
|
||||||
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
|
integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==
|
||||||
|
|
||||||
|
"@types/node@*":
|
||||||
|
version "24.3.1"
|
||||||
|
resolved "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz"
|
||||||
|
integrity sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~7.10.0"
|
||||||
|
|
||||||
|
"@types/yauzl@^2.9.1":
|
||||||
|
version "2.10.3"
|
||||||
|
resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz"
|
||||||
|
integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.2:
|
agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.2:
|
||||||
version "7.1.4"
|
version "7.1.4"
|
||||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
|
resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz"
|
||||||
@@ -355,6 +396,44 @@ autoprefixer@^10.4.21:
|
|||||||
picocolors "^1.1.1"
|
picocolors "^1.1.1"
|
||||||
postcss-value-parser "^4.2.0"
|
postcss-value-parser "^4.2.0"
|
||||||
|
|
||||||
|
b4a@^1.6.4:
|
||||||
|
version "1.6.7"
|
||||||
|
resolved "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz"
|
||||||
|
integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==
|
||||||
|
|
||||||
|
bare-events@*, bare-events@^2.2.0, bare-events@^2.5.4:
|
||||||
|
version "2.6.1"
|
||||||
|
resolved "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz"
|
||||||
|
integrity sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==
|
||||||
|
|
||||||
|
bare-fs@^4.0.1:
|
||||||
|
version "4.2.3"
|
||||||
|
resolved "https://registry.npmjs.org/bare-fs/-/bare-fs-4.2.3.tgz"
|
||||||
|
integrity sha512-1aGs5pRVLToMQ79elP+7cc0u0s/wXAzfBv/7hDloT7WFggLqECCas5qqPky7WHCFdsBH5WDq6sD4fAoz5sJbtA==
|
||||||
|
dependencies:
|
||||||
|
bare-events "^2.5.4"
|
||||||
|
bare-path "^3.0.0"
|
||||||
|
bare-stream "^2.6.4"
|
||||||
|
|
||||||
|
bare-os@^3.0.1:
|
||||||
|
version "3.6.2"
|
||||||
|
resolved "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz"
|
||||||
|
integrity sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==
|
||||||
|
|
||||||
|
bare-path@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz"
|
||||||
|
integrity sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==
|
||||||
|
dependencies:
|
||||||
|
bare-os "^3.0.1"
|
||||||
|
|
||||||
|
bare-stream@^2.6.4:
|
||||||
|
version "2.7.0"
|
||||||
|
resolved "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz"
|
||||||
|
integrity sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==
|
||||||
|
dependencies:
|
||||||
|
streamx "^2.21.0"
|
||||||
|
|
||||||
basic-ftp@^5.0.2:
|
basic-ftp@^5.0.2:
|
||||||
version "5.0.5"
|
version "5.0.5"
|
||||||
resolved "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz"
|
resolved "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz"
|
||||||
@@ -397,15 +476,20 @@ browserslist@^4.0.0, browserslist@^4.24.4, browserslist@^4.25.1, "browserslist@>
|
|||||||
node-releases "^2.0.19"
|
node-releases "^2.0.19"
|
||||||
update-browserslist-db "^1.1.3"
|
update-browserslist-db "^1.1.3"
|
||||||
|
|
||||||
|
buffer-crc32@~0.2.3:
|
||||||
|
version "0.2.13"
|
||||||
|
resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz"
|
||||||
|
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
|
||||||
|
|
||||||
buffer-from@^1.0.0:
|
buffer-from@^1.0.0:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
|
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
|
||||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||||
|
|
||||||
camelcase@^5.0.0:
|
callsites@^3.0.0:
|
||||||
version "5.3.1"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz"
|
resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
|
||||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||||
|
|
||||||
caniuse-api@^3.0.0:
|
caniuse-api@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
@@ -455,6 +539,14 @@ chownr@^3.0.0:
|
|||||||
resolved "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz"
|
||||||
integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==
|
integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==
|
||||||
|
|
||||||
|
chromium-bidi@8.0.0:
|
||||||
|
version "8.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-8.0.0.tgz"
|
||||||
|
integrity sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==
|
||||||
|
dependencies:
|
||||||
|
mitt "^3.0.1"
|
||||||
|
zod "^3.24.1"
|
||||||
|
|
||||||
class-variance-authority@^0.7.1:
|
class-variance-authority@^0.7.1:
|
||||||
version "0.7.1"
|
version "0.7.1"
|
||||||
resolved "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz"
|
resolved "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz"
|
||||||
@@ -469,15 +561,6 @@ cli-tableau@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chalk "3.0.0"
|
chalk "3.0.0"
|
||||||
|
|
||||||
cliui@^6.0.0:
|
|
||||||
version "6.0.0"
|
|
||||||
resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz"
|
|
||||||
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
|
|
||||||
dependencies:
|
|
||||||
string-width "^4.2.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
wrap-ansi "^6.2.0"
|
|
||||||
|
|
||||||
cliui@^8.0.1:
|
cliui@^8.0.1:
|
||||||
version "8.0.1"
|
version "8.0.1"
|
||||||
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
|
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
|
||||||
@@ -519,6 +602,16 @@ commander@2.15.1:
|
|||||||
resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz"
|
resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz"
|
||||||
integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==
|
integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==
|
||||||
|
|
||||||
|
cosmiconfig@^9.0.0:
|
||||||
|
version "9.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz"
|
||||||
|
integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==
|
||||||
|
dependencies:
|
||||||
|
env-paths "^2.2.1"
|
||||||
|
import-fresh "^3.3.0"
|
||||||
|
js-yaml "^4.1.0"
|
||||||
|
parse-json "^5.2.0"
|
||||||
|
|
||||||
croner@~4.1.92:
|
croner@~4.1.92:
|
||||||
version "4.1.97"
|
version "4.1.97"
|
||||||
resolved "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz"
|
resolved "https://registry.npmjs.org/croner/-/croner-4.1.97.tgz"
|
||||||
@@ -649,7 +742,7 @@ debug@^3.2.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
ms "^2.1.1"
|
||||||
|
|
||||||
debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.7, debug@4:
|
debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.7, debug@^4.4.1, debug@4:
|
||||||
version "4.4.1"
|
version "4.4.1"
|
||||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz"
|
resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz"
|
||||||
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
|
||||||
@@ -663,11 +756,6 @@ debug@~4.3.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.3"
|
ms "^2.1.3"
|
||||||
|
|
||||||
decamelize@^1.2.0:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
|
|
||||||
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
|
|
||||||
|
|
||||||
degenerator@^5.0.0:
|
degenerator@^5.0.0:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz"
|
resolved "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz"
|
||||||
@@ -687,10 +775,10 @@ detect-libc@^2.0.3, detect-libc@^2.0.4:
|
|||||||
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz"
|
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz"
|
||||||
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
|
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
|
||||||
|
|
||||||
dijkstrajs@^1.0.1:
|
devtools-protocol@*, devtools-protocol@0.0.1495869:
|
||||||
version "1.0.3"
|
version "0.0.1495869"
|
||||||
resolved "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1495869.tgz"
|
||||||
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
|
integrity sha512-i+bkd9UYFis40RcnkW7XrOprCujXRAHg62IVh/Ah3G8MmNXpCGt1m0dTFhSdx/AVs8XEMbdOGRwdkR1Bcta8AA==
|
||||||
|
|
||||||
dom-serializer@^2.0.0:
|
dom-serializer@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -732,6 +820,13 @@ emoji-regex@^8.0.0:
|
|||||||
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
|
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
|
||||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||||
|
|
||||||
|
end-of-stream@^1.1.0:
|
||||||
|
version "1.4.5"
|
||||||
|
resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz"
|
||||||
|
integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==
|
||||||
|
dependencies:
|
||||||
|
once "^1.4.0"
|
||||||
|
|
||||||
enhanced-resolve@^5.18.3:
|
enhanced-resolve@^5.18.3:
|
||||||
version "5.18.3"
|
version "5.18.3"
|
||||||
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz"
|
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz"
|
||||||
@@ -752,6 +847,18 @@ entities@^4.2.0:
|
|||||||
resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz"
|
resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz"
|
||||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||||
|
|
||||||
|
env-paths@^2.2.1:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz"
|
||||||
|
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
|
||||||
|
|
||||||
|
error-ex@^1.3.1:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz"
|
||||||
|
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
|
||||||
|
dependencies:
|
||||||
|
is-arrayish "^0.2.1"
|
||||||
|
|
||||||
esbuild@^0.25.4:
|
esbuild@^0.25.4:
|
||||||
version "0.25.9"
|
version "0.25.9"
|
||||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz"
|
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz"
|
||||||
@@ -830,6 +937,17 @@ eventemitter2@~5.0.1, eventemitter2@5.0.1:
|
|||||||
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz"
|
resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz"
|
||||||
integrity sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==
|
integrity sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==
|
||||||
|
|
||||||
|
extract-zip@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz"
|
||||||
|
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
|
||||||
|
dependencies:
|
||||||
|
debug "^4.1.1"
|
||||||
|
get-stream "^5.1.0"
|
||||||
|
yauzl "^2.10.0"
|
||||||
|
optionalDependencies:
|
||||||
|
"@types/yauzl" "^2.9.1"
|
||||||
|
|
||||||
extrareqp2@^1.0.0:
|
extrareqp2@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/extrareqp2/-/extrareqp2-1.0.0.tgz"
|
||||||
@@ -837,6 +955,11 @@ extrareqp2@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects "^1.14.0"
|
follow-redirects "^1.14.0"
|
||||||
|
|
||||||
|
fast-fifo@^1.2.0, fast-fifo@^1.3.2:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz"
|
||||||
|
integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==
|
||||||
|
|
||||||
fast-json-patch@^3.1.0:
|
fast-json-patch@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz"
|
resolved "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz"
|
||||||
@@ -847,6 +970,13 @@ fclone@~1.0.11, fclone@1.0.11:
|
|||||||
resolved "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz"
|
resolved "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz"
|
||||||
integrity sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==
|
integrity sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==
|
||||||
|
|
||||||
|
fd-slicer@~1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
|
||||||
|
integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
|
||||||
|
dependencies:
|
||||||
|
pend "~1.2.0"
|
||||||
|
|
||||||
fdir@^6.4.4:
|
fdir@^6.4.4:
|
||||||
version "6.5.0"
|
version "6.5.0"
|
||||||
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
|
||||||
@@ -859,14 +989,6 @@ fill-range@^7.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
find-up@^4.1.0:
|
|
||||||
version "4.1.0"
|
|
||||||
resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz"
|
|
||||||
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
|
||||||
dependencies:
|
|
||||||
locate-path "^5.0.0"
|
|
||||||
path-exists "^4.0.0"
|
|
||||||
|
|
||||||
follow-redirects@^1.14.0:
|
follow-redirects@^1.14.0:
|
||||||
version "1.15.11"
|
version "1.15.11"
|
||||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz"
|
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz"
|
||||||
@@ -891,11 +1013,18 @@ function-bind@^1.1.2:
|
|||||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||||
|
|
||||||
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
get-caller-file@^2.0.5:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
||||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||||
|
|
||||||
|
get-stream@^5.1.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz"
|
||||||
|
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
|
||||||
|
dependencies:
|
||||||
|
pump "^3.0.0"
|
||||||
|
|
||||||
get-uri@^6.0.1:
|
get-uri@^6.0.1:
|
||||||
version "6.0.5"
|
version "6.0.5"
|
||||||
resolved "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz"
|
resolved "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz"
|
||||||
@@ -962,6 +1091,14 @@ iconv-lite@^0.4.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer ">= 2.1.2 < 3"
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
|
import-fresh@^3.3.0:
|
||||||
|
version "3.3.1"
|
||||||
|
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz"
|
||||||
|
integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
|
||||||
|
dependencies:
|
||||||
|
parent-module "^1.0.0"
|
||||||
|
resolve-from "^4.0.0"
|
||||||
|
|
||||||
ini@^1.3.5:
|
ini@^1.3.5:
|
||||||
version "1.3.8"
|
version "1.3.8"
|
||||||
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
|
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
|
||||||
@@ -972,6 +1109,11 @@ ip-address@^10.0.1:
|
|||||||
resolved "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz"
|
resolved "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz"
|
||||||
integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==
|
integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==
|
||||||
|
|
||||||
|
is-arrayish@^0.2.1:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
|
||||||
|
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
|
||||||
|
|
||||||
is-binary-path@~2.1.0:
|
is-binary-path@~2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
|
||||||
@@ -1023,18 +1165,23 @@ js-git@^0.7.8:
|
|||||||
git-sha1 "^0.1.2"
|
git-sha1 "^0.1.2"
|
||||||
pako "^0.2.5"
|
pako "^0.2.5"
|
||||||
|
|
||||||
"js-tokens@^3.0.0 || ^4.0.0":
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||||
|
|
||||||
js-yaml@~4.1.0:
|
js-yaml@^4.1.0, js-yaml@~4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
|
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
|
||||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse "^2.0.1"
|
argparse "^2.0.1"
|
||||||
|
|
||||||
|
json-parse-even-better-errors@^2.3.0:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz"
|
||||||
|
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
|
||||||
|
|
||||||
json-stringify-safe@^5.0.1:
|
json-stringify-safe@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
|
resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
|
||||||
@@ -1122,12 +1269,10 @@ lilconfig@^3.1.1, lilconfig@^3.1.3:
|
|||||||
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz"
|
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz"
|
||||||
integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
|
integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
|
||||||
|
|
||||||
locate-path@^5.0.0:
|
lines-and-columns@^1.1.6:
|
||||||
version "5.0.0"
|
version "1.2.4"
|
||||||
resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz"
|
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
|
||||||
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
|
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||||
dependencies:
|
|
||||||
p-locate "^4.1.0"
|
|
||||||
|
|
||||||
lodash.memoize@^4.1.2:
|
lodash.memoize@^4.1.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
@@ -1197,6 +1342,11 @@ minizlib@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minipass "^7.1.2"
|
minipass "^7.1.2"
|
||||||
|
|
||||||
|
mitt@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz"
|
||||||
|
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
|
||||||
|
|
||||||
mkdirp@^3.0.1:
|
mkdirp@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz"
|
||||||
@@ -1263,26 +1413,14 @@ nth-check@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
boolbase "^1.0.0"
|
boolbase "^1.0.0"
|
||||||
|
|
||||||
p-limit@^2.2.0:
|
once@^1.3.1, once@^1.4.0:
|
||||||
version "2.3.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
|
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
||||||
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
|
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||||
dependencies:
|
dependencies:
|
||||||
p-try "^2.0.0"
|
wrappy "1"
|
||||||
|
|
||||||
p-locate@^4.1.0:
|
pac-proxy-agent@^7.0.1, pac-proxy-agent@^7.1.0:
|
||||||
version "4.1.0"
|
|
||||||
resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz"
|
|
||||||
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
|
|
||||||
dependencies:
|
|
||||||
p-limit "^2.2.0"
|
|
||||||
|
|
||||||
p-try@^2.0.0:
|
|
||||||
version "2.2.0"
|
|
||||||
resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
|
|
||||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
|
||||||
|
|
||||||
pac-proxy-agent@^7.0.1:
|
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz"
|
resolved "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz"
|
||||||
integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==
|
integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==
|
||||||
@@ -1309,16 +1447,33 @@ pako@^0.2.5:
|
|||||||
resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz"
|
resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz"
|
||||||
integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
|
integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
|
||||||
|
|
||||||
path-exists@^4.0.0:
|
parent-module@^1.0.0:
|
||||||
version "4.0.0"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
|
||||||
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
|
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
|
||||||
|
dependencies:
|
||||||
|
callsites "^3.0.0"
|
||||||
|
|
||||||
|
parse-json@^5.2.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz"
|
||||||
|
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.0.0"
|
||||||
|
error-ex "^1.3.1"
|
||||||
|
json-parse-even-better-errors "^2.3.0"
|
||||||
|
lines-and-columns "^1.1.6"
|
||||||
|
|
||||||
path-parse@^1.0.7:
|
path-parse@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
|
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
|
||||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||||
|
|
||||||
|
pend@~1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz"
|
||||||
|
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||||
|
|
||||||
picocolors@^1.0.0, picocolors@^1.1.1:
|
picocolors@^1.0.0, picocolors@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
|
||||||
@@ -1433,11 +1588,6 @@ pm2@^6.0.5:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
pm2-sysmonit "^1.2.8"
|
pm2-sysmonit "^1.2.8"
|
||||||
|
|
||||||
pngjs@^5.0.0:
|
|
||||||
version "5.0.0"
|
|
||||||
resolved "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz"
|
|
||||||
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
|
||||||
|
|
||||||
postcss-calc@^10.1.1:
|
postcss-calc@^10.1.1:
|
||||||
version "10.1.1"
|
version "10.1.1"
|
||||||
resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz"
|
resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz"
|
||||||
@@ -1727,6 +1877,11 @@ pretty-hrtime@^1.0.3:
|
|||||||
resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz"
|
||||||
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
|
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
|
||||||
|
|
||||||
|
progress@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz"
|
||||||
|
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||||
|
|
||||||
promptly@^2:
|
promptly@^2:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz"
|
resolved "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz"
|
||||||
@@ -1734,6 +1889,20 @@ promptly@^2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
read "^1.0.4"
|
read "^1.0.4"
|
||||||
|
|
||||||
|
proxy-agent@^6.5.0:
|
||||||
|
version "6.5.0"
|
||||||
|
resolved "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz"
|
||||||
|
integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==
|
||||||
|
dependencies:
|
||||||
|
agent-base "^7.1.2"
|
||||||
|
debug "^4.3.4"
|
||||||
|
http-proxy-agent "^7.0.1"
|
||||||
|
https-proxy-agent "^7.0.6"
|
||||||
|
lru-cache "^7.14.1"
|
||||||
|
pac-proxy-agent "^7.1.0"
|
||||||
|
proxy-from-env "^1.1.0"
|
||||||
|
socks-proxy-agent "^8.0.5"
|
||||||
|
|
||||||
proxy-agent@~6.4.0:
|
proxy-agent@~6.4.0:
|
||||||
version "6.4.0"
|
version "6.4.0"
|
||||||
resolved "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz"
|
resolved "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz"
|
||||||
@@ -1753,14 +1922,37 @@ proxy-from-env@^1.1.0:
|
|||||||
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
|
||||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||||
|
|
||||||
qrcode@^1.5.4:
|
pump@^3.0.0:
|
||||||
version "1.5.4"
|
version "3.0.3"
|
||||||
resolved "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz"
|
resolved "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz"
|
||||||
integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
|
integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==
|
||||||
dependencies:
|
dependencies:
|
||||||
dijkstrajs "^1.0.1"
|
end-of-stream "^1.1.0"
|
||||||
pngjs "^5.0.0"
|
once "^1.3.1"
|
||||||
yargs "^15.3.1"
|
|
||||||
|
puppeteer-core@24.19.0:
|
||||||
|
version "24.19.0"
|
||||||
|
resolved "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.19.0.tgz"
|
||||||
|
integrity sha512-qsEys4OIb2VGC2tNWKAs4U0mnjkIAxueMOOzk2nEFM9g4Y8QuvYkEMtmwsEdvzNGsUFd7DprOQfABmlN7WBOlg==
|
||||||
|
dependencies:
|
||||||
|
"@puppeteer/browsers" "2.10.8"
|
||||||
|
chromium-bidi "8.0.0"
|
||||||
|
debug "^4.4.1"
|
||||||
|
devtools-protocol "0.0.1495869"
|
||||||
|
typed-query-selector "^2.12.0"
|
||||||
|
ws "^8.18.3"
|
||||||
|
|
||||||
|
puppeteer@^24.19.0:
|
||||||
|
version "24.19.0"
|
||||||
|
resolved "https://registry.npmjs.org/puppeteer/-/puppeteer-24.19.0.tgz"
|
||||||
|
integrity sha512-gUWgHX36m9K6yUbvNBEA7CXElIL92yXMoAVFrO8OpZkItqrruLVqYA8ikmfgwcw/cNfYgkt0n2+yP9jd9RSETA==
|
||||||
|
dependencies:
|
||||||
|
"@puppeteer/browsers" "2.10.8"
|
||||||
|
chromium-bidi "8.0.0"
|
||||||
|
cosmiconfig "^9.0.0"
|
||||||
|
devtools-protocol "0.0.1495869"
|
||||||
|
puppeteer-core "24.19.0"
|
||||||
|
typed-query-selector "^2.12.0"
|
||||||
|
|
||||||
react-dom@^18.3.1:
|
react-dom@^18.3.1:
|
||||||
version "18.3.1"
|
version "18.3.1"
|
||||||
@@ -1812,10 +2004,10 @@ require-in-the-middle@^5.0.0:
|
|||||||
module-details-from-path "^1.0.3"
|
module-details-from-path "^1.0.3"
|
||||||
resolve "^1.22.1"
|
resolve "^1.22.1"
|
||||||
|
|
||||||
require-main-filename@^2.0.0:
|
resolve-from@^4.0.0:
|
||||||
version "2.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
|
||||||
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||||
|
|
||||||
resolve@^1.1.7, resolve@^1.22.1:
|
resolve@^1.1.7, resolve@^1.22.1:
|
||||||
version "1.22.10"
|
version "1.22.10"
|
||||||
@@ -1853,7 +2045,7 @@ scheduler@^0.23.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
semver@^7.6.2:
|
semver@^7.6.2, semver@^7.7.2:
|
||||||
version "7.7.2"
|
version "7.7.2"
|
||||||
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
|
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
|
||||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||||
@@ -1872,11 +2064,6 @@ semver@~7.5.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^6.0.0"
|
lru-cache "^6.0.0"
|
||||||
|
|
||||||
set-blocking@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
|
|
||||||
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
|
|
||||||
|
|
||||||
shimmer@^1.2.0:
|
shimmer@^1.2.0:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz"
|
resolved "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz"
|
||||||
@@ -1937,6 +2124,16 @@ sprintf-js@1.1.2:
|
|||||||
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz"
|
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz"
|
||||||
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
|
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
|
||||||
|
|
||||||
|
streamx@^2.15.0, streamx@^2.21.0:
|
||||||
|
version "2.22.1"
|
||||||
|
resolved "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz"
|
||||||
|
integrity sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==
|
||||||
|
dependencies:
|
||||||
|
fast-fifo "^1.3.2"
|
||||||
|
text-decoder "^1.1.0"
|
||||||
|
optionalDependencies:
|
||||||
|
bare-events "^2.2.0"
|
||||||
|
|
||||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||||
@@ -2011,6 +2208,26 @@ tapable@^2.2.0:
|
|||||||
resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz"
|
resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz"
|
||||||
integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==
|
integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==
|
||||||
|
|
||||||
|
tar-fs@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz"
|
||||||
|
integrity sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==
|
||||||
|
dependencies:
|
||||||
|
pump "^3.0.0"
|
||||||
|
tar-stream "^3.1.5"
|
||||||
|
optionalDependencies:
|
||||||
|
bare-fs "^4.0.1"
|
||||||
|
bare-path "^3.0.0"
|
||||||
|
|
||||||
|
tar-stream@^3.1.5:
|
||||||
|
version "3.1.7"
|
||||||
|
resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz"
|
||||||
|
integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==
|
||||||
|
dependencies:
|
||||||
|
b4a "^1.6.4"
|
||||||
|
fast-fifo "^1.2.0"
|
||||||
|
streamx "^2.15.0"
|
||||||
|
|
||||||
tar@^7.4.3:
|
tar@^7.4.3:
|
||||||
version "7.4.3"
|
version "7.4.3"
|
||||||
resolved "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz"
|
resolved "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz"
|
||||||
@@ -2023,6 +2240,13 @@ tar@^7.4.3:
|
|||||||
mkdirp "^3.0.1"
|
mkdirp "^3.0.1"
|
||||||
yallist "^5.0.0"
|
yallist "^5.0.0"
|
||||||
|
|
||||||
|
text-decoder@^1.1.0:
|
||||||
|
version "1.2.3"
|
||||||
|
resolved "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz"
|
||||||
|
integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==
|
||||||
|
dependencies:
|
||||||
|
b4a "^1.6.4"
|
||||||
|
|
||||||
thenby@^1.3.4:
|
thenby@^1.3.4:
|
||||||
version "1.3.4"
|
version "1.3.4"
|
||||||
resolved "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz"
|
resolved "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz"
|
||||||
@@ -2065,6 +2289,16 @@ tx2@~1.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
json-stringify-safe "^5.0.1"
|
json-stringify-safe "^5.0.1"
|
||||||
|
|
||||||
|
typed-query-selector@^2.12.0:
|
||||||
|
version "2.12.0"
|
||||||
|
resolved "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz"
|
||||||
|
integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==
|
||||||
|
|
||||||
|
undici-types@~7.10.0:
|
||||||
|
version "7.10.0"
|
||||||
|
resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz"
|
||||||
|
integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==
|
||||||
|
|
||||||
universalify@^2.0.0:
|
universalify@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz"
|
resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz"
|
||||||
@@ -2093,20 +2327,6 @@ vizion@~2.2.1:
|
|||||||
ini "^1.3.5"
|
ini "^1.3.5"
|
||||||
js-git "^0.7.8"
|
js-git "^0.7.8"
|
||||||
|
|
||||||
which-module@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz"
|
|
||||||
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
|
|
||||||
|
|
||||||
wrap-ansi@^6.2.0:
|
|
||||||
version "6.2.0"
|
|
||||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz"
|
|
||||||
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
|
||||||
dependencies:
|
|
||||||
ansi-styles "^4.0.0"
|
|
||||||
string-width "^4.1.0"
|
|
||||||
strip-ansi "^6.0.0"
|
|
||||||
|
|
||||||
wrap-ansi@^7.0.0:
|
wrap-ansi@^7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||||
@@ -2116,15 +2336,20 @@ wrap-ansi@^7.0.0:
|
|||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
|
wrappy@1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
||||||
|
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||||
|
|
||||||
ws@^7.0.0, ws@~7.5.10:
|
ws@^7.0.0, ws@~7.5.10:
|
||||||
version "7.5.10"
|
version "7.5.10"
|
||||||
resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz"
|
resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz"
|
||||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||||
|
|
||||||
y18n@^4.0.0:
|
ws@^8.18.3:
|
||||||
version "4.0.3"
|
version "8.18.3"
|
||||||
resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz"
|
resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz"
|
||||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
@@ -2146,37 +2371,12 @@ yaml@^2.4.2:
|
|||||||
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
|
resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz"
|
||||||
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
|
integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
|
||||||
|
|
||||||
yargs-parser@^18.1.2:
|
|
||||||
version "18.1.3"
|
|
||||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz"
|
|
||||||
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
|
|
||||||
dependencies:
|
|
||||||
camelcase "^5.0.0"
|
|
||||||
decamelize "^1.2.0"
|
|
||||||
|
|
||||||
yargs-parser@^21.1.1:
|
yargs-parser@^21.1.1:
|
||||||
version "21.1.1"
|
version "21.1.1"
|
||||||
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
|
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
|
||||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||||
|
|
||||||
yargs@^15.3.1:
|
yargs@^17.0.0, yargs@^17.7.2:
|
||||||
version "15.4.1"
|
|
||||||
resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz"
|
|
||||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
|
||||||
dependencies:
|
|
||||||
cliui "^6.0.0"
|
|
||||||
decamelize "^1.2.0"
|
|
||||||
find-up "^4.1.0"
|
|
||||||
get-caller-file "^2.0.1"
|
|
||||||
require-directory "^2.1.1"
|
|
||||||
require-main-filename "^2.0.0"
|
|
||||||
set-blocking "^2.0.0"
|
|
||||||
string-width "^4.2.0"
|
|
||||||
which-module "^2.0.0"
|
|
||||||
y18n "^4.0.0"
|
|
||||||
yargs-parser "^18.1.2"
|
|
||||||
|
|
||||||
yargs@^17.0.0:
|
|
||||||
version "17.7.2"
|
version "17.7.2"
|
||||||
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz"
|
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz"
|
||||||
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
||||||
@@ -2188,3 +2388,16 @@ yargs@^17.0.0:
|
|||||||
string-width "^4.2.3"
|
string-width "^4.2.3"
|
||||||
y18n "^5.0.5"
|
y18n "^5.0.5"
|
||||||
yargs-parser "^21.1.1"
|
yargs-parser "^21.1.1"
|
||||||
|
|
||||||
|
yauzl@^2.10.0:
|
||||||
|
version "2.10.0"
|
||||||
|
resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz"
|
||||||
|
integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
|
||||||
|
dependencies:
|
||||||
|
buffer-crc32 "~0.2.3"
|
||||||
|
fd-slicer "~1.1.0"
|
||||||
|
|
||||||
|
zod@^3.24.1:
|
||||||
|
version "3.25.76"
|
||||||
|
resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz"
|
||||||
|
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||||
|
|||||||
Reference in New Issue
Block a user