package calendar import ( "context" "errors" "time" "github.com/jackc/pgx/v5" ) var ErrCalendarItemNotFound = errors.New("calendar item not found") type CalendarItem struct { ID string `json:"id"` ClientID string `json:"client_id"` ClientName string `json:"client_name"` Title string `json:"title"` Description string `json:"description"` ContentType string `json:"content_type"` Status string `json:"status"` OwnerID *string `json:"owner_id"` ScheduledDate string `json:"scheduled_date"` ScheduledAt *time.Time `json:"scheduled_at"` CopyText string `json:"copy_text"` InternalNotes string `json:"internal_notes"` ClientNotes string `json:"client_notes"` CreatedBy *string `json:"created_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type ListCalendarItemsFilter struct { ClientID string ViewerUserID string From string To string } func (s Store) ListCalendarItems(ctx context.Context, filter ListCalendarItemsFilter) ([]CalendarItem, error) { rows, err := s.db.Query(ctx, ` SELECT calendar_items.id::text, calendar_items.client_id::text, clients.name, calendar_items.title, calendar_items.description, calendar_items.content_type, calendar_items.status, calendar_items.owner_id::text, calendar_items.scheduled_date::text, calendar_items.scheduled_at, calendar_items.copy_text, calendar_items.internal_notes, calendar_items.client_notes, calendar_items.created_by::text, calendar_items.created_at, calendar_items.updated_at FROM calendar_items INNER JOIN clients ON clients.id = calendar_items.client_id WHERE calendar_items.scheduled_date >= $1::date AND calendar_items.scheduled_date <= $2::date AND ($3 = '' OR calendar_items.client_id::text = $3) AND ( $4 = '' OR EXISTS ( SELECT 1 FROM client_users WHERE client_users.client_id = calendar_items.client_id AND client_users.user_id = $4::uuid ) ) ORDER BY calendar_items.scheduled_date ASC, calendar_items.created_at ASC `, filter.From, filter.To, filter.ClientID, filter.ViewerUserID) if err != nil { return nil, err } defer rows.Close() items := []CalendarItem{} for rows.Next() { item, err := scanCalendarItem(rows) if err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func (s Store) FindCalendarItem(ctx context.Context, id string, viewerUserID string) (CalendarItem, error) { row := s.db.QueryRow(ctx, ` SELECT calendar_items.id::text, calendar_items.client_id::text, clients.name, calendar_items.title, calendar_items.description, calendar_items.content_type, calendar_items.status, calendar_items.owner_id::text, calendar_items.scheduled_date::text, calendar_items.scheduled_at, calendar_items.copy_text, calendar_items.internal_notes, calendar_items.client_notes, calendar_items.created_by::text, calendar_items.created_at, calendar_items.updated_at FROM calendar_items INNER JOIN clients ON clients.id = calendar_items.client_id WHERE calendar_items.id = $1::uuid AND ( $2 = '' OR EXISTS ( SELECT 1 FROM client_users WHERE client_users.client_id = calendar_items.client_id AND client_users.user_id = $2::uuid ) ) `, id, viewerUserID) return scanCalendarItem(row) } type CreateCalendarItemInput struct { ClientID string Title string Description string ContentType string Status string ScheduledDate string CopyText string InternalNotes string ClientNotes string CreatedBy string } func (s Store) CreateCalendarItem(ctx context.Context, input CreateCalendarItemInput) (CalendarItem, error) { row := s.db.QueryRow(ctx, ` INSERT INTO calendar_items ( client_id, title, description, content_type, status, scheduled_date, copy_text, internal_notes, client_notes, created_by ) VALUES ($1::uuid, $2, $3, $4, $5, $6::date, $7, $8, $9, NULLIF($10, '')::uuid) RETURNING calendar_items.id::text, calendar_items.client_id::text, (SELECT name FROM clients WHERE clients.id = calendar_items.client_id), calendar_items.title, calendar_items.description, calendar_items.content_type, calendar_items.status, calendar_items.owner_id::text, calendar_items.scheduled_date::text, calendar_items.scheduled_at, calendar_items.copy_text, calendar_items.internal_notes, calendar_items.client_notes, calendar_items.created_by::text, calendar_items.created_at, calendar_items.updated_at `, input.ClientID, input.Title, input.Description, input.ContentType, input.Status, input.ScheduledDate, input.CopyText, input.InternalNotes, input.ClientNotes, input.CreatedBy, ) return scanCalendarItem(row) } type UpdateCalendarItemInput struct { ID string Title string UpdateTitle bool Description string UpdateDescription bool Status string UpdateStatus bool ScheduledDate string UpdateScheduledDate bool CopyText string UpdateCopyText bool InternalNotes string UpdateInternalNotes bool ClientNotes string UpdateClientNotes bool } func (s Store) UpdateCalendarItem(ctx context.Context, input UpdateCalendarItemInput) (CalendarItem, error) { row := s.db.QueryRow(ctx, ` UPDATE calendar_items SET title = CASE WHEN $2 THEN $3 ELSE title END, description = CASE WHEN $4 THEN $5 ELSE description END, status = CASE WHEN $6 THEN $7 ELSE status END, scheduled_date = CASE WHEN $8 THEN NULLIF($9, '')::date ELSE scheduled_date END, copy_text = CASE WHEN $10 THEN $11 ELSE copy_text END, internal_notes = CASE WHEN $12 THEN $13 ELSE internal_notes END, client_notes = CASE WHEN $14 THEN $15 ELSE client_notes END, updated_at = now() WHERE id = $1::uuid RETURNING calendar_items.id::text, calendar_items.client_id::text, (SELECT name FROM clients WHERE clients.id = calendar_items.client_id), calendar_items.title, calendar_items.description, calendar_items.content_type, calendar_items.status, calendar_items.owner_id::text, calendar_items.scheduled_date::text, calendar_items.scheduled_at, calendar_items.copy_text, calendar_items.internal_notes, calendar_items.client_notes, calendar_items.created_by::text, calendar_items.created_at, calendar_items.updated_at `, input.ID, input.UpdateTitle, input.Title, input.UpdateDescription, input.Description, input.UpdateStatus, input.Status, input.UpdateScheduledDate, input.ScheduledDate, input.UpdateCopyText, input.CopyText, input.UpdateInternalNotes, input.InternalNotes, input.UpdateClientNotes, input.ClientNotes, ) return scanCalendarItem(row) } func (s Store) CancelCalendarItem(ctx context.Context, id string) (CalendarItem, error) { row := s.db.QueryRow(ctx, ` UPDATE calendar_items SET status = 'cancelado', updated_at = now() WHERE id = $1::uuid RETURNING calendar_items.id::text, calendar_items.client_id::text, (SELECT name FROM clients WHERE clients.id = calendar_items.client_id), calendar_items.title, calendar_items.description, calendar_items.content_type, calendar_items.status, calendar_items.owner_id::text, calendar_items.scheduled_date::text, calendar_items.scheduled_at, calendar_items.copy_text, calendar_items.internal_notes, calendar_items.client_notes, calendar_items.created_by::text, calendar_items.created_at, calendar_items.updated_at `, id) return scanCalendarItem(row) } func (s Store) DeleteCalendarItem(ctx context.Context, id string) error { command, err := s.db.Exec(ctx, ` DELETE FROM calendar_items WHERE id = $1::uuid `, id) if err != nil { return err } if command.RowsAffected() == 0 { return ErrCalendarItemNotFound } return nil } type calendarItemScanner interface { Scan(dest ...any) error } func scanCalendarItem(row calendarItemScanner) (CalendarItem, error) { var item CalendarItem err := row.Scan( &item.ID, &item.ClientID, &item.ClientName, &item.Title, &item.Description, &item.ContentType, &item.Status, &item.OwnerID, &item.ScheduledDate, &item.ScheduledAt, &item.CopyText, &item.InternalNotes, &item.ClientNotes, &item.CreatedBy, &item.CreatedAt, &item.UpdatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return CalendarItem{}, ErrCalendarItemNotFound } if err != nil { return CalendarItem{}, err } return item, nil }