Contacts

Organise people and organisations. Makes use of dynamic types.

screenshot

contacts

#!/usr/bin/pinafore
import "pinafore-gnome", "UILib" in
with GTK., UILib. in
run.Context $ fn context =>
let

    Mk.UI.Named = mk.UI.Named context;

    # something with a lifespan
    opentype Lifespanned;
    # birth and death dates
    birthDateOf = property @Lifespanned @Date !"lifespan.start.date" store;
    deathDateOf = property @Lifespanned @Date !"lifespan.end.date" store;

    # physical locations, that have addresses
    opentype Location;

    namespace Location of
        addressOf = property @Location @Text !"location.address.withincity" store;
        phoneOf = property @Location @Text !"location.telephone" store;
        cityOf = property @Location @Text !"location.address.city" store;
        postcodeOf = property @Location @Text !"location.address.postcode" store;
        provinceOf = property @Location @Text !"location.address.province" store;
        countryOf = property @Location @Text !"location.address.country" store;
    end;

    locationUI: WholeModel +Location -> Widget =
    fn loc =>
    vertical.Widget
    [
        layoutGrow.Widget $ labelled.Widget {"Address: "} $ textArea.Widget (fromWhole.TextModel $ addressOf.Location !$% loc),
        labelled.Widget {"City: "} $ textEntry.Widget $ cityOf.Location !$% loc,
        labelled.Widget {"Postcode/ZIP: "} $ textEntry.Widget $ postcodeOf.Location !$% loc,
        labelled.Widget {"State/Province: "} $ textEntry.Widget $ provinceOf.Location !$% loc,
        labelled.Widget {"Country: "} $ textEntry.Widget $ countryOf.Location !$% loc,
        labelled.Widget {"Phone: "} $ textEntry.Widget $ phoneOf.Location !$% loc
    ];

    let
        # a person or organisation
        dynamictype Contact;
        subtype Contact <: Named;
        subtype Contact <: Lifespanned;

        emailOf = property @Contact @Text !"contact.email" store;
        phoneOf = property @Contact @Text !"contact.telephone" store;
        homeOf = property @Contact @Location !"being.location" store;

        contactSet: FiniteSetModel Contact =
        property @Contact @Unit !"type.legalentity" store !@ {()};

        # a person
        dynamictype Person = !"type.person";
        subtype Person <: Contact;

        peopleSet: FiniteSetModel Person =
        maybeMap.FiniteSetModel (check @Person) contactSet;

        # family relationships between people
        motherOf = property @Person @Person !"relation.family.mother" store;
        fatherOf = property @Person @Person !"relation.family.father" store;
        childrenSet: WholeModel +Person -> FiniteSetModel Person =
        fn p => (motherOf !@% p) <:|:> (fatherOf !@% p);
        marriedOf = property @Person @Boolean !"relation.family.spouse.exist" store;

        # employment relationship
        employerOf = property @Person @Contact !"relation.employer" store;
        employeeSet: WholeModel +Contact -> FiniteSetModel Person =
        fn p => employerOf !@% p;

        # an organisation
        dynamictype Organisation = !"type.organisation";
        subtype Organisation <: Contact;
        organisationSet: FiniteSetModel Organisation =
        property @Organisation @Unit !"type.organisation" store !@ {()};
    end;

    ### User Interface

    let rec

        contactPages: WholeModel +Contact -> List (Widget *: Widget) =
        fn er =>
        [
            page.Widget {"Social"} $ vertical.Widget
            [
                labelled.Widget {"Email: "} $ textEntry.Widget $ emailOf !$% er,
                labelled.Widget {"Phone: "} $ textEntry.Widget $ phoneOf !$% er
            ],
            page.Widget {"Home"} $ locationUI $ homeOf !$% er
        ];

        personPages: WholeModel +Person -> List (Widget *: Widget) =
        fn p =>
        [
            page.Widget {"Work"} $ vertical.Widget
            [
                labelled.Widget {"Employer: "} $ pick.Widget (byName contactSet) $ employerOf !$% p
            ],
            page.Widget {"Family"} $ familyUI p
        ];

        familyUI: WholeModel +Person -> Widget =
        fn p =>
        vertical.Widget
        [
            labelled.Widget {"Birth: "} $ textEntry.Widget $ reverse.Prism asText.Date !$ birthDateOf !$% p,
            labelled.Widget {"Death: "} $ textEntry.Widget $ reverse.Prism asText.Date !$ deathDateOf !$% p,
            checkBox.Widget {"Married"} $ marriedOf !$% p,
            labelled.Widget {"Mother: "} $ pick.Widget (byName peopleSet) $ motherOf !$% p,
            labelled.Widget {"Father: "} $ pick.Widget (byName peopleSet) $ fatherOf !$% p,
            label.Widget {"Children:"},
            layoutGrow.Widget $ toWidget.SetWidget $ namedTable (childrenSet p) contactUI
        ];

        organisationPages: WholeModel +Organisation -> List (Widget *: Widget) =
        fn subj =>
        [
            page.Widget {"Employees"} $ toWidget.SetWidget $ namedTable (employeeSet subj) contactUI
        ];

        contactUI: Contact -> Widget =
        fn c =>
        let
            pages: List (Widget *: Widget) =
            contactPages {c} <>.List (c >- match
                p:? Person => personPages {p};
                o:? Organisation => organisationPages {o};
                end);
        in
        vertical.Widget
        [
            labelled.Widget {"Name: "} $ textEntry.Widget $ nameOf !$% {c},
            layoutGrow.Widget $ notebook.Widget unknown pages
        ];
    end;

    contactsPresentation: SetPresentation Contact =
    Mk.SetPresentation of
        setName = "Contacts";
        itemSet = contactSet;
        newItems =
        [
            Mk.NewItem of name = "New Person"; mkey = Just "Ctrl+K"; newItem = new.DynamicEntity @Person end,
            Mk.NewItem of name = "New Organisation"; newItem = new.DynamicEntity @Organisation end
        ];
        itemPane = namedPane contactUI;
        setWidget = namedTable contactSet contactUI;
    end;

    contactsWindow: Action Window =
    do
        p <- presentSetWithItem contactsPresentation;
        paneWindow p;
    end;

in contactsWindow