Add ticket system with QR scanner and PDF generation
- Add ticket validation and check-in API endpoints - Add PDF ticket generation with QR codes (pdfkit) - Add admin QR scanner page with camera support - Add admin site settings page - Update email templates with PDF ticket download link - Add checked_in_by_admin_id field for audit tracking - Update booking success page with ticket download - Various UI improvements to events and booking pages
This commit is contained in:
@@ -25,6 +25,8 @@ export default function AdminEventsPage() {
|
||||
titleEs: string;
|
||||
description: string;
|
||||
descriptionEs: string;
|
||||
shortDescription: string;
|
||||
shortDescriptionEs: string;
|
||||
startDatetime: string;
|
||||
endDatetime: string;
|
||||
location: string;
|
||||
@@ -41,6 +43,8 @@ export default function AdminEventsPage() {
|
||||
titleEs: '',
|
||||
description: '',
|
||||
descriptionEs: '',
|
||||
shortDescription: '',
|
||||
shortDescriptionEs: '',
|
||||
startDatetime: '',
|
||||
endDatetime: '',
|
||||
location: '',
|
||||
@@ -75,6 +79,8 @@ export default function AdminEventsPage() {
|
||||
titleEs: '',
|
||||
description: '',
|
||||
descriptionEs: '',
|
||||
shortDescription: '',
|
||||
shortDescriptionEs: '',
|
||||
startDatetime: '',
|
||||
endDatetime: '',
|
||||
location: '',
|
||||
@@ -90,14 +96,27 @@ export default function AdminEventsPage() {
|
||||
setEditingEvent(null);
|
||||
};
|
||||
|
||||
// Convert ISO UTC string to local datetime-local format (YYYY-MM-DDTHH:MM)
|
||||
const isoToLocalDatetime = (isoString: string): string => {
|
||||
const date = new Date(isoString);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const handleEdit = (event: Event) => {
|
||||
setFormData({
|
||||
title: event.title,
|
||||
titleEs: event.titleEs || '',
|
||||
description: event.description,
|
||||
descriptionEs: event.descriptionEs || '',
|
||||
startDatetime: event.startDatetime.slice(0, 16),
|
||||
endDatetime: event.endDatetime?.slice(0, 16) || '',
|
||||
shortDescription: event.shortDescription || '',
|
||||
shortDescriptionEs: event.shortDescriptionEs || '',
|
||||
startDatetime: isoToLocalDatetime(event.startDatetime),
|
||||
endDatetime: event.endDatetime ? isoToLocalDatetime(event.endDatetime) : '',
|
||||
location: event.location,
|
||||
locationUrl: event.locationUrl || '',
|
||||
price: event.price,
|
||||
@@ -134,6 +153,8 @@ export default function AdminEventsPage() {
|
||||
titleEs: formData.titleEs || undefined,
|
||||
description: formData.description,
|
||||
descriptionEs: formData.descriptionEs || undefined,
|
||||
shortDescription: formData.shortDescription || undefined,
|
||||
shortDescriptionEs: formData.shortDescriptionEs || undefined,
|
||||
startDatetime: new Date(formData.startDatetime).toISOString(),
|
||||
endDatetime: formData.endDatetime ? new Date(formData.endDatetime).toISOString() : undefined,
|
||||
location: formData.location,
|
||||
@@ -288,6 +309,33 @@ export default function AdminEventsPage() {
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Short Description (English)</label>
|
||||
<textarea
|
||||
value={formData.shortDescription}
|
||||
onChange={(e) => setFormData({ ...formData, shortDescription: e.target.value.slice(0, 300) })}
|
||||
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
||||
rows={2}
|
||||
maxLength={300}
|
||||
placeholder="Brief summary for SEO and cards (max 300 chars)"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{formData.shortDescription.length}/300 characters</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Short Description (Spanish)</label>
|
||||
<textarea
|
||||
value={formData.shortDescriptionEs}
|
||||
onChange={(e) => setFormData({ ...formData, shortDescriptionEs: e.target.value.slice(0, 300) })}
|
||||
className="w-full px-4 py-3 rounded-btn border border-secondary-light-gray focus:outline-none focus:ring-2 focus:ring-primary-yellow"
|
||||
rows={2}
|
||||
maxLength={300}
|
||||
placeholder="Resumen breve para SEO y tarjetas (máx 300 caracteres)"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{formData.shortDescriptionEs.length}/300 characters</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user