- Implement maintenance tool for adding, logging, and retrieving tasks - Create meals tool for managing recipes, meal plans, and shopping lists - Introduce reparse metadata tool for updating thought metadata - Add household knowledge, home maintenance, family calendar, meal planning, and professional CRM database migrations - Grant necessary permissions for new database tables
248 lines
8.0 KiB
Go
248 lines
8.0 KiB
Go
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()
|
|
}
|