forked from jamesp/sasa-membership
632e66e21d
- 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
441 lines
11 KiB
TypeScript
441 lines
11 KiB
TypeScript
import api from './api';
|
|
|
|
export interface RegisterData {
|
|
email: string;
|
|
password: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
phone?: string;
|
|
address?: string;
|
|
}
|
|
|
|
export interface LoginData {
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface ForgotPasswordData {
|
|
email: string;
|
|
}
|
|
|
|
export interface User {
|
|
id: number;
|
|
email: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
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;
|
|
description: string;
|
|
annual_fee: number;
|
|
benefits: string;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Membership {
|
|
id: number;
|
|
user_id: number;
|
|
tier_id: number;
|
|
status: string;
|
|
start_date: string;
|
|
end_date: string;
|
|
auto_renew: boolean;
|
|
created_at: string;
|
|
tier: MembershipTier;
|
|
}
|
|
|
|
export interface Payment {
|
|
id: number;
|
|
user_id: number;
|
|
membership_id: number | null;
|
|
amount: number;
|
|
payment_method: string;
|
|
status: string;
|
|
transaction_id: string | null;
|
|
payment_date: string | null;
|
|
notes: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ResetPasswordData {
|
|
token: string;
|
|
new_password: string;
|
|
}
|
|
|
|
export interface ChangePasswordData {
|
|
current_password: string;
|
|
new_password: string;
|
|
}
|
|
|
|
export interface MembershipCreateData {
|
|
tier_id: number;
|
|
start_date: string;
|
|
end_date: string;
|
|
auto_renew: boolean;
|
|
}
|
|
|
|
export interface MembershipTierUpdateData {
|
|
name?: string;
|
|
description?: string;
|
|
annual_fee?: number;
|
|
benefits?: string;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface MembershipUpdateData {
|
|
status?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
auto_renew?: boolean;
|
|
}
|
|
|
|
export interface PaymentCreateData {
|
|
amount: number;
|
|
payment_method: string;
|
|
membership_id?: number;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface PaymentUpdateData {
|
|
status?: string;
|
|
transaction_id?: string;
|
|
payment_date?: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface MembershipTierCreateData {
|
|
name: string;
|
|
description?: string;
|
|
annual_fee: number;
|
|
benefits?: string;
|
|
}
|
|
|
|
export interface Event {
|
|
id: number;
|
|
title: string;
|
|
description: string | null;
|
|
event_date: string;
|
|
event_time: string | null;
|
|
location: string | null;
|
|
max_attendees: number | null;
|
|
status: string;
|
|
created_by: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
rsvp_status?: string; // Current user's RSVP status
|
|
}
|
|
|
|
export interface EventCreateData {
|
|
title: string;
|
|
description?: string;
|
|
event_date: string;
|
|
event_time?: string;
|
|
location?: string;
|
|
max_attendees?: number;
|
|
}
|
|
|
|
export interface EventUpdateData {
|
|
title?: string;
|
|
description?: string;
|
|
event_date?: string;
|
|
event_time?: string;
|
|
location?: string;
|
|
max_attendees?: number;
|
|
status?: string;
|
|
}
|
|
|
|
export interface EventRSVP {
|
|
id: number;
|
|
event_id: number;
|
|
user_id: number;
|
|
status: string;
|
|
attended: boolean;
|
|
notes: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface EventRSVPData {
|
|
status: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export const authService = {
|
|
async register(data: RegisterData) {
|
|
const response = await api.post('/auth/register', data);
|
|
return response.data;
|
|
},
|
|
|
|
async login(data: LoginData) {
|
|
const response = await api.post('/auth/login-json', data);
|
|
localStorage.setItem('token', response.data.access_token);
|
|
return response.data;
|
|
},
|
|
|
|
async forgotPassword(data: ForgotPasswordData) {
|
|
const response = await api.post('/auth/forgot-password', data);
|
|
return response.data;
|
|
},
|
|
|
|
async resetPassword(data: ResetPasswordData) {
|
|
const response = await api.post('/auth/reset-password', data);
|
|
return response.data;
|
|
},
|
|
|
|
async changePassword(data: ChangePasswordData) {
|
|
const response = await api.post('/auth/change-password', data);
|
|
return response.data;
|
|
},
|
|
|
|
logout() {
|
|
localStorage.removeItem('token');
|
|
},
|
|
|
|
isAuthenticated() {
|
|
return !!localStorage.getItem('token');
|
|
}
|
|
};
|
|
|
|
export const userService = {
|
|
async getCurrentUser(): Promise<User> {
|
|
const response = await api.get('/users/me');
|
|
return response.data;
|
|
},
|
|
|
|
async updateProfile(data: Partial<User>) {
|
|
const response = await api.put('/users/me', data);
|
|
return response.data;
|
|
},
|
|
|
|
async getAllUsers(): Promise<User[]> {
|
|
const response = await api.get('/users/');
|
|
return response.data;
|
|
},
|
|
|
|
async updateUser(userId: number, data: Partial<User>): Promise<User> {
|
|
const response = await api.put(`/users/${userId}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
async deleteUser(userId: number): Promise<{ message: string }> {
|
|
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 = {
|
|
async getMyMemberships(): Promise<Membership[]> {
|
|
const response = await api.get('/memberships/my-memberships');
|
|
return response.data;
|
|
},
|
|
|
|
async createMembership(data: MembershipCreateData): Promise<Membership> {
|
|
const response = await api.post('/memberships/', data);
|
|
return response.data;
|
|
},
|
|
|
|
async updateMembership(membershipId: number, data: MembershipUpdateData): Promise<Membership> {
|
|
const response = await api.put(`/memberships/${membershipId}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
async getAllMemberships(): Promise<Membership[]> {
|
|
const response = await api.get('/memberships/');
|
|
return response.data;
|
|
},
|
|
|
|
async getTiers(): Promise<MembershipTier[]> {
|
|
const response = await api.get('/tiers/');
|
|
return response.data;
|
|
},
|
|
|
|
async createTier(data: MembershipTierCreateData): Promise<MembershipTier> {
|
|
const response = await api.post('/tiers/', data);
|
|
return response.data;
|
|
},
|
|
|
|
async updateTier(tierId: number, data: MembershipTierUpdateData): Promise<MembershipTier> {
|
|
const response = await api.put(`/tiers/${tierId}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
async deleteTier(tierId: number): Promise<{ message: string }> {
|
|
const response = await api.delete(`/tiers/${tierId}`);
|
|
return response.data;
|
|
},
|
|
|
|
async getAllTiers(showInactive: boolean = true): Promise<MembershipTier[]> {
|
|
const response = await api.get(`/tiers/?show_inactive=${showInactive}`);
|
|
return response.data;
|
|
}
|
|
};
|
|
|
|
export const paymentService = {
|
|
async getMyPayments(): Promise<Payment[]> {
|
|
const response = await api.get('/payments/my-payments');
|
|
return response.data;
|
|
},
|
|
|
|
async createPayment(data: PaymentCreateData): Promise<Payment> {
|
|
const response = await api.post('/payments/', data);
|
|
return response.data;
|
|
},
|
|
|
|
async updatePayment(paymentId: number, data: PaymentUpdateData): Promise<Payment> {
|
|
const response = await api.put(`/payments/${paymentId}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
async getAllPayments(): Promise<Payment[]> {
|
|
const response = await api.get('/payments/');
|
|
return response.data;
|
|
}
|
|
};
|
|
|
|
export const eventService = {
|
|
async getAllEvents(): Promise<Event[]> {
|
|
const response = await api.get('/events/');
|
|
return response.data;
|
|
},
|
|
|
|
async getUpcomingEvents(): Promise<Event[]> {
|
|
const response = await api.get('/events/upcoming');
|
|
return response.data;
|
|
},
|
|
|
|
async createEvent(data: EventCreateData): Promise<Event> {
|
|
const response = await api.post('/events/', data);
|
|
return response.data;
|
|
},
|
|
|
|
async updateEvent(eventId: number, data: EventUpdateData): Promise<Event> {
|
|
const response = await api.put(`/events/${eventId}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
async deleteEvent(eventId: number): Promise<{ message: string }> {
|
|
const response = await api.delete(`/events/${eventId}`);
|
|
return response.data;
|
|
},
|
|
|
|
async getEventRSVPs(eventId: number): Promise<EventRSVP[]> {
|
|
const response = await api.get(`/events/${eventId}/rsvps`);
|
|
return response.data;
|
|
},
|
|
|
|
async createOrUpdateRSVP(eventId: number, data: EventRSVPData): Promise<EventRSVP> {
|
|
const response = await api.post(`/events/${eventId}/rsvp`, data);
|
|
return response.data;
|
|
},
|
|
|
|
async getMyRSVPs(): Promise<EventRSVP[]> {
|
|
const response = await api.get('/events/my-rsvps');
|
|
return response.data;
|
|
}
|
|
};
|