feat(ui): add content editor components for skills and thoughts
Some checks failed
CI / build-and-test (push) Failing after -31m24s
Some checks failed
CI / build-and-test (push) Failing after -31m24s
* Implement ContentEditorField for inline editing of content * Create ContentEditorModal for editing content in a modal * Introduce FormerShell for managing forms related to skills and thoughts * Enhance SkillsPage and ThoughtsPage with new components for better content management
This commit is contained in:
@@ -1,235 +1,235 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
// import (
|
||||
// "context"
|
||||
// "fmt"
|
||||
// "strings"
|
||||
// "time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
// "github.com/google/uuid"
|
||||
|
||||
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
|
||||
ext "git.warky.dev/wdevs/amcs/internal/types"
|
||||
)
|
||||
// "git.warky.dev/wdevs/amcs/internal/generatedmodels"
|
||||
// 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{}
|
||||
}
|
||||
// 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)
|
||||
// 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
|
||||
var model generatedmodels.ModelPublicProfessionalContacts
|
||||
if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
return ext.ProfessionalContact{}, fmt.Errorf("insert contact: %w", err)
|
||||
}
|
||||
created.ID = model.ID.UUID()
|
||||
created.CreatedAt = model.CreatedAt.Time()
|
||||
created.UpdatedAt = model.UpdatedAt.Time()
|
||||
return created, nil
|
||||
}
|
||||
// created := c
|
||||
// var model generatedmodels.ModelPublicProfessionalContacts
|
||||
// if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
// return ext.ProfessionalContact{}, fmt.Errorf("insert contact: %w", err)
|
||||
// }
|
||||
// created.ID = model.ID.UUID()
|
||||
// created.CreatedAt = model.CreatedAt.Time()
|
||||
// created.UpdatedAt = model.UpdatedAt.Time()
|
||||
// return created, nil
|
||||
// }
|
||||
|
||||
func (db *DB) SearchContacts(ctx context.Context, query string, tags []string) ([]ext.ProfessionalContact, error) {
|
||||
args := []any{}
|
||||
conditions := []string{}
|
||||
// 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)))
|
||||
}
|
||||
// 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::text[], 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"
|
||||
// q := `select id, name, company, title, email, phone, linkedin_url, how_we_met, tags::text[], 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()
|
||||
// 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)
|
||||
}
|
||||
// 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::text[], notes, last_contacted, follow_up_date, created_at, updated_at
|
||||
from professional_contacts where id = $1
|
||||
`, id)
|
||||
// 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::text[], notes, last_contacted, follow_up_date, created_at, updated_at
|
||||
// from professional_contacts where id = $1
|
||||
// `, id)
|
||||
|
||||
var model generatedmodels.ModelPublicProfessionalContacts
|
||||
var tags []string
|
||||
if err := row.Scan(&model.ID, &model.Name, &model.Company, &model.Title, &model.Email, &model.Phone,
|
||||
&model.LinkedinURL, &model.HowWeMet, &tags, &model.Notes, &model.LastContacted, &model.FollowUpDate,
|
||||
&model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
return ext.ProfessionalContact{}, fmt.Errorf("get contact: %w", err)
|
||||
}
|
||||
c := professionalContactFromModel(model, tags)
|
||||
return c, nil
|
||||
}
|
||||
// var model generatedmodels.ModelPublicProfessionalContacts
|
||||
// var tags []string
|
||||
// if err := row.Scan(&model.ID, &model.Name, &model.Company, &model.Title, &model.Email, &model.Phone,
|
||||
// &model.LinkedinURL, &model.HowWeMet, &tags, &model.Notes, &model.LastContacted, &model.FollowUpDate,
|
||||
// &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
// return ext.ProfessionalContact{}, fmt.Errorf("get contact: %w", err)
|
||||
// }
|
||||
// c := professionalContactFromModel(model, tags)
|
||||
// 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()
|
||||
}
|
||||
// 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))
|
||||
// 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
|
||||
var model generatedmodels.ModelPublicContactInteractions
|
||||
if err := row.Scan(&model.ID, &model.CreatedAt); err != nil {
|
||||
return ext.ContactInteraction{}, fmt.Errorf("insert interaction: %w", err)
|
||||
}
|
||||
created.ID = model.ID.UUID()
|
||||
created.CreatedAt = model.CreatedAt.Time()
|
||||
return created, nil
|
||||
}
|
||||
// created := interaction
|
||||
// created.OccurredAt = occurredAt
|
||||
// var model generatedmodels.ModelPublicContactInteractions
|
||||
// if err := row.Scan(&model.ID, &model.CreatedAt); err != nil {
|
||||
// return ext.ContactInteraction{}, fmt.Errorf("insert interaction: %w", err)
|
||||
// }
|
||||
// created.ID = model.ID.UUID()
|
||||
// created.CreatedAt = model.CreatedAt.Time()
|
||||
// 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
|
||||
}
|
||||
// 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()
|
||||
// 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 model generatedmodels.ModelPublicContactInteractions
|
||||
if err := rows.Scan(&model.ID, &model.ContactID, &model.InteractionType, &model.OccurredAt, &model.Summary,
|
||||
&model.FollowUpNeeded, &model.FollowUpNotes, &model.CreatedAt); err != nil {
|
||||
return ext.ContactHistory{}, fmt.Errorf("scan interaction: %w", err)
|
||||
}
|
||||
interactions = append(interactions, contactInteractionFromModel(model))
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return ext.ContactHistory{}, err
|
||||
}
|
||||
// var interactions []ext.ContactInteraction
|
||||
// for rows.Next() {
|
||||
// var model generatedmodels.ModelPublicContactInteractions
|
||||
// if err := rows.Scan(&model.ID, &model.ContactID, &model.InteractionType, &model.OccurredAt, &model.Summary,
|
||||
// &model.FollowUpNeeded, &model.FollowUpNotes, &model.CreatedAt); err != nil {
|
||||
// return ext.ContactHistory{}, fmt.Errorf("scan interaction: %w", err)
|
||||
// }
|
||||
// interactions = append(interactions, contactInteractionFromModel(model))
|
||||
// }
|
||||
// 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()
|
||||
// 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 model generatedmodels.ModelPublicOpportunities
|
||||
if err := oppRows.Scan(&model.ID, &model.ContactID, &model.Title, &model.Description, &model.Stage, &model.Value,
|
||||
&model.ExpectedCloseDate, &model.Notes, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
return ext.ContactHistory{}, fmt.Errorf("scan opportunity: %w", err)
|
||||
}
|
||||
opportunities = append(opportunities, opportunityFromModel(model))
|
||||
}
|
||||
if err := oppRows.Err(); err != nil {
|
||||
return ext.ContactHistory{}, err
|
||||
}
|
||||
// var opportunities []ext.Opportunity
|
||||
// for oppRows.Next() {
|
||||
// var model generatedmodels.ModelPublicOpportunities
|
||||
// if err := oppRows.Scan(&model.ID, &model.ContactID, &model.Title, &model.Description, &model.Stage, &model.Value,
|
||||
// &model.ExpectedCloseDate, &model.Notes, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
// return ext.ContactHistory{}, fmt.Errorf("scan opportunity: %w", err)
|
||||
// }
|
||||
// opportunities = append(opportunities, opportunityFromModel(model))
|
||||
// }
|
||||
// if err := oppRows.Err(); err != nil {
|
||||
// return ext.ContactHistory{}, err
|
||||
// }
|
||||
|
||||
return ext.ContactHistory{
|
||||
Contact: contact,
|
||||
Interactions: interactions,
|
||||
Opportunities: opportunities,
|
||||
}, nil
|
||||
}
|
||||
// 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))
|
||||
// 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
|
||||
var model generatedmodels.ModelPublicOpportunities
|
||||
if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
return ext.Opportunity{}, fmt.Errorf("insert opportunity: %w", err)
|
||||
}
|
||||
created.ID = model.ID.UUID()
|
||||
created.CreatedAt = model.CreatedAt.Time()
|
||||
created.UpdatedAt = model.UpdatedAt.Time()
|
||||
return created, nil
|
||||
}
|
||||
// created := o
|
||||
// var model generatedmodels.ModelPublicOpportunities
|
||||
// if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
// return ext.Opportunity{}, fmt.Errorf("insert opportunity: %w", err)
|
||||
// }
|
||||
// created.ID = model.ID.UUID()
|
||||
// created.CreatedAt = model.CreatedAt.Time()
|
||||
// created.UpdatedAt = model.UpdatedAt.Time()
|
||||
// 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)
|
||||
// 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::text[], 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()
|
||||
// rows, err := db.pool.Query(ctx, `
|
||||
// select id, name, company, title, email, phone, linkedin_url, how_we_met, tags::text[], 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)
|
||||
}
|
||||
// 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 (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 model generatedmodels.ModelPublicProfessionalContacts
|
||||
var tags []string
|
||||
if err := rows.Scan(&model.ID, &model.Name, &model.Company, &model.Title, &model.Email, &model.Phone,
|
||||
&model.LinkedinURL, &model.HowWeMet, &tags, &model.Notes, &model.LastContacted, &model.FollowUpDate,
|
||||
&model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan contact: %w", err)
|
||||
}
|
||||
contacts = append(contacts, professionalContactFromModel(model, tags))
|
||||
}
|
||||
return contacts, rows.Err()
|
||||
}
|
||||
// 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 model generatedmodels.ModelPublicProfessionalContacts
|
||||
// var tags []string
|
||||
// if err := rows.Scan(&model.ID, &model.Name, &model.Company, &model.Title, &model.Email, &model.Phone,
|
||||
// &model.LinkedinURL, &model.HowWeMet, &tags, &model.Notes, &model.LastContacted, &model.FollowUpDate,
|
||||
// &model.CreatedAt, &model.UpdatedAt); err != nil {
|
||||
// return nil, fmt.Errorf("scan contact: %w", err)
|
||||
// }
|
||||
// contacts = append(contacts, professionalContactFromModel(model, tags))
|
||||
// }
|
||||
// return contacts, rows.Err()
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user