package main import ( "database/sql" "flag" "fmt" "log" "os" "time" _ "github.com/lib/pq" ) // Database connection details const ( host = "postgres.banded-neon.ts.net" user = "postgresql" dbname = "journal" ) const ( JOURNAL_TYPE_MOD = iota + 1 JOURNAL_TYPE_PERSONAL JOURNAL_TYPE_JOBBY JOURNAL_TYPE_SLEEP ) var db *sql.DB func init() { var err error // Connection string psqlInfo := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable", host, user, dbname) db, err = sql.Open("postgres", psqlInfo) if err != nil { log.Fatal(err) } if err = db.Ping(); err != nil { log.Fatal(err) } } // addMODMeeting adds a MOD meeting to the database func addMODMeeting(name string, date string, subject string) { var meetingId int query := fmt.Sprintf("INSERT INTO meetings (name, date, subject) VALUES ('%s', '%s', '%s') RETURNING id", name, date, subject) err := db.QueryRow(query).Scan(&meetingId) if err != nil { log.Fatal(err) } fmt.Printf("MOD meeting added with ID: %d\n", meetingId) } func addMODMeetingJournalEntry(entry string, meetingID int) { _, err := db.Exec(fmt.Sprintf("INSERT INTO journal_entries (entry, type, meeting_id) VALUES ('%s', %d, %d)", entry, JOURNAL_TYPE_MOD, meetingID)) if err != nil { log.Fatal(err) } } // addMODContact adds a MOD contact to the database func addMODContact(firstName, lastName string) { var contactId int query := `INSERT INTO contacts (first_name, last_name, contact_type) VALUES ($1, $2, 3) RETURNING id` err := db.QueryRow(query, firstName, lastName).Scan(&contactId) if err != nil { log.Fatal(err) } fmt.Printf("MOD contact added with ID: %d\n", contactId) } // addSleepEntry adds a sleep entry to the database func addSleepEntry(entry string, journalType int) { _, err := db.Exec("INSERT INTO journal_entries (entry, type) VALUES ('%s', %d)", entry, journalType) if err != nil { log.Fatal(err) } } // addJournalEntry adds a journal entry to the database func addJournalEntry(entry string, journalType int) { _, err := db.Exec(fmt.Sprintf("INSERT INTO journal_entries (entry, type) VALUES ('%s', %d)", entry, journalType)) if err != nil { log.Fatal(err) } } // List meetings func listMeetings() { rows, err := db.Query("SELECT id, name, date, subject FROM meetings ORDER BY date ASC") if err != nil { log.Fatal(err) } defer rows.Close() fmt.Println("Meetings:") for rows.Next() { var id int var date time.Time var name, subject string if err := rows.Scan(&id, &name, &date, &subject); err != nil { log.Fatal(err) } fmt.Printf("ID: %d\t %s\t %s (%s)\n", id, date.Format("2006-01-02"), name, subject) } } func listEntriesForMeeting(meetingID int) { // this should be a join which includes the meeting name rows, err := db.Query(fmt.Sprintf("select journal_entries.id, journal_entries.date_added, journal_entries.entry, meetings.name from journal_entries inner join meetings on journal_entries.meeting_id = meetings.id where journal_entries.meeting_id = %d order by journal_entries.date_added asc, journal_entries.id asc", meetingID)) if err != nil { log.Fatal(err) } defer rows.Close() fmt.Println(fmt.Sprintf("Entries for meeting ID: %d", meetingID)) for rows.Next() { var id int var date time.Time var entry string var meetingName string if err := rows.Scan(&id, &date, &entry, &meetingName); err != nil { log.Fatal(err) } fmt.Printf("%d\t %s\t %s\t %s\n", id, meetingName, date.Format("2006-01-02 - 15:04:05"), entry) } } // List journal entries, dependent on journalType func listjournalEntries(orientation string, journalType int) { var rows *sql.Rows var id sql.NullInt64 var dateAdded sql.NullTime var entry sql.NullString var header string var err error // if MOD journalType if journalType == JOURNAL_TYPE_MOD { rows, err = db.Query(fmt.Sprintf("SELECT id, date_added, entry FROM journal_entries WHERE type = %d AND meeting_id IS NULL ORDER BY date_added ASC, id ASC", journalType)) header = "MOD entries:" } else if journalType == JOURNAL_TYPE_PERSONAL { rows, err = db.Query(fmt.Sprintf("SELECT id, date_added, entry FROM journal_entries WHERE type = %d ORDER BY date_added ASC, id ASC", journalType)) header = "Personal entries:" } else if journalType == JOURNAL_TYPE_JOBBY { rows, err = db.Query(fmt.Sprintf("SELECT id, date_added, entry FROM journal_entries WHERE type = %d ORDER BY date_added ASC, id ASC", journalType)) header = "Jobblylog entries:" } else if journalType == JOURNAL_TYPE_SLEEP { rows, err = db.Query(fmt.Sprintf("SELECT id, date_added, entry FROM journal_entries WHERE type = %d ORDER BY date_added ASC, id ASC", journalType)) header = "Sleep entries:" } if err != nil { log.Fatal(err) } defer rows.Close() fmt.Println(header) var previousDateAdded sql.NullTime // Track the previous date only previousDateAdded.Time = time.Time{} // Initialize to zero value // Scan for each row for rows.Next() { if err := rows.Scan(&id, &dateAdded, &entry); err != nil { log.Fatal(err) } // Check if previousDateAdded is valid to avoid printing dash before first entry if previousDateAdded.Valid && dateAdded.Valid && !sameDay(previousDateAdded.Time, dateAdded.Time) { fmt.Println("----") // Print a dashed line between different day entries } if orientation == "vertical" { formatVertically(id, dateAdded, entry) } else if orientation == "horizontal" { formatHorizontally(dateAdded, entry) } previousDateAdded = dateAdded // Update previousDateAdded to current date } } // Function to determine if two times are on the same day func sameDay(t1, t2 time.Time) bool { return t1.Year() == t2.Year() && t1.YearDay() == t2.YearDay() } func formatVertically(id sql.NullInt64, dateAdded sql.NullTime, entry sql.NullString) { fmt.Printf("ID: %s\n", formatInt64(id)) fmt.Printf("Date Added: %s\n", formatTime(dateAdded)) fmt.Printf("Entry: %s\n", formatString(entry)) } func formatHorizontally(dateAdded sql.NullTime, entry sql.NullString) { fmt.Printf("%s %s | %s\n", formatShortTime(dateAdded), getWeekdayFromDate(dateAdded), formatString(entry)) } func getWeekdayFromDate(date sql.NullTime) string { return date.Time.Format("Monday") } func formatShortTime(t sql.NullTime) string { if t.Valid { return t.Time.Format("2006-01-02 15:04") } return "NULL" } // Helper functions to handle sql.Null* types func formatInt64(v sql.NullInt64) string { if v.Valid { return fmt.Sprintf("%d", v.Int64) } return "NULL" } func formatTime(v sql.NullTime) string { if v.Valid { return v.Time.Format(time.RFC3339) } return "NULL" } func formatString(v sql.NullString) string { if v.Valid { return v.String } return "NULL" } func main() { // Parse flags flag.Parse() // TODO: Break these up into subcommands: // - add // - list // - update // - delete // ref: https://www.golinuxcloud.com/golang-flags-examples/ // add command addCmd := flag.NewFlagSet("add", flag.ExitOnError) // add flags addPersonalEntryFlag := addCmd.Bool("personal", false, "Add a Personal entry") addMODEntryFlag := addCmd.Bool("mod", false, "Add a MOD entry") addMODMeetingFlag := addCmd.Bool("meeting", false, "Add a MOD meeting") addMODMeetingNoteFlag := addCmd.Bool("meeting-note", false, "Add a note to a MOD meeting") addJobbylogEntryFlag := addCmd.Bool("jobby", false, "Add a Jobbylog entry") addSleepEntryFlag := addCmd.Bool("sleep", false, "Add a sleep entry") entry := addCmd.String("entry", "", "Content of the journal entry") meetingID := addCmd.Int("meeting-id", 1, "ID of the meeting to add a note to") dateOfEntry := addCmd.String("date", "", "Date of entry") // list command listCmd := flag.NewFlagSet("list", flag.ExitOnError) listPersonalEntriesFlag := listCmd.Bool("personal", false, "List all personal entries") listMODEntriesFlag := listCmd.Bool("mod", false, "List all MOD entries, not including those associated with a meeting") listMODMeetingsFlag := listCmd.Bool("meetings", false, "List all MOD meetings") meetingID = listCmd.Int("meeting-id", 1, "ID of the meeting to add a note to") listEntriesForMODMeetingFlag := listCmd.Bool("meeting-entries", false, "List all entries for a MOD meeting") listJobbylogEntriesFlag := listCmd.Bool("jobby", false, "List all Jobblylog entries") listSleepEntriesFlag := listCmd.Bool("sleep", false, "List all sleep entries") orientationFlag := listCmd.String("orientation", "horizontal", "Orientation of the journal entries (vertical or horizontal)") if len(os.Args) < 2 { fmt.Println("expected 'add' or 'list' subcommands") os.Exit(1) } switch os.Args[1] { case "add": addCmd.Parse(os.Args[2:]) if *addPersonalEntryFlag { if *entry == "" { log.Fatal("Personal entry content must be provided - use the -entry flag") } addJournalEntry(*entry, JOURNAL_TYPE_PERSONAL) } else if *addSleepEntryFlag { if *entry == "" { log.Fatal("Sleep entry content must be provided - use the -entry flag") } addJournalEntry(*entry, JOURNAL_TYPE_SLEEP) } else if *addJobbylogEntryFlag { if *entry == "" { log.Fatal("Jobbylog entry content must be provided - use the -entry flag") } addJournalEntry(*entry, JOURNAL_TYPE_JOBBY) } else if *addMODEntryFlag { if *entry == "" { log.Fatal("MOD entry content must be provided - use the -entry flag") } addJournalEntry(*entry, JOURNAL_TYPE_MOD) } else if *addMODMeetingFlag { if *entry == "" { log.Fatal("Meeting name must be provided with -entry flag") } addMODMeeting(*entry, *dateOfEntry, "Default meeting") } else if *addMODMeetingNoteFlag { if *entry == "" { log.Fatal("Meeting note must be provided with -entry flag") } addMODMeetingJournalEntry(*entry, *meetingID) } else { addCmd.PrintDefaults() } os.Exit(1) case "list": listCmd.Parse(os.Args[2:]) if *listPersonalEntriesFlag { listjournalEntries(*orientationFlag, JOURNAL_TYPE_PERSONAL) } else if *listMODEntriesFlag { listjournalEntries(*orientationFlag, JOURNAL_TYPE_MOD) } else if *listJobbylogEntriesFlag { listjournalEntries(*orientationFlag, JOURNAL_TYPE_JOBBY) } else if *listSleepEntriesFlag { listjournalEntries(*orientationFlag, JOURNAL_TYPE_SLEEP) } else if *listMODMeetingsFlag { listMeetings() } else if *listEntriesForMODMeetingFlag { listEntriesForMeeting(*meetingID) } else { listCmd.PrintDefaults() } os.Exit(1) default: flag.PrintDefaults() os.Exit(1) } }