- Add POST /api/admin/settings/clear-stuck-jobs endpoint to clear stuck/failed asynq worker jobs from Redis (retry queue, archived, orphaned task metadata) - Add "Clear Stuck Jobs" button to admin settings UI - Remove TASK_REMINDER_MINUTE config - all jobs now run at minute 0 - Simplify formatCron to only take hour parameter - Update default notification times to CST-friendly hours 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1248 lines
34 KiB
TypeScript
1248 lines
34 KiB
TypeScript
import axios, { AxiosError, type AxiosInstance } from 'axios';
|
|
import type {
|
|
Notification,
|
|
NotificationDetail,
|
|
NotificationListParams,
|
|
NotificationStats,
|
|
Subscription,
|
|
SubscriptionDetail,
|
|
SubscriptionListParams,
|
|
UpdateSubscriptionRequest,
|
|
SubscriptionStats,
|
|
DashboardStats,
|
|
} from '@/types/models';
|
|
|
|
// In production, API is on same origin. In dev, use env var or localhost
|
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '';
|
|
|
|
// Create axios instance
|
|
const api: AxiosInstance = axios.create({
|
|
baseURL: `${API_BASE_URL}/api/admin`,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Add auth token to requests
|
|
api.interceptors.request.use((config) => {
|
|
if (typeof window !== 'undefined') {
|
|
const token = localStorage.getItem('admin_token');
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// Handle auth errors
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
(error: AxiosError) => {
|
|
if (error.response?.status === 401) {
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('admin_token');
|
|
window.location.href = '/admin/login/';
|
|
}
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Types
|
|
export interface AdminUser {
|
|
id: number;
|
|
email: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
role: 'super_admin' | 'admin' | 'moderator';
|
|
is_active: boolean;
|
|
last_login?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface LoginRequest {
|
|
email: string;
|
|
password: string;
|
|
}
|
|
|
|
export interface LoginResponse {
|
|
token: string;
|
|
admin: AdminUser;
|
|
}
|
|
|
|
export interface ApiError {
|
|
error: string;
|
|
}
|
|
|
|
// Auth API
|
|
export const authApi = {
|
|
login: async (data: LoginRequest): Promise<LoginResponse> => {
|
|
const response = await api.post<LoginResponse>('/auth/login', data);
|
|
return response.data;
|
|
},
|
|
|
|
logout: async (): Promise<void> => {
|
|
await api.post('/auth/logout');
|
|
},
|
|
|
|
me: async (): Promise<AdminUser> => {
|
|
const response = await api.get<AdminUser>('/auth/me');
|
|
return response.data;
|
|
},
|
|
|
|
refreshToken: async (): Promise<{ token: string }> => {
|
|
const response = await api.post<{ token: string }>('/auth/refresh');
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Types for User management
|
|
import type {
|
|
PaginatedResponse,
|
|
User,
|
|
UserDetail,
|
|
UserListParams,
|
|
CreateUserRequest,
|
|
UpdateUserRequest,
|
|
Residence,
|
|
ResidenceDetail,
|
|
ResidenceListParams,
|
|
CreateResidenceRequest,
|
|
UpdateResidenceRequest,
|
|
Task,
|
|
TaskDetail,
|
|
TaskListParams,
|
|
CreateTaskRequest,
|
|
UpdateTaskRequest,
|
|
Contractor,
|
|
ContractorDetail,
|
|
ContractorListParams,
|
|
CreateContractorRequest,
|
|
UpdateContractorRequest,
|
|
Document,
|
|
DocumentDetail,
|
|
DocumentListParams,
|
|
CreateDocumentRequest,
|
|
UpdateDocumentRequest,
|
|
} from '@/types/models';
|
|
|
|
// Users API
|
|
export const usersApi = {
|
|
list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
|
|
const response = await api.get<PaginatedResponse<User>>('/users', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<UserDetail> => {
|
|
const response = await api.get<UserDetail>(`/users/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreateUserRequest): Promise<User> => {
|
|
const response = await api.post<User>('/users', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateUserRequest): Promise<User> => {
|
|
const response = await api.put<User>(`/users/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/users/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/users/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Residences API
|
|
export const residencesApi = {
|
|
list: async (params?: ResidenceListParams): Promise<PaginatedResponse<Residence>> => {
|
|
const response = await api.get<PaginatedResponse<Residence>>('/residences', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<ResidenceDetail> => {
|
|
const response = await api.get<ResidenceDetail>(`/residences/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreateResidenceRequest): Promise<Residence> => {
|
|
const response = await api.post<Residence>('/residences', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateResidenceRequest): Promise<Residence> => {
|
|
const response = await api.put<Residence>(`/residences/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/residences/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/residences/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Tasks API
|
|
export const tasksApi = {
|
|
list: async (params?: TaskListParams): Promise<PaginatedResponse<Task>> => {
|
|
const response = await api.get<PaginatedResponse<Task>>('/tasks', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<TaskDetail> => {
|
|
const response = await api.get<TaskDetail>(`/tasks/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreateTaskRequest): Promise<Task> => {
|
|
const response = await api.post<Task>('/tasks', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateTaskRequest): Promise<Task> => {
|
|
const response = await api.put<Task>(`/tasks/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/tasks/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/tasks/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Contractors API
|
|
export const contractorsApi = {
|
|
list: async (params?: ContractorListParams): Promise<PaginatedResponse<Contractor>> => {
|
|
const response = await api.get<PaginatedResponse<Contractor>>('/contractors', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<ContractorDetail> => {
|
|
const response = await api.get<ContractorDetail>(`/contractors/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreateContractorRequest): Promise<Contractor> => {
|
|
const response = await api.post<Contractor>('/contractors', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateContractorRequest): Promise<Contractor> => {
|
|
const response = await api.put<Contractor>(`/contractors/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/contractors/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/contractors/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Documents API
|
|
export const documentsApi = {
|
|
list: async (params?: DocumentListParams): Promise<PaginatedResponse<Document>> => {
|
|
const response = await api.get<PaginatedResponse<Document>>('/documents', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<DocumentDetail> => {
|
|
const response = await api.get<DocumentDetail>(`/documents/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreateDocumentRequest): Promise<Document> => {
|
|
const response = await api.post<Document>('/documents', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateDocumentRequest): Promise<Document> => {
|
|
const response = await api.put<Document>(`/documents/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/documents/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/documents/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Notifications API
|
|
import type { UpdateNotificationRequest, UpdateCompletionRequest } from '@/types/models';
|
|
|
|
export const notificationsApi = {
|
|
list: async (params?: NotificationListParams): Promise<PaginatedResponse<Notification>> => {
|
|
const response = await api.get<PaginatedResponse<Notification>>('/notifications', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<NotificationDetail> => {
|
|
const response = await api.get<NotificationDetail>(`/notifications/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateNotificationRequest): Promise<Notification> => {
|
|
const response = await api.put<Notification>(`/notifications/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/notifications/${id}`);
|
|
},
|
|
|
|
getStats: async (): Promise<NotificationStats> => {
|
|
const response = await api.get<NotificationStats>('/notifications/stats');
|
|
return response.data;
|
|
},
|
|
|
|
sendTestNotification: async (data: { user_id: number; title: string; body: string }): Promise<{ message: string; notification_id: number; devices: { ios: number; android: number } }> => {
|
|
const response = await api.post('/notifications/send-test', data);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Emails API
|
|
export const emailsApi = {
|
|
sendTestEmail: async (data: { user_id: number; subject: string; body: string }): Promise<{ message: string; to: string }> => {
|
|
const response = await api.post('/emails/send-test', data);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Subscriptions API
|
|
export const subscriptionsApi = {
|
|
list: async (params?: SubscriptionListParams): Promise<PaginatedResponse<Subscription>> => {
|
|
const response = await api.get<PaginatedResponse<Subscription>>('/subscriptions', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<SubscriptionDetail> => {
|
|
const response = await api.get<SubscriptionDetail>(`/subscriptions/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
getByUser: async (userId: number): Promise<Subscription> => {
|
|
const response = await api.get<Subscription>(`/subscriptions/user/${userId}`);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateSubscriptionRequest): Promise<Subscription> => {
|
|
const response = await api.put<Subscription>(`/subscriptions/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
getStats: async (): Promise<SubscriptionStats> => {
|
|
const response = await api.get<SubscriptionStats>('/subscriptions/stats');
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Dashboard API
|
|
export const dashboardApi = {
|
|
getStats: async (): Promise<DashboardStats> => {
|
|
const response = await api.get<DashboardStats>('/dashboard/stats');
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Auth Tokens Types
|
|
export interface AuthToken {
|
|
key: string;
|
|
user_id: number;
|
|
username: string;
|
|
email: string;
|
|
created: string;
|
|
}
|
|
|
|
export interface AuthTokenListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
}
|
|
|
|
// Auth Tokens API
|
|
export const authTokensApi = {
|
|
list: async (params?: AuthTokenListParams): Promise<PaginatedResponse<AuthToken>> => {
|
|
const response = await api.get<PaginatedResponse<AuthToken>>('/auth-tokens', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (userId: number): Promise<AuthToken> => {
|
|
const response = await api.get<AuthToken>(`/auth-tokens/${userId}`);
|
|
return response.data;
|
|
},
|
|
|
|
revoke: async (userId: number): Promise<void> => {
|
|
await api.delete(`/auth-tokens/${userId}`);
|
|
},
|
|
|
|
bulkRevoke: async (userIds: number[]): Promise<void> => {
|
|
await api.delete('/auth-tokens/bulk', { data: { ids: userIds } });
|
|
},
|
|
};
|
|
|
|
// Task Completion Image
|
|
export interface TaskCompletionImage {
|
|
id: number;
|
|
image_url: string;
|
|
caption: string;
|
|
}
|
|
|
|
// Task Completions Types
|
|
export interface TaskCompletion {
|
|
id: number;
|
|
task_id: number;
|
|
task_title: string;
|
|
residence_id: number;
|
|
residence_name: string;
|
|
completed_by_id: number;
|
|
completed_by: string;
|
|
completed_at: string;
|
|
notes: string;
|
|
actual_cost: string | null;
|
|
rating: number | null;
|
|
images: TaskCompletionImage[];
|
|
created_at: string;
|
|
}
|
|
|
|
export interface CompletionListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
task_id?: number;
|
|
residence_id?: number;
|
|
user_id?: number;
|
|
}
|
|
|
|
// Completions API
|
|
export const completionsApi = {
|
|
list: async (params?: CompletionListParams): Promise<PaginatedResponse<TaskCompletion>> => {
|
|
const response = await api.get<PaginatedResponse<TaskCompletion>>('/completions', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<TaskCompletion> => {
|
|
const response = await api.get<TaskCompletion>(`/completions/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateCompletionRequest): Promise<TaskCompletion> => {
|
|
const response = await api.put<TaskCompletion>(`/completions/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/completions/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/completions/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Lookup Types
|
|
export interface LookupItem {
|
|
id: number;
|
|
name: string;
|
|
display_order: number;
|
|
description?: string;
|
|
icon?: string;
|
|
color?: string;
|
|
}
|
|
|
|
export interface CreateLookupRequest {
|
|
name: string;
|
|
display_order?: number;
|
|
description?: string;
|
|
icon?: string;
|
|
color?: string;
|
|
}
|
|
|
|
export interface UpdateLookupRequest {
|
|
name?: string;
|
|
display_order?: number;
|
|
description?: string;
|
|
icon?: string;
|
|
color?: string;
|
|
}
|
|
|
|
interface LookupListResponse {
|
|
data: LookupItem[];
|
|
total: number;
|
|
}
|
|
|
|
// Lookups API Factory
|
|
const createLookupApi = (endpoint: string) => ({
|
|
list: async (): Promise<LookupItem[]> => {
|
|
const response = await api.get<LookupListResponse>(`/lookups/${endpoint}`);
|
|
return response.data.data;
|
|
},
|
|
create: async (data: CreateLookupRequest): Promise<LookupItem> => {
|
|
const response = await api.post<LookupItem>(`/lookups/${endpoint}`, data);
|
|
return response.data;
|
|
},
|
|
update: async (id: number, data: UpdateLookupRequest): Promise<LookupItem> => {
|
|
const response = await api.put<LookupItem>(`/lookups/${endpoint}/${id}`, data);
|
|
return response.data;
|
|
},
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/lookups/${endpoint}/${id}`);
|
|
},
|
|
});
|
|
|
|
// Lookups API
|
|
export const lookupsApi = {
|
|
categories: createLookupApi('categories'),
|
|
priorities: createLookupApi('priorities'),
|
|
statuses: createLookupApi('statuses'),
|
|
frequencies: createLookupApi('frequencies'),
|
|
residenceTypes: createLookupApi('residence-types'),
|
|
specialties: createLookupApi('specialties'),
|
|
};
|
|
|
|
// Admin Users Types
|
|
export interface ManagedAdminUser {
|
|
id: number;
|
|
email: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
role: 'super_admin' | 'admin';
|
|
is_active: boolean;
|
|
last_login?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface AdminUserListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
role?: string;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface CreateAdminUserRequest {
|
|
email: string;
|
|
password: string;
|
|
first_name?: string;
|
|
last_name?: string;
|
|
role?: 'super_admin' | 'admin';
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface UpdateAdminUserRequest {
|
|
email?: string;
|
|
password?: string;
|
|
first_name?: string;
|
|
last_name?: string;
|
|
role?: 'super_admin' | 'admin';
|
|
is_active?: boolean;
|
|
}
|
|
|
|
// Admin Users API
|
|
export const adminUsersApi = {
|
|
list: async (params?: AdminUserListParams): Promise<PaginatedResponse<ManagedAdminUser>> => {
|
|
const response = await api.get<PaginatedResponse<ManagedAdminUser>>('/admin-users', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<ManagedAdminUser> => {
|
|
const response = await api.get<ManagedAdminUser>(`/admin-users/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreateAdminUserRequest): Promise<ManagedAdminUser> => {
|
|
const response = await api.post<ManagedAdminUser>('/admin-users', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateAdminUserRequest): Promise<ManagedAdminUser> => {
|
|
const response = await api.put<ManagedAdminUser>(`/admin-users/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/admin-users/${id}`);
|
|
},
|
|
};
|
|
|
|
// Notification Preferences Types
|
|
export interface NotificationPreference {
|
|
id: number;
|
|
user_id: number;
|
|
username: string;
|
|
email: string;
|
|
task_due_soon: boolean;
|
|
task_overdue: boolean;
|
|
task_completed: boolean;
|
|
task_assigned: boolean;
|
|
residence_shared: boolean;
|
|
warranty_expiring: boolean;
|
|
// Email preferences
|
|
email_task_completed: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface NotificationPrefsListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
}
|
|
|
|
export interface UpdateNotificationPrefRequest {
|
|
task_due_soon?: boolean;
|
|
task_overdue?: boolean;
|
|
task_completed?: boolean;
|
|
task_assigned?: boolean;
|
|
residence_shared?: boolean;
|
|
warranty_expiring?: boolean;
|
|
// Email preferences
|
|
email_task_completed?: boolean;
|
|
}
|
|
|
|
// Notification Preferences API
|
|
export const notificationPrefsApi = {
|
|
list: async (params?: NotificationPrefsListParams): Promise<PaginatedResponse<NotificationPreference>> => {
|
|
const response = await api.get<PaginatedResponse<NotificationPreference>>('/notification-prefs', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<NotificationPreference> => {
|
|
const response = await api.get<NotificationPreference>(`/notification-prefs/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
getByUser: async (userId: number): Promise<NotificationPreference> => {
|
|
const response = await api.get<NotificationPreference>(`/notification-prefs/user/${userId}`);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateNotificationPrefRequest): Promise<NotificationPreference> => {
|
|
const response = await api.put<NotificationPreference>(`/notification-prefs/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/notification-prefs/${id}`);
|
|
},
|
|
};
|
|
|
|
// Settings types
|
|
export interface SystemSettings {
|
|
enable_limitations: boolean;
|
|
}
|
|
|
|
export interface UpdateSettingsRequest {
|
|
enable_limitations?: boolean;
|
|
}
|
|
|
|
// Settings API
|
|
export const settingsApi = {
|
|
get: async (): Promise<SystemSettings> => {
|
|
const response = await api.get<SystemSettings>('/settings');
|
|
return response.data;
|
|
},
|
|
|
|
update: async (data: UpdateSettingsRequest): Promise<SystemSettings> => {
|
|
const response = await api.put<SystemSettings>('/settings', data);
|
|
return response.data;
|
|
},
|
|
|
|
seedLookups: async (): Promise<{ message: string }> => {
|
|
const response = await api.post<{ message: string }>('/settings/seed-lookups');
|
|
return response.data;
|
|
},
|
|
|
|
seedTestData: async (): Promise<{ message: string }> => {
|
|
const response = await api.post<{ message: string }>('/settings/seed-test-data');
|
|
return response.data;
|
|
},
|
|
|
|
clearAllData: async (): Promise<{ message: string; users_deleted: number; preserved_users: number }> => {
|
|
const response = await api.post<{ message: string; users_deleted: number; preserved_users: number }>('/settings/clear-all-data');
|
|
return response.data;
|
|
},
|
|
|
|
clearStuckJobs: async (): Promise<{ message: string; keys_deleted: number; deleted_keys: string[] }> => {
|
|
const response = await api.post<{ message: string; keys_deleted: number; deleted_keys: string[] }>('/settings/clear-stuck-jobs');
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Limitations types
|
|
export interface LimitationsSettings {
|
|
enable_limitations: boolean;
|
|
}
|
|
|
|
export interface TierLimits {
|
|
id: number;
|
|
tier: 'free' | 'pro';
|
|
properties_limit: number | null;
|
|
tasks_limit: number | null;
|
|
contractors_limit: number | null;
|
|
documents_limit: number | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface UpdateTierLimitsRequest {
|
|
properties_limit: number | null;
|
|
tasks_limit: number | null;
|
|
contractors_limit: number | null;
|
|
documents_limit: number | null;
|
|
}
|
|
|
|
export interface UpgradeTrigger {
|
|
id: number;
|
|
trigger_key: string;
|
|
title: string;
|
|
message: string;
|
|
promo_html: string;
|
|
button_text: string;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface TriggerKeyOption {
|
|
key: string;
|
|
label: string;
|
|
}
|
|
|
|
export interface CreateUpgradeTriggerRequest {
|
|
trigger_key: string;
|
|
title: string;
|
|
message: string;
|
|
promo_html?: string;
|
|
button_text?: string;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface UpdateUpgradeTriggerRequest {
|
|
trigger_key?: string;
|
|
title?: string;
|
|
message?: string;
|
|
promo_html?: string;
|
|
button_text?: string;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
// Limitations API
|
|
export const limitationsApi = {
|
|
// Settings
|
|
getSettings: async (): Promise<LimitationsSettings> => {
|
|
const response = await api.get<LimitationsSettings>('/limitations/settings');
|
|
return response.data;
|
|
},
|
|
|
|
updateSettings: async (data: { enable_limitations: boolean }): Promise<LimitationsSettings> => {
|
|
const response = await api.put<LimitationsSettings>('/limitations/settings', data);
|
|
return response.data;
|
|
},
|
|
|
|
// Tier Limits
|
|
listTierLimits: async (): Promise<TierLimits[]> => {
|
|
const response = await api.get<{ data: TierLimits[]; total: number }>('/limitations/tier-limits');
|
|
return response.data.data;
|
|
},
|
|
|
|
getTierLimits: async (tier: 'free' | 'pro'): Promise<TierLimits> => {
|
|
const response = await api.get<TierLimits>(`/limitations/tier-limits/${tier}`);
|
|
return response.data;
|
|
},
|
|
|
|
updateTierLimits: async (tier: 'free' | 'pro', data: UpdateTierLimitsRequest): Promise<TierLimits> => {
|
|
const response = await api.put<TierLimits>(`/limitations/tier-limits/${tier}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
// Upgrade Triggers
|
|
getAvailableTriggerKeys: async (): Promise<TriggerKeyOption[]> => {
|
|
const response = await api.get<TriggerKeyOption[]>('/limitations/upgrade-triggers/keys');
|
|
return response.data;
|
|
},
|
|
|
|
listUpgradeTriggers: async (): Promise<UpgradeTrigger[]> => {
|
|
const response = await api.get<{ data: UpgradeTrigger[]; total: number }>('/limitations/upgrade-triggers');
|
|
return response.data.data;
|
|
},
|
|
|
|
getUpgradeTrigger: async (id: number): Promise<UpgradeTrigger> => {
|
|
const response = await api.get<UpgradeTrigger>(`/limitations/upgrade-triggers/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
createUpgradeTrigger: async (data: CreateUpgradeTriggerRequest): Promise<UpgradeTrigger> => {
|
|
const response = await api.post<UpgradeTrigger>('/limitations/upgrade-triggers', data);
|
|
return response.data;
|
|
},
|
|
|
|
updateUpgradeTrigger: async (id: number, data: UpdateUpgradeTriggerRequest): Promise<UpgradeTrigger> => {
|
|
const response = await api.put<UpgradeTrigger>(`/limitations/upgrade-triggers/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
deleteUpgradeTrigger: async (id: number): Promise<void> => {
|
|
await api.delete(`/limitations/upgrade-triggers/${id}`);
|
|
},
|
|
};
|
|
|
|
// Confirmation Codes Types
|
|
export interface ConfirmationCode {
|
|
id: number;
|
|
user_id: number;
|
|
username: string;
|
|
email: string;
|
|
code: string;
|
|
expires_at: string;
|
|
is_used: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ConfirmationCodeListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
}
|
|
|
|
// Confirmation Codes API
|
|
export const confirmationCodesApi = {
|
|
list: async (params?: ConfirmationCodeListParams): Promise<PaginatedResponse<ConfirmationCode>> => {
|
|
const response = await api.get<PaginatedResponse<ConfirmationCode>>('/confirmation-codes', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<ConfirmationCode> => {
|
|
const response = await api.get<ConfirmationCode>(`/confirmation-codes/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/confirmation-codes/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/confirmation-codes/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Share Codes Types
|
|
export interface ShareCode {
|
|
id: number;
|
|
residence_id: number;
|
|
residence_name: string;
|
|
code: string;
|
|
created_by_id: number;
|
|
created_by: string;
|
|
is_active: boolean;
|
|
expires_at: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ShareCodeListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
}
|
|
|
|
export interface UpdateShareCodeRequest {
|
|
is_active: boolean;
|
|
}
|
|
|
|
// Share Codes API
|
|
export const shareCodesApi = {
|
|
list: async (params?: ShareCodeListParams): Promise<PaginatedResponse<ShareCode>> => {
|
|
const response = await api.get<PaginatedResponse<ShareCode>>('/share-codes', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<ShareCode> => {
|
|
const response = await api.get<ShareCode>(`/share-codes/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateShareCodeRequest): Promise<ShareCode> => {
|
|
const response = await api.put<ShareCode>(`/share-codes/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/share-codes/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/share-codes/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Password Reset Codes Types
|
|
export interface PasswordResetCode {
|
|
id: number;
|
|
user_id: number;
|
|
username: string;
|
|
email: string;
|
|
reset_token: string;
|
|
expires_at: string;
|
|
used: boolean;
|
|
attempts: number;
|
|
max_attempts: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface PasswordResetCodeListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
}
|
|
|
|
// Password Reset Codes API
|
|
export const passwordResetCodesApi = {
|
|
list: async (params?: PasswordResetCodeListParams): Promise<PaginatedResponse<PasswordResetCode>> => {
|
|
const response = await api.get<PaginatedResponse<PasswordResetCode>>('/password-reset-codes', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<PasswordResetCode> => {
|
|
const response = await api.get<PasswordResetCode>(`/password-reset-codes/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/password-reset-codes/${id}`);
|
|
},
|
|
|
|
bulkDelete: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/password-reset-codes/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Device Types
|
|
export interface APNSDevice {
|
|
id: number;
|
|
name: string;
|
|
active: boolean;
|
|
user_id: number | null;
|
|
username: string | null;
|
|
device_id: string;
|
|
registration_id: string;
|
|
date_created: string;
|
|
}
|
|
|
|
export interface GCMDevice {
|
|
id: number;
|
|
name: string;
|
|
active: boolean;
|
|
user_id: number | null;
|
|
username: string | null;
|
|
device_id: string;
|
|
registration_id: string;
|
|
cloud_message_type: string;
|
|
date_created: string;
|
|
}
|
|
|
|
export interface DeviceListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
}
|
|
|
|
export interface DeviceStats {
|
|
apns: { total: number; active: number };
|
|
gcm: { total: number; active: number };
|
|
total: number;
|
|
}
|
|
|
|
// Devices API
|
|
export const devicesApi = {
|
|
getStats: async (): Promise<DeviceStats> => {
|
|
const response = await api.get<DeviceStats>('/devices/stats');
|
|
return response.data;
|
|
},
|
|
|
|
listAPNS: async (params?: DeviceListParams): Promise<PaginatedResponse<APNSDevice>> => {
|
|
const response = await api.get<PaginatedResponse<APNSDevice>>('/devices/apns', { params });
|
|
return response.data;
|
|
},
|
|
|
|
listGCM: async (params?: DeviceListParams): Promise<PaginatedResponse<GCMDevice>> => {
|
|
const response = await api.get<PaginatedResponse<GCMDevice>>('/devices/gcm', { params });
|
|
return response.data;
|
|
},
|
|
|
|
updateAPNS: async (id: number, data: { active: boolean }): Promise<void> => {
|
|
await api.put(`/devices/apns/${id}`, data);
|
|
},
|
|
|
|
updateGCM: async (id: number, data: { active: boolean }): Promise<void> => {
|
|
await api.put(`/devices/gcm/${id}`, data);
|
|
},
|
|
|
|
deleteAPNS: async (id: number): Promise<void> => {
|
|
await api.delete(`/devices/apns/${id}`);
|
|
},
|
|
|
|
deleteGCM: async (id: number): Promise<void> => {
|
|
await api.delete(`/devices/gcm/${id}`);
|
|
},
|
|
|
|
bulkDeleteAPNS: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/devices/apns/bulk', { data: { ids } });
|
|
},
|
|
|
|
bulkDeleteGCM: async (ids: number[]): Promise<void> => {
|
|
await api.delete('/devices/gcm/bulk', { data: { ids } });
|
|
},
|
|
};
|
|
|
|
// Feature Benefit Types
|
|
export interface FeatureBenefit {
|
|
id: number;
|
|
feature_name: string;
|
|
free_tier_text: string;
|
|
pro_tier_text: string;
|
|
display_order: number;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface CreateFeatureBenefitRequest {
|
|
feature_name: string;
|
|
free_tier_text: string;
|
|
pro_tier_text: string;
|
|
display_order?: number;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface UpdateFeatureBenefitRequest {
|
|
feature_name?: string;
|
|
free_tier_text?: string;
|
|
pro_tier_text?: string;
|
|
display_order?: number;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
// Feature Benefits API
|
|
export const featureBenefitsApi = {
|
|
list: async (params?: { page?: number; per_page?: number; search?: string }): Promise<PaginatedResponse<FeatureBenefit>> => {
|
|
const response = await api.get<PaginatedResponse<FeatureBenefit>>('/feature-benefits', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<FeatureBenefit> => {
|
|
const response = await api.get<FeatureBenefit>(`/feature-benefits/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreateFeatureBenefitRequest): Promise<FeatureBenefit> => {
|
|
const response = await api.post<FeatureBenefit>('/feature-benefits', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateFeatureBenefitRequest): Promise<FeatureBenefit> => {
|
|
const response = await api.put<FeatureBenefit>(`/feature-benefits/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/feature-benefits/${id}`);
|
|
},
|
|
};
|
|
|
|
// Promotion Types
|
|
export interface Promotion {
|
|
id: number;
|
|
promotion_id: string;
|
|
title: string;
|
|
message: string;
|
|
link: string | null;
|
|
start_date: string;
|
|
end_date: string;
|
|
target_tier: 'free' | 'pro';
|
|
is_active: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface CreatePromotionRequest {
|
|
promotion_id: string;
|
|
title: string;
|
|
message: string;
|
|
link?: string;
|
|
start_date: string;
|
|
end_date: string;
|
|
target_tier?: 'free' | 'pro';
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface UpdatePromotionRequest {
|
|
promotion_id?: string;
|
|
title?: string;
|
|
message?: string;
|
|
link?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
target_tier?: 'free' | 'pro';
|
|
is_active?: boolean;
|
|
}
|
|
|
|
// Promotions API
|
|
export const promotionsApi = {
|
|
list: async (params?: { page?: number; per_page?: number; search?: string }): Promise<PaginatedResponse<Promotion>> => {
|
|
const response = await api.get<PaginatedResponse<Promotion>>('/promotions', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<Promotion> => {
|
|
const response = await api.get<Promotion>(`/promotions/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreatePromotionRequest): Promise<Promotion> => {
|
|
const response = await api.post<Promotion>('/promotions', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdatePromotionRequest): Promise<Promotion> => {
|
|
const response = await api.put<Promotion>(`/promotions/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/promotions/${id}`);
|
|
},
|
|
};
|
|
|
|
// Task Template Types
|
|
export interface TaskTemplateCategory {
|
|
id: number;
|
|
name: string;
|
|
description: string;
|
|
icon: string;
|
|
color: string;
|
|
display_order: number;
|
|
}
|
|
|
|
export interface TaskTemplateFrequency {
|
|
id: number;
|
|
name: string;
|
|
days: number;
|
|
display_order: number;
|
|
}
|
|
|
|
export interface TaskTemplate {
|
|
id: number;
|
|
title: string;
|
|
description: string;
|
|
category_id: number | null;
|
|
category?: TaskTemplateCategory;
|
|
frequency_id: number | null;
|
|
frequency?: TaskTemplateFrequency;
|
|
icon_ios: string;
|
|
icon_android: string;
|
|
tags: string;
|
|
display_order: number;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface TaskTemplateListParams {
|
|
page?: number;
|
|
per_page?: number;
|
|
search?: string;
|
|
sort_by?: string;
|
|
sort_dir?: 'asc' | 'desc';
|
|
category_id?: number;
|
|
frequency_id?: number;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface CreateTaskTemplateRequest {
|
|
title: string;
|
|
description?: string;
|
|
category_id?: number;
|
|
frequency_id?: number;
|
|
icon_ios?: string;
|
|
icon_android?: string;
|
|
tags?: string;
|
|
display_order?: number;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
export interface UpdateTaskTemplateRequest {
|
|
title?: string;
|
|
description?: string;
|
|
category_id?: number | null;
|
|
frequency_id?: number | null;
|
|
icon_ios?: string;
|
|
icon_android?: string;
|
|
tags?: string;
|
|
display_order?: number;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
// Task Templates API
|
|
export const taskTemplatesApi = {
|
|
list: async (params?: TaskTemplateListParams): Promise<PaginatedResponse<TaskTemplate>> => {
|
|
const response = await api.get<PaginatedResponse<TaskTemplate>>('/task-templates', { params });
|
|
return response.data;
|
|
},
|
|
|
|
get: async (id: number): Promise<TaskTemplate> => {
|
|
const response = await api.get<TaskTemplate>(`/task-templates/${id}`);
|
|
return response.data;
|
|
},
|
|
|
|
create: async (data: CreateTaskTemplateRequest): Promise<TaskTemplate> => {
|
|
const response = await api.post<TaskTemplate>('/task-templates', data);
|
|
return response.data;
|
|
},
|
|
|
|
update: async (id: number, data: UpdateTaskTemplateRequest): Promise<TaskTemplate> => {
|
|
const response = await api.put<TaskTemplate>(`/task-templates/${id}`, data);
|
|
return response.data;
|
|
},
|
|
|
|
delete: async (id: number): Promise<void> => {
|
|
await api.delete(`/task-templates/${id}`);
|
|
},
|
|
|
|
toggleActive: async (id: number): Promise<TaskTemplate> => {
|
|
const response = await api.post<TaskTemplate>(`/task-templates/${id}/toggle-active`);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
export default api;
|