Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions frontend/lib/core/models/ticket_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class TicketType {
final int maxCount;
final double price;
final String currency;
final DateTime? availableFrom; // Added this field

TicketType({
this.typeId,
Expand All @@ -13,6 +14,7 @@ class TicketType {
required this.maxCount,
required this.price,
required this.currency,
this.availableFrom, // Added this parameter
});

factory TicketType.fromJson(Map<String, dynamic> json) {
Expand All @@ -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,
);
}

Expand All @@ -35,8 +41,34 @@ class TicketType {
'max_count': maxCount,
'price': price,
'currency': currency,
'available_from': availableFrom?.toIso8601String(), // Added this field
};
}

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 {
Expand All @@ -45,12 +77,15 @@ 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;

final String? ticketTypeDescription;
final DateTime? ticketAvailableFrom;

TicketDetailsModel({
required this.ticketId,
this.typeId,
Expand All @@ -60,6 +95,8 @@ class TicketDetailsModel {
this.originalPrice,
this.eventName,
this.eventStartDate,
this.ticketTypeDescription,
this.ticketAvailableFrom,
});

factory TicketDetailsModel.fromJson(Map<String, dynamic> json) {
Expand All @@ -68,22 +105,38 @@ 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,
ticketTypeDescription: json['ticket_type_description'],
ticketAvailableFrom: json['ticket_available_from'] != null
? DateTime.parse(json['ticket_available_from'])
: null,
);
}

Map<String, dynamic> 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(),
};
}
}
10 changes: 5 additions & 5 deletions frontend/lib/core/repositories/event_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,13 @@ class ApiEventRepository implements EventRepository {

@override
Future<List<Event>> 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<List<TicketType>> 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();
}

Expand Down Expand Up @@ -75,6 +73,8 @@ class ApiEventRepository implements EventRepository {
return TicketType.fromJson(response);
}

// REMOVED: updateTicketType method

@override
Future<bool> deleteTicketType(int typeId) async {
final response = await _apiClient.delete('/ticket-types/$typeId');
Expand All @@ -86,4 +86,4 @@ class ApiEventRepository implements EventRepository {
final data = await _apiClient.get('/locations/');
return (data as List).map((e) => Location.fromJson(e)).toList();
}
}
}
99 changes: 97 additions & 2 deletions frontend/lib/presentation/organizer/cubit/event_form_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,111 @@ class EventFormCubit extends Cubit<EventFormState> {
}
}

Future<void> 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'));
}
}

Future<void> updateEvent(int eventId, Map<String, dynamic> 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));
} catch (e) {
emit(EventFormError('An unexpected error occurred: $e'));
}
}

/// Update event details only - ticket types are managed separately
Future<void> updateEventWithTicketTypes(
int eventId,
Map<String, dynamic> eventData,
List<TicketType> newTicketTypes
) async {
try {
emit(const EventFormSubmitting(locations: []));

// 1. Update the event details first
await _eventRepository.updateEvent(eventId, eventData);

// 2. Create only NEW ticket types (no deletion/updating of existing ones)
for (final ticketType in newTicketTypes) {
if (ticketType.typeId == null) {
await _createSingleTicketType(eventId, ticketType);
print('Created new ticket type: ${ticketType.description}');
}
}

emit(EventFormSuccess(eventId));
} on ApiException catch (e) {
emit(EventFormError(e.message));
} catch (e) {
emit(EventFormError('Failed to update event: $e'));
}
}

bool canDeleteTicketType(TicketType ticketType) {
if (ticketType.availableFrom == null) return false;
return ticketType.availableFrom!.isAfter(DateTime.now());
}

Future<void> 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<void> _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<void> createTicketType(int eventId, TicketType ticketType) async {
try {
await _createSingleTicketType(eventId, ticketType);
emit(EventFormTicketTypeCreated());

await loadExistingTicketTypes(eventId);
} on ApiException catch (e) {
emit(EventFormError(e.message));
} catch (e) {
emit(EventFormError('Failed to create ticket type: $e'));
}
}
}
22 changes: 22 additions & 0 deletions frontend/lib/presentation/organizer/cubit/event_form_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:resellio/core/models/models.dart';

abstract class EventFormState extends Equatable {
const EventFormState();

@override
List<Object?> get props => [];
}
Expand All @@ -13,7 +14,9 @@ class EventFormPrerequisitesLoading extends EventFormState {}

class EventFormPrerequisitesLoaded extends EventFormState {
final List<Location> locations;

const EventFormPrerequisitesLoaded({required this.locations});

@override
List<Object?> get props => [locations];
}
Expand All @@ -24,14 +27,33 @@ class EventFormSubmitting extends EventFormPrerequisitesLoaded {

class EventFormSuccess extends EventFormState {
final int eventId;

const EventFormSuccess(this.eventId);

@override
List<Object?> get props => [eventId];
}

class EventFormError extends EventFormState {
final String message;

const EventFormError(this.message);

@override
List<Object?> get props => [message];
}

class EventFormTicketTypesLoading extends EventFormState {}

class EventFormTicketTypesLoaded extends EventFormState {
final List<TicketType> ticketTypes;

const EventFormTicketTypesLoaded(this.ticketTypes);

@override
List<Object?> get props => [ticketTypes];
}

class EventFormTicketTypeCreated extends EventFormState {}

class EventFormTicketTypeDeleted extends EventFormState {}
Loading