Fairly massive overhaul of the Profile view; standardize tabs etc.

much cleaner and more consistent interface now, between the main
ProfileInfo component, and various *Tab components

also cleaner interface between client-side JS and server view methods

to my knowledge this is complete and breaks nothing..we'll see!
This commit is contained in:
Lance Edgar 2023-08-28 20:43:31 -05:00
parent 844c629a6a
commit 12e4779093
5 changed files with 1234 additions and 1019 deletions

View file

@ -36,6 +36,20 @@
</b-field>
</div>
<h3 class="block is-size-3">Relationships</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="By default a Person may have multiple Member accounts.">
<b-checkbox name="rattail.members.max_one_per_person"
v-model="simpleSettings['rattail.members.max_one_per_person']"
native-value="true"
@input="settingsNeedSaved = true">
Limit one (1) Member account per Person
</b-checkbox>
</b-field>
</div>
</%def>
<%def name="modify_this_page_vars()">

View file

@ -38,7 +38,12 @@
},
computed: {},
watch: {},
methods: {},
methods: {
changeContentTitle(newTitle) {
this.$emit('change-content-title', newTitle)
},
},
}
let ThisPageData = {

File diff suppressed because it is too large Load diff

View file

@ -390,6 +390,11 @@ class MemberView(MasterView):
{'section': 'rattail',
'option': 'members.straight_to_profile',
'type': bool},
# Relationships
{'section': 'rattail',
'option': 'members.max_one_per_person',
'type': bool},
]

View file

@ -455,63 +455,81 @@ class PersonView(MasterView):
self.viewing = True
app = self.get_rattail_app()
person = self.get_instance()
employee = app.get_employee(person)
context = {
'person': person,
'instance': person,
'instance_title': self.get_instance_title(person),
'today': localtime(self.rattail_config).date(),
'dynamic_content_title': self.get_context_content_title(person),
'tabchecks': self.get_context_tabchecks(person),
'person_data': self.get_context_person(person),
'phone_type_options': self.get_phone_type_options(),
'email_type_options': self.get_email_type_options(),
'max_lengths': self.get_max_lengths(),
'customers_data': self.get_context_customers(person),
# TODO: deprecate / remove this
'customer_xref_buttons': self.get_customer_xref_buttons(person),
'expose_customer_people': self.customers_should_expose_people(),
'expose_customer_shoppers': self.customers_should_expose_shoppers(),
'members_data': self.get_context_members(person),
'member_xref_buttons': self.get_member_xref_buttons(person),
'employee': employee,
'employee_data': self.get_context_employee(employee) if employee else {},
'employee_view_url': self.request.route_url('employees.view', uuid=employee.uuid) if employee else None,
'employee_history': employee.get_current_history() if employee else None,
'employee_history_data': self.get_context_employee_history(employee),
'notes_data': self.get_context_notes(person),
'note_type_options': self.get_note_type_options(),
'users_data': self.get_context_users(person),
'dynamic_content_title': self.get_context_content_title(person),
'max_one_member': app.get_membership_handler().max_one_per_person(),
}
if context['expose_customer_shoppers']:
shoppers = person.customer_shoppers
# TODO: what a hack! surely this belongs in handler at least..?
shoppers = [shopper for shopper in shoppers
if shopper.shopper_number != 1]
context['shoppers_data'] = self.get_context_shoppers(shoppers)
if self.request.has_perm('people_profile.view_versions'):
context['revisions_grid'] = self.profile_revisions_grid(person)
template = 'view_profile_buefy'
return self.render_to_response(template, context)
return self.render_to_response('view_profile_buefy', context)
# TODO: deprecate / remove this
def get_customer_xref_buttons(self, person):
buttons = []
for supp in self.iter_view_supplements():
if hasattr(supp, 'get_customer_xref_buttons'):
buttons.extend(supp.get_customer_xref_buttons(person) or [])
buttons = self.normalize_xref_buttons(buttons)
return buttons
def get_context_tabchecks(self, person):
app = self.get_rattail_app()
membership = app.get_membership_handler()
clientele = app.get_clientele_handler()
tabchecks = {}
def get_member_xref_buttons(self, person):
buttons = []
for supp in self.iter_view_supplements():
if hasattr(supp, 'get_member_xref_buttons'):
buttons.extend(supp.get_member_xref_buttons(person) or [])
buttons = self.normalize_xref_buttons(buttons)
return buttons
# TODO: for efficiency, should only calculate checks for tabs
# actually in use by app..(how) should that be configurable?
# personal
tabchecks['personal'] = True
# member
if membership.max_one_per_person():
member = app.get_member(person)
tabchecks['member'] = bool(member and member.active)
else:
members = membership.get_members_for_account_holder(person)
tabchecks['member'] = any([m.active for m in members])
# customer
customers = clientele.get_customers_for_account_holder(person)
tabchecks['customer'] = bool(customers)
# shopper
# TODO: what a hack! surely some of this belongs in handler
shoppers = person.customer_shoppers
shoppers = [shopper for shopper in shoppers
if shopper.shopper_number != 1]
tabchecks['shopper'] = bool(shoppers)
# employee
employee = app.get_employee(person)
tabchecks['employee'] = bool(employee and employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)
# notes
tabchecks['notes'] = bool(person.notes)
# user
tabchecks['user'] = bool(person.users)
return tabchecks
def profile_changed_response(self, person):
"""
Return common context result for all AJAX views which may
change the profile details. This is enough to update the
page-wide things, and let other tabs know they should be
refreshed when next displayed.
"""
return {
'person': self.get_context_person(person),
'tabchecks': self.get_context_tabchecks(person),
}
def template_kwargs_view_profile(self, **kwargs):
"""
@ -716,10 +734,12 @@ class PersonView(MasterView):
def get_context_member(self, member):
app = self.get_rattail_app()
person = app.get_person(member)
profile_url = None
if member.person:
if person:
profile_url = self.request.route_url('people.view_profile',
uuid=member.person_uuid)
uuid=person.uuid)
key = self.get_member_key_field()
equity_total = sum([payment.amount for payment in member.equity_payments])
@ -739,6 +759,7 @@ class PersonView(MasterView):
'view_url': self.request.route_url('members.view', uuid=member.uuid),
'view_profile_url': profile_url,
'equity_total_display': app.render_currency(equity_total),
'external_links': [],
}
membership_type = member.membership_type
@ -825,6 +846,19 @@ class PersonView(MasterView):
customer = handler.ensure_customer(person)
return customer
def profile_tab_personal(self):
"""
Fetch personal tab data for profile view.
"""
# TODO: no need to return primary person data, since that
# always comes back via normal profile_changed_response()
# ..so for now this is a no-op..
# person = self.get_instance()
return {
# 'person': self.get_context_person(person),
}
def profile_edit_name(self):
"""
View which allows a person's name to be updated.
@ -838,11 +872,7 @@ class PersonView(MasterView):
last=data['last_name'])
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
'dynamic_content_title': self.get_context_content_title(person),
}
return self.profile_changed_response(person)
def get_context_phones(self, person):
data = []
@ -872,10 +902,7 @@ class PersonView(MasterView):
return {'error': simple_error(error)}
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
}
return self.profile_changed_response(person)
def profile_update_phone(self):
"""
@ -902,10 +929,7 @@ class PersonView(MasterView):
return {'error': simple_error(error)}
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
}
return self.profile_changed_response(person)
def profile_delete_phone(self):
"""
@ -925,10 +949,7 @@ class PersonView(MasterView):
person.remove_phone(phone)
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
}
return self.profile_changed_response(person)
def profile_set_preferred_phone(self):
"""
@ -948,10 +969,7 @@ class PersonView(MasterView):
person.set_primary_phone(phone)
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
}
return self.profile_changed_response(person)
def get_context_emails(self, person):
data = []
@ -987,10 +1005,7 @@ class PersonView(MasterView):
return {'error': simple_error(error)}
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
}
return self.profile_changed_response(person)
def profile_update_email(self):
"""
@ -1013,10 +1028,7 @@ class PersonView(MasterView):
return {'error': simple_error(error)}
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
}
return self.profile_changed_response(person)
def profile_delete_email(self):
"""
@ -1036,11 +1048,7 @@ class PersonView(MasterView):
person.remove_email(email)
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
}
return self.profile_changed_response(person)
def profile_set_preferred_email(self):
"""
@ -1060,10 +1068,7 @@ class PersonView(MasterView):
person.set_primary_email(email)
self.Session.flush()
return {
'success': True,
'person': self.get_context_person(person),
}
return self.profile_changed_response(person)
def profile_edit_address(self):
"""
@ -1077,9 +1082,66 @@ class PersonView(MasterView):
self.people_handler.update_address(person, address, **data)
self.Session.flush()
return self.profile_changed_response(person)
def profile_tab_member(self):
"""
Fetch member tab data for profile view.
"""
app = self.get_rattail_app()
membership = app.get_membership_handler()
person = self.get_instance()
max_one_member = membership.max_one_per_person()
context = {
'max_one_member': max_one_member,
}
if max_one_member:
member = app.get_member(person)
context['member'] = {'exists': bool(member)}
if member:
context['member'].update(self.get_context_member(member))
else:
context['members'] = self.get_context_members(person)
return context
def profile_tab_customer(self):
"""
Fetch customer tab data for profile view.
"""
person = self.get_instance()
return {
'success': True,
'person': self.get_context_person(person),
'customers': self.get_context_customers(person),
}
def profile_tab_shopper(self):
"""
Fetch shopper tab data for profile view.
"""
person = self.get_instance()
# TODO: what a hack! surely some of this belongs in handler
shoppers = person.customer_shoppers
shoppers = [shopper for shopper in shoppers
if shopper.shopper_number != 1]
return {
'shoppers': self.get_context_shoppers(shoppers),
}
def profile_tab_employee(self):
"""
Fetch employee tab data for profile view.
"""
app = self.get_rattail_app()
person = self.get_instance()
employee = app.get_employee(person)
return {
'employee': self.get_context_employee(employee) if employee else {},
'employee_history': self.get_context_employee_history(employee),
}
def profile_start_employee(self):
@ -1099,18 +1161,7 @@ class PersonView(MasterView):
employee = handler.begin_employment(person, start_date,
employee_id=data['id'])
self.Session.flush()
return self.profile_start_employee_result(employee, start_date)
def profile_start_employee_result(self, employee, start_date):
person = employee.person
return {
'success': True,
'employee': self.get_context_employee(employee),
'employee_view_url': self.request.route_url('employees.view', uuid=employee.uuid),
'start_date': str(start_date),
'employee_history_data': self.get_context_employee_history(employee),
'dynamic_content_title': self.get_context_content_title(person),
}
return self.profile_changed_response(person)
def profile_end_employee(self):
"""
@ -1130,18 +1181,7 @@ class PersonView(MasterView):
handler.end_employment(employee, end_date,
revoke_access=data.get('revoke_access'))
self.Session.flush()
return self.profile_end_employee_result(employee, end_date)
def profile_end_employee_result(self, employee, end_date):
person = employee.person
return {
'success': True,
'employee': self.get_context_employee(employee),
'employee_view_url': self.request.route_url('employees.view', uuid=employee.uuid),
'end_date': str(end_date),
'employee_history_data': self.get_context_employee_history(employee),
'dynamic_content_title': self.get_context_content_title(person),
}
return self.profile_changed_response(person)
def profile_edit_employee_history(self):
"""
@ -1167,14 +1207,7 @@ class PersonView(MasterView):
history.end_date = end_date
self.Session.flush()
current_history = employee.get_current_history()
return {
'success': True,
'employee': self.get_context_employee(employee),
'start_date': str(current_history.start_date),
'end_date': str(current_history.end_date or ''),
'employee_history_data': self.get_context_employee_history(employee),
}
return self.profile_changed_response(person)
def profile_update_employee_id(self):
"""
@ -1188,11 +1221,27 @@ class PersonView(MasterView):
data = self.request.json_body
employee.id = data['employee_id']
self.Session.flush()
self.Session.flush()
return self.profile_changed_response(person)
def profile_tab_notes(self):
"""
Fetch notes tab data for profile view.
"""
person = self.get_instance()
return {
'success': True,
'employee': self.get_context_employee(employee),
'notes': self.get_context_notes(person),
'note_types': self.get_note_type_options(),
}
def profile_tab_user(self):
"""
Fetch user tab data for profile view.
"""
person = self.get_instance()
return {
'users': self.get_context_users(person),
}
def profile_revisions_grid(self, person):
@ -1413,12 +1462,12 @@ class PersonView(MasterView):
def profile_add_note(self):
person = self.get_instance()
form = self.make_note_form('create', person)
if form.validate():
note = self.create_note(person, form)
self.Session.flush()
return self.profile_add_note_success(note)
else:
return self.profile_add_note_failure(person, form)
if not form.validate():
return {'error': str(form.make_deform_form().error)}
note = self.create_note(person, form)
self.Session.flush()
return self.profile_changed_response(person)
def create_note(self, person, form):
note = model.PersonNote()
@ -1429,25 +1478,15 @@ class PersonView(MasterView):
person.notes.append(note)
return note
def profile_add_note_success(self, note, person=None):
return {
'notes': self.get_context_notes(person or note.person),
}
def profile_add_note_failure(self, person, form):
return {
'error': str(form.make_deform_form().error),
}
def profile_edit_note(self):
person = self.get_instance()
form = self.make_note_form('edit', person)
if form.validate():
note = self.update_note(person, form)
self.Session.flush()
return self.profile_edit_note_success(note)
else:
return self.profile_edit_note_failure(person, form)
if not form.validate():
return {'error': str(form.make_deform_form().error)}
note = self.update_note(person, form)
self.Session.flush()
return self.profile_changed_response(person)
def update_note(self, person, form):
note = self.Session.get(model.PersonNote, form.validated['uuid'])
@ -1455,32 +1494,20 @@ class PersonView(MasterView):
note.text = form.validated['note_text']
return note
def profile_edit_note_success(self, note):
return self.profile_add_note_success(note)
def profile_edit_note_failure(self, person, form):
return self.profile_add_note_failure(person, form)
def profile_delete_note(self):
person = self.get_instance()
form = self.make_note_form('delete', person)
if form.validate():
self.delete_note(person, form)
self.Session.flush()
return self.profile_delete_note_success(person)
else:
return self.profile_delete_note_failure(person, form)
if not form.validate():
return {'error': str(form.make_deform_form().error)}
self.delete_note(person, form)
self.Session.flush()
return self.profile_changed_response(person)
def delete_note(self, person, form):
note = self.Session.get(model.PersonNote, form.validated['uuid'])
self.Session.delete(note)
def profile_delete_note_success(self, person):
return self.profile_add_note_success(None, person=person)
def profile_delete_note_failure(self, person, form):
return self.profile_add_note_failure(person, form)
def make_user(self):
uuid = self.request.POST['person_uuid']
person = self.Session.get(model.Person, uuid)
@ -1553,6 +1580,14 @@ class PersonView(MasterView):
config.add_view(cls, attr='view_profile', route_name='{}.view_profile'.format(route_prefix),
permission='{}.view_profile'.format(permission_prefix))
# profile - refresh personal tab
config.add_route(f'{route_prefix}.profile_tab_personal',
f'{instance_url_prefix}/profile/tab-personal',
request_method='GET')
config.add_view(cls, attr='profile_tab_personal',
route_name=f'{route_prefix}.profile_tab_personal',
renderer='json')
# profile - edit personal details
config.add_tailbone_permission('people_profile',
'people_profile.edit_person',
@ -1648,6 +1683,38 @@ class PersonView(MasterView):
renderer='json',
permission='people_profile.edit_person')
# profile - refresh member tab
config.add_route(f'{route_prefix}.profile_tab_member',
f'{instance_url_prefix}/profile/tab-member',
request_method='GET')
config.add_view(cls, attr='profile_tab_member',
route_name=f'{route_prefix}.profile_tab_member',
renderer='json')
# profile - refresh customer tab
config.add_route(f'{route_prefix}.profile_tab_customer',
f'{instance_url_prefix}/profile/tab-customer',
request_method='GET')
config.add_view(cls, attr='profile_tab_customer',
route_name=f'{route_prefix}.profile_tab_customer',
renderer='json')
# profile - refresh shopper tab
config.add_route(f'{route_prefix}.profile_tab_shopper',
f'{instance_url_prefix}/profile/tab-shopper',
request_method='GET')
config.add_view(cls, attr='profile_tab_shopper',
route_name=f'{route_prefix}.profile_tab_shopper',
renderer='json')
# profile - refresh employee tab
config.add_route(f'{route_prefix}.profile_tab_employee',
f'{instance_url_prefix}/profile/tab-employee',
request_method='GET')
config.add_view(cls, attr='profile_tab_employee',
route_name=f'{route_prefix}.profile_tab_employee',
renderer='json')
# profile - start employee
config.add_route('{}.profile_start_employee'.format(route_prefix), '{}/profile/start-employee'.format(instance_url_prefix),
request_method='POST')
@ -1675,6 +1742,22 @@ class PersonView(MasterView):
renderer='json',
permission='employees.edit')
# profile - refresh notes tab
config.add_route(f'{route_prefix}.profile_tab_notes',
f'{instance_url_prefix}/profile/tab-notes',
request_method='GET')
config.add_view(cls, attr='profile_tab_notes',
route_name=f'{route_prefix}.profile_tab_notes',
renderer='json')
# profile - refresh user tab
config.add_route(f'{route_prefix}.profile_tab_user',
f'{instance_url_prefix}/profile/tab-user',
request_method='GET')
config.add_view(cls, attr='profile_tab_user',
route_name=f'{route_prefix}.profile_tab_user',
renderer='json')
# profile - revisions data
config.add_tailbone_permission('people_profile',
'people_profile.view_versions',