P3: Functional Design
Concepts
User
Purpose: authenticate accounts
OP: after a someone registers using a username and password, they are able to authenticate as that user anytime afterwards by providing the username and password pair
States:
- accounts: set User
- username, password: User → one String
Actions:
- register (name, pwd: string, out u: User)
If there are no accounts with username = name, instantiate an account with (username, password) = (name, pwd) and add it to accounts - authenticate (name, pwd: string out u: User)
Return the account where username = name and password = pwd - deleteUser(name, pwd: string)
Remove user from accounts where (username, password) = (name, pwd)
Session[User]
Purpose: allows user to be authenticated for longer period of time
OP: when a user is authenticated, the information related to the user account is retrieved and maintained until the user ends the session
States:
- active: set Session
- user: Session → one User
Actions:
- start(u: User, out ses:Session)
If the user is not in the set of active sessions, make a new session with user = u and add it to the active set before returning the session - getUser(ses: Session, out u: User)
Look through active and if there is a session = ses, return session.user - end(ses: Session)
Remove ses from the set of active
Membership[User,Team]
Purpose: keeps track of the organizations that an individual belongs to
OP: When a user becomes part of an organization, that organization gets added to the user's set of memberships
States:
- allMemberships: set Membership
- user: Membership → one User
- orgs: Membership → set Team
Actions:
- createMembership(u: User, out m: Membership)
Generate an id i, create a Membership m = (i, u, {}), and add m to allMemberships - getMembership(u: User, out m: set Team)
Look for user = u and return m.orgs - editMembership(u: User, o: set Teams)
Find Membership with id = i and set orgs = o - deleteMembership(id: string)
Delete the Membership with id = id from allMemberships
Team[User]
Purpose: allows association of a group of individuals as a single entity, wherein certain members (admins) possess authority of creating new member accounts
OP: After a group is created, only admins can add new members, promote a member to admin status, or remove other members from the group
States:
- allTeams: set Team
- name: Team → one string
- admins: Team → set User
- members: Team → set User
Actions:
- createTeam(u: User, n: String, out t: Team)
Generate an id i, create a Team t with (id, name, admin, members) = (i, n, {u},{}), and add t to allTeams - getTeam(id: string, out t: Team)
Return team t with id = id - addMember(id: string, u: User, editor: User)
Find the team with id = id in allTeams and if the editor is in the set of admins in the team,add u to the set of members - promoteMember(id: string, u: User, editor: User)
Find the team with id =id in allTeams, and if the editor is in the set of admins in the team, remove u from the team's members set, and add u to the set of admins - demoteMember(id: string, u: User, editor: User)
Find the team with id = id in allTeams, and if the editor is in the set of admins in the team, remove u from the team's admins set, and add u to the set of members - removeMember(id: string, u: User, editor: User)
Find the team with id = id in allTeams, and if the editor is in the set of admins in the team, remove u to the set of members if it exists, and remove u from the set of admins if it exists - deleteTeam(id: string, editor: User)
Delete the team with id = id from allTeams if editor is in the set of admins in the team
Patron
Purpose: offers individual patron information such as name, date of birth and photo identification
OP: Patron is stored on site and when retrieved by id, the information is displayed
States:
- allPatrons: set Patron
- name: Patron → one string
- birthday: Patron → one Date
- image: Patron → one Image
Actions:
- createPatron(n: string, birthday: Date, img: Image, out p: Patron)
Generate an id i, create a Patron p with (id, name, birthday, image) = (i, n, birthday, img), and add p to allPatrons - getPatron(id: string, out p: Patron)
Get Patron with id = id - updatePatron(id: string, n: string, dob: Date, img: Image)
Update patron profile information associated by their id - deletePatron(id: string)
Delete the Patron with id = id from allPatrons
HouseholdProfile[Team,Patron,Stock]
Purpose: offers household information such as members, preferred language, specific requests, dietary restrictions, and number of previous visits
OP: When multiple people are listed together as a household, searching by household ID will retrieve information about the household, specific household needs, and number of visits
States:
- allProfiles: set HouseholdProfile
- org: HouseholdProfile → one Team
- members: HouseholdProfile → some Patron
- dietaryRestrictions: HouseHoldProfile → set string
- language: HouseholdProfile → one string
- requests: HouseholdProfile → set Stock
- pastVisits: HouseholdProfile → one int
Actions:
- createProfile(o: Team, m: set Patron, d: set string, l: string, r: set Stock, out h: HouseholdProfile)
Generate an id i, create a HouseholdProfile p with (id, org, members, dietRestrictions, language, requests, pastVisits)=(i, o, m, d, l, r, 0), and add p to allProfiles - getProfilesByOwner(o: Team, out h: set HouseholdProfile)
Get all profiles with org = o and return - getProfileById(id: string, out h: HouseholdProfile)
Get HouseholdProfile with id = id - deletePatron(id: string)
Delete the Patron with id = id from allPatrons - updatePatrons(id: string, m: set Patron)
Update the HouseholdProfile with id = id with new set of members m - addVisit(id: string)
Increment pastVisits of the HouseholdProfile with id = id by 1 - resetVisits(id: string)
Set pastVisits of the HouseholdProfile with id = id to 0 - updateDetails(id: string, d: set string, l: string, r, set Stock)
Update the HouseholdProfile with id = id with (dietRestrictions, language, requests) = (d, l, r) - deleteProfile(id: string)
Delete the HouseholdProfile with id = id from allProfiles
LanguageAudio[Team]
Purpose: collects audio for a particular language in order to communicate specific phrases in that language
OP: When audio for a language is uploaded with the English translation, it will be added to the audio playlist for that language and will be played later to communicate to others
States:
- allPlaylists: set LanguageAudio
- owner: LanguageAudio → one Team
- language: LanguageAudio → one string
- audio: LanguageAudio → one Audio
- translation: LanguageAudio → one Audio
Actions:
- createAudio(l: string, a: audio, o: Team, t: translation, out p: LanguageAudio)
Generate an id i, create a LanguageAudio p with (id, owner, language, audio, translation)=(i, o, l, a, t), and add p to allPlaylists - updateAudio(id: string, l: string, a: audio, t: translation)
Update the LanguageAudio with id = id with (language, audio, translation)=(l, a, t) - getAudioById(id: string, out p: set LanguageAudio)
Get all LanguageAudio with id=string - getAudioByOwner(o: Team, out p: set LanguageAudio)
Get all LanguageAudio with owner = o - deleteAudio(id: string)
Delete the LanguageAudio with id = id from allPlaylists
Stock[Team]
Purpose: allows the owner to monitor the quantity of a resource
OP: When the owner modifies the quantity of an item, the item will be displayed with the updated quantity
States:
- allStocks: set Stock
- owner: Stock → one Team
- item: Stock → one string
- count: Stock → one int
- supplyLink → one string
- image: Stock → one Image
- maxPerDay: Stock → one int
- maxPerPerson: Stock → one int
Actions:
- createStock(o: string, i: string, cnt: int, l: string, img: Image, maxp: intout s: Stock)
Generate an id, check that cnt >= 0, create a Stock s with (id, owner, item, count, supplyLink, image)=(id, o, i, c, l, img), and add s to allStocks. maxPerDay is automatically updated based on datetime calculations when stock is accessed. - getStockByOwner(o: string, out s: set Stock)
Get all stock with owner = o - getStockByName(o: string, n: string, out s: set Stock)
Get all stock with name n belonging to owner o - getStockById(id: string, out s: Stock)
Get stock with identifier=id - updateStock(id: string, item: string, o: string, count: int, supplyLink: string, img: Image, maxd: int, maxp: int)
Update the Stock with id = id with (item, count, supplyLink, img) if the owner = o and count>=0 - deleteStock(id: string)
Delete the Stock with id = id from allStocks
Shift[Team, User]
Purpose: Allows people to sign up for a specific shift/time slot for an organization
OP: If a person selects their shift, the interval of time during which that person is scheduled to work will be displayed to others in that organization
States:
- allShifts: set Shift
- owner: Shift → one Team
- volunteers: Shift → set Users
- start: Shift → one Date
- end: Shift → one Date
Actions:
- createShift(org: string, s: Date, e: Date, out shift: Shift)
Create a shift belonging to org, with start and end time s and e - getShiftByOwner(o: string, out shift: set Shift)
Find all shift with owner = o and return - deleteShift(id: string)
Delete the Shift with id=id - claimShift(user: string, s: string)
Find the shift corresponding to id s, and associate user id with its volunteers - unclaimShift(user: string, s: string)
Find the shift corresponding to id s, and disassociate user id with its volunteers - unclaimShiftsByUser(user: string, org: string)
Find all shifts that have user id associated with them with owner org, and disassociate user id from all found shifts
App Level Synchronizations
include User
include Patron
include Session[User]
include Team[User]
include Membership[User, Team]
include HouseholdProfile[Team, Patron, Stock]
include LanguageAudio[Team]
include Stock[Team]
include Shift[Team, User]
registerOrganization(session: Session, name: string)
user = Session[User].user
org = Team[User].createTeam(user, name)
newMembership = Membership[User, Team].getMembership(u).add(user)
Membership[User, Team].editMembership(newMembership, org)
deleteOrganization(orgId: id, editor: User)
user = Session[User].user
{org.admins, org.members} = Team[User].getTeam(orgId)
if user in org.admins:
for u in org.admins | org.members
Remove the org from each u's membership
for shiftId in Shift[Team, User].getShift(orgId):
Delete all shifts created by team with orgId
for household in HouseholdProfile[Team, Patron, Stock].getHousehold(orgId):
Delete all Patron from each household
Delete all household
for stock in Stock[Team].getStockByOwner(orgId):
Delete all stock
for audio in LanguageAudio[Team].getAudioByOwner(orgId):
Delete all audio
Team[User].deleteTeam(org, editor)
getUserOrganizations(session: Session)
user = Session[User].user
return Membership[User, Team].getMembership(user)
registerUser(user: string, pass: string)
User.register(user, pass)
Membership[User, Team].createMembership(u)
deleteUser(session: Session, user: string, pass: string)
u = Session[User].user
User.deleteUser(user, pass)
allMemberships = Membership[User, Team].getMembership(u)
for membership in allMemberships:
Membership[User, Team].deleteMembership(membership)
Team[User].removeMember(membership, user)
Shift[Team, User].unclaimShiftsByUser(u, membership)
addMemberToOrganization(session: Session, org: Team, u: User, admin: boolean)
user = Session[User].user
membership = Membership[User, Team].getMembership(u)
Membership[User, Team].editMembership(membership.add(org))
Team[User].addMember(org, user, u)
if admin: Team[User].promoteMember(org, u, user)
updateMemberStatus(session: Session, org: Team, u: User, status: s)
user = Session[User].user
if status == ADMIN: Team[User].promoteMember(org, u, user)
else: Team[User].demoteMember(org, u, user)
removeMemberFromOrganization(session: Session, org: Team, u: User)
user = Session[User].user
membership = Membership[User, Team].getMembership(u)
Membership[User, Team].editMembership(membership.remove(org))
Team[User].removeMember(org, user, u)
createHouseholdProfile(session: Session, o: Team, name: string, birthday: Date, img: Image, diet: string, lang: string, req: set Stock)
u = Session[User].user
p = Patron.createPatron(n, birthday, img)
if Membership[User, Team].getMembership(u).includes(o):
HouseholdProfile[Team, Patron, Stock].createProfile(o, {p}, diet, lang, request)
updateHouseholdDetails(session: Session, id: string, diet: set string, lang: string, req: set stock)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(id)
if Membership[User, Team].getMembership(u).includes(h.owner):
h = HouseholdProfile[Team, Patron, Stock].updateDetails(id, diet, lang, req)
resetAllVisits(session: Session, orgId: string)
u = Session[User].user
org = Team[User].getTeam(orgId)
if org.admins.includes(u):
for h in HouseholdProfile[Team, Patron, Stock].getProfilesByOwner(org):
HouseholdProfile[Team, Patron, Stock].resetVisits(h)
getHouseholdAndAddVisit(session: Session, id: string)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(id)
if Membership[User, Team].getMembership(u).includes(h.owner):
h = HouseholdProfile[Team, Patron, Stock].addVisit(id)
return h
removeHouseholdProfile(session: Session, householdId: string, patronId: string)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(householdId)
if Membership[User, Team].getMembership(u).includes(h.owner):
for patronId in HouseholdProfile[Team, Patron, Stock] .getProfileById(householdId).members:
p = Patron.deletePatron(patronId)
HouseholdProfile[Team, Patron, Stock].deleteProfile(householdId)
addPatron(session: Session, householdId: string, name: string, birthday: Date, img: Image)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(householdId)
if Membership[User, Team].getMembership(u).includes(h.owner):
p = Patron.createPatron(n, birthday, img)
members = h.members.add(p)
HouseholdProfile[Team, Patron, Stock].updatePatrons(householdId, members)
updatePatron(session: Session, householdId: string, patronId: string, name: string, dob: Date, img: Image)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(householdId)
if Membership[User, Team].getMembership(u).includes(h.owner):
p = Patron.updatePatron(id, name, dob, img)
removePatron(session: Session, householdId: string, patronId: string)
u = Session[User].user
p = Patron.getPatron(patronId)
h = HouseholdProfile[Team, Patron, Stock].getProfileById(householdId)
if Membership[User, Team].getMembership(u).includes(h.owner):
members = h.members.remove(p)
HouseholdProfile[Team, Patron, Stock].updatePatrons(householdId, members)
getOrganizationLanguageAudio(session: Session, o: id)
u = Session[User].user
t = Team[User].getTeam(o)
if t.admin.includes(u) || t.member.includes(u):
return LanguageAudio[Team].getAudioByOwner(o)
addNewAudio(session: Session, orgId: string, language: string, aud: Audio, translation: string)
u = Session[User].user
if Membership[user, Team].getMembership(u).includes(o):
LanguageAudio[Team].createAudio(language, aud, orgId, t)
editAudio(session: Session, audioId: string, language: string, aud: Audio, translation: string)
u = Session[User].user
o = LanguageAudio[Team].getAudioById(audioId).owner
if Membership[user, Team].getMembership(u).includes(o):
LanguageAudio[Team].updateAudio(id, language, aud, translation)
deleteAudio(session: Session, audioId: string)
u = Session[User].user
o = LanguageAudio[Team].getAudioById(audioId).owner
if Membership[user, Team].getMembership(u).includes(o):
LanguageAudio[Team].deleteAudio(audioId)
getOrganizationInventory(session: Session, orgId: string)
u = Session[User].user
org = Team[User].getTeam(orgId)
inventory=Stock.getStockByOwner(org)
return inventory
addItemToInventory(session: Session, orgId: string, i: string, cnt: int, l: string, img: Image, maxp: int)
u = Session[User].user
org = Team[User].getTeam(orgId) //member verification
stocks = Stock.getStockByName(orgId, i)
if item not in stocks:
Stock.createStock(orgId, i, cnt, l, img, maxp)
return
updateStock(stocks[0].id, i, orgId, cnt, l, img, maxp)
removeItemsFromInventory(session: Session, orgId: string, id: string)
u = Session[User].user
org = Team[User].getTeam(orgId)
inventory=Stock.getStockByOwner(org)
stock = Stock.getStockById(id)
if stock in inventory:
Stock.deleteStock(id)
getAllocationByHousehold(session: Session, orgId: string, household: string)
inventory = getOrganizationInventory(session, orgId)
houses = HouseholdProfile.getProfilesByOwner(orgId)
total = 0
for house in houses:
total+= house.members.size()
allocation = new set[Stock,int]
for item in inventory:
allocation.add([item,item.count/total/5])
house = HouseholdProfile.getProfileById(household)
members = house.members.size()
new_allocation = new set[Stock,int]
for [item,cnt] in allocation:
new_allocation.add([item,cnt*members]
return new_allocation
getOrganizationShifts(session: Session, orgId: string)
u = Session[User].user
org = Team[User].getTeam(orgId) //member verification
return Shift.getShiftsByOwner(orgId)
include User
include Patron
include Session[User]
include Team[User]
include Membership[User, Team]
include HouseholdProfile[Team, Patron, Stock]
include LanguageAudio[Team]
include Stock[Team]
include Shift[Team, User]
registerOrganization(session: Session, name: string)
user = Session[User].user
org = Team[User].createTeam(user, name)
newMembership = Membership[User, Team].getMembership(u).add(user)
Membership[User, Team].editMembership(newMembership, org)
deleteOrganization(orgId: id, editor: User)
user = Session[User].user
{org.admins, org.members} = Team[User].getTeam(orgId)
if user in org.admins:
for u in org.admins | org.members
Remove the org from each u's membership
for shiftId in Shift[Team, User].getShift(orgId):
Delete all shifts created by team with orgId
for household in HouseholdProfile[Team, Patron, Stock].getHousehold(orgId):
Delete all Patron from each household
Delete all household
for stock in Stock[Team].getStockByOwner(orgId):
Delete all stock
for audio in LanguageAudio[Team].getAudioByOwner(orgId):
Delete all audio
Team[User].deleteTeam(org, editor)
getUserOrganizations(session: Session)
user = Session[User].user
return Membership[User, Team].getMembership(user)
registerUser(user: string, pass: string)
User.register(user, pass)
Membership[User, Team].createMembership(u)
deleteUser(session: Session, user: string, pass: string)
u = Session[User].user
User.deleteUser(user, pass)
allMemberships = Membership[User, Team].getMembership(u)
for membership in allMemberships:
Membership[User, Team].deleteMembership(membership)
Team[User].removeMember(membership, user)
Shift[Team, User].unclaimShiftsByUser(u, membership)
addMemberToOrganization(session: Session, org: Team, u: User, admin: boolean)
user = Session[User].user
membership = Membership[User, Team].getMembership(u)
Membership[User, Team].editMembership(membership.add(org))
Team[User].addMember(org, user, u)
if admin: Team[User].promoteMember(org, u, user)
updateMemberStatus(session: Session, org: Team, u: User, status: s)
user = Session[User].user
if status == ADMIN: Team[User].promoteMember(org, u, user)
else: Team[User].demoteMember(org, u, user)
removeMemberFromOrganization(session: Session, org: Team, u: User)
user = Session[User].user
membership = Membership[User, Team].getMembership(u)
Membership[User, Team].editMembership(membership.remove(org))
Team[User].removeMember(org, user, u)
createHouseholdProfile(session: Session, o: Team, name: string, birthday: Date, img: Image, diet: string, lang: string, req: set Stock)
u = Session[User].user
p = Patron.createPatron(n, birthday, img)
if Membership[User, Team].getMembership(u).includes(o):
HouseholdProfile[Team, Patron, Stock].createProfile(o, {p}, diet, lang, request)
updateHouseholdDetails(session: Session, id: string, diet: set string, lang: string, req: set stock)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(id)
if Membership[User, Team].getMembership(u).includes(h.owner):
h = HouseholdProfile[Team, Patron, Stock].updateDetails(id, diet, lang, req)
resetAllVisits(session: Session, orgId: string)
u = Session[User].user
org = Team[User].getTeam(orgId)
if org.admins.includes(u):
for h in HouseholdProfile[Team, Patron, Stock].getProfilesByOwner(org):
HouseholdProfile[Team, Patron, Stock].resetVisits(h)
getHouseholdAndAddVisit(session: Session, id: string)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(id)
if Membership[User, Team].getMembership(u).includes(h.owner):
h = HouseholdProfile[Team, Patron, Stock].addVisit(id)
return h
removeHouseholdProfile(session: Session, householdId: string, patronId: string)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(householdId)
if Membership[User, Team].getMembership(u).includes(h.owner):
for patronId in HouseholdProfile[Team, Patron, Stock] .getProfileById(householdId).members:
p = Patron.deletePatron(patronId)
HouseholdProfile[Team, Patron, Stock].deleteProfile(householdId)
addPatron(session: Session, householdId: string, name: string, birthday: Date, img: Image)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(householdId)
if Membership[User, Team].getMembership(u).includes(h.owner):
p = Patron.createPatron(n, birthday, img)
members = h.members.add(p)
HouseholdProfile[Team, Patron, Stock].updatePatrons(householdId, members)
updatePatron(session: Session, householdId: string, patronId: string, name: string, dob: Date, img: Image)
u = Session[User].user
h = HouseholdProfile[Team, Patron, Stock].getProfileById(householdId)
if Membership[User, Team].getMembership(u).includes(h.owner):
p = Patron.updatePatron(id, name, dob, img)
removePatron(session: Session, householdId: string, patronId: string)
u = Session[User].user
p = Patron.getPatron(patronId)
h = HouseholdProfile[Team, Patron, Stock].getProfileById(householdId)
if Membership[User, Team].getMembership(u).includes(h.owner):
members = h.members.remove(p)
HouseholdProfile[Team, Patron, Stock].updatePatrons(householdId, members)
getOrganizationLanguageAudio(session: Session, o: id)
u = Session[User].user
t = Team[User].getTeam(o)
if t.admin.includes(u) || t.member.includes(u):
return LanguageAudio[Team].getAudioByOwner(o)
addNewAudio(session: Session, orgId: string, language: string, aud: Audio, translation: string)
u = Session[User].user
if Membership[user, Team].getMembership(u).includes(o):
LanguageAudio[Team].createAudio(language, aud, orgId, t)
editAudio(session: Session, audioId: string, language: string, aud: Audio, translation: string)
u = Session[User].user
o = LanguageAudio[Team].getAudioById(audioId).owner
if Membership[user, Team].getMembership(u).includes(o):
LanguageAudio[Team].updateAudio(id, language, aud, translation)
deleteAudio(session: Session, audioId: string)
u = Session[User].user
o = LanguageAudio[Team].getAudioById(audioId).owner
if Membership[user, Team].getMembership(u).includes(o):
LanguageAudio[Team].deleteAudio(audioId)
getOrganizationInventory(session: Session, orgId: string)
u = Session[User].user
org = Team[User].getTeam(orgId)
inventory=Stock.getStockByOwner(org)
return inventory
addItemToInventory(session: Session, orgId: string, i: string, cnt: int, l: string, img: Image, maxp: int)
u = Session[User].user
org = Team[User].getTeam(orgId) //member verification
stocks = Stock.getStockByName(orgId, i)
if item not in stocks:
Stock.createStock(orgId, i, cnt, l, img, maxp)
return
updateStock(stocks[0].id, i, orgId, cnt, l, img, maxp)
removeItemsFromInventory(session: Session, orgId: string, id: string)
u = Session[User].user
org = Team[User].getTeam(orgId)
inventory=Stock.getStockByOwner(org)
stock = Stock.getStockById(id)
if stock in inventory:
Stock.deleteStock(id)
getAllocationByHousehold(session: Session, orgId: string, household: string)
inventory = getOrganizationInventory(session, orgId)
houses = HouseholdProfile.getProfilesByOwner(orgId)
total = 0
for house in houses:
total+= house.members.size()
allocation = new set[Stock,int]
for item in inventory:
allocation.add([item,item.count/total/5])
house = HouseholdProfile.getProfileById(household)
members = house.members.size()
new_allocation = new set[Stock,int]
for [item,cnt] in allocation:
new_allocation.add([item,cnt*members]
return new_allocation
getOrganizationShifts(session: Session, orgId: string)
u = Session[User].user
org = Team[User].getTeam(orgId) //member verification
return Shift.getShiftsByOwner(orgId)
Dependency Diagram
Wireframes
Heuristic Evaluation
Usability
Error Tolerance
- To enhance error tolerance and user awareness, the delete organization button currently shown with an 'x' symbol will be replaced by a conspicuous red button, acting as a visual cue to signal caution and prompt users to think twice before proceeding. Complementing this change, all delete-related actions will trigger confirmation prompts, adding an extra layer of protection against accidental deletions.
- Most actions can be easily reverted in case of user error with no consequence, such as changing a member's status between volunteer and user, adding and deleting volunteers, and adding and removing inventory units. This allows users convenient and efficient ways to handle recovery when an error occurs. However, it will also make it harder to detect any errors quickly, and make it easier to commit erroneous actions in the first place.
Efficiency
- To enhance efficiency and seamless user navigation, the app will implement a drop down that allows users to switch between organizations from the navigation bar, without navigating to the organization page itself. This approach not only enhances efficiency but also provides users with a quick visual indicator of which organization's inventories and patrons they are working with. However, this might be a tradeoff with Fitts Law because it might make the nav bar even more crowded than it already is and makes it harder to quickly get to the smaller links near it.
- To improve the efficiency of the organization's patron experience, the audio recordings in each patron's preferred language are available right on the household's patron page, so volunteers will be able to immediately access the necessary recordings without needing to navigate to a special page for all audio files. This will ensure a more streamlined, efficient process for food distribution. However, this may cause the page to lag because we are storing audio files and would introduce the need to implement something for perceptual fusion to account for time delays.
Accessibility
- The different language options for audio recordings should be displayed in that language, rather than just in English. This will allow for patrons who cannot read English to point to the desired language for the volunteer to play the recording, if necessary.
Physical Heuristics
Mapping
- There could be some confusion about the placement of the functionalities relating to managing the members and their statuses in the organization. Currently, these functionalities are under Settings, but it would be more understandable if actions regarding people went under Organization. Moreover, moving them under the selected food pantry name in Organization would make it clearer that the volunteers belong to this specific organization.
Situational Context
- While admin vs. volunteer separation is an important part of the functionality of the app, it may be difficult to determine which role a user is based solely on what the app conveys. This is important especially if a user is an admin at some organizations, and a volunteer at others, for example. To mitigate this issue, we can create some visual differences between the app's state when the user is an admin at the currently-selected organization versus when the user is a volunteer. Also, functionality only available for admins (e.g. resetting all patron visits) should not appear on the volunteer side. However, this may result in less consistency, since the two states will have some different functionalities and possibly appear slightly different visually.
- For updating an organization member's status, the app could do a better job of conveying the member's current state by not displaying both the option to promote and the option to demote, but rather only the action that corresponds to the specific member's current status.
- The wireframes context well in the form of pop-ups for adding or updating information, for example, in updating units on an inventory item or editing the status of an organization member; since the background behind the modal will still be the page that the user originally navigated from, where the user currently is will be easy to extract.
Linguistic Level
Consistency
- The interface is inconsistent with the usage of words such as "add," "create," and "new." For example, we have a "New Organization" button, an "Add Item" button, and a "Create" button for new inventory. Using more consistent terminology for these similar actions will result in a more intuitive user experience.
- Our symbols and icons, such as a microphone for audio recordings, a shopping cart for inventory items, and a pencil leading to an edit page, are consistent throughout our app, as well as throughout other platforms across several domains. This will make the navigation of our app smoother and more learnable for users.
- Throughout the app, destructive buttons, such as cancel, delete, and remove, are in gray on the left, while constructive buttons, such as add, update, and create, or shown in red on the right. This provides consistency across the different actions in the app, allowing users to gain more intuition about which button to press without relying on reading the labels. However, by making the pages more consistent, and the buttons more similar, we are reducing situational context.
Speak a user's language
- Currently, all inputs of patrons or volunteers are communicated through ID, which will be a string of numbers that identify a household or member. This is less understandable for users, however, since volunteers are likely to recognize patrons based on their appearance and name, not by their ID. It is also more error-prone, since it is easy to make a mistake in typing in a long string of numbers. It may be better to have a search feature that allows for ID inputs or household name/username inputs. However, this is a security tradeoff as volunteers can more easily search up targeted people and update their profiles more easily without an ID. This would contribute to discrimination as volunteers can mark people as visited or change their dietary restrictions without the person being there to prevent it.
- Conventionally, red is associated with destructive or dangerous actions, such as removal, deletion, or cancellation. However, we use red to signify positive/constructive actions, such as updates, additions, and creations. This may be unintuitive to users and may thereby cause accidental destructive actions to occur. Therefore we can change the color of the buttons.
Visual Design Study
Plan your Implementation
Week 1
Concept | Tasks | Assigned |
---|---|---|
Team | Create, edit, delete functions | Amanda |
Add, promote, demote, remove member functions | Amanda | |
Test functions | Peilin | |
Membership | Create, get, edit, delete membership functions | Amanda |
Test functions | Andrew | |
HouseholdProfile | Create, get (by owner, by ID), updateDetails, deleteProfile functions | Andrew |
updatePatrons, addVisit, resetVisits | Andrew | |
Test functions | Peilin | |
Patron | Create, get, update, delete patron functions | Peilin |
Test functions | Anna | |
Stock | Create, update, delete | Anna |
Get (by owner, name, ID) | Anna | |
Test functions | Amanda | |
Syncs | Team and Membership only | Amanda |
HouseholdProfile, Patron, Stock, Team, and Membership | Anna, Peilin | |
Test Syncs | Andrew | |
General Frontend | Discuss general theme of website (color, font, button styles) | All |
Homepage+Navbar | Peilin | |
Create Account / Sign In Page | Anna | |
Organization Page | Amanda | |
Component for each member | Amanda | |
Modal to promote/demote members | Andrew | |
Add/remove member | Amanda |
Week 2
Concept/Components | Tasks | Assigned |
---|---|---|
Shift | Create, delete, get shift functions | Anna |
Claim, unclaim, unclaimShiftsByUser functions | Anna | |
Test Functions | Andrew | |
LanguageAudio | All functions | Amanda |
Test Functions | Peilin | |
Syncs | Finish all Syncs | Andrew |
Test Syncs | Anna | |
Inventory Page | Stock Card component | Amanda |
Edit Inventory Modal | Andrew | |
Create New Stock Page | Anna | |
Sorting, Search Bar, and Styling | Peilin | |
Patron Page | Patron Component | Peilin |
Household Info Component | Amanda | |
Update Component | Anna | |
TimeSheet Page | Timesheet Table | Andrew |
Create Shift/Delete Shift Modal | Anna | |
Claim Shift/Unclaim Shift Actions | Peilin | |
Styling | Amanda | |
Media | Photo | Andrew |
Audio | Amanda |
Week 3
Tasks | Assigned |
---|---|
Populate app with test data | Anna, Andrew |
User Feedback | All |
Styling | Peilin, Amanda |
Finalize app | All |
Contingency Plans
In the event of unforeseen challenges during implementation, our strategic approach involves a meticulous reassessment of priorities to ensure a successful delivery of our app. In particular, we will consider the removal of less pivotal functionalities, such as shifts, due to their isolated nature. In particular, we will forego the features with more technical intricacies, such as the calendar for creating and claiming shifts. Furthermore, we will deprioritize analytics charts for donations, patron tracking, and inventory distribution, allowing us to concentrate on refining the app's essential functionalities. In the face of potential difficulties with MongoDB and technical features like storing and displaying media files such as images and audio, our contingency plan involves presenting images and audio recordings as accessible links instead. This comprehensive strategy underscores our commitment to adaptability and resilience, with the ultimate goal of delivering a robust and user-friendly application.