-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Admin 'Mark Past as Completed' button — auto-complete past events from UI #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,6 +85,27 @@ def upload_image | |
| render json: { event: @event.reload.as_json } | ||
| end | ||
|
|
||
| # POST /api/v1/admin/events/auto_complete_past | ||
| # Finds all upcoming/published events whose end_date (or date) is before today | ||
| # and marks them as completed. Returns a summary of what changed. | ||
| def auto_complete_past | ||
| today = Date.current | ||
| candidates = Event.where(status: %w[upcoming published]) | ||
| .where("COALESCE(end_date, date) < ?", today) | ||
|
|
||
| updated = [] | ||
| candidates.each do |event| | ||
| event.update!(status: "completed") | ||
| updated << { id: event.id, name: event.name, date: event.date, slug: event.slug } | ||
| end | ||
|
Comment on lines
+96
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Issuing one updated = candidates.map { |e| { id: e.id, name: e.name, date: e.date, slug: e.slug } }
candidates.update_all(status: "completed")This trades individual round-trips for two queries total (one Prompt To Fix With AIThis is a comment left during a code review.
Path: api/app/controllers/api/v1/admin/events_controller.rb
Line: 96-100
Comment:
**N+1 updates — consider bulk update**
Issuing one `UPDATE` query per event can be slow if many events need completing. `update_all` sends a single SQL statement. You can fetch the metadata you need in a separate (much cheaper) query before applying the bulk update:
```ruby
updated = candidates.map { |e| { id: e.id, name: e.name, date: e.date, slug: e.slug } }
candidates.update_all(status: "completed")
```
This trades individual round-trips for two queries total (one `SELECT`, one bulk `UPDATE`), which is significantly faster at any meaningful scale.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| render json: { | ||
| message: updated.any? ? "Marked #{updated.count} event(s) as completed." : "No past events needed updating.", | ||
| updated_count: updated.count, | ||
| updated_events: updated | ||
| } | ||
| end | ||
|
Comment on lines
+91
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The loop calls Wrap the block in a transaction so the whole operation succeeds or rolls back atomically: def auto_complete_past
today = Date.current
candidates = Event.where(status: %w[upcoming published])
.where("COALESCE(end_date, date) < ?", today)
updated = []
Event.transaction do
candidates.each do |event|
event.update!(status: "completed")
updated << { id: event.id, name: event.name, date: event.date, slug: event.slug }
end
end
render json: {
message: updated.any? ? "Marked #{updated.count} event(s) as completed." : "No past events needed updating.",
updated_count: updated.count,
updated_events: updated
}
endPrompt To Fix With AIThis is a comment left during a code review.
Path: api/app/controllers/api/v1/admin/events_controller.rb
Line: 91-107
Comment:
**Missing transaction — partial updates on failure**
The loop calls `event.update!` on each candidate individually with no wrapping transaction. If any update raises (e.g., a validation error on the 4th of 10 events), the first 3 events will already have been committed to `completed` while the remaining 7 are left unchanged. The endpoint will also return a 500 instead of a meaningful error, with no indication of which events were actually changed.
Wrap the block in a transaction so the whole operation succeeds or rolls back atomically:
```ruby
def auto_complete_past
today = Date.current
candidates = Event.where(status: %w[upcoming published])
.where("COALESCE(end_date, date) < ?", today)
updated = []
Event.transaction do
candidates.each do |event|
event.update!(status: "completed")
updated << { id: event.id, name: event.name, date: event.date, slug: event.slug }
end
end
render json: {
message: updated.any? ? "Marked #{updated.count} event(s) as completed." : "No past events needed updating.",
updated_count: updated.count,
updated_events: updated
}
end
```
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| private | ||
|
|
||
| def set_event | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { useEffect, useState, useCallback } from 'react' | ||
| import { CalendarDays, Plus, Pencil, Trash2, X, Loader2, Star, Clock, Trophy, Save, ChevronDown, ChevronUp, Radio, Hotel } from 'lucide-react' | ||
| import { CalendarDays, Plus, Pencil, Trash2, X, Loader2, Star, Clock, Trophy, Save, ChevronDown, ChevronUp, Radio, Hotel, CheckCircle2 } from 'lucide-react' | ||
| import { motion, AnimatePresence } from 'framer-motion' | ||
| import { Link } from 'react-router-dom' | ||
| import { api } from '../../services/api' | ||
|
|
@@ -46,6 +46,22 @@ export default function EventsAdmin() { | |
| const [success, setSuccess] = useState('') | ||
| const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null) | ||
| const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({}) | ||
| const [autoCompleting, setAutoCompleting] = useState(false) | ||
|
|
||
| const handleAutoCompletePast = async () => { | ||
| setAutoCompleting(true) | ||
| setError('') | ||
| try { | ||
| const res = await api.admin.autoCompletePastEvents() | ||
| setSuccess(res.message) | ||
| await loadEvents() | ||
| setTimeout(() => setSuccess(''), 5000) | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : 'Auto-complete failed') | ||
| } finally { | ||
| setAutoCompleting(false) | ||
| } | ||
| } | ||
|
Comment on lines
+51
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: web/src/pages/admin/EventsAdmin.tsx
Line: 51-64
Comment:
**`setTimeout` not cleared on unmount**
`setTimeout(() => setSuccess(''), 5000)` fires after 5 seconds, but if the user navigates away before it runs, it will call `setSuccess` on an unmounted component. While React 18 doesn't throw for this, it's still a state update after unmount that can cause confusion. Consider storing the timeout ref and clearing it in a cleanup effect, or using a pattern that cancels the timer on unmount.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| const loadEvents = useCallback(async () => { | ||
| try { | ||
|
|
@@ -199,13 +215,24 @@ export default function EventsAdmin() { | |
| <h1 className="font-heading text-2xl font-bold text-text-primary">Events</h1> | ||
| </div> | ||
| {!editing && ( | ||
| <button | ||
| onClick={() => { setForm(emptyForm); setEditing('new'); setError('') }} | ||
| className="flex items-center gap-2 px-4 py-2 bg-gold/10 text-gold text-sm font-medium hover:bg-gold/15 transition-colors" | ||
| > | ||
| <Plus className="w-4 h-4" /> | ||
| Create Event | ||
| </button> | ||
| <div className="flex items-center gap-2"> | ||
| <button | ||
| onClick={handleAutoCompletePast} | ||
| disabled={autoCompleting} | ||
| className="flex items-center gap-2 px-4 py-2 bg-white/5 text-text-secondary text-sm font-medium hover:bg-white/10 hover:text-text-primary transition-colors disabled:opacity-50" | ||
| title="Automatically mark any events whose date has passed as 'completed'" | ||
| > | ||
| {autoCompleting ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle2 className="w-4 h-4" />} | ||
| {autoCompleting ? 'Updating...' : 'Mark Past as Completed'} | ||
| </button> | ||
| <button | ||
| onClick={() => { setForm(emptyForm); setEditing('new'); setError('') }} | ||
| className="flex items-center gap-2 px-4 py-2 bg-gold/10 text-gold text-sm font-medium hover:bg-gold/15 transition-colors" | ||
| > | ||
| <Plus className="w-4 h-4" /> | ||
| Create Event | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every other action in this controller scopes its queries to a specific organization (e.g.,
Organization.first→org.events). This action queriesEventdirectly across all organizations. If the app ever has more than one organization in the database (even for testing or staging), this will blindly complete events that belong to a different tenant.Scope the query to the organization for consistency:
Prompt To Fix With AI