From 4f06b1fe508db0c44050bd2dad9000f346a16c3f Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Tue, 18 Nov 2025 19:43:22 +0000 Subject: [PATCH] Add equipment management to events - Add equipmentProvided and requiredItems fields to Event model - Update event creation form with equipment section - Allow organizers to specify if equipment is provided or BYO - Display required items list in event details - Update database schema and run migration --- .../migration.sql | 3 + server/prisma/schema.prisma | 2 + server/src/controllers/eventController.ts | 4 +- server/src/services/eventService.ts | 16 ++- src/components/event/EventDetailModal.tsx | 48 +++++++ src/components/event/EventForm.tsx | 130 ++++++++++++++++++ src/types/index.ts | 2 + 7 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 server/prisma/migrations/20251118193723_add_equipment_fields/migration.sql diff --git a/server/prisma/migrations/20251118193723_add_equipment_fields/migration.sql b/server/prisma/migrations/20251118193723_add_equipment_fields/migration.sql new file mode 100644 index 0000000..220aab1 --- /dev/null +++ b/server/prisma/migrations/20251118193723_add_equipment_fields/migration.sql @@ -0,0 +1,3 @@ +-- Add equipment fields to events table +ALTER TABLE `events` ADD COLUMN `equipment_provided` BOOLEAN DEFAULT false AFTER `photos`; +ALTER TABLE `events` ADD COLUMN `required_items` JSON AFTER `equipment_provided`; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 3fb21f6..821fd79 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -83,6 +83,8 @@ model Event { participantCount Int? @default(0) @map("participant_count") litterCollected Decimal? @map("litter_collected") @db.Decimal(10, 2) photos Json? + equipmentProvided Boolean? @default(false) @map("equipment_provided") + requiredItems Json? @map("required_items") createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) updatedAt DateTime? @default(now()) @updatedAt @map("updated_at") @db.Timestamp(0) registrations EventRegistration[] diff --git a/server/src/controllers/eventController.ts b/server/src/controllers/eventController.ts index 7a9a911..aaa20a0 100644 --- a/server/src/controllers/eventController.ts +++ b/server/src/controllers/eventController.ts @@ -109,7 +109,7 @@ export const eventController = { // Find or create user in database const userId = await findOrCreateUser(clerkId); - const { title, description, latitude, longitude, locationName, scheduledDate, duration } = req.body; + const { title, description, latitude, longitude, locationName, scheduledDate, duration, equipmentProvided, requiredItems } = req.body; // Validation if (!title || !description || !latitude || !longitude || !locationName || !scheduledDate || !duration) { @@ -129,6 +129,8 @@ export const eventController = { locationName, scheduledDate: new Date(scheduledDate), duration: parseInt(duration), + equipmentProvided: equipmentProvided !== undefined ? Boolean(equipmentProvided) : false, + requiredItems: Array.isArray(requiredItems) ? requiredItems : [], }; const event = await eventService.createEvent(eventData); diff --git a/server/src/services/eventService.ts b/server/src/services/eventService.ts index 994d5a1..2fc07a2 100644 --- a/server/src/services/eventService.ts +++ b/server/src/services/eventService.ts @@ -21,6 +21,8 @@ interface CreateEventData { locationName: string; scheduledDate: Date; duration: number; + equipmentProvided?: boolean; + requiredItems?: string[]; } interface UpdateEventData { @@ -31,6 +33,8 @@ interface UpdateEventData { status?: 'upcoming' | 'completed' | 'cancelled'; litterCollected?: number; photos?: string[]; + equipmentProvided?: boolean; + requiredItems?: string[]; } export const eventService = { @@ -93,6 +97,8 @@ export const eventService = { participantCount: event._count.registrations, litterCollected: event.litterCollected ? parseFloat(event.litterCollected.toString()) : undefined, photos: event.photos ? (Array.isArray(event.photos) ? event.photos : []) : undefined, + equipmentProvided: (event as any).equipmentProvided || false, + requiredItems: (event as any).requiredItems ? (Array.isArray((event as any).requiredItems) ? (event as any).requiredItems : []) : [], createdAt: event.createdAt, updatedAt: event.updatedAt, })); @@ -137,6 +143,8 @@ export const eventService = { participantCount: event._count.registrations, litterCollected: event.litterCollected ? parseFloat(event.litterCollected.toString()) : undefined, photos: event.photos ? (Array.isArray(event.photos) ? event.photos : []) : undefined, + equipmentProvided: (event as any).equipmentProvided || false, + requiredItems: (event as any).requiredItems ? (Array.isArray((event as any).requiredItems) ? (event as any).requiredItems : []) : [], createdAt: event.createdAt, updatedAt: event.updatedAt, }; @@ -145,16 +153,18 @@ export const eventService = { async createEvent(data: CreateEventData) { // Create point geometry for spatial queries const locationPoint = `POINT(${data.longitude} ${data.latitude})`; + const equipmentProvided = data.equipmentProvided !== undefined ? data.equipmentProvided : false; + const requiredItems = data.requiredItems && data.requiredItems.length > 0 ? JSON.stringify(data.requiredItems) : null; const event = await prisma.$executeRaw` INSERT INTO events ( id, organizer_id, title, description, location, latitude, longitude, - location_name, scheduled_date, duration, status, participant_count + location_name, scheduled_date, duration, status, participant_count, equipment_provided, required_items ) VALUES ( UUID(), ${data.organizerId}, ${data.title}, ${data.description}, ST_GeomFromText(${locationPoint}, 4326), ${data.latitude}, ${data.longitude}, ${data.locationName}, - ${data.scheduledDate}, ${data.duration}, 'upcoming', 0 + ${data.scheduledDate}, ${data.duration}, 'upcoming', 0, ${equipmentProvided}, ${requiredItems} ) `; @@ -195,6 +205,8 @@ export const eventService = { participantCount: createdEvent._count.registrations, litterCollected: createdEvent.litterCollected ? parseFloat(createdEvent.litterCollected.toString()) : undefined, photos: createdEvent.photos ? (Array.isArray(createdEvent.photos) ? createdEvent.photos : []) : undefined, + equipmentProvided: (createdEvent as any).equipmentProvided || false, + requiredItems: (createdEvent as any).requiredItems ? (Array.isArray((createdEvent as any).requiredItems) ? (createdEvent as any).requiredItems : []) : [], createdAt: createdEvent.createdAt, updatedAt: createdEvent.updatedAt, }; diff --git a/src/components/event/EventDetailModal.tsx b/src/components/event/EventDetailModal.tsx index 1e19d65..5a10067 100644 --- a/src/components/event/EventDetailModal.tsx +++ b/src/components/event/EventDetailModal.tsx @@ -186,6 +186,54 @@ export function EventDetailModal({ + {/* Equipment Information */} +
+ + + +
+

Equipment

+ {event.equipmentProvided ? ( +
+ + ✓ Equipment Provided + +

+ All necessary cleanup equipment will be provided by the organizer. +

+
+ ) : ( +
+ + Bring Your Own + + {event.requiredItems && event.requiredItems.length > 0 ? ( +
+

Please bring the following items:

+
    + {event.requiredItems.map((item, index) => ( +
  • + • + {item} +
  • + ))} +
+
+ ) : ( +

+ Participants should bring their own cleanup equipment. +

+ )} +
+ )} +
+
+
( initialData?.latitude && initialData?.longitude ? { latitude: initialData.latitude, longitude: initialData.longitude } @@ -328,6 +334,130 @@ export function EventForm({ onSubmit, onCancel, initialData, isSubmitting = fals {errors.duration &&

{errors.duration}

}
+ {/* Equipment Section */} +
+

Equipment

+ + {/* Equipment Provided Toggle */} +
+ +

+ Check this if you'll supply all necessary cleanup equipment +

+
+ + {/* Required Items List (only shown if equipment not provided) */} + {!formData.equipmentProvided && ( +
+ +

+ Add items that participants should bring (e.g., gloves, litter picker, bags, coat) +

+ + {/* Add Item Input */} +
+ setNewItem(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (newItem.trim() && !formData.requiredItems.includes(newItem.trim())) { + setFormData(prev => ({ + ...prev, + requiredItems: [...prev.requiredItems, newItem.trim()] + })); + setNewItem(''); + } + } + }} + placeholder="e.g., Gloves, Litter picker, Bags" + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm" + /> + +
+ + {/* Required Items List */} + {formData.requiredItems.length > 0 && ( +
+

Required Items:

+
    + {formData.requiredItems.map((item, index) => ( +
  • + • {item} + +
  • + ))} +
+
+ )} + + {formData.requiredItems.length === 0 && ( +
+

+ 💡 Tip: Add common items like gloves, litter pickers, bags, or appropriate clothing +

+
+ )} +
+ )} + + {formData.equipmentProvided && ( +
+

+ ✓ Participants will be informed that all equipment will be provided +

+
+ )} +
+ {/* Location Name */}