forked from jamesp/sasa-membership
Add member profile questions, admin tooling, legal pages, and fast tests
- Add configurable profile questions with conditional visibility, admin-only fields, user answers, and seeded onboarding/volunteer questions
- Add admin UI for managing profile questions and member profile answers
- Add volunteer level/profile data support across backend schemas, models, API, and migration
- Update dashboard/profile UI, super admin menu, membership service types, and related styling
- Add privacy policy, terms of service, cookie notice, and footer links
- Add frontend Vitest coverage for profile question logic
- Add backend pytest coverage for profile answer normalization and validation
- Update restart.sh to build, run frontend/backend unit tests, and restart only after tests pass
- Refresh README, quickstart, project structure, instructions, and Square docs to match current app features
- Protect feature flag reload behind super-admin access
- Restrict admin-triggered password resets so admins can only reset member accounts
- Replace email template HTML preview rendering with escaped text preview
- Update docs for feature flag reload access, password reset scope, and email template preview safety
-- test user questions are also made by AI and not very useful. but i didn't know what to put there so its good enough for a test
This commit is contained in:
@@ -26,11 +26,62 @@ export interface User {
|
||||
phone: string | null;
|
||||
address: string | null;
|
||||
role: string;
|
||||
volunteer_level: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
last_login: string | null;
|
||||
}
|
||||
|
||||
export type ProfileQuestionInputType = 'text' | 'number' | 'boolean' | 'date' | 'select';
|
||||
|
||||
export interface ProfileQuestionOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ProfileQuestion {
|
||||
id: number;
|
||||
key: string;
|
||||
label: string;
|
||||
help_text: string | null;
|
||||
input_type: ProfileQuestionInputType;
|
||||
placeholder: string | null;
|
||||
options: ProfileQuestionOption[];
|
||||
is_required: boolean;
|
||||
is_active: boolean;
|
||||
admin_only_edit: boolean;
|
||||
display_order: number;
|
||||
depends_on_question_id: number | null;
|
||||
depends_on_value: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ProfileQuestionForUser extends ProfileQuestion {
|
||||
answer: string | number | boolean | null;
|
||||
can_edit: boolean;
|
||||
}
|
||||
|
||||
export interface ProfileQuestionUpsertData {
|
||||
key: string;
|
||||
label: string;
|
||||
help_text?: string | null;
|
||||
input_type: ProfileQuestionInputType;
|
||||
placeholder?: string | null;
|
||||
options?: ProfileQuestionOption[] | null;
|
||||
is_required?: boolean;
|
||||
is_active?: boolean;
|
||||
admin_only_edit?: boolean;
|
||||
display_order?: number;
|
||||
depends_on_question_id?: number | null;
|
||||
depends_on_value?: string | null;
|
||||
}
|
||||
|
||||
export interface ProfileAnswerInput {
|
||||
question_id: number;
|
||||
value: string | number | boolean | null;
|
||||
}
|
||||
|
||||
export interface MembershipTier {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -230,6 +281,51 @@ export const userService = {
|
||||
const response = await api.delete(`/users/${userId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getMyProfileQuestions(): Promise<ProfileQuestionForUser[]> {
|
||||
const response = await api.get('/users/me/profile-questions');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateMyProfileAnswers(answers: ProfileAnswerInput[]): Promise<{ message: string }> {
|
||||
const response = await api.put('/users/me/profile-answers', { answers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getAdminProfileQuestions(includeInactive: boolean = true): Promise<ProfileQuestion[]> {
|
||||
const response = await api.get(`/users/admin/profile-questions?include_inactive=${includeInactive}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createAdminProfileQuestion(data: ProfileQuestionUpsertData): Promise<ProfileQuestion> {
|
||||
const response = await api.post('/users/admin/profile-questions', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateAdminProfileQuestion(questionId: number, data: Partial<ProfileQuestionUpsertData>): Promise<ProfileQuestion> {
|
||||
const response = await api.put(`/users/admin/profile-questions/${questionId}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deactivateAdminProfileQuestion(questionId: number): Promise<{ message: string }> {
|
||||
const response = await api.delete(`/users/admin/profile-questions/${questionId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getUserProfileAnswers(userId: number): Promise<ProfileQuestionForUser[]> {
|
||||
const response = await api.get(`/users/admin/users/${userId}/profile-answers`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateUserProfileAnswers(userId: number, answers: ProfileAnswerInput[]): Promise<{ message: string }> {
|
||||
const response = await api.put(`/users/admin/users/${userId}/profile-answers`, { answers });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async sendUserPasswordReset(userId: number): Promise<{ message: string }> {
|
||||
const response = await api.post(`/users/${userId}/send-password-reset`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const membershipService = {
|
||||
|
||||
Reference in New Issue
Block a user