Compare commits
5 Commits
ceb5a13297
...
feat/seo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5105964b39 | ||
|
|
fa99a167a5 | ||
|
|
9b33b73bb4 | ||
|
|
bc47027c22 | ||
|
|
7ef934d8a8 |
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;
|
||||||
|
}
|
||||||
141
app/assets/stylesheets/pdf.scss
Normal file
141
app/assets/stylesheets/pdf.scss
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;
|
||||||
|
}
|
||||||
@@ -14,4 +14,48 @@ class ApplicationController < ActionController::Base
|
|||||||
# - CSS nesting and :has() pseudo-class
|
# - CSS nesting and :has() pseudo-class
|
||||||
# allow_browser versions: :modern
|
# allow_browser versions: :modern
|
||||||
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# Generate SEO-friendly path for an event
|
||||||
|
def seo_event_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_event_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly booking URL for an event
|
||||||
|
def seo_book_tickets_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
book_event_tickets_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_book_tickets_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly checkout URL for an event
|
||||||
|
def seo_checkout_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_checkout_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_checkout_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly ticket URL
|
||||||
|
def seo_ticket_path(ticket)
|
||||||
|
ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||||
|
end
|
||||||
|
helper_method :seo_ticket_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly ticket view URL
|
||||||
|
def seo_ticket_view_path(ticket)
|
||||||
|
view_ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||||
|
end
|
||||||
|
helper_method :seo_ticket_view_path
|
||||||
|
|
||||||
|
# Generate SEO-friendly ticket download URL
|
||||||
|
def seo_ticket_download_path(ticket)
|
||||||
|
download_ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id)
|
||||||
|
end
|
||||||
|
helper_method :seo_ticket_download_path
|
||||||
end
|
end
|
||||||
|
|||||||
92
app/controllers/booking/payments_controller.rb
Normal file
92
app/controllers/booking/payments_controller.rb
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Handle payment callbacks for booking workflow
|
||||||
|
class Booking::PaymentsController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
# Handle successful payment callback
|
||||||
|
def success
|
||||||
|
session_id = params[:session_id]
|
||||||
|
|
||||||
|
# Check if Stripe is properly configured
|
||||||
|
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
||||||
|
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
||||||
|
|
||||||
|
unless stripe_configured
|
||||||
|
redirect_to root_path, alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur."
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
||||||
|
|
||||||
|
if stripe_session.payment_status == "paid"
|
||||||
|
# Get order_id from session metadata
|
||||||
|
order_id = stripe_session.metadata["order_id"]
|
||||||
|
|
||||||
|
unless order_id.present?
|
||||||
|
redirect_to dashboard_path, alert: "Informations de commande manquantes"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Find and update the order
|
||||||
|
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
||||||
|
@order.mark_as_paid!
|
||||||
|
|
||||||
|
# Schedule Stripe invoice generation in background
|
||||||
|
begin
|
||||||
|
StripeInvoiceGenerationJob.perform_later(@order.id)
|
||||||
|
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Send confirmation 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}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clear session data
|
||||||
|
session.delete(:pending_cart)
|
||||||
|
session.delete(:ticket_names)
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
|
||||||
|
render "payment_success"
|
||||||
|
else
|
||||||
|
redirect_to dashboard_path, alert: "Le paiement n'a pas été complété avec succès"
|
||||||
|
end
|
||||||
|
rescue Stripe::StripeError => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
||||||
|
redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}"
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
|
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
||||||
|
redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{error_message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle payment cancellation callback
|
||||||
|
def cancel
|
||||||
|
order_id = params[:order_id] || session[:draft_order_id]
|
||||||
|
|
||||||
|
if order_id.present?
|
||||||
|
order = current_user.orders.find_by(id: order_id, status: "draft")
|
||||||
|
|
||||||
|
if order&.can_retry_payment?
|
||||||
|
# Extract year and month from event start_time for SEO URL
|
||||||
|
year = order.event.start_time.year
|
||||||
|
month = format("%02d", order.event.start_time.month)
|
||||||
|
|
||||||
|
redirect_to event_checkout_path(year: year, month: month, slug: order.event.slug),
|
||||||
|
alert: "Le paiement a été annulé. Vous pouvez réessayer."
|
||||||
|
else
|
||||||
|
session.delete(:draft_order_id)
|
||||||
|
redirect_to root_path, alert: "Le paiement a été annulé et votre commande a expiré."
|
||||||
|
end
|
||||||
|
else
|
||||||
|
redirect_to root_path, alert: "Le paiement a été annulé"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -27,10 +27,33 @@ class EventsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
# Find and set the current event with eager-loaded associations
|
# Find and set the current event with eager-loaded associations
|
||||||
#
|
# Supports both old slug-only format and new SEO-friendly year/month/slug format
|
||||||
# Loads event with ticket types to avoid N+1 queries
|
# Loads event with ticket types to avoid N+1 queries
|
||||||
# Raises ActiveRecord::RecordNotFound if event doesn't exist
|
|
||||||
def set_event
|
def set_event
|
||||||
@event = Event.includes(:ticket_types).find(params[:id])
|
if params[:year] && params[:month]
|
||||||
|
# New SEO-friendly format: /events/2024/07/summer-party
|
||||||
|
year = params[:year].to_i
|
||||||
|
month = params[:month].to_i
|
||||||
|
start_of_month = Date.new(year, month, 1).beginning_of_month
|
||||||
|
end_of_month = start_of_month.end_of_month
|
||||||
|
|
||||||
|
@event = Event.includes(:ticket_types)
|
||||||
|
.where(slug: params[:slug])
|
||||||
|
.where(start_time: start_of_month..end_of_month)
|
||||||
|
.first!
|
||||||
|
else
|
||||||
|
# Legacy format: /events/summer-party (for backward compatibility)
|
||||||
|
@event = Event.includes(:ticket_types).find_by!(slug: params[:slug])
|
||||||
end
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
redirect_to events_path, alert: "Événement non trouvé"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate SEO-friendly path for an event
|
||||||
|
def seo_event_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_event_path
|
||||||
end
|
end
|
||||||
|
|||||||
17
app/controllers/legacy_redirects_controller.rb
Normal file
17
app/controllers/legacy_redirects_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Handle legacy URL redirects to new SEO-friendly URLs
|
||||||
|
class LegacyRedirectsController < ApplicationController
|
||||||
|
# Redirect old event URLs to new SEO-friendly format
|
||||||
|
# OLD: /events/summer-party-2024
|
||||||
|
# NEW: /events/2024/07/summer-party-2024
|
||||||
|
def event_redirect
|
||||||
|
event = Event.find_by(slug: params[:slug])
|
||||||
|
|
||||||
|
if event
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
redirect_to event_path(year: year, month: month, slug: event.slug), status: :moved_permanently
|
||||||
|
else
|
||||||
|
redirect_to events_path, alert: "Événement non trouvé"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# Handle order management and checkout process
|
# Handle order management and checkout process with SEO-friendly URLs
|
||||||
#
|
#
|
||||||
# This controller manages the order lifecycle from checkout to payment completion
|
# This controller manages the order lifecycle from checkout to payment completion
|
||||||
# Orders group multiple tickets together for better transaction management
|
# Orders group multiple tickets together for better transaction management
|
||||||
class OrdersController < ApplicationController
|
class OrdersController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
before_action :set_event_from_seo_params, only: [:new, :create, :checkout]
|
||||||
before_action :set_event, only: [ :new, :create ]
|
before_action :set_order_from_id, only: [:show, :retry_payment, :increment_payment_attempt]
|
||||||
|
|
||||||
# Display new order form with name collection
|
# Display new order form with name collection
|
||||||
#
|
#
|
||||||
@@ -15,7 +15,7 @@ class OrdersController < ApplicationController
|
|||||||
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||||
|
|
||||||
if @cart_data.empty?
|
if @cart_data.empty?
|
||||||
redirect_to event_path(@event.slug, @event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement"
|
redirect_to seo_event_path(@event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class OrdersController < ApplicationController
|
|||||||
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
@cart_data = params[:cart_data] || session[:pending_cart] || {}
|
||||||
|
|
||||||
if @cart_data.empty?
|
if @cart_data.empty?
|
||||||
redirect_to event_path(@event.slug, @event), alert: "Aucun billet sélectionné"
|
redirect_to seo_event_path(@event), alert: "Aucun billet sélectionné"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -87,40 +87,44 @@ class OrdersController < ApplicationController
|
|||||||
if success
|
if success
|
||||||
session[:draft_order_id] = @order.id
|
session[:draft_order_id] = @order.id
|
||||||
session.delete(:pending_cart)
|
session.delete(:pending_cart)
|
||||||
redirect_to checkout_order_path(@order)
|
year = @event.start_time.year
|
||||||
|
month = format("%02d", @event.start_time.month)
|
||||||
|
redirect_to event_checkout_path(year: year, month: month, slug: @event.slug)
|
||||||
else
|
else
|
||||||
redirect_to event_order_new_path(@event.slug, @event.id)
|
year = @event.start_time.year
|
||||||
|
month = format("%02d", @event.start_time.month)
|
||||||
|
redirect_to book_event_tickets_path(year: year, month: month, slug: @event.slug)
|
||||||
end
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
||||||
flash[:alert] = "Une erreur est survenue: #{error_message}"
|
flash[:alert] = "Une erreur est survenue: #{error_message}"
|
||||||
redirect_to event_order_new_path(@event.slug, @event.id)
|
year = @event.start_time.year
|
||||||
end
|
month = format("%02d", @event.start_time.month)
|
||||||
|
redirect_to book_event_tickets_path(year: year, month: month, slug: @event.slug)
|
||||||
# Display all user orders
|
|
||||||
def index
|
|
||||||
@orders = current_user.orders.includes(:event, tickets: :ticket_type)
|
|
||||||
.where(status: [ "paid", "completed" ])
|
|
||||||
.order(created_at: :desc)
|
|
||||||
.page(params[:page])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Display order summary
|
# Display order summary
|
||||||
#
|
|
||||||
#
|
|
||||||
def show
|
def show
|
||||||
@tickets = @order.tickets.includes(:ticket_type)
|
@tickets = @order.tickets.includes(:ticket_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Display payment page for an order
|
# Display payment page for an order (SEO-friendly checkout URL)
|
||||||
#
|
#
|
||||||
# Display a summary of all tickets in the order and permit user
|
# Display a summary of all tickets in the order and permit user
|
||||||
# to proceed to payment via Stripe
|
# to proceed to payment via Stripe
|
||||||
def checkout
|
def checkout
|
||||||
|
# Find order from session or create one
|
||||||
|
@order = current_user.orders.find_by(id: session[:draft_order_id], event: @event, status: "draft")
|
||||||
|
|
||||||
|
unless @order
|
||||||
|
redirect_to seo_event_path(@event), alert: "Aucune commande en attente trouvée"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
# Handle expired orders
|
# Handle expired orders
|
||||||
if @order.expired?
|
if @order.expired?
|
||||||
@order.expire_if_overdue!
|
@order.expire_if_overdue!
|
||||||
return redirect_to event_path(@order.event.slug, @order.event),
|
return redirect_to seo_event_path(@event),
|
||||||
alert: "Votre commande a expiré. Veuillez recommencer."
|
alert: "Votre commande a expiré. Veuillez recommencer."
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -149,110 +153,41 @@ class OrdersController < ApplicationController
|
|||||||
# Allow users to retry payment for failed/cancelled payments
|
# Allow users to retry payment for failed/cancelled payments
|
||||||
def retry_payment
|
def retry_payment
|
||||||
unless @order.can_retry_payment?
|
unless @order.can_retry_payment?
|
||||||
redirect_to event_path(@order.event.slug, @order.event),
|
redirect_to seo_event_path(@order.event),
|
||||||
alert: "Cette commande ne peut plus être payée"
|
alert: "Cette commande ne peut plus être payée"
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to checkout_order_path(@order)
|
year = @order.event.start_time.year
|
||||||
end
|
month = format("%02d", @order.event.start_time.month)
|
||||||
|
redirect_to event_checkout_path(year: year, month: month, slug: @order.event.slug)
|
||||||
# Handle successful payment
|
|
||||||
def payment_success
|
|
||||||
session_id = params[:session_id]
|
|
||||||
|
|
||||||
# Check if Stripe is properly configured
|
|
||||||
stripe_configured = Rails.application.config.stripe[:secret_key].present?
|
|
||||||
Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}"
|
|
||||||
|
|
||||||
unless stripe_configured
|
|
||||||
redirect_to root_path, alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur."
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
stripe_session = Stripe::Checkout::Session.retrieve(session_id)
|
|
||||||
|
|
||||||
if stripe_session.payment_status == "paid"
|
|
||||||
# Get order_id from session metadata
|
|
||||||
order_id = stripe_session.metadata["order_id"]
|
|
||||||
|
|
||||||
unless order_id.present?
|
|
||||||
redirect_to dashboard_path, alert: "Informations de commande manquantes"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Find and update the order
|
|
||||||
@order = current_user.orders.includes(tickets: :ticket_type).find(order_id)
|
|
||||||
@order.mark_as_paid!
|
|
||||||
|
|
||||||
# Schedule Stripe invoice generation in background
|
|
||||||
# This creates accounting records without blocking the payment success flow
|
|
||||||
begin
|
|
||||||
StripeInvoiceGenerationJob.perform_later(@order.id)
|
|
||||||
Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}"
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}"
|
|
||||||
# Don't fail the payment process due to job scheduling issues
|
|
||||||
end
|
|
||||||
|
|
||||||
# Email confirmation is handled by the order model's mark_as_paid! method
|
|
||||||
# to avoid duplicate emails
|
|
||||||
|
|
||||||
# Clear session data
|
|
||||||
session.delete(:pending_cart)
|
|
||||||
session.delete(:ticket_names)
|
|
||||||
session.delete(:draft_order_id)
|
|
||||||
|
|
||||||
render "payment_success"
|
|
||||||
else
|
|
||||||
redirect_to dashboard_path, alert: "Le paiement n'a pas été complété avec succès"
|
|
||||||
end
|
|
||||||
rescue Stripe::StripeError => e
|
|
||||||
error_message = e.message.present? ? e.message : "Erreur Stripe inconnue"
|
|
||||||
redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}"
|
|
||||||
rescue => e
|
|
||||||
error_message = e.message.present? ? e.message : "Erreur inconnue"
|
|
||||||
Rails.logger.error "Payment success error: #{e.class} - #{error_message}"
|
|
||||||
redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{error_message}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle payment failure/cancellation
|
|
||||||
def payment_cancel
|
|
||||||
order_id = params[:order_id] || session[:draft_order_id]
|
|
||||||
|
|
||||||
if order_id.present?
|
|
||||||
order = current_user.orders.find_by(id: order_id, status: "draft")
|
|
||||||
|
|
||||||
if order&.can_retry_payment?
|
|
||||||
redirect_to checkout_order_path(order),
|
|
||||||
alert: "Le paiement a été annulé. Vous pouvez réessayer."
|
|
||||||
else
|
|
||||||
session.delete(:draft_order_id)
|
|
||||||
redirect_to root_path, alert: "Le paiement a été annulé et votre commande a expiré."
|
|
||||||
end
|
|
||||||
else
|
|
||||||
redirect_to root_path, alert: "Le paiement a été annulé"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_order
|
def set_event_from_seo_params
|
||||||
@order = current_user.orders.includes(:tickets, :event).find(params[:id])
|
year = params[:year].to_i
|
||||||
|
month = params[:month].to_i
|
||||||
|
start_of_month = Date.new(year, month, 1).beginning_of_month
|
||||||
|
end_of_month = start_of_month.end_of_month
|
||||||
|
|
||||||
|
@event = Event.includes(:ticket_types)
|
||||||
|
.where(slug: params[:slug])
|
||||||
|
.where(start_time: start_of_month..end_of_month)
|
||||||
|
.first
|
||||||
|
|
||||||
|
return redirect_to events_path, alert: "Événement non trouvé" unless @event
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_order_from_id
|
||||||
|
@order = current_user.orders.includes(:tickets, :event).find(params[:order_id])
|
||||||
|
@event = @order.event
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path, alert: "Commande non trouvée"
|
redirect_to root_path, alert: "Commande non trouvée"
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_event
|
|
||||||
@event = Event.includes(:ticket_types).find(params[:id])
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
redirect_to events_path, alert: "Événement non trouvé"
|
|
||||||
end
|
|
||||||
|
|
||||||
def order_params
|
def order_params
|
||||||
params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ])
|
params.permit(tickets_attributes: [:ticket_type_id, :first_name, :last_name])
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_stripe_session
|
def create_stripe_session
|
||||||
@@ -271,15 +206,23 @@ class OrdersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
Stripe::Checkout::Session.create(
|
Stripe::Checkout::Session.create(
|
||||||
payment_method_types: [ "card" ],
|
payment_method_types: ["card"],
|
||||||
line_items: line_items,
|
line_items: line_items,
|
||||||
mode: "payment",
|
mode: "payment",
|
||||||
success_url: order_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
success_url: booking_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
||||||
cancel_url: order_payment_cancel_url,
|
cancel_url: booking_payment_cancelled_url + "?order_id=#{@order.id}",
|
||||||
metadata: {
|
metadata: {
|
||||||
order_id: @order.id,
|
order_id: @order.id,
|
||||||
user_id: current_user.id
|
user_id: current_user.id
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Generate SEO-friendly path for an event
|
||||||
|
def seo_event_path(event)
|
||||||
|
year = event.start_time.year
|
||||||
|
month = format("%02d", event.start_time.month)
|
||||||
|
event_path(year: year, month: month, slug: event.slug)
|
||||||
|
end
|
||||||
|
helper_method :seo_event_path
|
||||||
end
|
end
|
||||||
@@ -17,28 +17,30 @@ class PagesController < ApplicationController
|
|||||||
# User dashboard showing personalized content
|
# User dashboard showing personalized content
|
||||||
# Accessible only to authenticated users
|
# Accessible only to authenticated users
|
||||||
def dashboard
|
def dashboard
|
||||||
# User's orders with associated data
|
# Metrics for dashboard cards
|
||||||
@user_orders = current_user.orders.includes(:event, tickets: :ticket_type)
|
@booked_events = current_user.orders.joins(tickets: { ticket_type: :event })
|
||||||
.where(status: [ "paid", "completed" ])
|
.where(events: { state: :published })
|
||||||
.order(created_at: :desc)
|
.where(orders: { status: [ "paid", "completed" ] })
|
||||||
.limit(10)
|
.sum("1")
|
||||||
|
@events_today = Event.published.where("DATE(start_time) = ?", Date.current).count
|
||||||
|
@events_tomorrow = Event.published.where("DATE(start_time) = ?", Date.current + 1).count
|
||||||
|
@upcoming_events = Event.published.upcoming.count
|
||||||
|
|
||||||
|
# User's booked events
|
||||||
|
@user_booked_events = Event.joins(ticket_types: { tickets: :order })
|
||||||
|
.where(orders: { user: current_user }, tickets: { status: "active" })
|
||||||
|
.distinct
|
||||||
|
.limit(5)
|
||||||
|
|
||||||
# Draft orders that can be retried
|
# Draft orders that can be retried
|
||||||
@draft_orders = current_user.orders.includes(tickets: [ :ticket_type, :event ])
|
@draft_orders = current_user.orders.includes(tickets: [ :ticket_type, :event ])
|
||||||
.can_retry_payment
|
.can_retry_payment
|
||||||
.order(:expires_at)
|
.order(:expires_at)
|
||||||
|
|
||||||
# Simplified upcoming events preview - only show if user has orders
|
# Events sections
|
||||||
if @user_orders.any?
|
@today_events = Event.published.where("DATE(start_time) = ?", Date.current).order(start_time: :asc)
|
||||||
ordered_event_ids = @user_orders.map(&:event).map(&:id)
|
@tomorrow_events = Event.published.where("DATE(start_time) = ?", Date.current + 1).order(start_time: :asc)
|
||||||
@upcoming_preview_events = Event.published
|
@other_events = Event.published.upcoming.where.not("DATE(start_time) IN (?)", [ Date.current, Date.current + 1 ]).order(start_time: :asc).page(params[:page])
|
||||||
.upcoming
|
|
||||||
.where.not(id: ordered_event_ids)
|
|
||||||
.order(start_time: :asc)
|
|
||||||
.limit(6)
|
|
||||||
else
|
|
||||||
@upcoming_preview_events = []
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Events page showing all published events with pagination
|
# Events page showing all published events with pagination
|
||||||
|
|||||||
@@ -1,115 +1,127 @@
|
|||||||
# Legacy tickets controller - redirects to new order system
|
# Tickets controller - handles ticket viewing and downloads with SEO-friendly URLs
|
||||||
#
|
#
|
||||||
# This controller now primarily handles legacy redirects and backward compatibility
|
# This controller manages individual ticket display and downloads
|
||||||
# Most ticket creation functionality has been moved to OrdersController
|
# Uses event-slug-ticket-id format for SEO-friendly URLs
|
||||||
class TicketsController < ApplicationController
|
class TicketsController < ApplicationController
|
||||||
before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :download ]
|
before_action :authenticate_user!
|
||||||
before_action :set_event, only: [ :checkout, :retry_payment ]
|
before_action :set_ticket_from_seo_params, only: [:show, :view, :download, :retry_payment]
|
||||||
|
|
||||||
|
|
||||||
# Redirect to order-based checkout
|
|
||||||
def checkout
|
|
||||||
# Check for draft order
|
|
||||||
if session[:draft_order_id].present?
|
|
||||||
order = current_user.orders.find_by(id: session[:draft_order_id], status: "draft")
|
|
||||||
if order.present?
|
|
||||||
redirect_to order_checkout_path(order)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# No order found
|
|
||||||
@event = Event.includes(:ticket_types).find(params[:id])
|
|
||||||
redirect_to event_path(@event.slug, @event), alert: "Aucun billet en attente de paiement"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Redirect to order-based payment success
|
|
||||||
def payment_success
|
|
||||||
redirect_to order_payment_success_path(session_id: params[:session_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Redirect to order-based payment cancel
|
|
||||||
def payment_cancel
|
|
||||||
redirect_to order_payment_cancel_path
|
|
||||||
end
|
|
||||||
|
|
||||||
# Redirect retry payment to order system
|
|
||||||
def retry_payment
|
|
||||||
@event = Event.includes(:ticket_types).find(params[:id])
|
|
||||||
|
|
||||||
# Look for draft order for this event
|
|
||||||
order = current_user.orders.find_by(event: @event, status: "draft")
|
|
||||||
|
|
||||||
if order&.can_retry_payment?
|
|
||||||
redirect_to retry_payment_order_path(order)
|
|
||||||
else
|
|
||||||
redirect_to event_path(@event.slug, @event),
|
|
||||||
alert: "Aucune commande disponible pour un nouveau paiement"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Display ticket details
|
# Display ticket details
|
||||||
def show
|
def show
|
||||||
# Find ticket by qr code id
|
@event = @ticket.event
|
||||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user)
|
|
||||||
.find_by(tickets: { qr_code: params[:qr_code] })
|
|
||||||
|
|
||||||
if @ticket.nil?
|
|
||||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Display ticket in PDF-like format
|
||||||
|
def view
|
||||||
@event = @ticket.event
|
@event = @ticket.event
|
||||||
@order = @ticket.order
|
|
||||||
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Download PDF ticket - only accessible by ticket owner
|
# Download PDF ticket - only accessible by ticket owner
|
||||||
# User must be authenticated to download ticket
|
# User must be authenticated to download ticket
|
||||||
# TODO: change ID to an unique identifier (UUID)
|
|
||||||
def download
|
def download
|
||||||
# Find ticket by qr code id
|
# Generate PDF using Grover
|
||||||
@ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user)
|
begin
|
||||||
.find_by(tickets: { qr_code: params[:qr_code] })
|
Rails.logger.info "Starting PDF generation for ticket ID: #{@ticket.id}"
|
||||||
|
|
||||||
if @ticket.nil?
|
# Render the HTML template
|
||||||
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
html = render_to_string(
|
||||||
return
|
partial: "tickets/pdf_ticket",
|
||||||
end
|
layout: false,
|
||||||
|
locals: { ticket: @ticket }
|
||||||
|
)
|
||||||
|
|
||||||
|
Rails.logger.info "HTML template rendered successfully, length: #{html.length}"
|
||||||
|
|
||||||
|
# Configure Grover options for PDF generation
|
||||||
|
pdf_options = {
|
||||||
|
format: 'A4',
|
||||||
|
margin: {
|
||||||
|
top: '0.5in',
|
||||||
|
bottom: '0.5in',
|
||||||
|
left: '0.5in',
|
||||||
|
right: '0.5in'
|
||||||
|
},
|
||||||
|
print_background: true,
|
||||||
|
display_header_footer: false,
|
||||||
|
prefer_css_page_size: true,
|
||||||
|
launch_args: ["--no-sandbox", "--disable-setuid-sandbox"] # For better compatibility
|
||||||
|
}
|
||||||
|
|
||||||
# Generate PDF
|
# Generate PDF
|
||||||
pdf_content = @ticket.to_pdf
|
pdf = Grover.new(html, pdf_options).to_pdf
|
||||||
|
|
||||||
# Send PDF as download
|
Rails.logger.info "PDF generation completed for ticket ID: #{@ticket.id}"
|
||||||
send_data pdf_content,
|
|
||||||
filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf",
|
# Send PDF as download with SEO-friendly filename
|
||||||
type: "application/pdf",
|
send_data pdf,
|
||||||
disposition: "attachment"
|
filename: "billet-#{@ticket.event.slug}-#{@ticket.id}.pdf",
|
||||||
rescue ActiveRecord::RecordNotFound
|
type: 'application/pdf',
|
||||||
redirect_to dashboard_path, alert: "Billet non trouvé"
|
disposition: 'attachment'
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.error "Error generating ticket PDF: #{e.message}"
|
Rails.logger.error "PDF generation failed for ticket ID: #{@ticket.id} - Error: #{e.message}"
|
||||||
redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
|
Rails.logger.error e.backtrace.join("\n")
|
||||||
|
|
||||||
|
redirect_to view_ticket_path(event_slug: @ticket.event.slug, ticket_id: @ticket.id),
|
||||||
|
alert: "Erreur lors de la génération du PDF. Veuillez réessayer."
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Redirect retry payment to order system
|
||||||
|
def retry_payment
|
||||||
|
# Look for draft order for this ticket's event
|
||||||
|
order = current_user.orders.find_by(event: @ticket.event, status: "draft")
|
||||||
|
|
||||||
|
if order&.can_retry_payment?
|
||||||
|
year = order.event.start_time.year
|
||||||
|
month = format("%02d", order.event.start_time.month)
|
||||||
|
redirect_to event_checkout_path(year: year, month: month, slug: order.event.slug)
|
||||||
|
else
|
||||||
|
redirect_to seo_event_path(@ticket.event),
|
||||||
|
alert: "Aucune commande disponible pour un nouveau paiement"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Legacy redirects for backward compatibility
|
||||||
|
def payment_success
|
||||||
|
redirect_to booking_payment_success_path(session_id: params[:session_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def payment_cancel
|
||||||
|
redirect_to booking_payment_cancelled_path
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_event
|
def set_ticket_from_seo_params
|
||||||
event_id = params[:id] || session[:event_id]
|
# Parse event_slug and ticket_id from the SEO-friendly format: event-slug-123
|
||||||
|
slug_and_id = params[:event_slug_ticket_id] || "#{params[:event_slug]}-#{params[:ticket_id]}"
|
||||||
|
|
||||||
Rails.logger.debug "TicketsController#set_event - params[:id]: #{params[:id].inspect}, session[:event_id]: #{session[:event_id].inspect}"
|
# Split by last dash to separate event slug from ticket ID
|
||||||
|
parts = slug_and_id.split('-')
|
||||||
|
ticket_id = parts.pop
|
||||||
|
event_slug = parts.join('-')
|
||||||
|
|
||||||
unless event_id
|
# Find ticket and ensure it belongs to current user
|
||||||
Rails.logger.error "TicketsController#set_event - No event ID found"
|
@ticket = Ticket.joins(order: :user)
|
||||||
redirect_to events_path, alert: "Aucun événement spécifié"
|
.includes(:event, :ticket_type, order: :user)
|
||||||
return
|
.joins(:event)
|
||||||
|
.where(
|
||||||
|
tickets: { id: ticket_id },
|
||||||
|
orders: { user_id: current_user.id },
|
||||||
|
events: { slug: event_slug }
|
||||||
|
)
|
||||||
|
.first
|
||||||
|
|
||||||
|
unless @ticket
|
||||||
|
redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@event = Event.includes(:ticket_types).find(event_id)
|
# Generate SEO-friendly path for an event
|
||||||
Rails.logger.debug "TicketsController#set_event - Found event: #{@event.id} - #{@event.name}"
|
def seo_event_path(event)
|
||||||
rescue ActiveRecord::RecordNotFound
|
year = event.start_time.year
|
||||||
Rails.logger.error "TicketsController#set_event - Event not found with ID: #{event_id}"
|
month = format("%02d", event.start_time.month)
|
||||||
redirect_to events_path, alert: "Événement non trouvé"
|
event_path(year: year, month: month, slug: event.slug)
|
||||||
end
|
end
|
||||||
|
helper_method :seo_event_path
|
||||||
end
|
end
|
||||||
17
app/helpers/pdf_helper.rb
Normal file
17
app/helpers/pdf_helper.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module PdfHelper
|
||||||
|
require "rqrcode"
|
||||||
|
|
||||||
|
# Generate SVG QR code for tickets
|
||||||
|
def qr_code_tag(data)
|
||||||
|
qrcode = RQRCode::QRCode.new(data)
|
||||||
|
|
||||||
|
# Render as SVG
|
||||||
|
raw qrcode.as_svg(
|
||||||
|
offset: 0,
|
||||||
|
color: "000",
|
||||||
|
shape_rendering: "crispEdges",
|
||||||
|
module_size: 4,
|
||||||
|
standalone: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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 = `/orders/new/events/${this.eventSlugValue}.${this.eventIdValue}`;
|
const OrderNewUrl = `/events/${this.eventSlugValue}/orders/new`;
|
||||||
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,297 +0,0 @@
|
|||||||
require "prawn"
|
|
||||||
require "prawn/qrcode"
|
|
||||||
require "rqrcode"
|
|
||||||
|
|
||||||
# Service de génération de billets PDF utilisant Prawn
|
|
||||||
#
|
|
||||||
# Génère des billets PDF simples et compacts avec codes QR pour la validation d'entrée
|
|
||||||
# Design propre et minimaliste qui tient sur une seule page
|
|
||||||
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 ENV.fetch("APP_NAME", "Aperonight"), 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 10
|
|
||||||
|
|
||||||
# Ticket info box
|
|
||||||
pdf.stroke_color "E5E7EB"
|
|
||||||
pdf.fill_color "F9FAFB"
|
|
||||||
pdf.rounded_rectangle [ 0, pdf.cursor ], 310, 150, 10
|
|
||||||
pdf.fill_and_stroke
|
|
||||||
|
|
||||||
pdf.move_down 10
|
|
||||||
pdf.fill_color "000000"
|
|
||||||
pdf.font "Helvetica", size: 12
|
|
||||||
|
|
||||||
# Customer name
|
|
||||||
pdf.indent 10 do
|
|
||||||
pdf.text "Titulaire du billet :", style: :bold
|
|
||||||
pdf.text "#{ticket.first_name} #{ticket.last_name}"
|
|
||||||
end
|
|
||||||
pdf.move_down 8
|
|
||||||
|
|
||||||
# Ticket details
|
|
||||||
pdf.indent 10 do
|
|
||||||
pdf.text "Type de billet :", style: :bold
|
|
||||||
pdf.text ticket.ticket_type.name
|
|
||||||
end
|
|
||||||
pdf.move_down 8
|
|
||||||
|
|
||||||
pdf.indent 10 do
|
|
||||||
pdf.text "Prix :", style: :bold
|
|
||||||
pdf.text "#{ticket.price_euros} €"
|
|
||||||
end
|
|
||||||
pdf.move_down 8
|
|
||||||
|
|
||||||
pdf.indent 10 do
|
|
||||||
pdf.text "Date et heure :", style: :bold
|
|
||||||
pdf.text ticket.event.start_time.strftime("%d %B %Y à %H:%M")
|
|
||||||
end
|
|
||||||
pdf.move_down 20
|
|
||||||
|
|
||||||
# Informations sur le lieu
|
|
||||||
pdf.fill_color "374151"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 14
|
|
||||||
pdf.text "Informations sur le lieu"
|
|
||||||
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
|
|
||||||
|
|
||||||
# Code QR
|
|
||||||
pdf.fill_color "000000"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 14
|
|
||||||
pdf.text "Code QR", 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
|
|
||||||
|
|
||||||
# Generate QR code - prawn-qrcode expects the data string directly
|
|
||||||
pdf.print_qr_code(qr_code_data, extent: 120, align: :center)
|
|
||||||
|
|
||||||
pdf.move_down 15
|
|
||||||
|
|
||||||
# QR code text
|
|
||||||
pdf.font "Helvetica", size: 8
|
|
||||||
pdf.fill_color "6B7280"
|
|
||||||
pdf.text "#{ticket.qr_code}", align: :center
|
|
||||||
|
|
||||||
|
|
||||||
# Ticket ID
|
|
||||||
pdf.font "Helvetica", size: 8
|
|
||||||
pdf.fill_color "6B7280"
|
|
||||||
pdf.text "ID du billet : #{ticket.id}", align: :center
|
|
||||||
|
|
||||||
# Footer
|
|
||||||
pdf.move_down 30
|
|
||||||
pdf.stroke_color "E5E7EB"
|
|
||||||
pdf.horizontal_line 0, 310
|
|
||||||
pdf.move_down 6
|
|
||||||
|
|
||||||
pdf.font "Helvetica", size: 8
|
|
||||||
pdf.fill_color "6B7280"
|
|
||||||
pdf.text "Ce billet est valable pour une seule entrée.", align: :center
|
|
||||||
pdf.text "Présentez ce billet à l'entrée du lieu.", align: :center
|
|
||||||
pdf.move_down 5
|
|
||||||
pdf.text "Généré le #{Time.current.strftime('%d %B %Y à %H:%M')}", align: :center
|
|
||||||
end.render
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def create_simple_header(pdf)
|
|
||||||
# Nom de la marque
|
|
||||||
pdf.fill_color "6366F1"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 24
|
|
||||||
pdf.text "AperoNight", align: :center
|
|
||||||
|
|
||||||
pdf.move_down 5
|
|
||||||
pdf.font "Helvetica", size: 10
|
|
||||||
pdf.fill_color "64748B"
|
|
||||||
pdf.text "Billet d'entree", align: :center
|
|
||||||
|
|
||||||
pdf.move_down 20
|
|
||||||
|
|
||||||
# Simple divider line
|
|
||||||
pdf.stroke_color "E5E7EB"
|
|
||||||
pdf.horizontal_line 0, pdf.bounds.width
|
|
||||||
pdf.move_down 20
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_ticket_info(pdf)
|
|
||||||
# Nom de l'événement - proéminent
|
|
||||||
pdf.fill_color "1F2937"
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 18
|
|
||||||
pdf.text ticket.event.name, align: :center
|
|
||||||
pdf.move_down 15
|
|
||||||
|
|
||||||
# Two-column layout for ticket details
|
|
||||||
pdf.bounding_box([ 0, pdf.cursor ], width: pdf.bounds.width, height: 120) do
|
|
||||||
# Left column
|
|
||||||
pdf.bounding_box([ 0, pdf.cursor ], width: pdf.bounds.width / 2 - 20, height: 120) do
|
|
||||||
create_info_item(pdf, "Date", ticket.event.start_time.strftime("%d %B %Y"))
|
|
||||||
create_info_item(pdf, "Heure", ticket.event.start_time.strftime("%H:%M"))
|
|
||||||
create_info_item(pdf, "Lieu", ticket.event.venue_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Right column
|
|
||||||
pdf.bounding_box([ pdf.bounds.width / 2 + 20, pdf.cursor ], width: pdf.bounds.width / 2 - 20, height: 120) do
|
|
||||||
create_info_item(pdf, "Type", ticket.ticket_type.name)
|
|
||||||
create_info_item(pdf, "Prix", "#{sprintf('%.2f', ticket.price_euros)} €")
|
|
||||||
create_info_item(pdf, "Titulaire", "#{ticket.first_name} #{ticket.last_name}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
pdf.move_down 30
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_info_item(pdf, label, value)
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 9
|
|
||||||
pdf.fill_color "64748B"
|
|
||||||
pdf.text label.upcase
|
|
||||||
|
|
||||||
pdf.move_down 2
|
|
||||||
pdf.font "Helvetica", size: 11
|
|
||||||
pdf.fill_color "1F2937"
|
|
||||||
pdf.text value
|
|
||||||
pdf.move_down 12
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_qr_section(pdf)
|
|
||||||
# Center the QR code horizontally
|
|
||||||
qr_size = 120
|
|
||||||
x_position = (pdf.bounds.width - qr_size) / 2
|
|
||||||
|
|
||||||
pdf.bounding_box([ x_position, pdf.cursor ], width: qr_size, height: qr_size + 40) do
|
|
||||||
# QR Code title
|
|
||||||
pdf.font "Helvetica", style: :bold, size: 12
|
|
||||||
pdf.fill_color "1F2937"
|
|
||||||
pdf.text "Code d'entree", align: :center
|
|
||||||
pdf.move_down 10
|
|
||||||
|
|
||||||
# Generate QR code
|
|
||||||
generate_simple_qr_code(pdf, qr_size)
|
|
||||||
|
|
||||||
pdf.move_down 10
|
|
||||||
|
|
||||||
# QR code ID
|
|
||||||
pdf.font "Helvetica", size: 8
|
|
||||||
pdf.fill_color "64748B"
|
|
||||||
pdf.text "ID: #{ticket.qr_code[0..15]}...", align: :center
|
|
||||||
end
|
|
||||||
|
|
||||||
pdf.move_down 40
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_simple_qr_code(pdf, size)
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Generate QR code
|
|
||||||
pdf.print_qr_code(qr_code_data, extent: size, align: :center)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_simple_footer(pdf)
|
|
||||||
# Security notice
|
|
||||||
pdf.font "Helvetica", size: 8
|
|
||||||
pdf.fill_color "64748B"
|
|
||||||
pdf.text "Ce billet est valable pour une seule entree.", align: :center
|
|
||||||
pdf.text "Presentez ce code QR a l'entree de l'evenement.", align: :center
|
|
||||||
|
|
||||||
pdf.move_down 10
|
|
||||||
|
|
||||||
# Divider line
|
|
||||||
pdf.stroke_color "E5E7EB"
|
|
||||||
pdf.horizontal_line 0, pdf.bounds.width
|
|
||||||
pdf.move_down 5
|
|
||||||
|
|
||||||
# Generation timestamp
|
|
||||||
pdf.font "Helvetica", size: 7
|
|
||||||
pdf.fill_color "9CA3AF"
|
|
||||||
timestamp = "Genere le #{Time.current.strftime('%d/%m/%Y a %H:%M')}"
|
|
||||||
pdf.text timestamp, align: :center
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
61
app/views/booking/payments/success.html.erb
Normal file
61
app/views/booking/payments/success.html.erb
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<div class="min-h-screen bg-gradient-to-br from-green-50 to-green-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full bg-white rounded-2xl shadow-xl p-8">
|
||||||
|
<!-- Success Icon -->
|
||||||
|
<div class="flex justify-center mb-6">
|
||||||
|
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success Message -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">Paiement réussi !</h1>
|
||||||
|
<p class="text-gray-600">Votre commande a été confirmée et vos billets ont été envoyés par email.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Details -->
|
||||||
|
<% if @order&.present? %>
|
||||||
|
<div class="border-t border-gray-200 pt-6 mb-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<span class="text-sm font-medium text-gray-900">Commande #<%= @order.id %></span>
|
||||||
|
<span class="text-sm text-gray-500"><%= @order.created_at.strftime("%d/%m/%Y à %H:%M") %></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-sm text-gray-600">Événement:</span>
|
||||||
|
<span class="text-sm font-medium text-gray-900"><%= @order.event.name %></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-sm text-gray-600">Nombre de billets:</span>
|
||||||
|
<span class="text-sm font-medium text-gray-900"><%= @order.tickets.count %></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-sm text-gray-600">Total:</span>
|
||||||
|
<span class="text-sm font-bold text-green-600"><%= @order.total_amount_euros %>€</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<%= link_to dashboard_path, class: "w-full bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-4 rounded-lg transition-colors text-center block" do %>
|
||||||
|
Voir mes billets
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= link_to events_path, class: "w-full bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-3 px-4 rounded-lg transition-colors text-center block" do %>
|
||||||
|
Découvrir d'autres événements
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Text -->
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
Un email de confirmation a été envoyé à votre adresse email avec vos billets en pièce jointe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= link_to event_path(event.slug, event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
|
<%= link_to seo_event_path(event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
|
<div class="w-16 h-16 bg-slate-200 dark:bg-slate-700 rounded-lg overflow-hidden flex-shrink-0">
|
||||||
<%= image_tag event.image, alt: event.name, class: "w-full h-full object-cover" if event.image.present? %>
|
<%= image_tag event.image, alt: event.name, class: "w-full h-full object-cover" if event.image.present? %>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
>
|
>
|
||||||
<% if event.image.present? %>
|
<% if event.image.present? %>
|
||||||
<div class="h-48 overflow-hidden">
|
<div class="h-48 overflow-hidden">
|
||||||
<%= link_to event_path(event.slug, event) do %>
|
<%= link_to event_path(event) do %>
|
||||||
<img
|
<img
|
||||||
src="<%= event.image %>"
|
src="<%= event.image %>"
|
||||||
alt="<%= event.name %>"
|
alt="<%= event.name %>"
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= link_to event_path(event.slug, event), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %>
|
<%= link_to event_path(event), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %>
|
||||||
Détails
|
Détails
|
||||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<%= link_to ticket_download_path(ticket.qr_code, format: :pdf),
|
<%= link_to download_ticket_path(ticket, format: :pdf),
|
||||||
class: "inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-lg hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
class: "inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-lg hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
||||||
<svg class="w-4 h-4 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">
|
||||||
<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>
|
<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>
|
||||||
|
|||||||
@@ -1,4 +1,62 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
<!-- SEO Meta Tags for Event -->
|
||||||
|
<% content_for :title, "#{@event.name} - #{@event.start_time.strftime('%d/%m/%Y')} | Aperonight" %>
|
||||||
|
<% content_for :description, @event.description.truncate(160) %>
|
||||||
|
<% content_for :keywords, "#{@event.name}, événement, soirée, #{@event.venue_name}, billets, réservation" %>
|
||||||
|
<% content_for :canonical_url, seo_event_path(@event) %>
|
||||||
|
<% content_for :og_image, @event.image if @event.image.present? %>
|
||||||
|
|
||||||
|
<!-- Structured Data for Event -->
|
||||||
|
<% content_for :head do %>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Event",
|
||||||
|
"name": "<%= @event.name %>",
|
||||||
|
"description": "<%= strip_tags(@event.description) %>",
|
||||||
|
"startDate": "<%= @event.start_time.iso8601 %>",
|
||||||
|
"endDate": "<%= @event.end_time&.iso8601 || (@event.start_time + 4.hours).iso8601 %>",
|
||||||
|
"eventStatus": "https://schema.org/EventScheduled",
|
||||||
|
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
|
||||||
|
"location": {
|
||||||
|
"@type": "Place",
|
||||||
|
"name": "<%= @event.venue_name %>",
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "<%= @event.venue_address %>"
|
||||||
|
},
|
||||||
|
"geo": {
|
||||||
|
"@type": "GeoCoordinates",
|
||||||
|
"latitude": <%= @event.latitude %>,
|
||||||
|
"longitude": <%= @event.longitude %>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"organizer": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "<%= @event.user.company_name.present? ? @event.user.company_name : "#{@event.user.first_name} #{@event.user.last_name}" %>",
|
||||||
|
"email": "<%= @event.user.email %>"
|
||||||
|
},
|
||||||
|
<% if @event.image.present? %>
|
||||||
|
"image": [
|
||||||
|
"<%= @event.image %>"
|
||||||
|
],
|
||||||
|
<% end %>
|
||||||
|
"offers": [
|
||||||
|
<% @event.ticket_types.each_with_index do |ticket_type, index| %>
|
||||||
|
{
|
||||||
|
"@type": "Offer",
|
||||||
|
"name": "<%= ticket_type.name %>",
|
||||||
|
"price": "<%= ticket_type.price_cents / 100.0 %>",
|
||||||
|
"priceCurrency": "EUR",
|
||||||
|
"availability": "https://schema.org/InStock",
|
||||||
|
"url": "<%= seo_book_tickets_path(@event) %>"
|
||||||
|
}<%= ',' if index < @event.ticket_types.count - 1 %>
|
||||||
|
<% end %>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<nav class="mb-6" aria-label="Breadcrumb">
|
<nav class="mb-6" aria-label="Breadcrumb">
|
||||||
@@ -197,7 +255,7 @@
|
|||||||
|
|
||||||
<!-- Right Column: Ticket Selection -->
|
<!-- Right Column: Ticket Selection -->
|
||||||
<div class="lg:col-span-1">
|
<div class="lg:col-span-1">
|
||||||
<%= form_with url: event_order_new_path(@event.slug, @event.id), method: :get, id: "checkout_form", local: true, data: {
|
<%= form_with url: book_event_tickets_path(year: @event.start_time.year, month: format("%02d", @event.start_time.month), slug: @event.slug), method: :get, id: "checkout_form", local: true, data: {
|
||||||
controller: "ticket-selection",
|
controller: "ticket-selection",
|
||||||
ticket_selection_target: "form",
|
ticket_selection_target: "form",
|
||||||
ticket_selection_event_slug_value: @event.slug,
|
ticket_selection_event_slug_value: @event.slug,
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title><%= content_for(:title) || "Aperonight" %></title>
|
<title><%= content_for(:title) || "Aperonight - Événements et Soirées" %></title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
|
<!-- SEO Meta Tags -->
|
||||||
|
<meta name="description" content="<%= content_for(:description) || "Découvrez et réservez vos billets pour les meilleurs événements et soirées. Aperonight vous connecte aux événements incontournables près de chez vous." %>">
|
||||||
|
<meta name="keywords" content="<%= content_for(:keywords) || "événements, soirées, billets, réservation, nightlife, fêtes" %>">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<link rel="canonical" href="<%= content_for(:canonical_url) || request.original_url %>">
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="<%= request.original_url %>">
|
||||||
|
<meta property="og:title" content="<%= content_for(:title) || "Aperonight - Événements et Soirées" %>">
|
||||||
|
<meta property="og:description" content="<%= content_for(:description) || "Découvrez et réservez vos billets pour les meilleurs événements et soirées." %>">
|
||||||
|
<meta property="og:image" content="<%= content_for(:og_image) || asset_url('aperonight-og-image.jpg') %>">
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image">
|
||||||
|
<meta property="twitter:url" content="<%= request.original_url %>">
|
||||||
|
<meta property="twitter:title" content="<%= content_for(:title) || "Aperonight - Événements et Soirées" %>">
|
||||||
|
<meta property="twitter:description" content="<%= content_for(:description) || "Découvrez et réservez vos billets pour les meilleurs événements et soirées." %>">
|
||||||
|
<meta property="twitter:image" content="<%= content_for(:og_image) || asset_url('aperonight-og-image.jpg') %>">
|
||||||
|
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<%= csp_meta_tag %>
|
<%= csp_meta_tag %>
|
||||||
<%= yield :head %>
|
<%= yield :head %>
|
||||||
|
|||||||
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>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<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-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= link_to event_path(@order.event.slug, @order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
<%= link_to event_path(@order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
<%= @order.event.name %>
|
<%= @order.event.name %>
|
||||||
<% 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-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -289,7 +289,7 @@
|
|||||||
<!-- Order Actions -->
|
<!-- Order Actions -->
|
||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
<%= link_to event_path(@order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<svg class="w-4 h-4 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">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center justify-between mb-8">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100">Toutes mes commandes</h1>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 mt-2">Consultez l'historique de toutes vos commandes</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<%= link_to dashboard_path, class: "inline-flex items-center px-4 py-2 bg-purple-100 hover:bg-purple-200 text-purple-700 font-medium rounded-lg transition-colors duration-200" do %>
|
|
||||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i>
|
|
||||||
Retour au tableau de bord
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Orders List -->
|
|
||||||
<% if @orders.any? %>
|
|
||||||
<div class="space-y-6">
|
|
||||||
<% @orders.each do |order| %>
|
|
||||||
<div class="card hover-lift">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-start justify-between mb-4">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center space-x-3 mb-2">
|
|
||||||
<h3 class="font-semibold text-slate-900 dark:text-slate-100"><%= order.event.name %></h3>
|
|
||||||
<span class="text-xs px-2 py-1 rounded-full <%= order.status == 'paid' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : order.status == 'completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' %>">
|
|
||||||
<%= order.status.humanize %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-4 text-sm text-slate-600 dark:text-slate-400 mb-3">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i data-lucide="calendar" class="w-4 h-4 mr-1"></i>
|
|
||||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i data-lucide="map-pin" class="w-4 h-4 mr-1"></i>
|
|
||||||
<%= order.event.venue_name %>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i data-lucide="shopping-bag" class="w-4 h-4 mr-1"></i>
|
|
||||||
<%= pluralize(order.tickets.count, 'billet') %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
|
||||||
Commande #<%= order.id %> • <%= order.created_at.strftime("%d/%m/%Y") %> • <%= order.total_amount_euros %>€
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center space-x-2 ml-4">
|
|
||||||
<%= link_to order_path(order),
|
|
||||||
class: "inline-flex items-center px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors duration-200" do %>
|
|
||||||
<i data-lucide="eye" class="w-4 h-4 mr-2"></i>
|
|
||||||
Voir détails
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick tickets preview -->
|
|
||||||
<div class="border-t border-slate-200 dark:border-slate-600 pt-3">
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<% order.tickets.limit(3).each do |ticket| %>
|
|
||||||
<div class="flex items-center justify-between text-sm bg-slate-50 dark:bg-slate-700 rounded p-2">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
|
||||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
|
||||||
<span class="text-slate-500">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<%= link_to ticket_download_path(ticket.qr_code),
|
|
||||||
class: "text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200" do %>
|
|
||||||
<i data-lucide="download" class="w-3 h-3"></i>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if order.tickets.count > 3 %>
|
|
||||||
<div class="text-xs text-slate-500 text-center">
|
|
||||||
et <%= order.tickets.count - 3 %> autre<%= order.tickets.count - 3 > 1 ? 's' : '' %> billet<%= order.tickets.count - 3 > 1 ? 's' : '' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
<div class="mt-8">
|
|
||||||
<%= paginate @orders %>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<div class="text-center py-12">
|
|
||||||
<div class="w-16 h-16 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<i data-lucide="shopping-bag" class="w-8 h-8 text-slate-400"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-medium text-slate-900 dark:text-slate-100 mb-2">Aucune commande</h3>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 mb-6">Vous n'avez encore passé aucune commande.</p>
|
|
||||||
<%= link_to events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
|
||||||
<i data-lucide="search" class="w-4 h-4 mr-2"></i>
|
|
||||||
Découvrir les événements
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<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-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= link_to event_path(@event.slug, @event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
<%= link_to event_path(@event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
<%= @event.name %>
|
<%= @event.name %>
|
||||||
<% 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-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<p class="text-gray-600 max-w-md mx-auto">Veuillez fournir les prénoms et noms des personnes qui utiliseront les billets.</p>
|
<p class="text-gray-600 max-w-md mx-auto">Veuillez fournir les prénoms et noms des personnes qui utiliseront les billets.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= form_with url: event_order_create_path(@event.slug, @event.id), method: :post, local: true, class: "space-y-8" do |form| %>
|
<%= form_with url: event_orders_path(@event), method: :post, local: true, class: "space-y-8" do |form| %>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center mb-2">
|
<div class="flex items-center justify-center mb-2">
|
||||||
<div class="bg-purple-600 rounded-full p-2 mr-3">
|
<div class="bg-purple-600 rounded-full p-2 mr-3">
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-4 pt-6">
|
<div class="flex flex-col sm:flex-row gap-4 pt-6">
|
||||||
<%= link_to "Retour", event_path(@event.slug, @event), 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" %>
|
<%= link_to "Retour", event_path(@event), 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" %>
|
||||||
<%= form.submit "Procéder au paiement", 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" %>
|
<%= form.submit "Procéder au paiement", 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" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
|
||||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="text-center mb-8">
|
|
||||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
|
||||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
||||||
<!-- Event & Order Details -->
|
|
||||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
|
||||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Détails de Votre Commande</h2>
|
|
||||||
<% if @order %>
|
|
||||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
|
||||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-1 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
||||||
</svg>
|
|
||||||
<span class="text-red-600 font-medium">
|
|
||||||
Paiement annulé
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<p class="text-gray-600">Aucune commande trouvée.</p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if @order %>
|
|
||||||
<!-- Event Information -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Événement</h3>
|
|
||||||
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
|
|
||||||
<h4 class="font-semibold text-purple-900 text-lg"><%= @order.event.name %></h4>
|
|
||||||
<div class="mt-2 space-y-1 text-sm text-purple-700">
|
|
||||||
<% if @order.event.start_time %>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
||||||
</svg>
|
|
||||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if @order.event.venue_name.present? %>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<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" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
||||||
</svg>
|
|
||||||
<%= @order.event.venue_name %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if @order.event.venue_address.present? %>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
|
||||||
</svg>
|
|
||||||
<%= @order.event.venue_address %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Summary -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
|
||||||
|
|
||||||
<% @order.tickets.each do |ticket| %>
|
|
||||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<h4 class="text-sm font-medium text-gray-900 truncate"><%= ticket.ticket_type.name %></h4>
|
|
||||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
|
||||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
|
||||||
</svg>
|
|
||||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center text-xs text-red-600 mt-1">
|
|
||||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
||||||
</svg>
|
|
||||||
En attente de paiement
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<div class="text-lg font-semibold text-gray-900"><%= ticket.price_euros %>€</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Total -->
|
|
||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
|
||||||
<div class="flex items-center justify-between text-lg">
|
|
||||||
<span class="font-medium text-gray-900">Total à payer</span>
|
|
||||||
<span class="font-bold text-2xl text-red-600">
|
|
||||||
<%= @order.total_amount_euros %>€
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions & Ticket Access -->
|
|
||||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
|
||||||
<% if @order&.can_retry_payment? %>
|
|
||||||
<!-- Payment Required -->
|
|
||||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Paiement Requis</h2>
|
|
||||||
<p class="text-sm text-gray-600">Votre commande nécessite un paiement</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<%= link_to checkout_order_path(@order), class: "block w-full text-center py-3 px-4 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors" do %>
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
|
||||||
</svg>
|
|
||||||
Procéder au Paiement
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<!-- Navigation Actions -->
|
|
||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
|
||||||
<div class="space-y-3">
|
|
||||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
|
||||||
</svg>
|
|
||||||
Retour au Tableau de Bord
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
Voir l'Événement Complet
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,36 +1,35 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
<div class="min-h-screen bg-gradient-to-br from-green-50 to-emerald-50 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">
|
||||||
<!-- Header -->
|
<!-- Success Header -->
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-12">
|
||||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
<div class="mx-auto w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mb-6">
|
||||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
<h1 class="text-4xl font-bold text-gray-900 mb-4">Paiement réussi !</h1>
|
||||||
|
<p class="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||||
|
Félicitations ! Votre commande a été traitée avec succès. Vous allez recevoir vos billets par email d'ici quelques minutes.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 & Order Details -->
|
<!-- Order Summary -->
|
||||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Détails de Votre Commande</h2>
|
<h2 class="text-2xl font-bold text-gray-900 mb-2">Récapitulatif de la commande</h2>
|
||||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
|
||||||
<div class="flex flex-col">
|
Commande #<%= @order.id %>
|
||||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
|
||||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg class="w-4 h-4 mr-1 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-green-600 font-medium">
|
<span class="text-green-600 font-medium">Payée</span>
|
||||||
Payée
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
<svg class="w-4 h-4 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">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
<%= l(@order.event.start_time, format: :long) %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @order.event.venue_name.present? %>
|
<% if @order.event.venue_name.present? %>
|
||||||
@@ -58,21 +57,13 @@
|
|||||||
<%= @order.event.venue_name %>
|
<%= @order.event.venue_name %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @order.event.venue_address.present? %>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
|
||||||
</svg>
|
|
||||||
<%= @order.event.venue_address %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Summary -->
|
<!-- Tickets List -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Vos billets</h3>
|
||||||
|
|
||||||
<% @order.tickets.each do |ticket| %>
|
<% @order.tickets.each do |ticket| %>
|
||||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
||||||
@@ -102,47 +93,46 @@
|
|||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||||
<div class="flex items-center justify-between text-lg">
|
<div class="flex items-center justify-between text-lg">
|
||||||
<span class="font-medium text-gray-900">Total payé</span>
|
<span class="font-medium text-gray-900">Total payé</span>
|
||||||
<span class="font-bold text-2xl text-green-600">
|
<span class="font-bold text-2xl text-green-600"><%= @order.total_amount_euros %>€</span>
|
||||||
<%= @order.total_amount_euros %>€
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions & Ticket Access -->
|
<!-- Next Steps -->
|
||||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
||||||
<!-- Ticket Access -->
|
|
||||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Accédez à Vos Billets</h2>
|
<h2 class="text-xl font-bold text-gray-900 mb-2">Prochaines étapes</h2>
|
||||||
<p class="text-sm text-gray-600">Téléchargez ou consultez vos billets</p>
|
<p class="text-sm text-gray-600">Que faire maintenant ?</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
<!-- Email Confirmation -->
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-blue-600 font-semibold text-sm">1</span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h3 class="font-semibold text-gray-900 mb-1">Vérifiez votre email</h3>
|
||||||
|
<p class="text-gray-600 text-sm">Nous avons envoyé vos billets à <strong><%= current_user.email %></strong>. Vérifiez aussi vos spams.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Download Tickets -->
|
<!-- Download Tickets -->
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||||
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<span class="text-purple-600 font-semibold text-sm">2</span>
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<h3 class="font-semibold text-gray-900 mb-1">Télécharger Vos Billets</h3>
|
<h3 class="font-semibold text-gray-900 mb-1">Téléchargez vos billets</h3>
|
||||||
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<% @order.tickets.each_with_index do |ticket, index| %>
|
<% @order.tickets.each do |ticket| %>
|
||||||
<div class="flex items-center justify-between p-3 border border-purple-200 rounded-lg bg-purple-50 hover:bg-purple-100 transition-colors">
|
<%= link_to download_ticket_path(ticket), class: "inline-flex items-center px-3 py-2 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-purple-50 hover:bg-purple-100 transition-colors mr-2 mb-2" do %>
|
||||||
<%= link_to ticket_path(ticket.qr_code), class: "flex-1 flex items-center text-purple-700 hover:text-purple-800 font-medium" do %>
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div class="flex items-center justify-center w-6 h-6 bg-purple-200 text-purple-800 text-xs font-bold rounded-full mr-3">
|
|
||||||
<%= index + 1 %>
|
|
||||||
</div>
|
|
||||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
|
||||||
<% end %>
|
|
||||||
<%= link_to ticket_download_path(ticket.qr_code), class: "ml-3 p-2 text-purple-600 hover:text-purple-800 hover:bg-purple-200 rounded-lg transition-colors", title: "Télécharger le billet PDF" do %>
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<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" 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"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,34 +141,46 @@
|
|||||||
<!-- Event Day -->
|
<!-- Event Day -->
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
<div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<span class="text-green-600 font-semibold text-sm">3</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<h3 class="font-semibold text-gray-900 mb-1">Le Jour de l'Événement</h3>
|
<h3 class="font-semibold text-gray-900 mb-1">Le jour J</h3>
|
||||||
<p class="text-gray-600 text-sm">Présentez votre billet (QR code) à l'entrée. Arrivez un peu en avance !</p>
|
<p class="text-gray-600 text-sm">Présentez votre billet (QR code) à l'entrée. Arrivez un peu en avance !</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Actions -->
|
<!-- Contact Support -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4 mt-8">
|
||||||
|
<h4 class="font-medium text-gray-900 mb-2">Besoin d'aide ?</h4>
|
||||||
|
<p class="text-gray-600 text-sm mb-3">Si vous avez des questions ou des problèmes avec votre commande, n'hésitez pas à nous contacter.</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<%= link_to "mailto:support@example.com", class: "inline-flex items-center text-sm text-purple-600 hover:text-purple-700" do %>
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||||
|
</svg>
|
||||||
|
Contactez le support
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<svg class="w-4 h-4 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">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
<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"/>
|
||||||
</svg>
|
</svg>
|
||||||
Retour au Tableau de Bord
|
Voir tous mes billets
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
<%= link_to events_path, class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<svg class="w-4 h-4 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">
|
||||||
<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" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Voir l'Événement Complet
|
Découvrir d'autres événements
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,105 +1,66 @@
|
|||||||
<div class="min-h-screen bg-gradient-to-br from-purple-50 to-indigo-50 py-8">
|
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-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">
|
||||||
<!-- Header -->
|
<!-- Breadcrumb -->
|
||||||
<div class="text-center mb-8">
|
<nav class="mb-8" aria-label="Breadcrumb">
|
||||||
<div class="mx-auto w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center mb-4">
|
<ol class="flex items-center space-x-2 text-sm">
|
||||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<%= link_to root_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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 class="w-4 h-4 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<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"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
Accueil
|
||||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Détails de la Commande</h1>
|
<% end %>
|
||||||
</div>
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
<!-- Event & Order Details -->
|
</svg>
|
||||||
|
<%= link_to events_path, class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
|
Événements
|
||||||
|
<% end %>
|
||||||
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
<%= link_to seo_event_path(@order.event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
|
<%= @order.event.name %>
|
||||||
|
<% end %>
|
||||||
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
<li class="font-medium text-gray-900" aria-current="page">Commande #<%= @order.id %></li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8">
|
||||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
<div class="border-b border-gray-200 pb-6 mb-6">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">Informations</h2>
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">Détails de la commande</h1>
|
||||||
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
<div class="flex items-center text-sm text-gray-600 space-x-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
|
||||||
<div class="flex flex-col">
|
Commande #<%= @order.id %>
|
||||||
<span class="font-medium">Commande n°<%= @order.id %></span>
|
|
||||||
<span class="text-xs text-gray-500"><%= @order.created_at.strftime("%d %B %Y") %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<svg class="w-4 h-4 mr-1 <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-yellow-600' %>" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
||||||
<% else %>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
||||||
<% end %>
|
|
||||||
</svg>
|
|
||||||
<span class="<%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-yellow-600' %> font-medium">
|
|
||||||
<%= case @order.status
|
|
||||||
when 'paid' then 'Payé'
|
|
||||||
when 'completed' then 'Terminé'
|
|
||||||
else @order.status.humanize
|
|
||||||
end %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event Information -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Événement</h3>
|
|
||||||
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
|
|
||||||
<h4 class="font-semibold text-purple-900 text-lg"><%= @order.event.name %></h4>
|
|
||||||
<div class="mt-2 space-y-1 text-sm text-purple-700">
|
|
||||||
<% if @order.event.start_time %>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= @order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
<%= @order.status.titleize %>
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if @order.event.venue_name.present? %>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<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" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
||||||
</svg>
|
|
||||||
<%= @order.event.venue_name %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if @order.event.venue_address.present? %>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/>
|
|
||||||
</svg>
|
|
||||||
<%= @order.event.venue_address %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Order Items -->
|
||||||
<!-- Summary -->
|
<div class="space-y-4 mb-6">
|
||||||
<div class="space-y-4">
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Billets commandés</h3>
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Récapitulatif</h3>
|
|
||||||
|
|
||||||
<% @tickets.each do |ticket| %>
|
<% @tickets.each do |ticket| %>
|
||||||
<div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-b-0">
|
<div class="flex items-center justify-between py-4 border-b border-gray-100 last:border-b-0">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h4 class="text-sm font-medium text-gray-900 truncate"><%= ticket.ticket_type.name %></h4>
|
<h4 class="text-sm font-medium text-gray-900"><%= ticket.ticket_type.name %></h4>
|
||||||
<div class="flex items-center text-xs text-gray-500 mt-1">
|
<div class="flex items-center text-xs text-gray-500 mt-1">
|
||||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
<%= ticket.first_name %> <%= ticket.last_name %>
|
||||||
</div>
|
</div>
|
||||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
<div class="text-xs text-gray-500 mt-1">
|
||||||
<div class="flex items-center text-xs text-green-600 mt-1">
|
Statut: <%= ticket.status.titleize %>
|
||||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
||||||
</svg>
|
|
||||||
Actif
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<div class="text-lg font-semibold text-gray-900"><%= ticket.price_euros %>€</div>
|
<div class="text-lg font-semibold text-gray-900"><%= ticket.price_euros %>€</div>
|
||||||
@@ -107,112 +68,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Order Total -->
|
||||||
<!-- Total -->
|
<div class="border-t border-gray-200 pt-6">
|
||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
|
||||||
<div class="flex items-center justify-between text-lg">
|
<div class="flex items-center justify-between text-lg">
|
||||||
<span class="font-medium text-gray-900">Total <%= @order.status == 'paid' || @order.status == 'completed' ? 'payé' : 'à payer' %></span>
|
<span class="font-medium text-gray-900">Total</span>
|
||||||
<span class="font-bold text-2xl <%= @order.status == 'paid' || @order.status == 'completed' ? 'text-green-600' : 'text-purple-600' %>">
|
<span class="font-bold text-2xl text-purple-600"><%= @order.total_amount_euros %>€</span>
|
||||||
<%= @order.total_amount_euros %>€
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">TVA incluse</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Actions -->
|
||||||
|
|
||||||
<!-- Actions & Ticket Access -->
|
|
||||||
<div class="bg-white rounded-2xl shadow-xl p-6 md:p-8 h-fit">
|
|
||||||
<% if @order.status == 'paid' || @order.status == 'completed' %>
|
|
||||||
<!-- Ticket Access -->
|
|
||||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Accédez à Vos Billets</h2>
|
|
||||||
<p class="text-sm text-gray-600">Téléchargez ou consultez vos billets</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- Download Tickets -->
|
|
||||||
<div class="flex items-start">
|
|
||||||
<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<h3 class="font-semibold text-gray-900 mb-1">Télécharger Vos Billets</h3>
|
|
||||||
<p class="text-gray-600 text-sm mb-3">Gardez vos billets sur votre téléphone ou imprimez-les.</p>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<% @tickets.each_with_index do |ticket, index| %>
|
|
||||||
<div class="flex items-center justify-between p-3 border border-purple-200 rounded-lg bg-purple-50 hover:bg-purple-100 transition-colors">
|
|
||||||
<%= link_to ticket_path(ticket.qr_code), class: "flex-1 flex items-center text-purple-700 hover:text-purple-800 font-medium" do %>
|
|
||||||
<div class="flex items-center justify-center w-6 h-6 bg-purple-200 text-purple-800 text-xs font-bold rounded-full mr-3">
|
|
||||||
<%= index + 1 %>
|
|
||||||
</div>
|
|
||||||
<%= ticket.first_name %> <%= ticket.last_name %>
|
|
||||||
<% end %>
|
|
||||||
<%= link_to ticket_download_path(ticket.qr_code), class: "ml-3 p-2 text-purple-600 hover:text-purple-800 hover:bg-purple-200 rounded-lg transition-colors", title: "Télécharger le billet PDF" do %>
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Event Day -->
|
|
||||||
<div class="flex items-start">
|
|
||||||
<div class="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<h3 class="font-semibold text-gray-900 mb-1">Le Jour de l'Événement</h3>
|
|
||||||
<p class="text-gray-600 text-sm">Présentez votre billet (QR code) à l'entrée. Arrivez un peu en avance !</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
|
||||||
<!-- Payment Required -->
|
|
||||||
<div class="border-b border-gray-200 pb-6 mb-6">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 mb-2">Paiement Requis</h2>
|
|
||||||
<p class="text-sm text-gray-600">Votre commande nécessite un paiement</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if @order.can_retry_payment? %>
|
|
||||||
<div class="mb-6">
|
|
||||||
<%= link_to checkout_order_path(@order), class: "block w-full text-center py-3 px-4 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors" do %>
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
|
||||||
</svg>
|
|
||||||
Procéder au Paiement
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<!-- Navigation Actions -->
|
|
||||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||||
<div class="space-y-3">
|
<div class="flex space-x-4">
|
||||||
<%= link_to dashboard_path, class: "block w-full text-center py-3 px-4 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors" do %>
|
<%= link_to seo_event_path(@order.event), class: "bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center">
|
||||||
<svg class="w-4 h-4 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">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||||
</svg>
|
</svg>
|
||||||
Retour au Tableau de Bord
|
Retour à l'événement
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to event_path(@order.event.slug, @order.event), class: "block w-full text-center py-3 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors" do %>
|
<% if @order.can_retry_payment? %>
|
||||||
<div class="flex items-center justify-center">
|
<%= link_to booking_summary_path(@order), class: "bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors" do %>
|
||||||
|
<div class="flex items-center">
|
||||||
<svg class="w-4 h-4 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">
|
||||||
<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" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Voir l'Événement Complet
|
Procéder au paiement
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,69 +1,74 @@
|
|||||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6">
|
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<!-- Simplified header -->
|
<!-- Hero section with metrics -->
|
||||||
<div class="my-6 sm:my-8">
|
<div class="mt-4 mb-8">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<h1 class="text-3xl font-bold text-slate-900 dark:text-slate-100">Tableau de bord</h1>
|
||||||
<h1 class="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-slate-100">Mon tableau de bord</h1>
|
|
||||||
<p class="text-sm sm:text-base text-slate-600 dark:text-slate-400 mt-1">Gérez vos commandes et accédez à vos billets</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Promoter Actions -->
|
<!-- Promoter Actions -->
|
||||||
<% if current_user.promoter? %>
|
<% if current_user.promoter? %>
|
||||||
<div class="flex flex-col xs:flex-row items-stretch xs:items-center gap-2">
|
<div class="flex items-center space-x-3">
|
||||||
<%= link_to promoter_events_path, class: "inline-flex items-center justify-center px-3 py-2 sm:px-4 sm:py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200 text-sm" do %>
|
<%= link_to promoter_events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200" do %>
|
||||||
<i data-lucide="calendar-plus" class="w-4 h-4 mr-1 sm:mr-2"></i>
|
<i data-lucide="calendar-plus" class="w-4 h-4 mr-2"></i>
|
||||||
<span class="whitespace-nowrap">Mes Événements</span>
|
Mes événements
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to new_promoter_event_path, class: "inline-flex items-center justify-center px-3 py-2 sm:px-4 sm:py-2 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200 text-sm" do %>
|
<%= link_to new_promoter_event_path, class: "inline-flex items-center px-4 py-2 bg-black text-white font-medium rounded-lg hover:bg-gray-800 transition-colors duration-200" do %>
|
||||||
<i data-lucide="plus" class="w-4 h-4 mr-1 sm:mr-2"></i>
|
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
|
||||||
<span class="whitespace-nowrap">Créer un Événement</span>
|
Créer un événement
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
|
||||||
<%= link_to events_path, class: "inline-flex items-center justify-center px-3 py-2 sm:px-4 sm:py-2 bg-purple-600 text-white font-medium rounded-lg hover:bg-purple-700 transition-colors duration-200 text-sm" do %>
|
|
||||||
<i data-lucide="search" class="w-4 h-4 mr-1 sm:mr-2"></i>
|
|
||||||
<span class="whitespace-nowrap">Découvrir des Événements</span>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
|
||||||
|
<%= render partial: 'components/metric_card', locals: { title: "Mes réservations", value: @booked_events, classes: "from-green-100 to-emerald-100" } %>
|
||||||
|
|
||||||
|
<%= render partial: 'components/metric_card', locals: { title: "Événements aujourd'hui", value: @events_today, classes: "from-blue-100 to-sky-100" } %>
|
||||||
|
|
||||||
|
<%= render partial: 'components/metric_card', locals: { title: "Événements demain", value: @events_tomorrow, classes: "from-purple-100 to-indigo-100" } %>
|
||||||
|
|
||||||
|
<%= render partial: 'components/metric_card', locals: { title: "À venir", value: @upcoming_events, classes: "from-orange-100 to-amber-100" } %>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Draft orders needing payment -->
|
<!-- Draft orders needing payment -->
|
||||||
<% if @draft_orders.any? %>
|
<% if @draft_orders.any? %>
|
||||||
<div class="card mb-6 sm:mb-8 border-orange-200 bg-orange-50">
|
<div class="card hover-lift mb-8 border-orange-200 bg-orange-50">
|
||||||
<div class="card-header bg-orange-100 rounded-lg">
|
<div class="card-header bg-orange-100 rounded-lg">
|
||||||
<div class="mx-4 py-3 sm:py-4">
|
|
||||||
<h2 class="text-lg sm:text-2xl font-bold text-orange-900 flex items-center">
|
<div class="mx-4 py-4">
|
||||||
<svg class="w-5 h-5 sm:w-6 sm:h-6 mr-2 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<h2 class="text-2xl font-bold text-orange-900 flex items-center">
|
||||||
|
<svg class="w-6 h-6 mr-2 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Commandes en Attente de Paiement
|
Commandes en attente de paiement
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm sm:text-base text-orange-700 mt-1">Vous avez des commandes qui nécessitent un paiement</p>
|
<p class="text-orange-700 mt-1">Vous avez des commandes qui nécessitent un paiement</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<% @draft_orders.each do |order| %>
|
<% @draft_orders.each do |order| %>
|
||||||
<div class="bg-white rounded-lg p-3 sm:p-4 border border-orange-200">
|
<div class="bg-white rounded-lg p-4 border border-orange-200">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
|
<div class="flex items-start justify-between mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-semibold text-gray-900 text-sm sm:text-base"><%= order.event.name %></h3>
|
<h3 class="font-semibold text-gray-900"><%= order.event.name %></h3>
|
||||||
<p class="text-xs sm:text-sm text-gray-600 mt-1">
|
<p class="text-sm text-gray-600">
|
||||||
<svg class="w-3 h-3 sm:w-4 sm:h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<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" 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"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs sm:text-sm font-medium text-orange-600 bg-orange-100 px-2 py-1 rounded-full whitespace-nowrap">
|
<span class="text-sm font-medium text-orange-600 bg-orange-100 px-2 py-1 rounded-full">
|
||||||
Order #<%= order.id %>
|
Commande #<%= order.id %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2 mb-4">
|
<div class="grid gap-2 mb-4">
|
||||||
<% order.tickets.each do |ticket| %>
|
<% order.tickets.each do |ticket| %>
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between text-xs sm:text-sm bg-gray-50 rounded p-2 gap-2">
|
<div class="flex items-center justify-between text-sm bg-gray-50 rounded p-2">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
||||||
<span class="text-gray-600">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
<span class="text-gray-600">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
||||||
@@ -75,21 +80,19 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-xs sm:text-sm text-gray-600">
|
<div class="text-sm text-gray-600">
|
||||||
<div class="mb-1 sm:mb-0">
|
|
||||||
Tentatives: <%= order.payment_attempts %>/3
|
Tentatives: <%= order.payment_attempts %>/3
|
||||||
</div>
|
|
||||||
<% if order.expiring_soon? %>
|
<% if order.expiring_soon? %>
|
||||||
<span class="text-orange-600 font-medium">⚠️ Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
<span class="text-orange-600 font-medium ml-2">⚠️ Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="text-gray-500">Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
<span class="text-gray-500 ml-2">Expire dans <%= time_ago_in_words(order.expires_at) %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= link_to retry_payment_order_path(order), method: :post,
|
<%= link_to retry_payment_order_path(order), method: :post,
|
||||||
class: "inline-flex items-center px-3 py-2 sm:px-4 sm:py-2 bg-orange-600 text-white text-xs sm:text-sm font-medium rounded-lg hover:bg-orange-700 transition-colors duration-200 whitespace-nowrap" do %>
|
class: "inline-flex items-center px-4 py-2 bg-orange-600 text-white text-sm font-medium rounded-lg hover:bg-orange-700 transition-colors duration-200" do %>
|
||||||
Reprendre le Paiement (€<%= order.total_amount_euros %>)
|
Reprendre le paiement (<%= order.total_amount_euros %>€)
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,148 +102,96 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<!-- User's Orders Section -->
|
<!-- User's booked events -->
|
||||||
<div class="card mb-6 sm:mb-8">
|
<div class="card hover-lift mb-8">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="flex items-center justify-between">
|
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Mes événements réservés</h2>
|
||||||
<h2 class="text-xl sm:text-2xl font-bold text-slate-900 dark:text-slate-100">Mes Commandes</h2>
|
|
||||||
<span class="text-xs sm:text-sm text-slate-600 dark:text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-1 sm:px-3 sm:py-1 rounded-full">
|
|
||||||
<%= pluralize(@user_orders.count, 'commande') %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<% if @user_orders.any? %>
|
<% if @user_booked_events.any? %>
|
||||||
<div class="space-y-4">
|
<ul class="space-y-4">
|
||||||
<% @user_orders.each do |order| %>
|
<% @user_booked_events.each do |event| %>
|
||||||
<div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-3 sm:p-4 hover:shadow-md transition-shadow">
|
<li>
|
||||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
|
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||||
<div class="flex-1">
|
</li>
|
||||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
|
||||||
<h3 class="font-semibold text-slate-900 dark:text-slate-100 text-sm sm:text-base"><%= order.event.name %></h3>
|
|
||||||
<span class="text-xs px-2 py-1 rounded-full <%= order.status == 'paid' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' : order.status == 'completed' ? 'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100' : 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' %>">
|
|
||||||
<%= order.status.humanize %>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-3 text-xs sm:text-sm text-slate-600 dark:text-slate-400 mb-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i data-lucide="calendar" class="w-3 h-3 sm:w-4 sm:h-4 mr-1"></i>
|
|
||||||
<%= order.event.start_time.strftime("%d %B %Y à %H:%M") %>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i data-lucide="map-pin" class="w-3 h-3 sm:w-4 sm:h-4 mr-1"></i>
|
|
||||||
<%= order.event.venue_name %>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<i data-lucide="shopping-bag" class="w-3 h-3 sm:w-4 sm:h-4 mr-1"></i>
|
|
||||||
<%= pluralize(order.tickets.count, 'billet') %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-xs sm:text-sm text-slate-500 dark:text-slate-400 mt-2">
|
|
||||||
Order #<%= order.id %> • <%= order.created_at.strftime("%m/%d/%Y") %> • €<%= order.total_amount_euros %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<%= link_to order_path(order),
|
|
||||||
class: "inline-flex items-center px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-xs sm:text-sm font-medium rounded-lg transition-colors duration-200 whitespace-nowrap" do %>
|
|
||||||
<i data-lucide="eye" class="w-3 h-3 sm:w-4 sm:h-4 mr-1"></i>
|
|
||||||
Voir les Détails
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
<% if @booked_events > 5 %>
|
||||||
|
<div class="mt-6 text-center">
|
||||||
<!-- Quick tickets preview -->
|
<%= link_to "Voir toutes mes réservations", "#", class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium transition-colors duration-200" %>
|
||||||
<div class="border-t border-slate-200 dark:border-slate-600 pt-3">
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<% order.tickets.limit(3).each do |ticket| %>
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between text-xs sm:text-sm bg-slate-50 dark:bg-slate-700 rounded p-2 gap-2">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
|
||||||
<span class="font-medium"><%= ticket.ticket_type.name %></span>
|
|
||||||
<span class="text-slate-500 text-xs">- <%= ticket.first_name %> <%= ticket.last_name %></span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<%= link_to ticket_download_path(ticket.qr_code),
|
|
||||||
class: "text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200" do %>
|
|
||||||
<i data-lucide="download" class="w-3 h-3"></i>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% if order.tickets.count > 3 %>
|
|
||||||
<div class="text-xs text-slate-500 text-center">
|
|
||||||
et <%= pluralize(order.tickets.count - 3, 'autre billet') %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if @user_orders.count >= 10 %>
|
|
||||||
<div class="mt-4 sm:mt-6 text-center">
|
|
||||||
<%= link_to "Voir Toutes Mes Commandes", orders_path, class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium transition-colors duration-200 text-sm" %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="text-center py-8 sm:py-12">
|
<div class="text-center py-8">
|
||||||
<div class="w-12 h-12 sm:w-16 sm:h-16 bg-slate-100 dark:bg-slate-700 rounded-full flex items-center justify-center mx-auto mb-3 sm:mb-4">
|
<p class="text-slate-600 dark:text-slate-400 mb-4">Vous n'avez encore réservé aucun événement.</p>
|
||||||
<i data-lucide="shopping-bag" class="w-6 h-6 sm:w-8 sm:h-8 text-slate-400"></i>
|
<%= link_to "Découvrir les événements", events_path, class: "inline-flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200" %>
|
||||||
</div>
|
|
||||||
<h3 class="text-base sm:text-lg font-medium text-slate-900 dark:text-slate-100 mb-1 sm:mb-2">Aucune Commande</h3>
|
|
||||||
<p class="text-sm sm:text-base text-slate-600 dark:text-slate-400 mb-4 sm:mb-6">Vous n'avez pas encore passé de commandes.</p>
|
|
||||||
<%= link_to events_path, class: "inline-flex items-center px-3 py-2 sm:px-4 sm:py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors duration-200 text-sm" do %>
|
|
||||||
<i data-lucide="search" class="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2"></i>
|
|
||||||
Découvrir des Événements
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Events Preview - Simplified -->
|
<!-- Today's events -->
|
||||||
<% if @user_orders.any? %>
|
<div class="card hover-lift mb-8">
|
||||||
<div class="my-6 sm:my-8 card">
|
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements du jour</h2>
|
||||||
<h2 class="text-lg sm:text-xl font-bold text-slate-900 dark:text-slate-100">Découvrir d'autres événements</h2>
|
|
||||||
<%= link_to events_path, class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium text-sm transition-colors duration-200 whitespace-nowrap" do %>
|
|
||||||
Voir tout →
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<% if @upcoming_preview_events.any? %>
|
<% if @today_events.any? %>
|
||||||
<div class="grid gap-3 sm:gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
<ul class="space-y-4">
|
||||||
<% @upcoming_preview_events.each do |event| %>
|
<% @today_events.each do |event| %>
|
||||||
<div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-3 sm:p-4 hover:shadow-md transition-shadow">
|
<li>
|
||||||
<h4 class="font-medium text-slate-900 dark:text-slate-100 mb-2 text-sm sm:text-base"><%= event.name %></h4>
|
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||||
<div class="text-xs sm:text-sm text-slate-600 dark:text-slate-400 space-y-1">
|
</li>
|
||||||
<div class="flex items-center">
|
<% end %>
|
||||||
<i data-lucide="calendar" class="w-3 h-3 mr-1"></i>
|
</ul>
|
||||||
<%= event.start_time.strftime("%d %B") %>
|
<% else %>
|
||||||
</div>
|
<p class="text-slate-600 dark:text-slate-400">Aucun évenement aujourd'hui.</p>
|
||||||
<div class="flex items-center">
|
|
||||||
<i data-lucide="map-pin" class="w-3 h-3 mr-1"></i>
|
|
||||||
<%= event.venue_name %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 sm:mt-3">
|
|
||||||
<%= link_to event_path(event.slug, event), class: "text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 text-xs sm:text-sm font-medium" do %>
|
|
||||||
Voir l'Événement →
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tomorrow's events -->
|
||||||
|
<div class="card hover-lift mb-8">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Évenements de demain</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if @tomorrow_events.any? %>
|
||||||
|
<ul class="space-y-4">
|
||||||
|
<% @tomorrow_events.each do |event| %>
|
||||||
|
<li>
|
||||||
|
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||||
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-slate-600 dark:text-slate-400">Aucune partie demain.</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Other upcoming events with pagination -->
|
||||||
|
<div class="card hover-lift">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Autres évenements à venir</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if @other_events.any? %>
|
||||||
|
<ul class="space-y-4">
|
||||||
|
<% @other_events.each do |event| %>
|
||||||
|
<li>
|
||||||
|
<%= render partial: 'components/event_item', locals: { event: event } %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<%= paginate @other_events %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p class="text-slate-600 dark:text-slate-400 text-sm">Aucun événement à venir pour le moment.</p>
|
<p class="text-slate-600 dark:text-slate-400">Aucune autre partie à venir.</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<div class="featured-events-grid" data-controller="featured-event">
|
<div class="featured-events-grid" data-controller="featured-event">
|
||||||
<% @featured_events.each do |event| %>
|
<% @featured_events.each do |event| %>
|
||||||
<div class="featured-event-card" data-featured-event-target="card">
|
<div class="featured-event-card" data-featured-event-target="card">
|
||||||
<%= link_to event_path(event.slug, event) do %>
|
<%= link_to event_path(event) do %>
|
||||||
<img src="<%= event.image %>" alt="<%= event.name %>" class="featured-event-image" data-featured-event-target="animated">
|
<img src="<%= event.image %>" alt="<%= event.name %>" class="featured-event-image" data-featured-event-target="animated">
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="featured-event-content">
|
<div class="featured-event-content">
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<p class="featured-event-description"><%= event.description %></p>
|
<p class="featured-event-description"><%= event.description %></p>
|
||||||
<div class="featured-event-footer">
|
<div class="featured-event-footer">
|
||||||
<span class="featured-event-price">€<%= event.ticket_types.minimum(:price_cents).to_f / 100 %></span>
|
<span class="featured-event-price">€<%= event.ticket_types.minimum(:price_cents).to_f / 100 %></span>
|
||||||
<%= link_to "Réserver une place", event_path(event.slug, event), class: "btn btn-sm btn-primary" %>
|
<%= link_to "Réserver une place", event_path(event), class: "btn btn-sm btn-primary" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
<p class="text-sm text-green-700">Cet événement est visible publiquement et les utilisateurs peuvent acheter des billets.</p>
|
<p class="text-sm text-green-700">Cet événement est visible publiquement et les utilisateurs peuvent acheter des billets.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<%= link_to event_path(@event.slug, @event), target: "_blank", class: "text-green-600 hover:text-green-800 font-medium text-sm" do %>
|
<%= link_to event_path(@event), target: "_blank", class: "text-green-600 hover:text-green-800 font-medium text-sm" do %>
|
||||||
Voir publiquement <i data-lucide="external-link" class="w-4 h-4 inline ml-1"></i>
|
Voir publiquement <i data-lucide="external-link" class="w-4 h-4 inline ml-1"></i>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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.email.split('@').first %>,
|
||||||
Bonjour <%= @user.first_name %>,
|
|
||||||
<% else %>
|
|
||||||
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>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<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-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= link_to event_path(@event.slug, @event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
<%= link_to event_path(@event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
<%= @event.name %>
|
<%= @event.name %>
|
||||||
<% 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-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
d="M9 5l7 7-7 7"
|
d="M9 5l7 7-7 7"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<%= link_to event_path(@event.slug, @event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
<%= link_to event_path(@event), class: "text-gray-500 hover:text-purple-600 transition-colors" do %>
|
||||||
<%= @event.name %>
|
<%= @event.name %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<svg
|
<svg
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-4 pt-6">
|
<div class="flex flex-col sm:flex-row gap-4 pt-6">
|
||||||
<%= link_to "Retour",
|
<%= link_to "Retour",
|
||||||
event_path(@event.slug, @event),
|
event_path(@event),
|
||||||
class:
|
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" %>
|
"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" %>
|
||||||
<%= form.submit "Procéder au paiement",
|
<%= form.submit "Procéder au paiement",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<%= link_to ticket_download_path(ticket.qr_code, format: :pdf),
|
<%= link_to download_ticket_path(ticket, format: :pdf),
|
||||||
class: "inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-lg hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
class: "inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-lg hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 text-sm font-medium shadow-sm" do %>
|
||||||
<svg class="w-4 h-4 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">
|
||||||
<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>
|
<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>
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
<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>
|
||||||
<p class="text-purple-100">ID: #<%= @ticket.id %></p>
|
<p class="text-purple-100">ID: #<%= @ticket.id %></p>
|
||||||
</div>
|
</div>
|
||||||
<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 order_path(@order),
|
<%= 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 aux informations de commande
|
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
|
|
||||||
115
config/routes.rb
115
config/routes.rb
@@ -1,80 +1,72 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
# Health check
|
||||||
|
|
||||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
|
||||||
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
|
||||||
get "up" => "rails/health#show", as: :rails_health_check
|
get "up" => "rails/health#show", as: :rails_health_check
|
||||||
|
|
||||||
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
|
# Root
|
||||||
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
|
|
||||||
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
|
|
||||||
|
|
||||||
# Defines the root path route ("/")
|
|
||||||
root "pages#home"
|
root "pages#home"
|
||||||
|
|
||||||
# === Devise ===
|
# === Authentication ===
|
||||||
# Routes for devise authentication Gem
|
|
||||||
# Bind devise to user
|
|
||||||
devise_for :users, path: "auth", path_names: {
|
devise_for :users, path: "auth", path_names: {
|
||||||
sign_in: "sign_in", # Route for user login
|
sign_in: "sign_in",
|
||||||
sign_out: "sign_out", # Route for user logout
|
sign_out: "sign_out",
|
||||||
password: "reset-password", # Route for changing password
|
password: "reset-password",
|
||||||
confirmation: "verification", # Route for account confirmation
|
confirmation: "verification",
|
||||||
unlock: "unblock", # Route for account unlock
|
unlock: "unblock",
|
||||||
# registration: "account", # Route for user account
|
sign_up: "signup"
|
||||||
sign_up: "signup" # Route for user registration
|
}, controllers: {
|
||||||
},
|
sessions: "auth/sessions",
|
||||||
controllers: {
|
registrations: "auth/registrations",
|
||||||
sessions: "auth/sessions", # Custom controller for sessions
|
passwords: "auth/passwords",
|
||||||
registrations: "auth/registrations", # Custom controller for registrations
|
confirmation: "auth/confirmations"
|
||||||
passwords: "auth/passwords", # Custom controller for passwords
|
|
||||||
confirmation: "auth/confirmations" # Custom controller for confirmations
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# === Pages ===
|
# === Main App - SEO Friendly URLs ===
|
||||||
get "dashboard", to: "pages#dashboard", as: "dashboard"
|
get "dashboard", to: "pages#dashboard"
|
||||||
|
|
||||||
# === Events ===
|
# Events with date-based SEO structure
|
||||||
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/:year/:month/:slug", to: "events#show", as: "event",
|
||||||
|
constraints: { year: /\d{4}/, month: /\d{2}/ }
|
||||||
|
|
||||||
# === Orders (scoped to events) ===
|
# Booking workflow with semantic URLs
|
||||||
get "orders/new/events/:slug.:id", to: "orders#new", as: "event_order_new"
|
get "events/:year/:month/:slug/book-tickets", to: "orders#new", as: "book_event_tickets",
|
||||||
post "orders/create/events/:slug.:id", to: "orders#create", as: "event_order_create"
|
constraints: { year: /\d{4}/, month: /\d{2}/ }
|
||||||
|
post "events/:year/:month/:slug/book-tickets", to: "orders#create", as: "create_booking",
|
||||||
|
constraints: { year: /\d{4}/, month: /\d{2}/ }
|
||||||
|
|
||||||
resources :orders, only: [ :index, :show ] do
|
# Checkout process with semantic URLs
|
||||||
member do
|
get "events/:year/:month/:slug/checkout", to: "orders#checkout", as: "event_checkout",
|
||||||
get :checkout
|
constraints: { year: /\d{4}/, month: /\d{2}/ }
|
||||||
post :retry_payment
|
get "booking/:order_id/summary", to: "orders#show", as: "booking_summary"
|
||||||
post :increment_payment_attempt
|
post "booking/:order_id/retry-payment", to: "orders#retry_payment", as: "retry_booking_payment"
|
||||||
end
|
post "booking/:order_id/increment-attempts", to: "orders#increment_payment_attempt", as: "increment_booking_attempts"
|
||||||
|
|
||||||
|
# Individual tickets with descriptive URLs
|
||||||
|
get "tickets/:event_slug-:ticket_id", to: "tickets#show", as: "ticket"
|
||||||
|
get "tickets/:event_slug-:ticket_id/view", to: "tickets#view", as: "view_ticket"
|
||||||
|
get "tickets/:event_slug-:ticket_id/download", to: "tickets#download", as: "download_ticket"
|
||||||
|
post "tickets/:event_slug-:ticket_id/retry-payment", to: "tickets#retry_payment", as: "retry_ticket_payment"
|
||||||
|
|
||||||
|
# Payment callbacks with descriptive paths
|
||||||
|
namespace :booking do
|
||||||
|
get "payment-success", to: "payments#success", as: "payment_success"
|
||||||
|
get "payment-cancelled", to: "payments#cancel", as: "payment_cancelled"
|
||||||
end
|
end
|
||||||
|
|
||||||
get "orders/payments/success", to: "orders#payment_success", as: "order_payment_success"
|
# Legacy redirects for backward compatibility
|
||||||
get "orders/payments/cancel", to: "orders#payment_cancel", as: "order_payment_cancel"
|
get "events/:slug", to: "legacy_redirects#event_redirect"
|
||||||
|
|
||||||
# Legacy routes - redirect to order system
|
# Legacy payment routes
|
||||||
get "events/:slug.:id/tickets/checkout", to: "tickets#checkout", as: "ticket_checkout"
|
get "payments/success", to: redirect("/booking/payment-success")
|
||||||
post "events/:slug.:id/tickets/retry", to: "tickets#retry_payment", as: "ticket_retry_payment"
|
get "payments/cancel", to: redirect("/booking/payment-cancelled")
|
||||||
get "payments/success", to: "tickets#payment_success", as: "payment_success"
|
|
||||||
get "payments/cancel", to: "tickets#payment_cancel", as: "payment_cancel"
|
|
||||||
|
|
||||||
# === Tickets ===
|
# === Promoter Dashboard ===
|
||||||
get "tickets/:qr_code", to: "tickets#show", as: "ticket"
|
|
||||||
get "tickets/:qr_code/download", to: "tickets#download", as: "ticket_download"
|
|
||||||
|
|
||||||
# === Promoter Routes ===
|
|
||||||
namespace :promoter do
|
namespace :promoter do
|
||||||
resources :events do
|
resources :events, path: "my-events" do
|
||||||
member do
|
member do
|
||||||
patch :publish
|
patch :publish, :unpublish, :cancel, :mark_sold_out
|
||||||
patch :unpublish
|
|
||||||
patch :cancel
|
|
||||||
patch :mark_sold_out
|
|
||||||
end
|
end
|
||||||
|
resources :ticket_types, path: "ticket-options" do
|
||||||
# Nested ticket types routes
|
|
||||||
resources :ticket_types do
|
|
||||||
member do
|
member do
|
||||||
post :duplicate
|
post :duplicate
|
||||||
end
|
end
|
||||||
@@ -82,17 +74,14 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# === API ===
|
||||||
# API routes versioning
|
|
||||||
namespace :api do
|
namespace :api do
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
# RESTful routes for event management
|
resources :events, except: [:new, :edit] do
|
||||||
resources :events, only: [ :index, :show, :create, :update, :destroy ] do
|
|
||||||
member do
|
member do
|
||||||
post :store_cart
|
post :store_cart
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# resources :ticket_types, only: [ :index, :show, :create, :update, :destroy ]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -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