package store import ( "context" "fmt" "strings" "time" "github.com/google/uuid" ext "git.warky.dev/wdevs/amcs/internal/types" ) func (db *DB) AddProfessionalContact(ctx context.Context, c ext.ProfessionalContact) (ext.ProfessionalContact, error) { if c.Tags == nil { c.Tags = []string{} } row := db.pool.QueryRow(ctx, ` insert into professional_contacts (name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, follow_up_date) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning id, created_at, updated_at `, c.Name, nullStr(c.Company), nullStr(c.Title), nullStr(c.Email), nullStr(c.Phone), nullStr(c.LinkedInURL), nullStr(c.HowWeMet), c.Tags, nullStr(c.Notes), c.FollowUpDate) created := c if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil { return ext.ProfessionalContact{}, fmt.Errorf("insert contact: %w", err) } return created, nil } func (db *DB) SearchContacts(ctx context.Context, query string, tags []string) ([]ext.ProfessionalContact, error) { args := []any{} conditions := []string{} if q := strings.TrimSpace(query); q != "" { args = append(args, "%"+q+"%") idx := len(args) conditions = append(conditions, fmt.Sprintf( "(name ILIKE $%[1]d OR company ILIKE $%[1]d OR title ILIKE $%[1]d OR notes ILIKE $%[1]d)", idx)) } if len(tags) > 0 { args = append(args, tags) conditions = append(conditions, fmt.Sprintf("tags @> $%d", len(args))) } q := `select id, name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, last_contacted, follow_up_date, created_at, updated_at from professional_contacts` if len(conditions) > 0 { q += " where " + strings.Join(conditions, " and ") } q += " order by name" rows, err := db.pool.Query(ctx, q, args...) if err != nil { return nil, fmt.Errorf("search contacts: %w", err) } defer rows.Close() return scanContacts(rows) } func (db *DB) GetContact(ctx context.Context, id uuid.UUID) (ext.ProfessionalContact, error) { row := db.pool.QueryRow(ctx, ` select id, name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, last_contacted, follow_up_date, created_at, updated_at from professional_contacts where id = $1 `, id) var c ext.ProfessionalContact var company, title, email, phone, linkedInURL, howWeMet, notes *string if err := row.Scan(&c.ID, &c.Name, &company, &title, &email, &phone, &linkedInURL, &howWeMet, &c.Tags, ¬es, &c.LastContacted, &c.FollowUpDate, &c.CreatedAt, &c.UpdatedAt); err != nil { return ext.ProfessionalContact{}, fmt.Errorf("get contact: %w", err) } c.Company = strVal(company) c.Title = strVal(title) c.Email = strVal(email) c.Phone = strVal(phone) c.LinkedInURL = strVal(linkedInURL) c.HowWeMet = strVal(howWeMet) c.Notes = strVal(notes) if c.Tags == nil { c.Tags = []string{} } return c, nil } func (db *DB) LogInteraction(ctx context.Context, interaction ext.ContactInteraction) (ext.ContactInteraction, error) { occurredAt := interaction.OccurredAt if occurredAt.IsZero() { occurredAt = time.Now() } row := db.pool.QueryRow(ctx, ` insert into contact_interactions (contact_id, interaction_type, occurred_at, summary, follow_up_needed, follow_up_notes) values ($1, $2, $3, $4, $5, $6) returning id, created_at `, interaction.ContactID, interaction.InteractionType, occurredAt, interaction.Summary, interaction.FollowUpNeeded, nullStr(interaction.FollowUpNotes)) created := interaction created.OccurredAt = occurredAt if err := row.Scan(&created.ID, &created.CreatedAt); err != nil { return ext.ContactInteraction{}, fmt.Errorf("insert interaction: %w", err) } return created, nil } func (db *DB) GetContactHistory(ctx context.Context, contactID uuid.UUID) (ext.ContactHistory, error) { contact, err := db.GetContact(ctx, contactID) if err != nil { return ext.ContactHistory{}, err } rows, err := db.pool.Query(ctx, ` select id, contact_id, interaction_type, occurred_at, summary, follow_up_needed, follow_up_notes, created_at from contact_interactions where contact_id = $1 order by occurred_at desc `, contactID) if err != nil { return ext.ContactHistory{}, fmt.Errorf("get interactions: %w", err) } defer rows.Close() var interactions []ext.ContactInteraction for rows.Next() { var i ext.ContactInteraction var followUpNotes *string if err := rows.Scan(&i.ID, &i.ContactID, &i.InteractionType, &i.OccurredAt, &i.Summary, &i.FollowUpNeeded, &followUpNotes, &i.CreatedAt); err != nil { return ext.ContactHistory{}, fmt.Errorf("scan interaction: %w", err) } i.FollowUpNotes = strVal(followUpNotes) interactions = append(interactions, i) } if err := rows.Err(); err != nil { return ext.ContactHistory{}, err } oppRows, err := db.pool.Query(ctx, ` select id, contact_id, title, description, stage, value, expected_close_date, notes, created_at, updated_at from opportunities where contact_id = $1 order by created_at desc `, contactID) if err != nil { return ext.ContactHistory{}, fmt.Errorf("get opportunities: %w", err) } defer oppRows.Close() var opportunities []ext.Opportunity for oppRows.Next() { var o ext.Opportunity var description, notes *string if err := oppRows.Scan(&o.ID, &o.ContactID, &o.Title, &description, &o.Stage, &o.Value, &o.ExpectedCloseDate, ¬es, &o.CreatedAt, &o.UpdatedAt); err != nil { return ext.ContactHistory{}, fmt.Errorf("scan opportunity: %w", err) } o.Description = strVal(description) o.Notes = strVal(notes) opportunities = append(opportunities, o) } if err := oppRows.Err(); err != nil { return ext.ContactHistory{}, err } return ext.ContactHistory{ Contact: contact, Interactions: interactions, Opportunities: opportunities, }, nil } func (db *DB) CreateOpportunity(ctx context.Context, o ext.Opportunity) (ext.Opportunity, error) { row := db.pool.QueryRow(ctx, ` insert into opportunities (contact_id, title, description, stage, value, expected_close_date, notes) values ($1, $2, $3, $4, $5, $6, $7) returning id, created_at, updated_at `, o.ContactID, o.Title, nullStr(o.Description), o.Stage, o.Value, o.ExpectedCloseDate, nullStr(o.Notes)) created := o if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil { return ext.Opportunity{}, fmt.Errorf("insert opportunity: %w", err) } return created, nil } func (db *DB) GetFollowUpsDue(ctx context.Context, daysAhead int) ([]ext.ProfessionalContact, error) { if daysAhead <= 0 { daysAhead = 7 } cutoff := time.Now().AddDate(0, 0, daysAhead) rows, err := db.pool.Query(ctx, ` select id, name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, last_contacted, follow_up_date, created_at, updated_at from professional_contacts where follow_up_date <= $1 order by follow_up_date asc `, cutoff) if err != nil { return nil, fmt.Errorf("get follow-ups: %w", err) } defer rows.Close() return scanContacts(rows) } func (db *DB) AppendThoughtToContactNotes(ctx context.Context, contactID uuid.UUID, thoughtContent string) error { _, err := db.pool.Exec(ctx, ` update professional_contacts set notes = coalesce(notes, '') || $2 where id = $1 `, contactID, thoughtContent) if err != nil { return fmt.Errorf("append thought to contact: %w", err) } return nil } func scanContacts(rows interface { Next() bool Scan(...any) error Err() error Close() }) ([]ext.ProfessionalContact, error) { defer rows.Close() var contacts []ext.ProfessionalContact for rows.Next() { var c ext.ProfessionalContact var company, title, email, phone, linkedInURL, howWeMet, notes *string if err := rows.Scan(&c.ID, &c.Name, &company, &title, &email, &phone, &linkedInURL, &howWeMet, &c.Tags, ¬es, &c.LastContacted, &c.FollowUpDate, &c.CreatedAt, &c.UpdatedAt); err != nil { return nil, fmt.Errorf("scan contact: %w", err) } c.Company = strVal(company) c.Title = strVal(title) c.Email = strVal(email) c.Phone = strVal(phone) c.LinkedInURL = strVal(linkedInURL) c.HowWeMet = strVal(howWeMet) c.Notes = strVal(notes) if c.Tags == nil { c.Tags = []string{} } contacts = append(contacts, c) } return contacts, rows.Err() }