diff --git a/addbook.rex b/addbook.rex index 8f48320..5f746bf 100755 --- a/addbook.rex +++ b/addbook.rex @@ -18,7 +18,8 @@ app~run() Do forever if .environment['STOPNOW'] = 1 then do app~cleanup() /* Clean up before exiting */ - address system 'clear' + call SysCls + Say .environment["REXX_PATH"] Say "Exiting Address Book." exit 0 end @@ -27,18 +28,6 @@ end Exit -::routine SysWait - use arg seconds - address system 'sleep' seconds - return - - -::ROUTINE setEnv - .local~home = SysGetpwnam("gmgauthier", "d") - .local~projectRoot = .home||"/Projects/rexx-address-book" - .local~dbPath = .projectRoot||"/db/contacts.sqlite" - - ::CLASS AddressBookApp PUBLIC ::METHOD Init @@ -64,300 +53,20 @@ Exit db~closeDb() return +::ROUTINE setEnv + .environment~home = SysGetpwnam("gmgauthier", "d") + .environment~projectRoot = .home||"/Projects/rexx-address-book" + .environment~pkgPath = .projectRoot||"/app" + .environment~dbPath = .projectRoot||"/db" + .environment["REXX_PATH"] = .projectRoot||";"||.pkgPath||";"||.dbPath||";" -::CLASS AddressBookDB PUBLIC - - ::method init - expose db - - /* Create database directory if it doesn't exist */ - if SysFileExists(.dbPath) == .false then DO - SAY "Initializing new address book" - db = .ooSQLiteConnection~new(.dbPath) - self~createTables - END - Else Do - db = .ooSQLiteConnection~new(.dbPath,.ooSQLite~OPEN_READWRITE) - End - return - - ::METHOD getFileName - expose db - return db~fileName() - - ::METHOD closeDb - expose db - db~Close() - RETURN - - ::method createTables - expose db - - /* Contacts table */ - db~exec("CREATE TABLE IF NOT EXISTS contacts ("||, - "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, - "first_name TEXT,"||, - "last_name TEXT,"||, - "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"||, - "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"||, - ")") - - /* Phone numbers table */ - db~exec("CREATE TABLE IF NOT EXISTS phone_numbers ("||, - "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, - "contact_id INTEGER,"||, - "type TEXT,"||, -- home, work, mobile, etc. - "number TEXT,"||, - "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, - ")") - - /* Email addresses table */ - db~exec("CREATE TABLE IF NOT EXISTS email_addresses ("||, - "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, - "contact_id INTEGER,"||, - "type TEXT,"||, -- home, work, etc. - "email TEXT,"||, - "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, - ")") - - /* Physical addresses table */ - db~exec("CREATE TABLE IF NOT EXISTS addresses ("||, - "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, - "contact_id INTEGER,"||, - "type TEXT,"||, -- home, work, etc. - "street TEXT,"||, - "city TEXT,"||, - "state TEXT,"||, - "postal_code TEXT,"||, - "country TEXT,"||, - "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, - ")") - return - -/* UI handling class */ -::class AddressBookUI public - - ::method init - expose win mainMenu - return - - ::method initialize - expose win mainMenu - - win = self~DrawMainPanel(0, .true) /* default colour, draw border box */ - self~setupMainMenu(win) - return - - ::METHOD DrawMainPanel - use arg clrset, with_box - win = .window~new() /* The first invocation must be with no arguments */ - win~curs_set(0) /* Hide the cursor by default */ - win~keypad(1) /* Enable function keys and arrow keys */ - win~raw - win~noecho - win~clear - win~cbreak - - self~SetPanelColor(win, clrset) - - if with_box = .true then DO - win~box(0, 0) /* Draw box with default ACS characters */ - win~refresh - END - - return win - - ::METHOD DrawSubPanel - use arg height, width, starty, startx, clrset, title, with_box - new_win = .window~new(height, width, starty, startx) - new_win~curs_set(0) - - /* Set the panel color */ - self~SetPanelColor(new_win, clrset) - - /* Display title */ - new_win~attron(new_win~A_BOLD) - new_win~mvaddstr(starty-(starty-2), (width - title~length) % 2, title) - new_win~attroff(new_win~A_BOLD) - new_win~refresh - if with_box = .true then DO - new_win~box(0,0) - new_win~refresh - END - return new_win - - ::METHOD SetPanelColor - use arg panel, clrset - - panel~start_color() - SELECT - /* white text on blue background */ - when clrset = 1 then panel~init_pair(2, panel~COLOR_WHITE, panel~COLOR_BLUE) - /* white text on grey background */ - when clrset = 2 then panel~init_pair(3, panel~COLOR_WHITE, panel~COLOR_GREEN) - /* yellow text on red background */ - when clrset = 3 then panel~init_pair(4, panel~COLOR_YELLOW, panel~COLOR_RED) - /* black text on white background */ - when clrset = 4 then panel~init_pair(5, panel~COLOR_BLACK, panel~COLOR_WHITE) - /* black text on green background */ - when clrset = 5 then panel~init_pair(6, panel~COLOR_BLACK, panel~COLOR_GREEN) - /* black text on cyan background */ - when clrset = 6 then panel~init_pair(7, panel~COLOR_BLACK, panel~COLOR_CYAN) - /* Default to white text on black background */ - OTHERWISE DO - clrset=0 - panel~init_pair(1, panel~COLOR_WHITE, panel~COLOR_BLACK) - END - END - panel~bkgd(panel~color_pair(clrset+1)) - panel~refresh - - ::method setupMainMenu - expose mainMenu win menuwin menuItems menu_keys - use arg win - - max_y = win~lines - max_x = win~cols - - menu_height = 21 - menu_width = 40 - start_y = (max_y - menu_height) % 2 - start_x = (max_x - menu_width) % 2 - clrset=1 - title = "Rexx Address Book" - with_box = .true - - menuwin = self~DrawSubPanel(menu_height, menu_width, start_y, start_x, clrset, title, with_box) - menuItems = .array~of("[A]dd Contact", "[R]emove Contact", "[E]dit Contact", "[S]earch", "[L]ist All", "E[X]it") - menu_keys = .array~of("a", "r", "e", "s", "l", "x") - - /* Display menu items */ - selected = 1 - self~DrawMenu(menuwin, menuItems, selected, win) - /* menuwin~mvaddstr(menu_height - 2, 3, "Type 'X' to exit") */ - menuwin~refresh - RETURN - - ::method DrawMenu - expose win menuItems selected mainwin - use arg win, items, selected, mainwin - - do i = 1 to items~items - if i = selected then do - win~attron(mainwin~COLOR_PAIR(2)) - win~attron(mainwin~A_BOLD) - win~mvaddstr(i+3, 2, items[i]) - win~attroff(mainwin~COLOR_PAIR(2)) - win~attroff(mainwin~A_BOLD) - end - else do - win~mvaddstr(i+3, 2, items[i]) - end - end - win~refresh - return - - /** MAIN LOOP **/ - ::method mainLoop - expose win mainMenu menuwin - - running = .true - - do while running - menuwin~refresh - key = win~getch - - menu_keys = .array~of("a", "r", "e", "s", "l", "x") - - select - when key = win~KEY_UP then do - if selected > 1 then selected = selected - 1 - call DrawMenu menuwin, menuItems, selected, win - end - when key = win~KEY_DOWN then do - if selected < menuItems~items then selected = selected + 1 - call DrawMenu menuwin, menuItems, selected, win - end - when key = D2C(10) | key = D2C(13) then do /* Enter key - numeric codes only */ - menuwin~refresh - call ProcessSelection menu_keys[selected], home - return - end - when key = D2C(88) | key = D2C(120) | key = "x" | key = C2D("x") then do - menuwin~endwin - .environment['STOPNOW'] = 1 - RETURN - end - otherwise do - if datatype(key) = 'CHAR' then do - key = lower(key) - pos = self~findInArray(menu_keys, key) - if pos > 0 then do - menuwin~mvaddstr(19 - 1, 18, "Letter selection ["||key||"]") - menuwin~refresh - call SysWait 0.5 - self~ProcessSelection(menuwin, key) - return - end - end - end - end - end /* do while running */ - return - - /* Process selection */ - ::METHOD ProcessSelection - use arg menuwin, key_char - select - when key_char = 'a' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the ADD panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - when key_char = 'r' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the REMOVE panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - when key_char = 'e' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the EDIT panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - when key_char = 's' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the SEARCH panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - when key_char = 'l' then do - menuwin~mvaddstr(19 - 3, 5, "I would launch the LIST panel ");menuwin~refresh; - menuwin~refresh - call SysWait 1 - END - otherwise nop - end - return - - ::METHOD findInArray - use arg array, item - do i = 1 to array~items - if array[i] = item then return i - end - return 0 /* Not found */ - - ::method cleanup - expose win - /* Clean up ncurses */ - win~endwin - return - -/* Handle program termination */ -::ROUTINE ProgramHalt - signal off halt - Say "PROGRAM TERMINATED AT THE KEYBOARD" - exit 0 /** External Libraries **/ +::requires 'app/appdb.cls' +::requires 'app/appui.cls' +::requires 'app/utils.rex' + ::requires 'ooSQLite.cls' ::requires "rxunixsys" LIBRARY ::requires 'ncurses.cls' + diff --git a/app/appdb.cls b/app/appdb.cls new file mode 100644 index 0000000..398cb66 --- /dev/null +++ b/app/appdb.cls @@ -0,0 +1,411 @@ +::requires 'ooSQLite.cls' +::requires "rxunixsys" LIBRARY +::requires 'ncurses.cls' +::requires 'app/utils.rex' + +::CLASS AddressBookDB PUBLIC + + ::method init + expose db + + /* Create database directory if it doesn't exist */ + if SysFileExists(.dbPath) == .false then DO + SAY "Initializing new address book" + db = .ooSQLiteConnection~new(.dbPath) + self~createTables + END + Else Do + Say "Initializing existing address book" + db = .ooSQLiteConnection~new(.dbPath,.ooSQLite~OPEN_READWRITE) + End + return + + ::METHOD getFileName + expose db + return db~fileName() + + ::METHOD closeDb + expose db + db~Close() + RETURN + + ::METHOD createTables + expose db + + /* Contacts table */ + db~exec("CREATE TABLE IF NOT EXISTS contacts ("||, + "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, + "first_name TEXT,"||, + "last_name TEXT,"||, + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,"||, + "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"||, + ")") + + /* Phone numbers table */ + db~exec("CREATE TABLE IF NOT EXISTS phone_numbers ("||, + "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, + "contact_id INTEGER,"||, + "type TEXT,"||, -- home, work, mobile, etc. + "number TEXT,"||, + "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, + ")") + + /* Email addresses table */ + db~exec("CREATE TABLE IF NOT EXISTS email_addresses ("||, + "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, + "contact_id INTEGER,"||, + "type TEXT,"||, -- home, work, etc. + "email TEXT,"||, + "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, + ")") + + /* Physical addresses table */ + db~exec("CREATE TABLE IF NOT EXISTS addresses ("||, + "id INTEGER PRIMARY KEY AUTOINCREMENT,"||, + "contact_id INTEGER,"||, + "type TEXT,"||, -- home, work, etc. + "street TEXT,"||, + "city TEXT,"||, + "state TEXT,"||, + "postal_code TEXT,"||, + "country TEXT,"||, + "FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE"||, + ")") + return + + /* Contact CRUD Operations */ + ::METHOD addContact + expose db + use arg contactDict /* Use a Rexx 'directory' to pass multiple values around */ + sql = "INSERT INTO contacts (first_name, last_name) VALUES ('"contactDict~firstName"', '"contactDict~lastName"')" + rc = db~exec(sql) + if rc \= 0 then do + say "Error adding contact:" db~errMsg() + return -1 + end + contactId = db~lastInsertRowId() /* the row id of 'contacts' table is the master id */ + self~addPhoneNumber(contactId, contactDict["PHONE_TYPE"], contactDict["PHONE_NUMBER"]) + self~addEmailAddress(contactId, contactDict["EMAIL_TYPE"], contactDict["EMAIL_ADDRESS"]) + self~addRealAddress(contactId, contactDict~addressType, contactDict~street, contactDict~city, contactDict~state, + contactDict~postalCode, contactDict~country) + return contactId + + ::METHOD getContact + expose db + use arg contactId + returnedContent = .Directory~new() + sql1 = "SELECT * FROM contacts WHERE id = "contactId + contacts = db~exec(sql1,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES) + /* The result will be rendered as an array of Rexx 'directories' + * which are basically analogous to python dictionaries. */ + + if contacts = .nil then DO + Say 'NO CONTACTS FOUND' + return + END + if contacts~size < 1 then do + Say 'NO CONTACT FOUND' + return + END + return contacts[1] /* Rexx is 1-indexed */ +/*** + returnedContent~firstName = contact["FIRST_NAME"] + returnedContent~lastName = contact["LAST_NAME"] + sql2 = "SELECT * FROM email_addresses WHERE contact_id = "contactId + emails = db~exec(sql2,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES) + email_array = .Array~new() + do row over emails + email_array.append(row["EMAIL"]) + END + returnedContent~emailAddresses = email_array + + sql3 = "SELECT * FROM phone_numbers WHERE contact_id = "contactId + phones = db~exec(sql3,.true,.ooSQLite~OO_ARRAY_OF_DIRECTORIES) + phone_dir = .Directory~new() + phone_array = .Array~new() + do phone over phones + phone_dir~put(phone["NUMBER"], phone["TYPE"]) + phone_array.append(phone_dir) + END + returnedContent~phoneNumbers = phone_array + + sql4 = "SELECT * FROM addresses WHERE contact_id = "contactId + addresses = db~exec(sql3,.true,.ooSQLite~~OO_ARRAY_OF_DIRECTORIES) + address_dir = .Dictionary~new() + address_array = .Array~new() + do address over addresses + address_dir~type = address["TYPE"] + address_dir~street = address["STREET"] + address_dir~city = address["CITY"] + address_dir~state = address["STATE"] + address_dir~postal_code = address["POSTAL_CODE"] + address_dir~country = address["COUNTRY"] + address_array.append(address_dir) + END + returnedContent~addresses = address_array + return returnedContent +***/ + + ::METHOD getAllContacts + expose db + sql = "SELECT * FROM contacts ORDER BY last_name, first_name" + contacts = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + return contacts + + ::METHOD searchContacts + expose db + use arg searchTerm + + contactsList = .Array~new() + + sql = "SELECT id FROM contacts WHERE first_name LIKE '%"searchTerm"%' OR last_name LIKE '%"searchTerm"%' ORDER BY last_name, first_name" + contactIds = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + + do contactDir over contactIds + contactId = contactDir["ID"] + contact = self~getContact(contactId) + if contact \= .nil then do + contactsList~append(contact) + end + end + return contactsList + + ::METHOD updateContact + expose db + use arg contactId, firstName, lastName + sql = "UPDATE contacts SET first_name = '"firstName"', last_name = '"lastName"' WHERE id = "contactId + rc = db~exec(sql) + if rc \= 0 then do + say "Error updating contact:" db~errMsg() + return -1 + end + return rc +/** + self~removeContactPhones(contactId) + self~removeContactEmails(contactId) + self~removeContactAddresses(contactId) + + /* Then add new entries */ + self~addPhoneNumber(contactId, contactDict["PHONE_TYPE"], contactDict["PHONE_NUMBER"]) + self~addEmailAddress(contactId, contactDict["EMAIL_TYPE"], contactDict["EMAIL_ADDRESS"]) + self~addAddress(contactId, contactDict~addressType, contactDict~street, contactDict~city, contactDict~state, + contactDict~postalCode, contactDict~country) + return 0 +**/ + + ::METHOD deleteContact + expose db + use arg contactId + + /* Delete all related records first (assuming foreign key constraints) */ + self~removeContactPhones(contactId) + self~removeContactEmails(contactId) + self~removeContactAddresses(contactId) + + /* Delete the contact record */ + sql = "DELETE FROM contacts WHERE id = "contactId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error deleting contact:" db~errMsg() + return -1 + end + return 0 + + ::METHOD getPhoneNumbers + expose db + use arg contactId + + phones = .Array~new + sql = "SELECT id, type, number FROM phone_numbers WHERE contact_id = "contactId + phones = db~exec(sql,.true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + + return phones + + ::METHOD addPhoneNumber + expose db + use arg contactId, phoneType, phoneNumber + + if phoneType = .nil | phoneNumber = .nil then return 0 + + sql = "INSERT INTO phone_numbers (contact_id, type, number) VALUES ("contactId", '"phoneType"', '"phoneNumber"')" + rc = db~exec(sql) + if rc \= 0 then DO + Say "Unable to insert phone number for contact id "contactId + return rc + END + phone_id = db~lastInsertRowId() + return phone_id + + ::METHOD updatePhoneNumber + expose db + use arg phoneId, phoneType, phoneNumber + + sql = "UPDATE phone_numbers SET type = '"phoneType"', number = '"phoneNumber"' WHERE id = "phoneId + rc = db~exec(sql) + if rc \= 0 then DO + Say "Unable to update phone number or type for phone id "contactId + return rc + END + + return rc + + ::METHOD deletePhoneNumber + expose db + use arg phoneId + + stmt = db~prepare("DELETE FROM phone_numbers WHERE id = ?") + stmt~bind(1, phoneId) + stmt~step + + return db~changes() > 0 + + + + /* Email address operations */ + + ::METHOD addEmailAddress + expose db + use arg contactId, emailType, emailAddress + + if emailType = .nil | emailAddress = .nil then return 0 + + sql = "INSERT INTO email_addresses (contact_id, type, email) VALUES ("contactId", '"emailType"', '"emailAddress"')" + rc = db~exec(sql) + if rc \= 0 then do + say "Error adding email address:" db~errMsg() + return -1 + end + + email_id = db~lastInsertRowId() + + return email_id + + ::METHOD getEmailAddresses + expose db + use arg contactId + + sql = "SELECT id, type, email FROM email_addresses WHERE contact_id = "contactId + emails = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + return emails + + ::METHOD updateEmailAddress + expose db + use arg emailId, emailType, emailAddress + + sql = "UPDATE email_addresses SET type = '"emailType"', email = '"emailaddress"' WHERE id = "emailId + rc = db~exec(sql) + if rc \= 0 then do + say "Error adding email address:" db~errMsg() + return -1 + end + return rc + + + ::METHOD deleteEmailAddress + expose db + use arg emailId + + stmt = db~prepare("DELETE FROM email_addresses WHERE id = ?") + stmt~bind(1, emailId) + stmt~step + + return db~changes() > 0 + + /* Physical address operations */ + + ::METHOD addRealAddress + expose db + use arg contactId, addressType, street, city, state, postalCode, country + + if addressType = .nil | street = .nil then return 0 + + sql = "INSERT INTO addresses (contact_id, type, street, city, state, postal_code, country)", + "VALUES ("contactId", '"addressType"', '"street"', '"city"', '"state"', '"postalCode"', '"country"')" + rc = db~exec(sql) + + if rc \= 0 then do + say "Error adding address:" db~errMsg() + return -1 + end + addr_id = db~lastInsertRowId() + return addr_id + + ::METHOD getRealAddresses + expose db + use arg contactId + + sql = "SELECT id, type, street, city, state, postal_code, country" || , + " FROM addresses WHERE contact_id = " contactId + addresses = db~exec(sql, .true, .ooSQLite~OO_ARRAY_OF_DIRECTORIES) + return addresses + + ::METHOD updateRealAddress + expose db + use arg addressId, addressType, street, city, state, postalCode, country + + sql = "UPDATE addresses SET type = '"addressType"', street = '"street"', city = '"city"'," || , + " state = '"state"', postal_code = '"postalCode"', country = '"country"' WHERE id = "addressId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error updating address:" db~errMsg() + return -1 + end + + return rc + + ::METHOD deleteAddress + expose db + use arg addressId + + sql="DELETE FROM addresses WHERE id = "addressId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error removing address:" db~errMsg() + return -1 + end + + return 0 + + ::METHOD removeContactPhones + expose db + use arg contactId + + sql = "DELETE FROM phone_numbers WHERE contact_id = "contactId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error removing phone numbers:" db~errMsg() + return -1 + end + + return 0 + + ::METHOD removeContactEmails + expose db + use arg contactId + + sql = "DELETE FROM email_addresses WHERE contact_id = "contactId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error removing email addresses:" db~errMsg() + return -1 + end + return 0 + + ::METHOD removeContactAddresses + expose db + use arg contactId + + sql = "DELETE FROM addresses WHERE contact_id = "contactId + rc = db~exec(sql) + + if rc \= 0 then do + say "Error removing addresses:" db~errMsg() + return -1 + end + return 0 + diff --git a/app/appui.cls b/app/appui.cls new file mode 100644 index 0000000..734ffbc --- /dev/null +++ b/app/appui.cls @@ -0,0 +1,220 @@ +::requires 'ooSQLite.cls' +::requires "rxunixsys" LIBRARY +::requires 'ncurses.cls' +::requires 'app/utils.rex' + +::class AddressBookUI public + + ::method init + expose win mainMenu + return + + ::method initialize + expose win mainMenu + + win = self~DrawMainPanel(0, .true) /* default colour, draw border box */ + self~setupMainMenu(win) + return + + ::METHOD DrawMainPanel + use arg clrset, with_box + win = .window~new() /* The first invocation must be with no arguments */ + win~curs_set(0) /* Hide the cursor by default */ + win~keypad(1) /* Enable function keys and arrow keys */ + win~raw + win~noecho + win~clear + win~cbreak + + self~SetPanelColor(win, clrset) + + if with_box = .true then DO + win~box(0, 0) /* Draw box with default ACS characters */ + win~refresh + END + + return win + + ::METHOD DrawSubPanel + use arg height, width, starty, startx, clrset, title, with_box + new_win = .window~new(height, width, starty, startx) + new_win~curs_set(0) + + /* Set the panel color */ + self~SetPanelColor(new_win, clrset) + + /* Display title */ + new_win~attron(new_win~A_BOLD) + new_win~mvaddstr(starty-(starty-2), (width - title~length) % 2, title) + new_win~attroff(new_win~A_BOLD) + new_win~refresh + if with_box = .true then DO + new_win~box(0,0) + new_win~refresh + END + return new_win + + ::METHOD SetPanelColor + use arg panel, clrset + + panel~start_color() + SELECT + /* white text on blue background */ + when clrset = 1 then panel~init_pair(2, panel~COLOR_WHITE, panel~COLOR_BLUE) + /* white text on grey background */ + when clrset = 2 then panel~init_pair(3, panel~COLOR_WHITE, panel~COLOR_GREEN) + /* yellow text on red background */ + when clrset = 3 then panel~init_pair(4, panel~COLOR_YELLOW, panel~COLOR_RED) + /* black text on white background */ + when clrset = 4 then panel~init_pair(5, panel~COLOR_BLACK, panel~COLOR_WHITE) + /* black text on green background */ + when clrset = 5 then panel~init_pair(6, panel~COLOR_BLACK, panel~COLOR_GREEN) + /* black text on cyan background */ + when clrset = 6 then panel~init_pair(7, panel~COLOR_BLACK, panel~COLOR_CYAN) + /* Default to white text on black background */ + OTHERWISE DO + clrset=0 + panel~init_pair(1, panel~COLOR_WHITE, panel~COLOR_BLACK) + END + END + panel~bkgd(panel~color_pair(clrset+1)) + panel~refresh + + ::method setupMainMenu + expose mainMenu win menuwin menuItems menu_keys + use arg win + + max_y = win~lines + max_x = win~cols + + menu_height = 21 + menu_width = 40 + start_y = (max_y - menu_height) % 2 + start_x = (max_x - menu_width) % 2 + clrset=1 + title = "Rexx Address Book" + with_box = .true + + menuwin = self~DrawSubPanel(menu_height, menu_width, start_y, start_x, clrset, title, with_box) + menuItems = .array~of("[A]dd Contact", "[R]emove Contact", "[E]dit Contact", "[S]earch", "[L]ist All", "E[X]it") + menu_keys = .array~of("a", "r", "e", "s", "l", "x") + + /* Display menu items */ + selected = 1 + self~DrawMenu(menuwin, menuItems, selected, win) + /* menuwin~mvaddstr(menu_height - 2, 3, "Type 'X' to exit") */ + menuwin~refresh + RETURN + + ::method DrawMenu + expose win menuItems selected mainwin + use arg win, items, selected, mainwin + + do i = 1 to items~items + if i = selected then do + win~attron(mainwin~COLOR_PAIR(2)) + win~attron(mainwin~A_BOLD) + win~mvaddstr(i+3, 2, items[i]) + win~attroff(mainwin~COLOR_PAIR(2)) + win~attroff(mainwin~A_BOLD) + end + else do + win~mvaddstr(i+3, 2, items[i]) + end + end + win~refresh + return + + /** MAIN LOOP **/ + ::method mainLoop + expose win mainMenu menuwin + + running = .true + + do while running + menuwin~refresh + key = win~getch + + menu_keys = .array~of("a", "r", "e", "s", "l", "x") + + select + when key = win~KEY_UP then do + if selected > 1 then selected = selected - 1 + call DrawMenu menuwin, menuItems, selected, win + end + when key = win~KEY_DOWN then do + if selected < menuItems~items then selected = selected + 1 + call DrawMenu menuwin, menuItems, selected, win + end + when key = D2C(10) | key = D2C(13) then do /* Enter key - numeric codes only */ + menuwin~refresh + call ProcessSelection menu_keys[selected], home + return + end + when key = D2C(88) | key = D2C(120) | key = "x" | key = C2D("x") then do + menuwin~endwin + .environment['STOPNOW'] = 1 + RETURN + end + otherwise do + if datatype(key) = 'CHAR' then do + key = lower(key) + pos = self~findInArray(menu_keys, key) + if pos > 0 then do + menuwin~mvaddstr(19 - 1, 18, "Letter selection ["||key||"]") + menuwin~refresh + call SysWait 0.5 + self~ProcessSelection(menuwin, key) + return + end + end + end + end + end /* do while running */ + return + + /* Process selection */ + ::METHOD ProcessSelection + use arg menuwin, key_char + select + when key_char = 'a' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the ADD panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + when key_char = 'r' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the REMOVE panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + when key_char = 'e' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the EDIT panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + when key_char = 's' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the SEARCH panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + when key_char = 'l' then do + menuwin~mvaddstr(19 - 3, 5, "I would launch the LIST panel ");menuwin~refresh; + menuwin~refresh + call SysWait 1 + END + otherwise nop + end + return + + ::METHOD findInArray + use arg array, item + do i = 1 to array~items + if array[i] = item then return i + end + return 0 /* Not found */ + + ::method cleanup + expose win + /* Clean up ncurses */ + win~endwin + return diff --git a/app/utils.rex b/app/utils.rex new file mode 100644 index 0000000..3a86a17 --- /dev/null +++ b/app/utils.rex @@ -0,0 +1,11 @@ +::ROUTINE SysWait PUBLIC + use arg seconds + address system 'sleep' seconds + return + + +/* Handle program termination */ +::ROUTINE ProgramHalt + signal off halt + Say "PROGRAM TERMINATED AT THE KEYBOARD" + exit 0 diff --git a/db/test_contacts.sqlite b/db/test_contacts.sqlite new file mode 100644 index 0000000..b59c7b4 Binary files /dev/null and b/db/test_contacts.sqlite differ diff --git a/tests/test_appdb.rexx b/tests/test_appdb.rexx new file mode 100755 index 0000000..096e1ff --- /dev/null +++ b/tests/test_appdb.rexx @@ -0,0 +1,326 @@ +#!/usr/bin/env rexx +/*************************************************************************** + * Rexx Address Book - Database Test Script * + * * + * This script tests the database functionality without the ncurses UI. * + * It steps through creating, reading, updating, and deleting contacts * + * along with their associated data. * + ***************************************************************************/ + +/* Setup environment */ +.local~home = SysGetpwnam("gmgauthier", "d") +.local~projectRoot = .home||"/Projects/rexx-address-book" +.local~appPkg = .projectRoot||"/app" +.local~dbPath = .projectRoot||"/db/test_contacts.sqlite" /* Use a test database */ + + +tests = .TestSuite~new() + +DO + tests~TestCreateContact + tests~TestGetContact + tests~TestGetAllContacts + tests~TestAddPhoneNumber + tests~TestAddEmailAddress + tests~TestAddRealAddress + tests~TestFullDetailRetrieval + tests~TestUpdateContact +END + +tests~tearDown + +exit 0 + +::requires 'app/appdb.cls' +::requires 'ooSQLite.cls' +::requires "rxunixsys" LIBRARY +::requires 'ncurses.cls' +::requires 'app/utils.rex' + + +::CLASS TestSuite + ::METHOD init + self~setUp() + + ::METHOD setUp + expose db contactDict + if SysFileExists(.dbPath) then do + call SysFileDelete .dbPath + say "Deleted existing test database." + end + say "Initializing test database at:" .dbPath + db = .AddressBookDB~new() + self~setupFixtureData() + + ::METHOD tearDown + expose db + db~closeDb() + say "Test completed!" + + ::METHOD setupFixtureData + expose db contactDict addressDict emailArray phoneArray + + emailArray = .Array~new() + emailDict1 = .Directory~new() + emailDict1~Type = "HOME" + emailDict1~Number = "john.doe@home.com" + emailArray~append(emailDict1) + emailDict2 = .Directory~new() + emailDict2~Type = "WORK" + emailDict2~Number = "john.doe@work.com" + emailArray~append(emailDict2) + + phoneArray = .Array~new() + phoneDict1 = .Directory~new() + phoneDict1~Type = "HOME" + phoneDict1~Number = "123-456-7890" + phoneArray~append(phoneDict1) + phoneDict2 = .Directory~new() + phoneDict2~Type = "WORK" + phoneDict2~Number = "987-654-3210" + phoneArray~append(phoneDict2) + + contactDict = .Directory~new() + contactDict~firstName = "John" + contactDict~lastName = "Doe" + + + ::METHOD TestCreateContact + expose db contactDict + /* Test contact creation */ + say "" + say "=== TESTING CONTACT CREATION ===" + say "Creating contact:" contactDict~firstName contactDict~lastName + contactId = db~addContact(contactDict) + say "Contact created with ID:" contactId + + ::METHOD TestGetContact + expose db contactDict + /* Test retrieving contact */ + say "" + say "=== TESTING CONTACT RETRIEVAL ===" + contact = db~getContact(1) + if contact \= .nil then do + say "Contact retrieved successfully:" + say " ID:" contact['ID'] + say " Name:" contact['FIRST_NAME'] contact['LAST_NAME'] + say " Created at:" contact['CREATED_AT'] + end + else do + say "ERROR: Failed to retrieve contact!" + end + + ::METHOD TestGetAllContacts + expose db contactDict + /* Get all contacts */ + say "" + say "=== TESTING GET ALL CONTACTS ===" + /* add some extras */ + db~addContact(contactDict) + db~addContact(contactDict) + + allContacts = db~getAllContacts() + say "Total contacts in database:" allContacts~items() + do contact over allContacts + say " " contact['FIRST_NAME'] contact['LAST_NAME'] "(ID:" contact['ID']")" + end + + ::METHOD TestAddPhoneNumber + expose db contactDict + /* Test adding phone numbers */ + say "" + say "=== TESTING PHONE NUMBER ADDITION ===" + Say "Adding phone numbers for contact id: 1" + + say "Adding home phone number..." + homePhoneId = db~addPhoneNumber(1, "Home", "555-123-4567") + say "Added phone with ID:" homePhoneId + + say "Adding mobile phone number..." + mobilePhoneId = db~addPhoneNumber(1, "Mobile", "555-987-6543") + say "Added phone with ID:" mobilePhoneId + + ::METHOD TestAddEmailAddress + expose db contactDict + /* Test adding email addresses */ + say "" + say "=== TESTING EMAIL ADDITION ===" + Say "Adding email addresses for contact id: 1" + + say "Adding personal email..." + personalEmailId = db~addEmailAddress(1, "Personal", "john.doe@personal.example") + say "Added email with ID:" personalEmailId + + say "Adding work email..." + workEmailId = db~addEmailAddress(1, "Work", "john.doe@work.example") + say "Added email with ID:" workEmailId + + ::METHOD TestAddRealAddress + expose db contactDict + /* Test adding physical address */ + say "" + say "=== TESTING ADDRESS ADDITION ===" + Say "Adding real addresses for contact id: 1" + + say "Adding home address..." + homeAddressId = db~addRealAddress(1, "Home", "123 Main St", "Anytown", "NY", "12345", "USA") + say "Added address with ID:" homeAddressId + + ::METHOD TestFullDetailRetrieval + expose db ContactDict + /* Read the complete contact details */ + say "" + say "=== TESTING COMPLETE CONTACT RETRIEVAL ===" + contact = db~getContact(1) + if contact \= .nil then do + say "Contact retrieved successfully:" + say " ID:" contact['ID'] + say " Name:" contact['FIRST_NAME'] contact['LAST_NAME'] + say " Created at:" contact['CREATED_AT'] + + phones = db~getPhoneNumbers(1) + say "Phone numbers:" + do phone over phones + say " " phone['TYPE']":" phone['NUMBER'] "(ID:" phone['ID']")" + end + + emails = db~getEmailAddresses(1) + say "Email addresses:" + do email over emails + say " " email['TYPE']":" email['EMAIL'] "(ID:" email['ID']")" + end + + addresses = db~getRealAddresses(1) + say "Addresses:" + do address over addresses + say " " address['TYPE']" (ID: "address['ID']"):" + say " Street:" address['STREET'] + say " City:" address['CITY'] + say " State:" address['STATE'] + say " Postal code:" address['POSTAL_CODE'] + say " Country:" address['COUNTRY'] + end + end + else do + say "ERROR: Failed to retrieve complete contact details!" + end + + ::METHOD TestUpdateContact + expose db ContactDict + /* Test updating contact information */ + say "" + say "=== TESTING CONTACT UPDATE ===" + say "Updating contact name..." + updatedFirstName = "Jonathan" + updatedLastName = "Dorian" + result = db~updateContact(1, updatedFirstName, updatedLastName) + say "Update successful:" result + + say "Updating home phone number..." + result = db~updatePhoneNumber(1, "Home", "555-111-2222") + say "Update successful:" result + + say "Updating work email..." + result = db~updateEmailAddress(2, "Work", "jonathan.dorian@hospital.example") + say "Update successful:" result + + say "Updating home address..." + result = db~updateRealAddress(1, "Home", "456 Oak Ave", "Springfield", "IL", "54321", "USA") + say "Update successful:" result + + /* Read the updated contact details */ + say "" + say "=== TESTING UPDATED CONTACT RETRIEVAL ===" + contact = db~getContact(1) + if contact \= .nil then do + say "Updated contact:" + say " ID:" contact['ID'] + say " Name:" contact['FIRST_NAME'] contact['LAST_NAME'] + say " Created at:" contact['CREATED_AT'] + + say "Updated phone numbers:" + phones = db~getPhoneNumbers(1) + do phone over phones + say " " phone['TYPE']":" phone['NUMBER'] "(ID:" phone['ID']")" + end + + emails = db~getEmailAddresses(1) + say "Email addresses:" + do email over emails + say " " email['TYPE']":" email['EMAIL'] "(ID:" email['ID']")" + end + + addresses = db~getRealAddresses(1) + say "Addresses:" + do address over addresses + say " " address['TYPE']" (ID: "address['ID']"):" + say " Street:" address['STREET'] + say " City:" address['CITY'] + say " State:" address['STATE'] + say " Postal code:" address['POSTAL_CODE'] + say " Country:" address['COUNTRY'] + end + end + else do + say "ERROR: Failed to retrieve updated contact details!" + end + + ::METHOD TestDeletion + say "" + say "=== TESTING DELETION ===" + + /* Test deleting an email */ + say "Deleting personal email..." + result = db~deleteEmailAddress(personalEmailId) + say "Deletion successful:" result + + /* Get contact to verify deletion */ + contact = db~getContact(contactId) + say "Email addresses after deletion:" + do email over contact['emails'] + say " " email['type']":" email['email'] "(ID:" email['id']")" + end + + ::METHOD OtherStuff + /* Test deleting the entire contact */ + say "Deleting entire contact..." + result = db~deleteContact(contactId) + say "Deletion successful:" result + + /* Try to retrieve the deleted contact */ + contact = db~getContact(contactId) + if contact = .nil then + say "Contact successfully deleted - could not retrieve contact with ID" contactId + else + say "ERROR: Contact was not properly deleted!" + + /* Test searching functionality */ + say "" + say "=== TESTING SEARCH FUNCTIONALITY ===" + + /* Add multiple contacts for search testing */ + say "Adding test contacts for search..." + db~addContact("Jane", "Smith") + db~addContact("John", "Johnson") + db~addContact("Bob", "Smith") + db~addContact("Sarah", "Williams") + + /* Search for contacts */ + say "Searching for 'Smith'..." + results = db~searchContacts("Smith") + say "Found" results~items() "contacts:" + do contact over results + say " " contact['firstName'] contact['lastName'] "(ID:" contact['id']")" + end + + say "Searching for 'John'..." + results = db~searchContacts("John") + say "Found" results~items() "contacts:" + do contact over results + say " " contact['firstName'] contact['lastName'] "(ID:" contact['id']")" + end + + + + +