FRE-683: Add contacts & attachments management
- Implemented contact CRUD operations (list, add, edit, delete) - Implemented attachment management (list, upload, download) - Created internal/contact/manager.go for contact persistence - Created internal/attachment/manager.go for attachment storage - Added CLI commands in cmd/contacts.go and cmd/attachments.go - Integrated contact and attachment commands into root CLI Files: - internal/contact/types.go - Contact data models - internal/contact/manager.go - Contact CRUD operations - internal/attachment/manager.go - Attachment file operations - cmd/contacts.go - Contact CLI commands - cmd/attachments.go - Attachment CLI commands Contacts stored in ~/.config/pop/contacts.json Attachments stored in ~/.config/pop/attachments/
This commit is contained in:
206
internal/contact/manager.go
Normal file
206
internal/contact/manager.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package contact
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/frenocorp/pop/internal/config"
|
||||
)
|
||||
|
||||
type ContactManager struct {
|
||||
configDir string
|
||||
contactsFile string
|
||||
}
|
||||
|
||||
func NewContactManager() *ContactManager {
|
||||
cfg := config.NewConfigManager()
|
||||
configDir := cfg.ConfigDir()
|
||||
return &ContactManager{
|
||||
configDir: configDir,
|
||||
contactsFile: filepath.Join(configDir, "contacts.json"),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ContactManager) List(req ListContactsRequest) (*ListContactsResponse, error) {
|
||||
contacts, err := m.loadContacts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filtered []Contact
|
||||
for _, c := range contacts {
|
||||
if req.Search != "" {
|
||||
search := req.Search
|
||||
if !contains(c.Email, search) && !contains(c.Name, search) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if req.IsFavorite != nil && *req.IsFavorite != c.IsFavorite {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
|
||||
start := req.Page * req.PageSize
|
||||
end := start + req.PageSize
|
||||
if start > len(filtered) {
|
||||
start = len(filtered)
|
||||
}
|
||||
if end > len(filtered) {
|
||||
end = len(filtered)
|
||||
}
|
||||
|
||||
return &ListContactsResponse{
|
||||
Total: len(filtered),
|
||||
Contacts: filtered[start:end],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *ContactManager) Create(req CreateContactRequest) (*Contact, error) {
|
||||
contacts, err := m.loadContacts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("contact_%d", time.Now().UnixNano())
|
||||
contact := Contact{
|
||||
ID: id,
|
||||
Email: req.Email,
|
||||
Name: req.Name,
|
||||
Phone: req.Phone,
|
||||
Address: req.Address,
|
||||
Notes: req.Notes,
|
||||
IsFavorite: req.IsFavorite,
|
||||
CustomFields: req.CustomFields,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
contacts = append(contacts, contact)
|
||||
if err := m.saveContacts(contacts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &contact, nil
|
||||
}
|
||||
|
||||
func (m *ContactManager) Get(id string) (*Contact, error) {
|
||||
contacts, err := m.loadContacts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, c := range contacts {
|
||||
if c.ID == id {
|
||||
return &c, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("contact not found: %s", id)
|
||||
}
|
||||
|
||||
func (m *ContactManager) Update(id string, req UpdateContactRequest) (*Contact, error) {
|
||||
contacts, err := m.loadContacts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, c := range contacts {
|
||||
if c.ID == id {
|
||||
if req.Email != nil {
|
||||
c.Email = *req.Email
|
||||
}
|
||||
if req.Name != nil {
|
||||
c.Name = *req.Name
|
||||
}
|
||||
if req.Phone != nil {
|
||||
c.Phone = *req.Phone
|
||||
}
|
||||
if req.Address != nil {
|
||||
c.Address = *req.Address
|
||||
}
|
||||
if req.Notes != nil {
|
||||
c.Notes = *req.Notes
|
||||
}
|
||||
if req.IsFavorite != nil {
|
||||
c.IsFavorite = *req.IsFavorite
|
||||
}
|
||||
if req.CustomFields != nil {
|
||||
c.CustomFields = req.CustomFields
|
||||
}
|
||||
c.UpdatedAt = time.Now()
|
||||
contacts[i] = c
|
||||
|
||||
if err := m.saveContacts(contacts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("contact not found: %s", id)
|
||||
}
|
||||
|
||||
func (m *ContactManager) Delete(id string) error {
|
||||
contacts, err := m.loadContacts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, c := range contacts {
|
||||
if c.ID == id {
|
||||
contacts = append(contacts[:i], contacts[i+1:]...)
|
||||
return m.saveContacts(contacts)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("contact not found: %s", id)
|
||||
}
|
||||
|
||||
func (m *ContactManager) loadContacts() ([]Contact, error) {
|
||||
data, err := os.ReadFile(m.contactsFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []Contact{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var contacts []Contact
|
||||
if err := json.Unmarshal(data, &contacts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return contacts, nil
|
||||
}
|
||||
|
||||
func (m *ContactManager) saveContacts(contacts []Contact) error {
|
||||
if err := os.MkdirAll(m.configDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(contacts, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(m.contactsFile, data, 0600)
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && len(substr) > 0 &&
|
||||
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
|
||||
findSubstring(s, substr)))
|
||||
}
|
||||
|
||||
func findSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
54
internal/contact/types.go
Normal file
54
internal/contact/types.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package contact
|
||||
|
||||
import "time"
|
||||
|
||||
type Contact struct {
|
||||
ID string `json:"ID"`
|
||||
Email string `json:"Email"`
|
||||
Name string `json:"Name,omitempty"`
|
||||
Phone string `json:"Phone,omitempty"`
|
||||
Address string `json:"Address,omitempty"`
|
||||
Notes string `json:"Notes,omitempty"`
|
||||
IsFavorite bool `json:"IsFavorite,omitempty"`
|
||||
CustomFields map[string]string `json:"CustomFields,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type ContactGroup struct {
|
||||
ID string `json:"ID"`
|
||||
Name string `json:"Name"`
|
||||
ContactIDs []string `json:"ContactIDs,omitempty"`
|
||||
}
|
||||
|
||||
type ListContactsRequest struct {
|
||||
Page int `json:"Page"`
|
||||
PageSize int `json:"PageSize"`
|
||||
Search string `json:"Search,omitempty"`
|
||||
IsFavorite *bool `json:"IsFavorite,omitempty"`
|
||||
}
|
||||
|
||||
type ListContactsResponse struct {
|
||||
Total int `json:"Total"`
|
||||
Contacts []Contact `json:"Contacts"`
|
||||
}
|
||||
|
||||
type CreateContactRequest struct {
|
||||
Email string `json:"Email"`
|
||||
Name string `json:"Name,omitempty"`
|
||||
Phone string `json:"Phone,omitempty"`
|
||||
Address string `json:"Address,omitempty"`
|
||||
Notes string `json:"Notes,omitempty"`
|
||||
IsFavorite bool `json:"IsFavorite,omitempty"`
|
||||
CustomFields map[string]string `json:"CustomFields,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateContactRequest struct {
|
||||
Email *string `json:"Email,omitempty"`
|
||||
Name *string `json:"Name,omitempty"`
|
||||
Phone *string `json:"Phone,omitempty"`
|
||||
Address *string `json:"Address,omitempty"`
|
||||
Notes *string `json:"Notes,omitempty"`
|
||||
IsFavorite *bool `json:"IsFavorite,omitempty"`
|
||||
CustomFields map[string]string `json:"CustomFields,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user