Contact Book Manager

 import json

import csv

import os

import re

from datetime import datetime

from pathlib import Path


# ============================================================

# CONFIGURATION

# ============================================================


DATA_FILE   = "contacts.json"

BACKUP_FILE = "contacts_backup.json"


# ============================================================

# LOAD & SAVE

# ============================================================


def load_contacts():

    if Path(DATA_FILE).exists():

        try:

            with open(DATA_FILE, "r", encoding="utf-8") as f:

                return json.load(f)

        except:

            pass

    return {"contacts": [], "groups": []}



def save_contacts(data):

    with open(DATA_FILE, "w", encoding="utf-8") as f:

        json.dump(data, f, indent=2, ensure_ascii=False)



def backup_contacts(data):

    with open(BACKUP_FILE, "w", encoding="utf-8") as f:

        json.dump(data, f, indent=2, ensure_ascii=False)

    print(f"  Backup saved: {BACKUP_FILE}")



# ============================================================

# VALIDATION

# ============================================================


def validate_phone(phone):

    """Accept digits, spaces, +, -, (, )"""

    return bool(re.match(r"^[\d\s\+\-\(\)]{7,20}$", phone.strip()))



def validate_email(email):

    return bool(re.match(r"^[\w\.\+\-]+@[\w\-]+\.[a-zA-Z]{2,}$", email.strip()))



# ============================================================

# ADD CONTACT

# ============================================================


def add_contact(data):

    print("\n" + "="*50)

    print("  ADD NEW CONTACT")

    print("="*50)


    # Required

    name = input("  Full Name*    : ").strip()

    if not name:

        print("  Name is required.")

        return


    # Check duplicate

    if any(c["name"].lower() == name.lower() for c in data["contacts"]):

        overwrite = input(f"  '{name}' already exists. Add anyway? (y/n): ").strip().lower()

        if overwrite != "y":

            return


    # Optional fields

    phone = input("  Phone Number  : ").strip()

    if phone and not validate_phone(phone):

        print("  Invalid phone format. Saving as-is.")


    email = input("  Email         : ").strip()

    if email and not validate_email(email):

        print("  Invalid email format. Saving as-is.")


    address   = input("  Address       : ").strip()

    birthday  = input("  Birthday (DD-MM-YYYY): ").strip()

    company   = input("  Company       : ").strip()

    notes     = input("  Notes         : ").strip()


    # Groups

    if data["groups"]:

        print(f"\n  Available groups: {', '.join(data['groups'])}")

    group = input("  Group (or new group name): ").strip()

    if group and group not in data["groups"]:

        data["groups"].append(group)


    contact = {

        "id":         len(data["contacts"]) + 1,

        "name":       name,

        "phone":      phone,

        "email":      email,

        "address":    address,

        "birthday":   birthday,

        "company":    company,

        "notes":      notes,

        "group":      group,

        "favourite":  False,

        "created_at": datetime.now().strftime("%d-%m-%Y %H:%M"),

        "updated_at": datetime.now().strftime("%d-%m-%Y %H:%M"),

    }


    data["contacts"].append(contact)

    save_contacts(data)

    print(f"\n  Contact saved: {name}")



# ============================================================

# VIEW CONTACT

# ============================================================


def view_contact(contact):

    w = 50

    fav = " ★" if contact.get("favourite") else ""

    print("\n" + "="*w)

    print(f"  {contact['name']}{fav}")

    print("="*w)


    fields = [

        ("Phone",    contact.get("phone",    "")),

        ("Email",    contact.get("email",    "")),

        ("Company",  contact.get("company",  "")),

        ("Address",  contact.get("address",  "")),

        ("Birthday", contact.get("birthday", "")),

        ("Group",    contact.get("group",    "")),

        ("Notes",    contact.get("notes",    "")),

        ("Added",    contact.get("created_at", "")),

        ("Updated",  contact.get("updated_at", "")),

    ]

    for label, value in fields:

        if value:

            print(f"  {label:<10}: {value}")


    print("="*w)



# ============================================================

# LIST CONTACTS

# ============================================================


def list_contacts(data, group_filter=None, favourites_only=False,

                  sort_by="name"):

    contacts = data["contacts"]


    if group_filter:

        contacts = [c for c in contacts

                    if c.get("group", "").lower() == group_filter.lower()]

    if favourites_only:

        contacts = [c for c in contacts if c.get("favourite")]


    if not contacts:

        print("\n  No contacts found.")

        return []


    # Sort

    if sort_by == "name":

        contacts = sorted(contacts, key=lambda c: c["name"].lower())

    elif sort_by == "company":

        contacts = sorted(contacts, key=lambda c: c.get("company", "").lower())

    elif sort_by == "group":

        contacts = sorted(contacts, key=lambda c: c.get("group", "").lower())

    elif sort_by == "recent":

        contacts = sorted(contacts, key=lambda c: c.get("updated_at", ""),

                          reverse=True)


    label = ""

    if group_filter:

        label = f"  Group: {group_filter}"

    elif favourites_only:

        label = "  Favourites"


    print("\n" + "="*65)

    print(f"  CONTACTS  ({len(contacts)} total){label}")

    print("="*65)

    print(f"  {'#':<5} {'NAME':<22} {'PHONE':<16} "

          f"{'EMAIL':<22} {'GRP'}")

    print("  " + "-"*60)


    for i, c in enumerate(contacts, 1):

        fav   = "★" if c.get("favourite") else " "

        name  = c["name"][:20]

        phone = c.get("phone", "")[:14]

        email = c.get("email", "")[:20]

        group = c.get("group", "")[:8]

        print(f"  {fav}{i:<4} {name:<22} {phone:<16} "

              f"{email:<22} {group}")


    print("="*65)

    return contacts



# ============================================================

# SEARCH CONTACTS

# ============================================================


def search_contacts(data, query):

    query = query.lower().strip()

    results = []


    for c in data["contacts"]:

        if (query in c["name"].lower()

                or query in c.get("phone", "").lower()

                or query in c.get("email", "").lower()

                or query in c.get("company", "").lower()

                or query in c.get("address", "").lower()

                or query in c.get("notes", "").lower()):

            results.append(c)


    if not results:

        print(f"\n  No contacts found for: '{query}'")

        return []


    print(f"\n  Found {len(results)} result(s) for '{query}':")

    for i, c in enumerate(results, 1):

        fav   = "★" if c.get("favourite") else " "

        phone = c.get("phone", "N/A")

        email = c.get("email", "")

        print(f"  {fav}[{i}] {c['name']}  |  {phone}  |  {email}")


    return results



# ============================================================

# EDIT CONTACT

# ============================================================


def edit_contact(data, contact):

    print(f"\n  Editing: {contact['name']}")

    print("  (Press Enter to keep current value)\n")


    fields = [

        ("name",     "Full Name",    contact.get("name", "")),

        ("phone",    "Phone",        contact.get("phone", "")),

        ("email",    "Email",        contact.get("email", "")),

        ("company",  "Company",      contact.get("company", "")),

        ("address",  "Address",      contact.get("address", "")),

        ("birthday", "Birthday",     contact.get("birthday", "")),

        ("notes",    "Notes",        contact.get("notes", "")),

        ("group",    "Group",        contact.get("group", "")),

    ]


    for key, label, current in fields:

        new_val = input(f"  {label:<12} [{current}]: ").strip()

        if new_val:

            contact[key] = new_val


    # Favourite toggle

    fav = input(f"  Favourite? (y/n) [{'+' if contact.get('favourite') else '-'}]: ").strip().lower()

    if fav == "y":

        contact["favourite"] = True

    elif fav == "n":

        contact["favourite"] = False


    contact["updated_at"] = datetime.now().strftime("%d-%m-%Y %H:%M")


    # Update in data

    for i, c in enumerate(data["contacts"]):

        if c["id"] == contact["id"]:

            data["contacts"][i] = contact

            break


    # Add new group if needed

    if contact["group"] and contact["group"] not in data["groups"]:

        data["groups"].append(contact["group"])


    save_contacts(data)

    print(f"\n  Contact updated: {contact['name']}")



# ============================================================

# DELETE CONTACT

# ============================================================


def delete_contact(data, contact):

    confirm = input(f"\n  Delete '{contact['name']}'? (yes/no): ").strip().lower()

    if confirm == "yes":

        data["contacts"] = [c for c in data["contacts"]

                            if c["id"] != contact["id"]]

        save_contacts(data)

        print(f"  Deleted: {contact['name']}")

    else:

        print("  Cancelled.")



# ============================================================

# BIRTHDAY REMINDERS

# ============================================================


def birthday_reminders(data):

    today     = datetime.today()

    upcoming  = []


    for c in data["contacts"]:

        bday = c.get("birthday", "")

        if not bday:

            continue

        try:

            bday_dt = datetime.strptime(bday, "%d-%m-%Y")

            # This year's birthday

            this_year = bday_dt.replace(year=today.year)

            if this_year < today:

                this_year = bday_dt.replace(year=today.year + 1)

            days_left = (this_year - today).days

            age = today.year - bday_dt.year

            if this_year.year > today.year:

                age -= 1

            upcoming.append((days_left, c["name"], bday, age + 1))

        except:

            continue


    upcoming.sort(key=lambda x: x[0])


    print("\n" + "="*52)

    print("  UPCOMING BIRTHDAYS")

    print("="*52)


    if not upcoming:

        print("  No birthdays found. Add birthdays when creating contacts.")

        return


    for days, name, bday, age in upcoming[:15]:

        if days == 0:

            when = "TODAY!"

        elif days == 1:

            when = "Tomorrow"

        elif days <= 7:

            when = f"In {days} days"

        elif days <= 30:

            when = f"In {days} days"

        else:

            when = f"In {days} days"


        print(f"  {when:<15} {name:<25} {bday}  (turns {age})")


    print("="*52)



# ============================================================

# EXPORT TO CSV

# ============================================================


def export_csv(data, filename=None):

    contacts = data["contacts"]

    if not contacts:

        print("  No contacts to export.")

        return


    if not filename:

        filename = f"contacts_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"


    fields = ["name", "phone", "email", "company",

              "address", "birthday", "group", "notes"]


    with open(filename, "w", newline="", encoding="utf-8") as f:

        writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")

        writer.writeheader()

        for c in contacts:

            writer.writerow({k: c.get(k, "") for k in fields})


    print(f"\n  Exported {len(contacts)} contact(s) to: {filename}")



# ============================================================

# IMPORT FROM CSV

# ============================================================


def import_csv(data, filename):

    if not Path(filename).exists():

        print(f"  File not found: {filename}")

        return


    imported = 0

    skipped  = 0


    with open(filename, "r", encoding="utf-8", newline="") as f:

        reader = csv.DictReader(f)

        for row in reader:

            name = row.get("name", "").strip()

            if not name:

                skipped += 1

                continue


            # Skip if already exists

            if any(c["name"].lower() == name.lower()

                   for c in data["contacts"]):

                skipped += 1

                continue


            contact = {

                "id":         len(data["contacts"]) + 1,

                "name":       name,

                "phone":      row.get("phone", ""),

                "email":      row.get("email", ""),

                "address":    row.get("address", ""),

                "birthday":   row.get("birthday", ""),

                "company":    row.get("company", ""),

                "notes":      row.get("notes", ""),

                "group":      row.get("group", ""),

                "favourite":  False,

                "created_at": datetime.now().strftime("%d-%m-%Y %H:%M"),

                "updated_at": datetime.now().strftime("%d-%m-%Y %H:%M"),

            }

            data["contacts"].append(contact)


            # Add group

            if contact["group"] and contact["group"] not in data["groups"]:

                data["groups"].append(contact["group"])


            imported += 1


    save_contacts(data)

    print(f"\n  Imported: {imported}  |  Skipped (duplicate/empty): {skipped}")



# ============================================================

# GROUPS

# ============================================================


def manage_groups(data):

    print("\n" + "="*45)

    print("  GROUPS")

    print("="*45)


    if not data["groups"]:

        print("  No groups yet.")

    else:

        for i, g in enumerate(data["groups"], 1):

            count = sum(1 for c in data["contacts"]

                        if c.get("group") == g)

            print(f"  [{i}] {g}  ({count} contacts)")


    print("\n  1. View contacts in a group")

    print("  2. Rename a group")

    print("  3. Delete a group")

    print("  4. Back")


    sub = input("\n  > ").strip()


    if sub == "1":

        group = input("  Group name: ").strip()

        list_contacts(data, group_filter=group)


    elif sub == "2":

        old = input("  Current group name: ").strip()

        new = input("  New group name    : ").strip()

        if old and new:

            for c in data["contacts"]:

                if c.get("group") == old:

                    c["group"] = new

            if old in data["groups"]:

                data["groups"].remove(old)

            if new not in data["groups"]:

                data["groups"].append(new)

            save_contacts(data)

            print(f"  Renamed '{old}' to '{new}'")


    elif sub == "3":

        group = input("  Group to delete: ").strip()

        confirm = input(f"  Delete group '{group}'? Contacts will be ungrouped. (yes/no): ").strip().lower()

        if confirm == "yes":

            for c in data["contacts"]:

                if c.get("group") == group:

                    c["group"] = ""

            if group in data["groups"]:

                data["groups"].remove(group)

            save_contacts(data)

            print(f"  Group deleted: {group}")



# ============================================================

# STATS

# ============================================================


def show_stats(data):

    contacts = data["contacts"]

    total    = len(contacts)


    if not total:

        print("\n  No contacts yet.")

        return


    with_phone  = sum(1 for c in contacts if c.get("phone"))

    with_email  = sum(1 for c in contacts if c.get("email"))

    with_bday   = sum(1 for c in contacts if c.get("birthday"))

    favourites  = sum(1 for c in contacts if c.get("favourite"))


    group_counts = {}

    for c in contacts:

        g = c.get("group") or "Ungrouped"

        group_counts[g] = group_counts.get(g, 0) + 1


    print("\n" + "="*48)

    print("  CONTACT BOOK STATISTICS")

    print("="*48)

    print(f"  Total contacts  : {total}")

    print(f"  With phone      : {with_phone}")

    print(f"  With email      : {with_email}")

    print(f"  With birthday   : {with_bday}")

    print(f"  Favourites      : {favourites}")

    print(f"\n  By group:")

    for g, count in sorted(group_counts.items(), key=lambda x: -x[1]):

        bar = "█" * min(count, 20)

        print(f"  {g:<20} {count:>4}  {bar}")

    print("="*48)



# ============================================================

# SELECT CONTACT HELPER

# ============================================================


def select_contact(data, prompt="  Search contact: "):

    query = input(prompt).strip()

    if not query:

        return None

    results = search_contacts(data, query)

    if not results:

        return None

    if len(results) == 1:

        return results[0]

    try:

        idx = int(input("  Select number: ").strip()) - 1

        if 0 <= idx < len(results):

            return results[idx]

    except ValueError:

        pass

    return None



# ============================================================

# MAIN MENU

# ============================================================


def print_menu(data):

    total = len(data["contacts"])

    favs  = sum(1 for c in data["contacts"] if c.get("favourite"))

    print("\n" + "-"*50)

    print(f"  CONTACT BOOK  [{total} contacts  ★{favs} favourites]")

    print("-"*50)

    print("  1.  Add new contact")

    print("  2.  View all contacts")

    print("  3.  Search contact")

    print("  4.  View contact details")

    print("  5.  Edit contact")

    print("  6.  Delete contact")

    print("  7.  View favourites")

    print("  8.  Birthday reminders")

    print("  9.  Manage groups")

    print("  10. Export to CSV")

    print("  11. Import from CSV")

    print("  12. Statistics")

    print("  13. Backup contacts")

    print("  0.  Exit")

    print("-"*50)



def main():

    print("\n" + "="*55)

    print("     CONTACT BOOK MANAGER")

    print("="*55)


    data = load_contacts()

    if data["contacts"]:

        print(f"\n  Loaded {len(data['contacts'])} contact(s) from {DATA_FILE}")

    else:

        print("\n  No contacts yet. Start by adding one!")


    while True:

        print_menu(data)

        choice = input("  > ").strip()


        if choice == "1":

            add_contact(data)


        elif choice == "2":

            print("\n  Sort by: 1=Name  2=Company  3=Group  4=Recent")

            sort_opt = input("  Sort (default Name): ").strip()

            sort_map = {"1": "name", "2": "company",

                        "3": "group", "4": "recent"}

            sort_by  = sort_map.get(sort_opt, "name")

            list_contacts(data, sort_by=sort_by)


        elif choice == "3":

            query = input("\n  Search: ").strip()

            search_contacts(data, query)


        elif choice == "4":

            contact = select_contact(data)

            if contact:

                view_contact(contact)


        elif choice == "5":

            contact = select_contact(data)

            if contact:

                edit_contact(data, contact)


        elif choice == "6":

            contact = select_contact(data)

            if contact:

                view_contact(contact)

                delete_contact(data, contact)


        elif choice == "7":

            list_contacts(data, favourites_only=True)


        elif choice == "8":

            birthday_reminders(data)


        elif choice == "9":

            manage_groups(data)


        elif choice == "10":

            fname = input("  Output filename (Enter=auto): ").strip() or None

            export_csv(data, fname)


        elif choice == "11":

            fname = input("  CSV file path: ").strip()

            import_csv(data, fname)


        elif choice == "12":

            show_stats(data)


        elif choice == "13":

            backup_contacts(data)


        elif choice == "0":

            print("\n  Goodbye!\n")

            break


        else:

            print("  Invalid choice.")



# ============================================================

# RUN

# ============================================================


if __name__ == "__main__":

    main()

No comments: