From ae2b8085a1470d4219b8668aad6e9e9818176bd2 Mon Sep 17 00:00:00 2001 From: WojciechMat Date: Mon, 16 Jun 2025 17:02:50 +0200 Subject: [PATCH 1/4] Add event ticket types on edit --- frontend/lib/core/models/ticket_model.dart | 82 +++++- .../core/repositories/event_repository.dart | 9 +- .../organizer/cubit/event_form_cubit.dart | 132 ++++++++- .../organizer/cubit/event_form_state.dart | 24 ++ .../organizer/pages/edit_event_page.dart | 262 +++++++++++++++--- .../organizer/widgets/ticket_type_form.dart | 185 +++++++++++++ 6 files changed, 640 insertions(+), 54 deletions(-) create mode 100644 frontend/lib/presentation/organizer/widgets/ticket_type_form.dart diff --git a/frontend/lib/core/models/ticket_model.dart b/frontend/lib/core/models/ticket_model.dart index e009220..ed6acb5 100644 --- a/frontend/lib/core/models/ticket_model.dart +++ b/frontend/lib/core/models/ticket_model.dart @@ -5,6 +5,7 @@ class TicketType { final int maxCount; final double price; final String currency; + final DateTime? availableFrom; // Added this field TicketType({ this.typeId, @@ -13,6 +14,7 @@ class TicketType { required this.maxCount, required this.price, required this.currency, + this.availableFrom, // Added this parameter }); factory TicketType.fromJson(Map json) { @@ -24,6 +26,10 @@ class TicketType { // Price can be int or double from JSON price: (json['price'] as num).toDouble(), currency: json['currency'] ?? 'USD', + // Parse availableFrom field + availableFrom: json['available_from'] != null + ? DateTime.parse(json['available_from']) + : null, ); } @@ -35,8 +41,35 @@ class TicketType { 'max_count': maxCount, 'price': price, 'currency': currency, + 'available_from': availableFrom?.toIso8601String(), // Added this field }; } + + // Add copyWith method for easier updates + TicketType copyWith({ + int? typeId, + int? eventId, + String? description, + int? maxCount, + double? price, + String? currency, + DateTime? availableFrom, + }) { + return TicketType( + typeId: typeId ?? this.typeId, + eventId: eventId ?? this.eventId, + description: description ?? this.description, + maxCount: maxCount ?? this.maxCount, + price: price ?? this.price, + currency: currency ?? this.currency, + availableFrom: availableFrom ?? this.availableFrom, + ); + } + + @override + String toString() { + return 'TicketType{typeId: $typeId, eventId: $eventId, description: $description, maxCount: $maxCount, price: $price, currency: $currency, availableFrom: $availableFrom}'; + } } class TicketDetailsModel { @@ -45,12 +78,16 @@ class TicketDetailsModel { final String? seat; final int? ownerId; final double? resellPrice; - final double? originalPrice; // The price the user paid for the ticket + final double? originalPrice; // The price the user paid for the ticket // These fields are not in the base model but can be added for convenience final String? eventName; final DateTime? eventStartDate; + // Add ticket type details for convenience + final String? ticketTypeDescription; + final DateTime? ticketAvailableFrom; // Added this field + TicketDetailsModel({ required this.ticketId, this.typeId, @@ -60,6 +97,8 @@ class TicketDetailsModel { this.originalPrice, this.eventName, this.eventStartDate, + this.ticketTypeDescription, + this.ticketAvailableFrom, // Added this parameter }); factory TicketDetailsModel.fromJson(Map json) { @@ -68,22 +107,39 @@ class TicketDetailsModel { typeId: json['type_id'], seat: json['seat'], ownerId: json['owner_id'], - resellPrice: - json['resell_price'] != null - ? (json['resell_price'] as num).toDouble() - : null, - originalPrice: - json['original_price'] != null - ? (json['original_price'] as num).toDouble() - : null, + resellPrice: json['resell_price'] != null + ? (json['resell_price'] as num).toDouble() + : null, + originalPrice: json['original_price'] != null + ? (json['original_price'] as num).toDouble() + : null, // Handle both snake_case (from backend) and camelCase (from mock data) eventName: json['event_name'] ?? json['eventName'], - eventStartDate: - json['event_start_date'] != null - ? DateTime.parse(json['event_start_date']) - : json['eventStartDate'] != null + eventStartDate: json['event_start_date'] != null + ? DateTime.parse(json['event_start_date']) + : json['eventStartDate'] != null ? DateTime.parse(json['eventStartDate']) : null, + // Add ticket type description and available from + ticketTypeDescription: json['ticket_type_description'], + ticketAvailableFrom: json['ticket_available_from'] != null + ? DateTime.parse(json['ticket_available_from']) + : null, ); } + + Map toJson() { + return { + 'ticket_id': ticketId, + 'type_id': typeId, + 'seat': seat, + 'owner_id': ownerId, + 'resell_price': resellPrice, + 'original_price': originalPrice, + 'event_name': eventName, + 'event_start_date': eventStartDate?.toIso8601String(), + 'ticket_type_description': ticketTypeDescription, + 'ticket_available_from': ticketAvailableFrom?.toIso8601String(), + }; + } } diff --git a/frontend/lib/core/repositories/event_repository.dart b/frontend/lib/core/repositories/event_repository.dart index 51b9e39..2ea9c64 100644 --- a/frontend/lib/core/repositories/event_repository.dart +++ b/frontend/lib/core/repositories/event_repository.dart @@ -12,6 +12,7 @@ abstract class EventRepository { Future notifyParticipants(int eventId, String message); Future createTicketType(Map data); Future deleteTicketType(int typeId); + Future updateTicketType(int typeId, Map data); Future> getLocations(); } @@ -32,6 +33,12 @@ class ApiEventRepository implements EventRepository { return (data as List).map((e) => Event.fromJson(e)).toList(); } + @override + Future updateTicketType(int typeId, Map data) async { + final response = await _apiClient.put('/ticket-types/$typeId', data: data); + return TicketType.fromJson(response); + } + @override Future> getOrganizerEvents(int organizerId) async { final data = @@ -86,4 +93,4 @@ class ApiEventRepository implements EventRepository { final data = await _apiClient.get('/locations/'); return (data as List).map((e) => Location.fromJson(e)).toList(); } -} \ No newline at end of file +} diff --git a/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart b/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart index dbb4011..5c887a6 100644 --- a/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart +++ b/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart @@ -39,11 +39,24 @@ class EventFormCubit extends Cubit { } } + Future loadExistingTicketTypes(int eventId) async { + try { + emit(EventFormTicketTypesLoading()); + final ticketTypes = await _eventRepository.getTicketTypesForEvent(eventId); + + emit(EventFormTicketTypesLoaded(ticketTypes)); + } on ApiException catch (e) { + emit(EventFormError('Failed to load ticket types: ${e.message}')); + } catch (e) { + emit(EventFormError('Failed to load ticket types: $e')); + } + } + + /// Simple event update without ticket types Future updateEvent(int eventId, Map eventData) async { try { emit(const EventFormSubmitting(locations: [])); - final updatedEvent = - await _eventRepository.updateEvent(eventId, eventData); + final updatedEvent = await _eventRepository.updateEvent(eventId, eventData); emit(EventFormSuccess(updatedEvent.id)); } on ApiException catch (e) { emit(EventFormError(e.message)); @@ -51,4 +64,119 @@ class EventFormCubit extends Cubit { emit(EventFormError('An unexpected error occurred: $e')); } } + + Future updateEventWithTicketTypes( + int eventId, + Map eventData, + List additionalTicketTypes + ) async { + try { + emit(const EventFormSubmitting(locations: [])); + + // 1. Update the event details first + await _eventRepository.updateEvent(eventId, eventData); + + // 2. Get existing ticket types to compare + final existingTicketTypes = await _eventRepository.getTicketTypesForEvent(eventId); + final existingAdditionalTypes = existingTicketTypes + .where((t) => (t.description ?? '') != "Standard Ticket") + .toList(); + + // 3. Delete removed ticket types + for (final existing in existingAdditionalTypes) { + final stillExists = additionalTicketTypes.any((t) => + t.typeId != null && t.typeId == existing.typeId); + + if (!stillExists && existing.typeId != null) { + await _eventRepository.deleteTicketType(existing.typeId!); + print('Deleted ticket type: ${existing.description}'); + } + } + + // 4. Create or update ticket types + for (final ticketType in additionalTicketTypes) { + if (ticketType.typeId == null) { + // Create new ticket type + await _createSingleTicketType(eventId, ticketType); + print('Created new ticket type: ${ticketType.description}'); + } else { + // Update existing ticket type + await _updateSingleTicketType(ticketType); + print('Updated ticket type: ${ticketType.description}'); + } + } + + emit(EventFormSuccess(eventId)); + } on ApiException catch (e) { + emit(EventFormError(e.message)); + } catch (e) { + emit(EventFormError('Failed to update event: $e')); + } + } + + Future _createSingleTicketType(int eventId, TicketType ticketType) async { + if (ticketType.availableFrom == null) { + throw Exception('Available from date is required for ticket type: ${ticketType.description}'); + } + + await _eventRepository.createTicketType({ + 'event_id': eventId, + 'description': ticketType.description ?? '', + 'max_count': ticketType.maxCount, + 'price': ticketType.price, + 'currency': ticketType.currency, + 'available_from': ticketType.availableFrom!.toIso8601String(), + }); + } + + Future _updateSingleTicketType(TicketType ticketType) async { + if (ticketType.typeId == null) { + throw Exception('Cannot update ticket type without ID'); + } + + if (ticketType.availableFrom == null) { + throw Exception('Available from date is required for ticket type: ${ticketType.description}'); + } + + await _eventRepository.updateTicketType(ticketType.typeId!, { + 'description': ticketType.description ?? '', + 'max_count': ticketType.maxCount, + 'price': ticketType.price, + 'currency': ticketType.currency, + 'available_from': ticketType.availableFrom!.toIso8601String(), + }); + } + + Future createTicketType(int eventId, TicketType ticketType) async { + try { + await _createSingleTicketType(eventId, ticketType); + emit(EventFormTicketTypeCreated()); + } on ApiException catch (e) { + emit(EventFormError(e.message)); + } catch (e) { + emit(EventFormError('Failed to create ticket type: $e')); + } + } + + Future updateTicketType(TicketType ticketType) async { + try { + await _updateSingleTicketType(ticketType); + emit(EventFormTicketTypeUpdated()); + } on ApiException catch (e) { + emit(EventFormError(e.message)); + } catch (e) { + emit(EventFormError('Failed to update ticket type: $e')); + } + } + + Future deleteTicketType(int typeId) async { + try { + await _eventRepository.deleteTicketType(typeId); + emit(EventFormTicketTypeDeleted()); + } on ApiException catch (e) { + emit(EventFormError(e.message)); + } catch (e) { + emit(EventFormError('Failed to delete ticket type: $e')); + } + } } diff --git a/frontend/lib/presentation/organizer/cubit/event_form_state.dart b/frontend/lib/presentation/organizer/cubit/event_form_state.dart index 4b26e67..29b777b 100644 --- a/frontend/lib/presentation/organizer/cubit/event_form_state.dart +++ b/frontend/lib/presentation/organizer/cubit/event_form_state.dart @@ -3,6 +3,7 @@ import 'package:resellio/core/models/models.dart'; abstract class EventFormState extends Equatable { const EventFormState(); + @override List get props => []; } @@ -13,7 +14,9 @@ class EventFormPrerequisitesLoading extends EventFormState {} class EventFormPrerequisitesLoaded extends EventFormState { final List locations; + const EventFormPrerequisitesLoaded({required this.locations}); + @override List get props => [locations]; } @@ -24,14 +27,35 @@ class EventFormSubmitting extends EventFormPrerequisitesLoaded { class EventFormSuccess extends EventFormState { final int eventId; + const EventFormSuccess(this.eventId); + @override List get props => [eventId]; } class EventFormError extends EventFormState { final String message; + const EventFormError(this.message); + @override List get props => [message]; } + +class EventFormTicketTypesLoading extends EventFormState {} + +class EventFormTicketTypesLoaded extends EventFormState { + final List ticketTypes; + + const EventFormTicketTypesLoaded(this.ticketTypes); + + @override + List get props => [ticketTypes]; +} + +class EventFormTicketTypeCreated extends EventFormState {} + +class EventFormTicketTypeUpdated extends EventFormState {} + +class EventFormTicketTypeDeleted extends EventFormState {} // FIXED: Added missing body diff --git a/frontend/lib/presentation/organizer/pages/edit_event_page.dart b/frontend/lib/presentation/organizer/pages/edit_event_page.dart index 6b605a2..c93e122 100644 --- a/frontend/lib/presentation/organizer/pages/edit_event_page.dart +++ b/frontend/lib/presentation/organizer/pages/edit_event_page.dart @@ -9,6 +9,7 @@ import 'package:resellio/presentation/common_widgets/primary_button.dart'; import 'package:resellio/presentation/main_page/page_layout.dart'; import 'package:resellio/presentation/organizer/cubit/event_form_cubit.dart'; import 'package:resellio/presentation/organizer/cubit/event_form_state.dart'; +import 'package:resellio/presentation/organizer/widgets/ticket_type_form.dart'; class EditEventPage extends StatelessWidget { final Event event; @@ -17,7 +18,8 @@ class EditEventPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => EventFormCubit(context.read()), + create: (context) => EventFormCubit(context.read()) + ..loadExistingTicketTypes(event.id), child: _EditEventView(event: event), ); } @@ -42,6 +44,9 @@ class _EditEventViewState extends State<_EditEventView> { DateTime? _startDate; DateTime? _endDate; + // FIXED: Changed from TicketTypeData to TicketType + List _additionalTicketTypes = []; + @override void initState() { super.initState(); @@ -95,8 +100,68 @@ class _EditEventViewState extends State<_EditEventView> { }); } + void _addTicketType() { + setState(() { + _additionalTicketTypes.add(TicketType( + eventId: widget.event.id, + description: '', + maxCount: 0, + price: 0.0, + currency: 'USD', + availableFrom: DateTime.now().add(Duration(hours: 1)), + )); + }); + } + + void _removeTicketType(int index) { + setState(() { + _additionalTicketTypes.removeAt(index); + }); + } + + void _updateTicketType(int index, TicketType ticketType) { + setState(() { + _additionalTicketTypes[index] = ticketType; + }); + } + + bool _validateTicketTypes() { + for (int i = 0; i < _additionalTicketTypes.length; i++) { + final ticketType = _additionalTicketTypes[i]; + + if ((ticketType.description ?? '').isEmpty) { + _showError('Please fill description for ticket type ${i + 1}.'); + return false; + } + if (ticketType.maxCount <= 0) { + _showError('Please enter valid ticket count for ticket type ${i + 1}.'); + return false; + } + if (ticketType.price < 0) { + _showError('Please enter valid price for ticket type ${i + 1}.'); + return false; + } + if (ticketType.availableFrom == null) { + _showError('Please set available from date for ticket type ${i + 1}.'); + return false; + } + if (ticketType.availableFrom!.isAfter(_startDate!)) { + _showError('Available from date for ticket type ${i + 1} cannot be after event start date.'); + return false; + } + } + return true; + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + backgroundColor: Colors.red, + )); + } + void _submitForm() { - if (_formKey.currentState!.validate()) { + if (_formKey.currentState!.validate() && _validateTicketTypes()) { final eventData = { 'name': _nameController.text, 'description': _descriptionController.text, @@ -104,14 +169,17 @@ class _EditEventViewState extends State<_EditEventView> { 'end_date': _endDate!.toIso8601String(), 'minimum_age': int.tryParse(_minimumAgeController.text), }; + context .read() - .updateEvent(widget.event.id, eventData); + .updateEventWithTicketTypes(widget.event.id, eventData, _additionalTicketTypes); } } @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return PageLayout( title: 'Edit Event', showBackButton: true, @@ -133,6 +201,13 @@ class _EditEventViewState extends State<_EditEventView> { backgroundColor: Colors.red), ); } + if (state is EventFormTicketTypesLoaded) { + setState(() { + _additionalTicketTypes = state.ticketTypes + .where((t) => (t.description ?? '') != "Standard Ticket") + .toList(); + }); + } }, child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), @@ -141,42 +216,12 @@ class _EditEventViewState extends State<_EditEventView> { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - CustomTextFormField( - controller: _nameController, - labelText: 'Event Name', - validator: (v) => - v!.isEmpty ? 'Event name is required' : null, - ), - const SizedBox(height: 16), - CustomTextFormField( - controller: _descriptionController, - labelText: 'Description', - keyboardType: TextInputType.multiline, - ), - const SizedBox(height: 16), - CustomTextFormField( - controller: _startDateController, - labelText: 'Start Date & Time', - readOnly: true, - onTap: () => _selectDateTime(context, true), - validator: (v) => - v!.isEmpty ? 'Start date is required' : null, - ), - const SizedBox(height: 16), - CustomTextFormField( - controller: _endDateController, - labelText: 'End Date & Time', - readOnly: true, - onTap: () => _selectDateTime(context, false), - validator: (v) => v!.isEmpty ? 'End date is required' : null, - ), - const SizedBox(height: 16), - CustomTextFormField( - controller: _minimumAgeController, - labelText: 'Minimum Age (Optional)', - keyboardType: TextInputType.number, - ), + _buildEventDetailsSection(theme), + const SizedBox(height: 32), + + _buildTicketTypesSection(theme), const SizedBox(height: 32), + BlocBuilder( builder: (context, state) { return PrimaryButton( @@ -193,4 +238,145 @@ class _EditEventViewState extends State<_EditEventView> { ), ); } + + Widget _buildEventDetailsSection(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Event Details", style: theme.textTheme.headlineSmall), + const SizedBox(height: 20), + CustomTextFormField( + controller: _nameController, + labelText: 'Event Name', + validator: (v) => v!.isEmpty ? 'Event name is required' : null, + ), + const SizedBox(height: 16), + CustomTextFormField( + controller: _descriptionController, + labelText: 'Description', + keyboardType: TextInputType.multiline, + maxLines: 4, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: CustomTextFormField( + controller: _startDateController, + labelText: 'Start Date & Time', + readOnly: true, + onTap: () => _selectDateTime(context, true), + validator: (v) => v!.isEmpty ? 'Start date is required' : null, + ), + ), + const SizedBox(width: 16), + Expanded( + child: CustomTextFormField( + controller: _endDateController, + labelText: 'End Date & Time', + readOnly: true, + onTap: () => _selectDateTime(context, false), + validator: (v) => v!.isEmpty ? 'End date is required' : null, + ), + ), + ], + ), + const SizedBox(height: 16), + CustomTextFormField( + controller: _minimumAgeController, + labelText: 'Minimum Age (Optional)', + keyboardType: TextInputType.number, + ), + ], + ), + ), + ); + } + + Widget _buildTicketTypesSection(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Additional Ticket Types", style: theme.textTheme.headlineSmall), + OutlinedButton.icon( + onPressed: _addTicketType, + icon: const Icon(Icons.add), + label: const Text('Add Type'), + ), + ], + ), + const SizedBox(height: 8), + Text( + "Add different ticket types with varying prices (VIP, Early Bird, etc.)", + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 20), + if (_additionalTicketTypes.isEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.colorScheme.outlineVariant.withOpacity(0.5), + ), + ), + child: Column( + children: [ + Icon( + Icons.confirmation_number_outlined, + size: 48, + color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), + ), + const SizedBox(height: 12), + Text( + 'No additional ticket types yet', + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + 'Standard tickets are already available. Add premium types here.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), + ), + ), + ], + ), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _additionalTicketTypes.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: TicketTypeForm( + ticketType: _additionalTicketTypes[index], + index: index + 2, + onChanged: (ticketType) => _updateTicketType(index, ticketType), + onDelete: () => _removeTicketType(index), + ), + ); + }, + ), + ], + ), + ), + ); + } } diff --git a/frontend/lib/presentation/organizer/widgets/ticket_type_form.dart b/frontend/lib/presentation/organizer/widgets/ticket_type_form.dart new file mode 100644 index 0000000..9a620cb --- /dev/null +++ b/frontend/lib/presentation/organizer/widgets/ticket_type_form.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:resellio/presentation/common_widgets/custom_text_form_field.dart'; +import 'package:resellio/core/models/models.dart'; + +class TicketTypeForm extends StatefulWidget { + final TicketType ticketType; + final int index; + final Function(TicketType) onChanged; + final VoidCallback onDelete; + + const TicketTypeForm({ + super.key, + required this.ticketType, + required this.index, + required this.onChanged, + required this.onDelete, + }); + + @override + State createState() => _TicketTypeFormState(); +} + +class _TicketTypeFormState extends State { + late TextEditingController _descriptionController; + late TextEditingController _maxCountController; + late TextEditingController _priceController; + late TextEditingController _availableFromController; + + @override + void initState() { + super.initState(); + _descriptionController = TextEditingController(text: widget.ticketType.description ?? ''); + _maxCountController = TextEditingController(text: widget.ticketType.maxCount.toString()); + _priceController = TextEditingController(text: widget.ticketType.price.toString()); + _availableFromController = TextEditingController( + text: widget.ticketType.availableFrom != null + ? DateFormat.yMd().add_jm().format(widget.ticketType.availableFrom!) + : '' + ); + + _descriptionController.addListener(_updateTicketType); + _maxCountController.addListener(_updateTicketType); + _priceController.addListener(_updateTicketType); + } + + @override + void dispose() { + _descriptionController.dispose(); + _maxCountController.dispose(); + _priceController.dispose(); + _availableFromController.dispose(); + super.dispose(); + } + + void _updateTicketType() { + final description = _descriptionController.text; + final maxCount = int.tryParse(_maxCountController.text) ?? 0; + final price = double.tryParse(_priceController.text) ?? 0.0; + + // Use copyWith method from the unified model + widget.onChanged(widget.ticketType.copyWith( + description: description, + maxCount: maxCount, + price: price, + currency: 'USD', + // Preserve the existing availableFrom date + )); + } + + Future _selectAvailableFromDateTime(BuildContext context) async { + final DateTime? date = await showDatePicker( + context: context, + initialDate: widget.ticketType.availableFrom ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime(2101), + ); + if (date == null) return; + + final TimeOfDay? time = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(widget.ticketType.availableFrom ?? DateTime.now()), + ); + if (time == null) return; + + final selectedDateTime = DateTime(date.year, date.month, date.day, time.hour, time.minute); + + setState(() { + _availableFromController.text = DateFormat.yMd().add_jm().format(selectedDateTime); + }); + + // Update the ticket type with the new date using copyWith + widget.onChanged(widget.ticketType.copyWith(availableFrom: selectedDateTime)); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.5), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Ticket Type ${widget.index}', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + IconButton( + onPressed: widget.onDelete, + icon: Icon( + Icons.delete_outline, + color: colorScheme.error, + ), + tooltip: 'Remove ticket type', + ), + ], + ), + const SizedBox(height: 16), + CustomTextFormField( + controller: _descriptionController, + labelText: 'Description (e.g., VIP, Early Bird)', + validator: (v) => v!.isEmpty ? 'Description is required' : null, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: CustomTextFormField( + controller: _maxCountController, + labelText: 'Ticket Count', + keyboardType: TextInputType.number, + validator: (v) { + if (v!.isEmpty) return 'Count is required'; + if (int.tryParse(v) == null || int.parse(v) <= 0) { + return 'Enter valid count'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: CustomTextFormField( + controller: _priceController, + labelText: 'Price (\$)', + prefixText: '\$ ', + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: (v) { + if (v!.isEmpty) return 'Price is required'; + if (double.tryParse(v) == null || double.parse(v) < 0) { + return 'Enter valid price'; + } + return null; + }, + ), + ), + ], + ), + const SizedBox(height: 16), + CustomTextFormField( + controller: _availableFromController, + labelText: 'Available From Date & Time *', + readOnly: true, + onTap: () => _selectAvailableFromDateTime(context), + validator: (v) => v!.isEmpty ? 'Available from date is required' : null, + ), + ], + ), + ); + } +} From b2006821636fd88976373c74a62ed77b512c53da Mon Sep 17 00:00:00 2001 From: WojciechMat Date: Mon, 16 Jun 2025 17:27:02 +0200 Subject: [PATCH 2/4] Better handling of available events and modifying event types --- .../core/repositories/event_repository.dart | 15 +- .../organizer/cubit/event_form_cubit.dart | 98 ++++------ .../organizer/cubit/event_form_state.dart | 4 +- .../organizer/pages/edit_event_page.dart | 182 +++++++++++++----- .../organizer/widgets/ticket_type_form.dart | 168 ++++++++++++++-- 5 files changed, 329 insertions(+), 138 deletions(-) diff --git a/frontend/lib/core/repositories/event_repository.dart b/frontend/lib/core/repositories/event_repository.dart index 2ea9c64..473f2c9 100644 --- a/frontend/lib/core/repositories/event_repository.dart +++ b/frontend/lib/core/repositories/event_repository.dart @@ -12,7 +12,6 @@ abstract class EventRepository { Future notifyParticipants(int eventId, String message); Future createTicketType(Map data); Future deleteTicketType(int typeId); - Future updateTicketType(int typeId, Map data); Future> getLocations(); } @@ -33,23 +32,15 @@ class ApiEventRepository implements EventRepository { return (data as List).map((e) => Event.fromJson(e)).toList(); } - @override - Future updateTicketType(int typeId, Map data) async { - final response = await _apiClient.put('/ticket-types/$typeId', data: data); - return TicketType.fromJson(response); - } - @override Future> getOrganizerEvents(int organizerId) async { - final data = - await _apiClient.get('/events', queryParams: {'organizer_id': organizerId}); + final data = await _apiClient.get('/events', queryParams: {'organizer_id': organizerId}); return (data as List).map((e) => Event.fromJson(e)).toList(); } @override Future> getTicketTypesForEvent(int eventId) async { - final data = await _apiClient - .get('/ticket-types/', queryParams: {'event_id': eventId}); + final data = await _apiClient.get('/ticket-types/', queryParams: {'event_id': eventId}); return (data as List).map((t) => TicketType.fromJson(t)).toList(); } @@ -82,6 +73,8 @@ class ApiEventRepository implements EventRepository { return TicketType.fromJson(response); } + // REMOVED: updateTicketType method + @override Future deleteTicketType(int typeId) async { final response = await _apiClient.delete('/ticket-types/$typeId'); diff --git a/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart b/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart index 5c887a6..f73de3d 100644 --- a/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart +++ b/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart @@ -43,7 +43,6 @@ class EventFormCubit extends Cubit { try { emit(EventFormTicketTypesLoading()); final ticketTypes = await _eventRepository.getTicketTypesForEvent(eventId); - emit(EventFormTicketTypesLoaded(ticketTypes)); } on ApiException catch (e) { emit(EventFormError('Failed to load ticket types: ${e.message}')); @@ -65,10 +64,11 @@ class EventFormCubit extends Cubit { } } + /// Update event details only - ticket types are managed separately Future updateEventWithTicketTypes( int eventId, Map eventData, - List additionalTicketTypes + List newTicketTypes // Only new ticket types to be created ) async { try { emit(const EventFormSubmitting(locations: [])); @@ -76,33 +76,11 @@ class EventFormCubit extends Cubit { // 1. Update the event details first await _eventRepository.updateEvent(eventId, eventData); - // 2. Get existing ticket types to compare - final existingTicketTypes = await _eventRepository.getTicketTypesForEvent(eventId); - final existingAdditionalTypes = existingTicketTypes - .where((t) => (t.description ?? '') != "Standard Ticket") - .toList(); - - // 3. Delete removed ticket types - for (final existing in existingAdditionalTypes) { - final stillExists = additionalTicketTypes.any((t) => - t.typeId != null && t.typeId == existing.typeId); - - if (!stillExists && existing.typeId != null) { - await _eventRepository.deleteTicketType(existing.typeId!); - print('Deleted ticket type: ${existing.description}'); - } - } - - // 4. Create or update ticket types - for (final ticketType in additionalTicketTypes) { + // 2. Create only NEW ticket types (no deletion/updating of existing ones) + for (final ticketType in newTicketTypes) { if (ticketType.typeId == null) { - // Create new ticket type await _createSingleTicketType(eventId, ticketType); print('Created new ticket type: ${ticketType.description}'); - } else { - // Update existing ticket type - await _updateSingleTicketType(ticketType); - print('Updated ticket type: ${ticketType.description}'); } } @@ -114,31 +92,41 @@ class EventFormCubit extends Cubit { } } - Future _createSingleTicketType(int eventId, TicketType ticketType) async { - if (ticketType.availableFrom == null) { - throw Exception('Available from date is required for ticket type: ${ticketType.description}'); - } - - await _eventRepository.createTicketType({ - 'event_id': eventId, - 'description': ticketType.description ?? '', - 'max_count': ticketType.maxCount, - 'price': ticketType.price, - 'currency': ticketType.currency, - 'available_from': ticketType.availableFrom!.toIso8601String(), - }); + bool canDeleteTicketType(TicketType ticketType) { + if (ticketType.availableFrom == null) return false; + return ticketType.availableFrom!.isAfter(DateTime.now()); } - Future _updateSingleTicketType(TicketType ticketType) async { - if (ticketType.typeId == null) { - throw Exception('Cannot update ticket type without ID'); + Future deleteTicketType(int typeId, TicketType ticketType) async { + try { + // Check if deletion is allowed + if (!canDeleteTicketType(ticketType)) { + emit(EventFormError( + 'Cannot delete ticket type "${ticketType.description ?? ''}" - sales have already started or no availability date set.' + )); + return; + } + + await _eventRepository.deleteTicketType(typeId); + emit(EventFormTicketTypeDeleted()); + + if (ticketType.eventId != null) { + await loadExistingTicketTypes(ticketType.eventId); + } + } on ApiException catch (e) { + emit(EventFormError(e.message)); + } catch (e) { + emit(EventFormError('Failed to delete ticket type: $e')); } + } + Future _createSingleTicketType(int eventId, TicketType ticketType) async { if (ticketType.availableFrom == null) { throw Exception('Available from date is required for ticket type: ${ticketType.description}'); } - await _eventRepository.updateTicketType(ticketType.typeId!, { + await _eventRepository.createTicketType({ + 'event_id': eventId, 'description': ticketType.description ?? '', 'max_count': ticketType.maxCount, 'price': ticketType.price, @@ -151,32 +139,12 @@ class EventFormCubit extends Cubit { try { await _createSingleTicketType(eventId, ticketType); emit(EventFormTicketTypeCreated()); - } on ApiException catch (e) { - emit(EventFormError(e.message)); - } catch (e) { - emit(EventFormError('Failed to create ticket type: $e')); - } - } - Future updateTicketType(TicketType ticketType) async { - try { - await _updateSingleTicketType(ticketType); - emit(EventFormTicketTypeUpdated()); + await loadExistingTicketTypes(eventId); } on ApiException catch (e) { emit(EventFormError(e.message)); } catch (e) { - emit(EventFormError('Failed to update ticket type: $e')); - } - } - - Future deleteTicketType(int typeId) async { - try { - await _eventRepository.deleteTicketType(typeId); - emit(EventFormTicketTypeDeleted()); - } on ApiException catch (e) { - emit(EventFormError(e.message)); - } catch (e) { - emit(EventFormError('Failed to delete ticket type: $e')); + emit(EventFormError('Failed to create ticket type: $e')); } } } diff --git a/frontend/lib/presentation/organizer/cubit/event_form_state.dart b/frontend/lib/presentation/organizer/cubit/event_form_state.dart index 29b777b..8ba10f6 100644 --- a/frontend/lib/presentation/organizer/cubit/event_form_state.dart +++ b/frontend/lib/presentation/organizer/cubit/event_form_state.dart @@ -56,6 +56,4 @@ class EventFormTicketTypesLoaded extends EventFormState { class EventFormTicketTypeCreated extends EventFormState {} -class EventFormTicketTypeUpdated extends EventFormState {} - -class EventFormTicketTypeDeleted extends EventFormState {} // FIXED: Added missing body +class EventFormTicketTypeDeleted extends EventFormState {} diff --git a/frontend/lib/presentation/organizer/pages/edit_event_page.dart b/frontend/lib/presentation/organizer/pages/edit_event_page.dart index c93e122..f879ff5 100644 --- a/frontend/lib/presentation/organizer/pages/edit_event_page.dart +++ b/frontend/lib/presentation/organizer/pages/edit_event_page.dart @@ -201,13 +201,6 @@ class _EditEventViewState extends State<_EditEventView> { backgroundColor: Colors.red), ); } - if (state is EventFormTicketTypesLoaded) { - setState(() { - _additionalTicketTypes = state.ticketTypes - .where((t) => (t.description ?? '') != "Standard Ticket") - .toList(); - }); - } }, child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), @@ -295,7 +288,6 @@ class _EditEventViewState extends State<_EditEventView> { ), ); } - Widget _buildTicketTypesSection(ThemeData theme) { return Card( child: Padding( @@ -306,58 +298,78 @@ class _EditEventViewState extends State<_EditEventView> { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("Additional Ticket Types", style: theme.textTheme.headlineSmall), + Text("Ticket Types Management", style: theme.textTheme.headlineSmall), OutlinedButton.icon( onPressed: _addTicketType, icon: const Icon(Icons.add), - label: const Text('Add Type'), + label: const Text('Add New Type'), ), ], ), const SizedBox(height: 8), Text( - "Add different ticket types with varying prices (VIP, Early Bird, etc.)", + "Existing ticket types are read-only. You can only delete types that haven't started selling yet.", style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 20), - if (_additionalTicketTypes.isEmpty) - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: theme.colorScheme.outlineVariant.withOpacity(0.5), - ), - ), - child: Column( - children: [ - Icon( - Icons.confirmation_number_outlined, - size: 48, - color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), - ), - const SizedBox(height: 12), - Text( - 'No additional ticket types yet', - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 4), - Text( - 'Standard tickets are already available. Add premium types here.', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), - ), - ), - ], + + // Show existing ticket types (from backend) + BlocBuilder( + builder: (context, state) { + if (state is EventFormTicketTypesLoaded) { + final existingTypes = state.ticketTypes + .where((t) => (t.description ?? '') != "Standard Ticket") + .toList(); + + if (existingTypes.isNotEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Existing Ticket Types", + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + ...existingTypes.asMap().entries.map((entry) { + final index = entry.key; + final ticketType = entry.value; + final cubit = context.read(); + final canDelete = cubit.canDeleteTicketType(ticketType); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TicketTypeForm( + ticketType: ticketType, + index: index + 2, // Start from 2 (1 is standard) + isDeletable: canDelete, + onDelete: canDelete && ticketType.typeId != null + ? () => _deleteExistingTicketType(ticketType.typeId!, ticketType) + : null, + ), + ); + }).toList(), + const SizedBox(height: 20), + ], + ); + } + } + return const SizedBox.shrink(); + }, + ), + + // Show new ticket types being added + if (_additionalTicketTypes.isNotEmpty) ...[ + Text( + "New Ticket Types", + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, ), - ) - else + ), + const SizedBox(height: 12), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -365,18 +377,92 @@ class _EditEventViewState extends State<_EditEventView> { itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(bottom: 16), - child: TicketTypeForm( + child: EditableTicketTypeForm( ticketType: _additionalTicketTypes[index], - index: index + 2, + index: index + 100, // Use high numbers to distinguish from existing onChanged: (ticketType) => _updateTicketType(index, ticketType), onDelete: () => _removeTicketType(index), ), ); }, ), + ], + + // Empty state + if (_additionalTicketTypes.isEmpty) + BlocBuilder( + builder: (context, state) { + // Only show empty state if there are no existing types either + final hasExistingTypes = state is EventFormTicketTypesLoaded && + state.ticketTypes.any((t) => (t.description ?? '') != "Standard Ticket"); + + if (!hasExistingTypes) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.colorScheme.outlineVariant.withOpacity(0.5), + ), + ), + child: Column( + children: [ + Icon( + Icons.confirmation_number_outlined, + size: 48, + color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), + ), + const SizedBox(height: 12), + Text( + 'No additional ticket types yet', + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + 'Standard tickets are already available. Add premium types here.', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant.withOpacity(0.7), + ), + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ), ], ), ), ); } + + void _deleteExistingTicketType(int typeId, TicketType ticketType) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Ticket Type'), + content: Text('Are you sure you want to delete "${ticketType.description}"?\n\nThis action cannot be undone.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Delete'), + ), + ], + ), + ); + + if (confirmed == true) { + context.read().deleteTicketType(typeId, ticketType); + } + } } diff --git a/frontend/lib/presentation/organizer/widgets/ticket_type_form.dart b/frontend/lib/presentation/organizer/widgets/ticket_type_form.dart index 9a620cb..1f5f179 100644 --- a/frontend/lib/presentation/organizer/widgets/ticket_type_form.dart +++ b/frontend/lib/presentation/organizer/widgets/ticket_type_form.dart @@ -1,15 +1,162 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:resellio/presentation/common_widgets/custom_text_form_field.dart'; import 'package:resellio/core/models/models.dart'; +import 'package:resellio/presentation/common_widgets/custom_text_form_field.dart'; + +// Read-only ticket type display +class TicketTypeForm extends StatelessWidget { + final TicketType ticketType; + final int index; + final VoidCallback? onDelete; + final bool isDeletable; + + const TicketTypeForm({ + super.key, + required this.ticketType, + required this.index, + this.onDelete, + this.isDeletable = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + // Determine the display status + final isActive = ticketType.availableFrom?.isAfter(DateTime.now()) ?? false; + final statusColor = isActive ? Colors.green : Colors.orange; + final statusText = isActive ? 'Not yet available' : 'Sales active'; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: colorScheme.outlineVariant.withOpacity(0.5), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ticketType.description ?? 'Unnamed Ticket Type', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: statusColor, width: 1), + ), + child: Text( + statusText, + style: theme.textTheme.labelSmall?.copyWith( + color: statusColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + if (isDeletable && onDelete != null) + IconButton( + onPressed: onDelete, + icon: Icon( + Icons.delete_outline, + color: colorScheme.error, + ), + tooltip: 'Remove ticket type', + ) + else if (!isDeletable) + Tooltip( + message: 'Cannot delete - tickets may have been sold', + child: Icon( + Icons.lock_outline, + color: colorScheme.onSurfaceVariant.withOpacity(0.5), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Display ticket type details (read-only) + _buildInfoRow( + context, + 'Count:', + '${ticketType.maxCount} tickets', + Icons.confirmation_number_outlined, + ), + const SizedBox(height: 8), + _buildInfoRow( + context, + 'Price:', + '${ticketType.currency} \$${ticketType.price.toStringAsFixed(2)}', + Icons.attach_money, + ), + const SizedBox(height: 8), + if (ticketType.availableFrom != null) + _buildInfoRow( + context, + 'Available from:', + DateFormat.yMd().add_jm().format(ticketType.availableFrom!), + Icons.schedule, + ), + ], + ), + ); + } -class TicketTypeForm extends StatefulWidget { + Widget _buildInfoRow(BuildContext context, String label, String value, IconData icon) { + final theme = Theme.of(context); + return Row( + children: [ + Icon( + icon, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Text( + label, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: theme.textTheme.bodyMedium, + ), + ), + ], + ); + } +} + +// Editable version for creating new ticket types +class EditableTicketTypeForm extends StatefulWidget { final TicketType ticketType; final int index; final Function(TicketType) onChanged; final VoidCallback onDelete; - const TicketTypeForm({ + const EditableTicketTypeForm({ super.key, required this.ticketType, required this.index, @@ -18,10 +165,10 @@ class TicketTypeForm extends StatefulWidget { }); @override - State createState() => _TicketTypeFormState(); + State createState() => _EditableTicketTypeFormState(); } -class _TicketTypeFormState extends State { +class _EditableTicketTypeFormState extends State { late TextEditingController _descriptionController; late TextEditingController _maxCountController; late TextEditingController _priceController; @@ -58,13 +205,11 @@ class _TicketTypeFormState extends State { final maxCount = int.tryParse(_maxCountController.text) ?? 0; final price = double.tryParse(_priceController.text) ?? 0.0; - // Use copyWith method from the unified model widget.onChanged(widget.ticketType.copyWith( description: description, maxCount: maxCount, price: price, currency: 'USD', - // Preserve the existing availableFrom date )); } @@ -89,7 +234,6 @@ class _TicketTypeFormState extends State { _availableFromController.text = DateFormat.yMd().add_jm().format(selectedDateTime); }); - // Update the ticket type with the new date using copyWith widget.onChanged(widget.ticketType.copyWith(availableFrom: selectedDateTime)); } @@ -101,10 +245,11 @@ class _TicketTypeFormState extends State { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withOpacity(0.3), + color: colorScheme.primaryContainer.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( - color: colorScheme.outlineVariant.withOpacity(0.5), + color: colorScheme.primary.withOpacity(0.3), + width: 2, ), ), child: Column( @@ -114,9 +259,10 @@ class _TicketTypeFormState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Ticket Type ${widget.index}', + 'New Ticket Type', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, + color: colorScheme.primary, ), ), IconButton( From e547fe47255d52f62cb149e0b83f08aa766762df Mon Sep 17 00:00:00 2001 From: WojciechMat Date: Mon, 16 Jun 2025 17:31:42 +0200 Subject: [PATCH 3/4] clean --- frontend/lib/core/models/ticket_model.dart | 7 ++----- .../lib/presentation/organizer/cubit/event_form_cubit.dart | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/lib/core/models/ticket_model.dart b/frontend/lib/core/models/ticket_model.dart index ed6acb5..260090c 100644 --- a/frontend/lib/core/models/ticket_model.dart +++ b/frontend/lib/core/models/ticket_model.dart @@ -45,7 +45,6 @@ class TicketType { }; } - // Add copyWith method for easier updates TicketType copyWith({ int? typeId, int? eventId, @@ -84,9 +83,8 @@ class TicketDetailsModel { final String? eventName; final DateTime? eventStartDate; - // Add ticket type details for convenience final String? ticketTypeDescription; - final DateTime? ticketAvailableFrom; // Added this field + final DateTime? ticketAvailableFrom; TicketDetailsModel({ required this.ticketId, @@ -98,7 +96,7 @@ class TicketDetailsModel { this.eventName, this.eventStartDate, this.ticketTypeDescription, - this.ticketAvailableFrom, // Added this parameter + this.ticketAvailableFrom, }); factory TicketDetailsModel.fromJson(Map json) { @@ -120,7 +118,6 @@ class TicketDetailsModel { : json['eventStartDate'] != null ? DateTime.parse(json['eventStartDate']) : null, - // Add ticket type description and available from ticketTypeDescription: json['ticket_type_description'], ticketAvailableFrom: json['ticket_available_from'] != null ? DateTime.parse(json['ticket_available_from']) diff --git a/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart b/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart index f73de3d..795131a 100644 --- a/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart +++ b/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart @@ -68,7 +68,7 @@ class EventFormCubit extends Cubit { Future updateEventWithTicketTypes( int eventId, Map eventData, - List newTicketTypes // Only new ticket types to be created + List newTicketTypes ) async { try { emit(const EventFormSubmitting(locations: [])); From aa5c4d594fef15139ff2ce4b1a07a4b8ba41a940 Mon Sep 17 00:00:00 2001 From: WojciechMat Date: Mon, 16 Jun 2025 17:34:09 +0200 Subject: [PATCH 4/4] clean --- frontend/lib/presentation/organizer/cubit/event_form_cubit.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart b/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart index 795131a..bbbc8b8 100644 --- a/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart +++ b/frontend/lib/presentation/organizer/cubit/event_form_cubit.dart @@ -51,7 +51,6 @@ class EventFormCubit extends Cubit { } } - /// Simple event update without ticket types Future updateEvent(int eventId, Map eventData) async { try { emit(const EventFormSubmitting(locations: []));