diff --git a/app/models/user.py b/app/models/user.py index 838ae78..a1a982c 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -12,6 +12,7 @@ class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100), unique=True, nullable=False, index=True) + full_name = db.Column(db.String(150), nullable=True) email = db.Column(db.String(255), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(255), nullable=False) role = db.Column( @@ -37,5 +38,10 @@ class User(UserMixin, db.Model): def check_password(self, password): return check_password_hash(self.password_hash, password) + @property + def display_name(self): + """Return full name if set, otherwise fall back to username.""" + return self.full_name.strip() if self.full_name and self.full_name.strip() else self.username + def __repr__(self): return f'' diff --git a/app/routes/auth.py b/app/routes/auth.py index 7754a25..b233481 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -74,6 +74,7 @@ def profile(): form = ProfileForm(user=current_user, obj=current_user) if form.validate_on_submit(): + current_user.full_name = form.full_name.data.strip() or None current_user.email = form.email.data if form.new_password.data: @@ -134,6 +135,7 @@ def create_user(): if form.validate_on_submit(): user = User( username=form.username.data, + full_name=form.full_name.data.strip() or None, email=form.email.data, role=form.role.data ) @@ -158,9 +160,10 @@ def edit_user(user_id): form = UserForm(user=user, obj=user) if form.validate_on_submit(): - user.username = form.username.data - user.email = form.email.data - user.role = form.role.data + user.username = form.username.data + user.full_name = form.full_name.data.strip() or None + user.email = form.email.data + user.role = form.role.data if form.password.data: user.set_password(form.password.data) diff --git a/app/routes/customers.py b/app/routes/customers.py index 4f05bb0..951ea5b 100644 --- a/app/routes/customers.py +++ b/app/routes/customers.py @@ -107,10 +107,11 @@ def create(): if form.validate_on_submit(): user = User( - username = form.username.data, - email = form.email.data, - role = 'customer', - active = True, + username = form.username.data, + full_name = form.full_name.data.strip() or None, + email = form.email.data, + role = 'customer', + active = True, ) user.set_password(form.password.data) db.session.add(user) @@ -139,8 +140,9 @@ def edit(customer_id): form = CustomerUserForm(user=customer, obj=customer) if form.validate_on_submit(): - customer.username = form.username.data - customer.email = form.email.data + customer.username = form.username.data + customer.full_name = form.full_name.data.strip() or None + customer.email = form.email.data if form.password.data: customer.set_password(form.password.data) db.session.commit() diff --git a/app/templates/auth/profile.html b/app/templates/auth/profile.html index a86faf0..c921aaf 100644 --- a/app/templates/auth/profile.html +++ b/app/templates/auth/profile.html @@ -23,7 +23,10 @@ -

{{ current_user.username }}

+

{{ current_user.display_name }}

+ {% if current_user.full_name %} +

@{{ current_user.username }}

+ {% endif %}

{{ current_user.email }}

@@ -92,6 +95,15 @@
{{ form.hidden_tag() }} + +
+ {{ form.full_name.label(class="form-label fw-semibold") }} + {{ form.full_name(class="form-control", placeholder="e.g. Jane Smith") }} + {% for error in form.full_name.errors %} +
{{ error }}
+ {% endfor %} +
+
{{ form.email.label(class="form-label fw-semibold") }} diff --git a/app/templates/auth/user_form.html b/app/templates/auth/user_form.html index 500b8fb..5ebd61f 100644 --- a/app/templates/auth/user_form.html +++ b/app/templates/auth/user_form.html @@ -56,6 +56,18 @@ {% endif %}
+
+ {{ form.full_name.label(class="form-label") }} + {{ form.full_name(class="form-control", placeholder="e.g. Jane Smith") }} + {% if form.full_name.errors %} +
+ {% for error in form.full_name.errors %}{{ error }}{% endfor %} +
+ {% endif %} +
+ + +
{{ form.email.label(class="form-label") }} {{ form.email(class="form-control") }} diff --git a/app/templates/auth/users.html b/app/templates/auth/users.html index c55fd80..9487e93 100644 --- a/app/templates/auth/users.html +++ b/app/templates/auth/users.html @@ -21,6 +21,7 @@ Username + Full Name Email Role Created @@ -32,6 +33,7 @@ {% for user in users %} {{ user.username }} + {{ user.full_name or '—' }} {{ user.email }} diff --git a/app/templates/customers/form.html b/app/templates/customers/form.html index 22d60c6..84ea766 100644 --- a/app/templates/customers/form.html +++ b/app/templates/customers/form.html @@ -52,6 +52,18 @@
{% endif %}
+
+ {{ form.full_name.label(class="form-label") }} + {{ form.full_name(class="form-control", placeholder="e.g. Jane Smith") }} + {% if form.full_name.errors %} +
+ {% for e in form.full_name.errors %}{{ e }}{% endfor %} +
+ {% endif %} +
+ + +
{{ form.email.label(class="form-label") }} {{ form.email(class="form-control") }} diff --git a/app/templates/customers/index.html b/app/templates/customers/index.html index c0b214e..c4a0d66 100644 --- a/app/templates/customers/index.html +++ b/app/templates/customers/index.html @@ -25,6 +25,7 @@ Username + Full Name Email Status Assigned Projects @@ -46,6 +47,7 @@ + {{ customer.full_name or '—' }} {{ customer.email }} {% if customer.active %} diff --git a/app/utils/forms.py b/app/utils/forms.py index 72f9213..b16588d 100644 --- a/app/utils/forms.py +++ b/app/utils/forms.py @@ -18,6 +18,7 @@ class LoginForm(FlaskForm): class ProfileForm(FlaskForm): """Self-service profile update form — available to all authenticated users.""" + full_name = StringField('Full Name', validators=[Optional(), Length(max=150)]) email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)]) current_password = PasswordField('Current Password', validators=[Optional()]) new_password = PasswordField('New Password', validators=[Optional(), Length(min=6, max=100)]) @@ -45,6 +46,7 @@ class ProfileForm(FlaskForm): class UserForm(FlaskForm): username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)]) + full_name = StringField('Full Name', validators=[Optional(), Length(max=150)]) email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)]) password = PasswordField('Password', validators=[Optional(), Length(min=6, max=100)]) confirm_password = PasswordField('Confirm Password', validators=[Optional(), EqualTo('password')]) @@ -101,7 +103,7 @@ class FacilityForm(FlaskForm): class AreaForm(FlaskForm): name = StringField('Area Name', validators=[DataRequired(), Length(max=255)]) area_type = SelectField('Area Type', choices=[ - ('building','Building'), ('restroom','Restroom'), ('lobby','Lobby'), ('hallway','Hallway'), + ('restroom','Restroom'), ('lobby','Lobby'), ('hallway','Hallway'), ('office','Office'), ('kitchen','Kitchen'), ('storage','Storage'), ('outdoor','Outdoor'), ('other','Other'), ], validators=[Optional()]) @@ -198,6 +200,7 @@ class CustomerUserForm(FlaskForm): Password is required on create; optional on edit. """ username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)]) + full_name = StringField('Full Name', validators=[Optional(), Length(max=150)]) email = StringField('Email', validators=[DataRequired(), Email(), Length(max=255)]) password = PasswordField('Password', validators=[Optional(), Length(min=8)]) confirm_password = PasswordField('Confirm Password', diff --git a/migrations/versions/phase9_user_full_name.py b/migrations/versions/phase9_user_full_name.py new file mode 100644 index 0000000..4346881 --- /dev/null +++ b/migrations/versions/phase9_user_full_name.py @@ -0,0 +1,29 @@ +"""Phase 9: Add full_name column to users table + +Revision ID: phase9_user_full_name +Revises: phase8_notification_matrix +Create Date: 2026-04-02 +""" +from alembic import op +import sqlalchemy as sa + +revision = 'phase9_user_full_name' +down_revision = 'phase8_notification_matrix' +branch_labels = None +depends_on = None + + +def upgrade(): + bind = op.get_bind() + inspector = sa.inspect(bind) + columns = [c['name'] for c in inspector.get_columns('users')] + + if 'full_name' not in columns: + op.add_column( + 'users', + sa.Column('full_name', sa.String(150), nullable=True, default=None), + ) + + +def downgrade(): + op.drop_column('users', 'full_name')