Skip to content

feat: Add comprehensive vCard import UI with organization, avatar, and notes support #294

@brycehans

Description

@brycehans

Imported from monicahq/monica#7919
Originally filed by @cabraham2 on 2026-01-08
Status when imported: open


Summary

This PR implements a complete vCard import feature with a web UI, enabling users to import contacts from standard vCard files (.vcf) with full support for organizations, photos, notes, and all standard contact fields.

Motivation

Monica currently lacks a user-facing way to import contacts from vCard files (the standard format used by Google Contacts, iCloud, Outlook, etc.). Users migrating from other contact management systems cannot easily bring their data into Monica.

What This Adds

New Web UI

  • Import page at /vaults/{vault}/contacts/import
  • Upload vCard files and preview all contacts before import
  • Select which contacts to import (checkbox selection)
  • Progress bar with real-time percentage during import
  • Error details showing which contacts failed and why

Complete vCard Support

  • Organizations (ORG field): Links contacts to companies via company_id
  • Avatars (PHOTO field): Imports contact photos
  • Job Information (TITLE field): Preserves job titles
  • Notes (NOTE field): Imports all notes
  • Addresses: Full support with proper formatting (HOME/WORK/OTHER)
  • Contact Information: Phone, email, all types supported

Technical Implementation

  • Chunked Processing: Handles large files (1000+ contacts) without PHP timeout
    • Processes 50 contacts per batch
    • Real-time progress tracking
  • Robust Error Handling:
    • Detailed error messages for failed imports
    • Shows contact names and reasons for failure
    • Continues processing remaining contacts on error
  • Clean Architecture:
    • New DAV importers: ImportJobInformation, ImportAvatar, ImportNotes
    • Order attribute system for proper import sequence
    • Complete PHPDoc documentation

Real-World Testing

Tested with Google Contacts export (1,260 contacts, 2.7 MB):

  • 1,238 contacts imported successfully (98.3% success rate)
  • ❌ 22 failures = invalid vCards in source file (no names/empty entries)
  • ⚡ No timeouts with chunked processing
  • 🔄 Full progress feedback to users

Technical Documentation

See docs/VCARD_IMPORT_ENHANCEMENTS.md for:

  • Complete architecture overview
  • Import flow sequence diagrams
  • Order attribute system explanation
  • Error handling strategy

Files Added

Controllers & Services:

  • ContactImportController.php - Web endpoint for import
  • ParseVCardFile.php - vCard parsing service
  • ContactImportViewHelper.php - Data preparation for UI

DAV Importers:

  • ImportJobInformation.php - Organization/job import
  • ImportAvatar.php - Photo import
  • ImportNotes.php - Notes import

Frontend:

  • resources/js/Pages/Vault/Contact/Import/Index.vue - Import UI

Tests:

  • Unit tests for ParseVCardFile
  • Feature tests for ContactImportController

Breaking Changes

None. This is a new feature that doesn't affect existing functionality.

Checklist

  • Complete feature tested with 1,260 real contacts
  • All code has PHPDoc documentation
  • Unit and feature tests included
  • Follows Monica's architecture patterns
  • No breaking changes
  • Technical documentation provided

🏗️ Architecture - Diagramme de flux

┌─────────────────────────────────────────────────────────────────┐
│                    Import vCard (.vcf file)                     │
│                     ContactImportController                     │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│              Sabre\VObject\Reader::read($vcard)                 │
│                  Parse vCard format (RFC 6350)                  │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                    DAV Import Pipeline                          │
│              Multiple Importers (Order attribute)               │
└────────────────────────────┬────────────────────────────────────┘
                             │
         ┌───────────────────┼───────────────────┐
         │                   │                   │
         ▼                   ▼                   ▼
┌──────────────────┐  ┌─────────────┐  ┌────────────────────┐
│  ImportAvatar    │  │ ImportNotes │  │ ImportJobInform..  │
│  Order(20)       │  │ Order(30)   │  │ Order(41) - NEW    │
│                  │  │             │  │                    │
│ PHOTO field →    │  │ NOTE field  │  │ ORG field →        │
│ storage/photos   │  │ → notes tbl │  │ Company + Link     │
└──────────────────┘  └─────────────┘  └────────────────────┘
         │                   │                   │
         │                   ▼                   ▼
         │          ┌──────────────┐   ┌──────────────────┐
         │          │ CreateNote() │   │ CreateCompany()  │
         │          └──────────────┘   │ or find existing │
         │                             │                  │
         │                             │ UpdateJobInfo()  │
         │                             │ → contacts.      │
         │                             │   company_id     │
         │                             └──────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────────────┐
│                        ImportAddress                            │
│                         Order(40)                               │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
         ┌───────────────────┴───────────────────┐
         │                                       │
         ▼                                       ▼
┌─────────────────────┐              ┌─────────────────────────┐
│  getAddressType()   │              │ formatStreetAddress()   │
│                     │              │ - NEW FUNCTION          │
│ HOME,pref → home    │              │                         │
│ Create if missing   │              │ "61\nRue" → "61, Rue"   │
│                     │              │ "61Rue" → "61, Rue"     │
└─────────────────────┘              └─────────────────────────┘
         │                                       │
         └───────────────────┬───────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                  vCard ADR field mapping                        │
│                                                                 │
│  parts[0]: PO Box (unused)                                      │
│  parts[1]: Extended (apartment/suite) → line_2                  │
│  parts[2]: Street → line_1 (via formatStreetAddress)            │
│  parts[3]: City                                                 │
│  parts[4]: Province                                             │
│  parts[5]: Postal Code                                          │
│  parts[6]: Country                                              │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│            UpdateAddress() or CreateAddress()                   │
│                 + AssociateAddressToContact()                   │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                    Contact Feed Display                         │
│                  ActionFeedAddress helper                       │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
         ┌───────────────────┴───────────────────┐
         │                                       │
         ▼                                       ▼
┌─────────────────────┐              ┌─────────────────────────┐
│  Map Image (opt.)   │              │  Address Display        │
│                     │              │                         │
│ MapHelper::         │              │ line_1: "61, Rue..."    │
│ getStaticImage()    │              │ line_2: "Apt 2"         │
│                     │              │ city, province, etc.    │
│ If Mapbox config:   │              │                         │
│ → Generate URL      │              │ Type: 🏡 Home           │
│                     │              │                         │
│ If not configured:  │              │ View on map (OSM link)  │
│ → Return null       │              │                         │
│ → No broken image   │              └─────────────────────────┘
└─────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────────────┐
│      ContactModuleAddressImageController::show()                │
│                                                                 │
│   If MapHelper returns null → abort(404)                        │
│   Else → Proxy Mapbox API request → Stream image                │
└─────────────────────────────────────────────────────────────────┘

Made with ❤️ for Monica CRM

Metadata

Metadata

Assignees

No one assigned

    Labels

    from-upstream-prOriginally a pull request in monicahq/monicaimportedImported in bulk from monicahq/monicascope:unknownCould not classify automatically; needs triagetriage:doneReviewed; status reflected in other labelstriage:wontfixReviewed and out of scope for v4 maintenance

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions