diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 5e901d0..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "workbench.colorCustomizations": { - "activityBar.background": "#221489", - "titleBar.activeBackground": "#301DC0", - "titleBar.activeForeground": "#F9F9FE" - } -} \ No newline at end of file diff --git a/src/scripts/database/AuthSyncService.ts b/src/scripts/database/AuthSyncService.ts index 8900d76..4af0bed 100644 --- a/src/scripts/database/AuthSyncService.ts +++ b/src/scripts/database/AuthSyncService.ts @@ -1,8 +1,12 @@ -import { Authentication } from '../pocketbase/Authentication'; -import { DataSyncService } from './DataSyncService'; -import { DexieService } from './DexieService'; -import { Collections } from '../../schemas/pocketbase/schema'; -import { SendLog } from '../pocketbase/SendLog'; +import { Authentication } from "../pocketbase/Authentication"; +import { DataSyncService } from "./DataSyncService"; +import { DexieService } from "./DexieService"; +import { Collections } from "../../schemas/pocketbase/schema"; +import { SendLog } from "../pocketbase/SendLog"; + +// Check if we're in a browser environment +const isBrowser = + typeof window !== "undefined" && typeof window.indexedDB !== "undefined"; /** * Service to handle data synchronization during authentication flows @@ -27,7 +31,7 @@ export class AuthSyncService { Collections.OFFICERS, Collections.REIMBURSEMENTS, Collections.RECEIPTS, - Collections.SPONSORS + Collections.SPONSORS, ]; private constructor() { @@ -36,8 +40,10 @@ export class AuthSyncService { this.dexieService = DexieService.getInstance(); this.logger = SendLog.getInstance(); - // Listen for auth state changes - this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this)); + // Listen for auth state changes only in browser + if (isBrowser) { + this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this)); + } } /** @@ -54,6 +60,8 @@ export class AuthSyncService { * Handle authentication state changes */ private async handleAuthStateChange(isAuthenticated: boolean): Promise { + if (!isBrowser) return; + if (isAuthenticated) { // User just logged in await this.handleLogin(); @@ -67,8 +75,10 @@ export class AuthSyncService { * Handle login by syncing user data */ public async handleLogin(): Promise { + if (!isBrowser) return true; + if (this.isSyncing) { - console.log('Sync already in progress, queueing login sync'); + console.log("Sync already in progress, queueing login sync"); if (this.syncPromise) { this.syncPromise = this.syncPromise.then(() => this.performLoginSync()); } else { @@ -78,37 +88,44 @@ export class AuthSyncService { } this.syncPromise = this.performLoginSync(); - return this.syncPromise.then(() => Object.keys(this.syncErrors).length === 0); + return this.syncPromise.then( + () => Object.keys(this.syncErrors).length === 0, + ); } /** * Perform the actual login sync */ private async performLoginSync(): Promise { + if (!isBrowser) return; + if (!this.auth.isAuthenticated()) { - console.log('Not authenticated, skipping login sync'); + console.log("Not authenticated, skipping login sync"); return; } this.isSyncing = true; this.syncErrors = {}; - + try { - console.log('Starting login sync process...'); - + console.log("Starting login sync process..."); + // Display sync notification if in browser environment - this.showSyncNotification('Syncing your data...'); - + this.showSyncNotification("Syncing your data..."); + // Sync user-specific data first const userId = this.auth.getUserId(); if (userId) { // First sync the current user's data - await this.dataSync.syncCollection(Collections.USERS, `id = "${userId}"`); - + await this.dataSync.syncCollection( + Collections.USERS, + `id = "${userId}"`, + ); + // Log the sync operation - console.log('User data synchronized on login'); + console.log("User data synchronized on login"); } - + // Sync all collections in parallel with conflict resolution await Promise.all( this.collectionsToSync.map(async (collection) => { @@ -119,32 +136,34 @@ export class AuthSyncService { console.error(`Error syncing ${collection}:`, error); this.syncErrors[collection] = error as Error; } - }) + }), ); - + // Verify sync was successful const syncVerification = await this.verifySyncSuccess(); - + if (syncVerification.success) { - console.log('Login sync completed successfully'); - this.showSyncNotification('Data sync complete!', 'success'); + console.log("Login sync completed successfully"); + this.showSyncNotification("Data sync complete!", "success"); } else { - console.warn('Login sync completed with issues:', syncVerification.errors); - this.showSyncNotification('Some data could not be synced', 'warning'); + console.warn( + "Login sync completed with issues:", + syncVerification.errors, + ); + this.showSyncNotification("Some data could not be synced", "warning"); } - } catch (error) { - console.error('Error during login sync:', error); - this.showSyncNotification('Failed to sync data', 'error'); + console.error("Error during login sync:", error); + this.showSyncNotification("Failed to sync data", "error"); } finally { this.isSyncing = false; - + // Process any queued sync operations if (this.syncQueue.length > 0) { const nextSync = this.syncQueue.shift(); - if (nextSync === 'login') { + if (nextSync === "login") { this.handleLogin(); - } else if (nextSync === 'logout') { + } else if (nextSync === "logout") { this.handleLogout(); } } @@ -155,37 +174,39 @@ export class AuthSyncService { * Handle logout by clearing user data */ public async handleLogout(): Promise { + if (!isBrowser) return true; + if (this.isSyncing) { - console.log('Sync already in progress, queueing logout cleanup'); - this.syncQueue.push('logout'); + console.log("Sync already in progress, queueing logout cleanup"); + this.syncQueue.push("logout"); return true; } this.isSyncing = true; - + try { - console.log('Starting logout cleanup process...'); - + console.log("Starting logout cleanup process..."); + // Ensure any pending changes are synced before logout await this.syncPendingChanges(); - + // Clear all data from IndexedDB await this.dexieService.clearAllData(); - - console.log('Logout cleanup completed successfully'); + + console.log("Logout cleanup completed successfully"); return true; } catch (error) { - console.error('Error during logout cleanup:', error); + console.error("Error during logout cleanup:", error); return false; } finally { this.isSyncing = false; - + // Process any queued sync operations if (this.syncQueue.length > 0) { const nextSync = this.syncQueue.shift(); - if (nextSync === 'login') { + if (nextSync === "login") { this.handleLogin(); - } else if (nextSync === 'logout') { + } else if (nextSync === "logout") { this.handleLogout(); } } @@ -196,51 +217,67 @@ export class AuthSyncService { * Sync any pending changes before logout */ private async syncPendingChanges(): Promise { + if (!isBrowser) return; + // This would be implemented if we had offline capabilities // For now, we just log that we would sync pending changes - console.log('Checking for pending changes to sync before logout...'); + console.log("Checking for pending changes to sync before logout..."); // In a real implementation, this would sync any offline changes } /** * Verify that sync was successful by checking data in IndexedDB */ - private async verifySyncSuccess(): Promise<{ success: boolean; errors: Record }> { + private async verifySyncSuccess(): Promise<{ + success: boolean; + errors: Record; + }> { + if (!isBrowser) return { success: true, errors: {} }; + const errors: Record = {}; - + // Check each collection that had errors for (const [collection, error] of Object.entries(this.syncErrors)) { errors[collection] = error.message; } - + // Check if user data was synced properly const userId = this.auth.getUserId(); if (userId) { try { - const user = await this.dataSync.getItem(Collections.USERS, userId, false); + const user = await this.dataSync.getItem( + Collections.USERS, + userId, + false, + ); if (!user) { - errors['user_verification'] = 'User data not found in IndexedDB after sync'; + errors["user_verification"] = + "User data not found in IndexedDB after sync"; } } catch (error) { - errors['user_verification'] = `Error verifying user data: ${(error as Error).message}`; + errors["user_verification"] = + `Error verifying user data: ${(error as Error).message}`; } } - + return { success: Object.keys(errors).length === 0, - errors + errors, }; } /** * Show a notification to the user about sync status */ - private showSyncNotification(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info'): void { + private showSyncNotification( + message: string, + type: "info" | "success" | "warning" | "error" = "info", + ): void { // Only run in browser environment - if (typeof window === 'undefined') return; - + if (!isBrowser) return; + // Check if toast function exists (from react-hot-toast or similar) - if (typeof window.toast === 'function') { + if (typeof window.toast === "function") { window.toast(message, { type }); } else { // Fallback to console @@ -253,8 +290,8 @@ export class AuthSyncService { */ public async forceSyncAll(): Promise { if (this.isSyncing) { - console.log('Sync already in progress, queueing full sync'); - this.syncQueue.push('login'); // Reuse login sync logic + console.log("Sync already in progress, queueing full sync"); + this.syncQueue.push("login"); // Reuse login sync logic return true; } @@ -279,6 +316,9 @@ export class AuthSyncService { // Add toast type to window for TypeScript declare global { interface Window { - toast?: (message: string, options?: { type: 'info' | 'success' | 'warning' | 'error' }) => void; + toast?: ( + message: string, + options?: { type: "info" | "success" | "warning" | "error" }, + ) => void; } -} \ No newline at end of file +} diff --git a/src/scripts/database/DataSyncService.ts b/src/scripts/database/DataSyncService.ts index 71dcb6c..f9024db 100644 --- a/src/scripts/database/DataSyncService.ts +++ b/src/scripts/database/DataSyncService.ts @@ -1,16 +1,20 @@ -import { DexieService } from './DexieService'; -import { Get } from '../pocketbase/Get'; -import { Update } from '../pocketbase/Update'; -import { Authentication } from '../pocketbase/Authentication'; -import { Collections, type BaseRecord } from '../../schemas/pocketbase/schema'; -import type Dexie from 'dexie'; +import { DexieService } from "./DexieService"; +import { Get } from "../pocketbase/Get"; +import { Update } from "../pocketbase/Update"; +import { Authentication } from "../pocketbase/Authentication"; +import { Collections, type BaseRecord } from "../../schemas/pocketbase/schema"; +import type Dexie from "dexie"; + +// Check if we're in a browser environment +const isBrowser = + typeof window !== "undefined" && typeof window.indexedDB !== "undefined"; // Interface for tracking offline changes interface OfflineChange { id: string; collection: string; recordId: string; - operation: 'create' | 'update' | 'delete'; + operation: "create" | "update" | "delete"; data?: any; timestamp: number; synced: boolean; @@ -32,14 +36,14 @@ export class DataSyncService { this.get = Get.getInstance(); this.update = Update.getInstance(); this.auth = Authentication.getInstance(); - - // Initialize offline changes table - this.initOfflineChangesTable(); - - // Check for network status - if (typeof window !== 'undefined') { - window.addEventListener('online', this.handleOnline.bind(this)); - window.addEventListener('offline', this.handleOffline.bind(this)); + + // Initialize offline changes table only in browser + if (isBrowser) { + this.initOfflineChangesTable(); + + // Check for network status + window.addEventListener("online", this.handleOnline.bind(this)); + window.addEventListener("offline", this.handleOffline.bind(this)); this.offlineMode = !navigator.onLine; } } @@ -55,16 +59,21 @@ export class DataSyncService { * Initialize the offline changes table */ private initOfflineChangesTable(): void { + if (!isBrowser) return; + try { const db = this.dexieService.getDB(); // Check if the table exists in the schema - if ('offlineChanges' in db) { - this.offlineChangesTable = db.offlineChanges as Dexie.Table; + if ("offlineChanges" in db) { + this.offlineChangesTable = db.offlineChanges as Dexie.Table< + OfflineChange, + string + >; } else { - console.warn('Offline changes table not found in schema'); + console.warn("Offline changes table not found in schema"); } } catch (error) { - console.error('Error initializing offline changes table:', error); + console.error("Error initializing offline changes table:", error); } } @@ -72,7 +81,9 @@ export class DataSyncService { * Handle device coming online */ private async handleOnline(): Promise { - console.log('Device is online, syncing pending changes...'); + if (!isBrowser) return; + + console.log("Device is online, syncing pending changes..."); this.offlineMode = false; await this.syncOfflineChanges(); } @@ -81,7 +92,9 @@ export class DataSyncService { * Handle device going offline */ private handleOffline(): void { - console.log('Device is offline, enabling offline mode...'); + if (!isBrowser) return; + + console.log("Device is offline, enabling offline mode..."); this.offlineMode = true; } @@ -89,11 +102,17 @@ export class DataSyncService { * Sync a specific collection from PocketBase to IndexedDB */ public async syncCollection( - collection: string, - filter: string = '', - sort: string = '-created', - expand: Record | string[] | string = {} + collection: string, + filter: string = "", + sort: string = "-created", + expand: Record | string[] | string = {}, ): Promise { + // Skip in non-browser environments + if (!isBrowser) { + console.log(`Skipping sync for ${collection} in non-browser environment`); + return []; + } + // Prevent multiple syncs of the same collection at the same time if (this.syncInProgress[collection]) { console.log(`Sync already in progress for ${collection}`); @@ -101,7 +120,7 @@ export class DataSyncService { } this.syncInProgress[collection] = true; - + try { // Check if we're authenticated if (!this.auth.isAuthenticated()) { @@ -114,65 +133,73 @@ export class DataSyncService { console.log(`Device is offline, using cached data for ${collection}`); const db = this.dexieService.getDB(); const table = this.getTableForCollection(collection); - return table ? (await table.toArray() as T[]) : []; + return table ? ((await table.toArray()) as T[]) : []; } console.log(`Syncing ${collection}...`); - + // Normalize expand parameter to be an array of strings let normalizedExpand: string[] | undefined; - + if (expand) { - if (typeof expand === 'string') { + if (typeof expand === "string") { // If expand is a string, convert it to an array normalizedExpand = [expand]; } else if (Array.isArray(expand)) { // If expand is already an array, use it as is normalizedExpand = expand; - } else if (typeof expand === 'object') { + } else if (typeof expand === "object") { // If expand is an object, extract the keys normalizedExpand = Object.keys(expand); } } - + // Get data from PocketBase const items = await this.get.getAll(collection, filter, sort, { - expand: normalizedExpand + expand: normalizedExpand, }); console.log(`Fetched ${items.length} items from ${collection}`); - + // Get the database table const db = this.dexieService.getDB(); const table = this.getTableForCollection(collection); - + if (!table) { console.error(`No table found for collection ${collection}`); return []; } - + // Get existing items to handle conflicts const existingItems = await table.toArray(); - const existingItemsMap = new Map(existingItems.map(item => [item.id, item])); - + const existingItemsMap = new Map( + existingItems.map((item) => [item.id, item]), + ); + // Handle conflicts and merge changes - const itemsToStore = await Promise.all(items.map(async (item) => { - const existingItem = existingItemsMap.get(item.id); - - if (existingItem) { - // Check for conflicts (local changes vs server changes) - const resolvedItem = await this.resolveConflict(collection, existingItem, item); - return resolvedItem; - } - - return item; - })); - + const itemsToStore = await Promise.all( + items.map(async (item) => { + const existingItem = existingItemsMap.get(item.id); + + if (existingItem) { + // Check for conflicts (local changes vs server changes) + const resolvedItem = await this.resolveConflict( + collection, + existingItem, + item, + ); + return resolvedItem; + } + + return item; + }), + ); + // Store in IndexedDB await table.bulkPut(itemsToStore); - + // Update last sync timestamp await this.dexieService.updateLastSync(collection); - + return itemsToStore as T[]; } catch (error) { console.error(`Error syncing ${collection}:`, error); @@ -188,30 +215,35 @@ export class DataSyncService { private async resolveConflict( collection: string, localItem: T, - serverItem: T + serverItem: T, ): Promise { // Check if there are pending offline changes for this item - const pendingChanges = await this.getPendingChangesForRecord(collection, localItem.id); - + const pendingChanges = await this.getPendingChangesForRecord( + collection, + localItem.id, + ); + if (pendingChanges.length > 0) { - console.log(`Found ${pendingChanges.length} pending changes for ${collection}:${localItem.id}`); - + console.log( + `Found ${pendingChanges.length} pending changes for ${collection}:${localItem.id}`, + ); + // Server-wins strategy by default, but preserve local changes that haven't been synced const mergedItem = { ...serverItem }; - + // Apply pending changes on top of server data for (const change of pendingChanges) { - if (change.operation === 'update' && change.data) { + if (change.operation === "update" && change.data) { // Apply each field change individually Object.entries(change.data).forEach(([key, value]) => { (mergedItem as any)[key] = value; }); } } - + return mergedItem; } - + // No pending changes, use server data return serverItem; } @@ -219,17 +251,23 @@ export class DataSyncService { /** * Get pending changes for a specific record */ - private async getPendingChangesForRecord(collection: string, recordId: string): Promise { + private async getPendingChangesForRecord( + collection: string, + recordId: string, + ): Promise { if (!this.offlineChangesTable) return []; - + try { return await this.offlineChangesTable - .where('collection') + .where("collection") .equals(collection) - .and(item => item.recordId === recordId && !item.synced) + .and((item) => item.recordId === recordId && !item.synced) .toArray(); } catch (error) { - console.error(`Error getting pending changes for ${collection}:${recordId}:`, error); + console.error( + `Error getting pending changes for ${collection}:${recordId}:`, + error, + ); return []; } } @@ -239,66 +277,73 @@ export class DataSyncService { */ public async syncOfflineChanges(): Promise { if (!this.offlineChangesTable || this.offlineMode) return false; - + try { // Get all unsynced changes const pendingChanges = await this.offlineChangesTable - .where('synced') + .where("synced") .equals(0) // Use 0 instead of false for indexable type .toArray(); - + if (pendingChanges.length === 0) { - console.log('No pending offline changes to sync'); + console.log("No pending offline changes to sync"); return true; } - + console.log(`Syncing ${pendingChanges.length} offline changes...`); - + // Group changes by collection for more efficient processing - const changesByCollection = pendingChanges.reduce((groups, change) => { - const key = change.collection; - if (!groups[key]) { - groups[key] = []; - } - groups[key].push(change); - return groups; - }, {} as Record); - + const changesByCollection = pendingChanges.reduce( + (groups, change) => { + const key = change.collection; + if (!groups[key]) { + groups[key] = []; + } + groups[key].push(change); + return groups; + }, + {} as Record, + ); + // Process each collection's changes for (const [collection, changes] of Object.entries(changesByCollection)) { // First sync the collection to get latest data await this.syncCollection(collection); - + // Then apply each change for (const change of changes) { try { - if (change.operation === 'update' && change.data) { - await this.update.updateFields(collection, change.recordId, change.data); - + if (change.operation === "update" && change.data) { + await this.update.updateFields( + collection, + change.recordId, + change.data, + ); + // Mark as synced await this.offlineChangesTable.update(change.id, { synced: true, - syncAttempts: change.syncAttempts + 1 + syncAttempts: change.syncAttempts + 1, }); } // Add support for create and delete operations as needed } catch (error) { console.error(`Error syncing change ${change.id}:`, error); - + // Increment sync attempts await this.offlineChangesTable.update(change.id, { - syncAttempts: change.syncAttempts + 1 + syncAttempts: change.syncAttempts + 1, }); } } - + // Sync again to ensure we have the latest data await this.syncCollection(collection); } - + return true; } catch (error) { - console.error('Error syncing offline changes:', error); + console.error("Error syncing offline changes:", error); return false; } } @@ -309,35 +354,40 @@ export class DataSyncService { public async recordOfflineChange( collection: string, recordId: string, - operation: 'create' | 'update' | 'delete', - data?: any + operation: "create" | "update" | "delete", + data?: any, ): Promise { if (!this.offlineChangesTable) return null; - + try { - const change: Omit = { + const change: Omit = { collection, recordId, operation, data, timestamp: Date.now(), synced: false, - syncAttempts: 0 + syncAttempts: 0, }; - + const id = await this.offlineChangesTable.add(change as OfflineChange); - console.log(`Recorded offline change: ${operation} on ${collection}:${recordId}`); - + console.log( + `Recorded offline change: ${operation} on ${collection}:${recordId}`, + ); + // Try to sync immediately if we're online if (!this.offlineMode) { - this.syncOfflineChanges().catch(err => { - console.error('Error syncing after recording change:', err); + this.syncOfflineChanges().catch((err) => { + console.error("Error syncing after recording change:", err); }); } - + return id; } catch (error) { - console.error(`Error recording offline change for ${collection}:${recordId}:`, error); + console.error( + `Error recording offline change for ${collection}:${recordId}:`, + error, + ); return null; } } @@ -346,85 +396,89 @@ export class DataSyncService { * Get data from IndexedDB, syncing from PocketBase if needed */ public async getData( - collection: string, + collection: string, forceSync: boolean = false, - filter: string = '', - sort: string = '-created', - expand: Record | string[] | string = {} + filter: string = "", + sort: string = "-created", + expand: Record | string[] | string = {}, ): Promise { const db = this.dexieService.getDB(); const table = this.getTableForCollection(collection); - + if (!table) { console.error(`No table found for collection ${collection}`); return []; } - + // Check if we need to sync const lastSync = await this.dexieService.getLastSync(collection); const now = Date.now(); const syncThreshold = 5 * 60 * 1000; // 5 minutes - - if (!this.offlineMode && (forceSync || (now - lastSync > syncThreshold))) { + + if (!this.offlineMode && (forceSync || now - lastSync > syncThreshold)) { try { await this.syncCollection(collection, filter, sort, expand); } catch (error) { console.error(`Error syncing ${collection}, using cached data:`, error); } } - + // Get data from IndexedDB let data = await table.toArray(); - + // Apply filter if provided if (filter) { // This is a simple implementation - in a real app, you'd want to parse the filter string // and apply it properly. This is just a basic example. data = data.filter((item: any) => { // Split filter by logical operators - const conditions = filter.split(' && '); - return conditions.every(condition => { + const conditions = filter.split(" && "); + return conditions.every((condition) => { // Parse condition (very basic implementation) - if (condition.includes('=')) { - const [field, value] = condition.split('='); - const cleanValue = value.replace(/"/g, ''); + if (condition.includes("=")) { + const [field, value] = condition.split("="); + const cleanValue = value.replace(/"/g, ""); return item[field] === cleanValue; } return true; }); }); } - + // Apply sort if provided if (sort) { - const isDesc = sort.startsWith('-'); + const isDesc = sort.startsWith("-"); const field = isDesc ? sort.substring(1) : sort; - + data.sort((a: any, b: any) => { if (a[field] < b[field]) return isDesc ? 1 : -1; if (a[field] > b[field]) return isDesc ? -1 : 1; return 0; }); } - + return data as T[]; } /** * Get a single item by ID */ - public async getItem(collection: string, id: string, forceSync: boolean = false): Promise { + public async getItem( + collection: string, + id: string, + forceSync: boolean = false, + ): Promise { const db = this.dexieService.getDB(); const table = this.getTableForCollection(collection); - + if (!table) { console.error(`No table found for collection ${collection}`); return undefined; } - + // Try to get from IndexedDB first - let item = await table.get(id) as T | undefined; - + let item = (await table.get(id)) as T | undefined; + // If not found or force sync, try to get from PocketBase if ((!item || forceSync) && !this.offlineMode) { try { @@ -437,7 +491,7 @@ export class DataSyncService { console.error(`Error fetching ${collection} item ${id}:`, error); } } - + return item; } @@ -447,41 +501,45 @@ export class DataSyncService { public async updateItem( collection: string, id: string, - data: Partial + data: Partial, ): Promise { const table = this.getTableForCollection(collection); - + if (!table) { console.error(`No table found for collection ${collection}`); return undefined; } - + // Get the current item - const currentItem = await table.get(id) as T | undefined; + const currentItem = (await table.get(id)) as T | undefined; if (!currentItem) { console.error(`Item ${id} not found in ${collection}`); return undefined; } - + // Update the item in IndexedDB - const updatedItem = { ...currentItem, ...data, updated: new Date().toISOString() }; + const updatedItem = { + ...currentItem, + ...data, + updated: new Date().toISOString(), + }; await table.put(updatedItem); - + // If offline, record the change for later sync if (this.offlineMode) { - await this.recordOfflineChange(collection, id, 'update', data); + await this.recordOfflineChange(collection, id, "update", data); return updatedItem; } - + // If online, update in PocketBase try { const result = await this.update.updateFields(collection, id, data); return result as T; } catch (error) { console.error(`Error updating ${collection} item ${id}:`, error); - + // Record as offline change to retry later - await this.recordOfflineChange(collection, id, 'update', data); + await this.recordOfflineChange(collection, id, "update", data); return updatedItem; } } @@ -503,9 +561,11 @@ export class DataSyncService { /** * Get the appropriate Dexie table for a collection */ - private getTableForCollection(collection: string): Dexie.Table | null { + private getTableForCollection( + collection: string, + ): Dexie.Table | null { const db = this.dexieService.getDB(); - + switch (collection) { case Collections.USERS: return db.users; @@ -528,4 +588,4 @@ export class DataSyncService { return null; } } -} \ No newline at end of file +} diff --git a/src/scripts/database/DexieService.ts b/src/scripts/database/DexieService.ts index 00963e9..f444100 100644 --- a/src/scripts/database/DexieService.ts +++ b/src/scripts/database/DexieService.ts @@ -1,21 +1,25 @@ -import Dexie from 'dexie'; -import type { - User, - Event, - EventRequest, - Log, - Officer, - Reimbursement, - Receipt, - Sponsor -} from '../../schemas/pocketbase/schema'; +import Dexie from "dexie"; +import type { + User, + Event, + EventRequest, + Log, + Officer, + Reimbursement, + Receipt, + Sponsor, +} from "../../schemas/pocketbase/schema"; + +// Check if we're in a browser environment +const isBrowser = + typeof window !== "undefined" && typeof window.indexedDB !== "undefined"; // Interface for tracking offline changes interface OfflineChange { id: string; collection: string; recordId: string; - operation: 'create' | 'update' | 'delete'; + operation: "create" | "update" | "delete"; data?: any; timestamp: number; synced: boolean; @@ -32,59 +36,84 @@ export class DashboardDatabase extends Dexie { receipts!: Dexie.Table; sponsors!: Dexie.Table; offlineChanges!: Dexie.Table; - + // Store last sync timestamps - syncInfo!: Dexie.Table<{id: string, collection: string, lastSync: number}, string>; + syncInfo!: Dexie.Table< + { id: string; collection: string; lastSync: number }, + string + >; constructor() { - super('IEEEDashboardDB'); - + super("IEEEDashboardDB"); + this.version(1).stores({ - users: 'id, email, name', - events: 'id, event_name, event_code, start_date, end_date, published', - eventRequests: 'id, name, status, requested_user, created, updated', - logs: 'id, user, type, created', - officers: 'id, user, role, type', - reimbursements: 'id, title, status, submitted_by, created', - receipts: 'id, created_by, date', - sponsors: 'id, user, company', - syncInfo: 'id, collection, lastSync' + users: "id, email, name", + events: "id, event_name, event_code, start_date, end_date, published", + eventRequests: "id, name, status, requested_user, created, updated", + logs: "id, user, type, created", + officers: "id, user, role, type", + reimbursements: "id, title, status, submitted_by, created", + receipts: "id, created_by, date", + sponsors: "id, user, company", + syncInfo: "id, collection, lastSync", }); // Add version 2 with offlineChanges table this.version(2).stores({ - offlineChanges: 'id, collection, recordId, operation, timestamp, synced, syncAttempts' + offlineChanges: + "id, collection, recordId, operation, timestamp, synced, syncAttempts", }); } // Initialize the database with default values async initialize() { const collections = [ - 'users', 'events', 'event_request', 'logs', - 'officers', 'reimbursement', 'receipts', 'sponsors' + "users", + "events", + "event_request", + "logs", + "officers", + "reimbursement", + "receipts", + "sponsors", ]; - + for (const collection of collections) { const exists = await this.syncInfo.get(collection); if (!exists) { await this.syncInfo.put({ id: collection, collection, - lastSync: 0 + lastSync: 0, }); } } } } +// Mock database for server-side rendering +class MockDashboardDatabase { + // Implement empty methods that won't fail during SSR + async initialize() { + // Do nothing + } +} + // Singleton pattern export class DexieService { private static instance: DexieService; - private db: DashboardDatabase; + private db: DashboardDatabase | MockDashboardDatabase; private constructor() { - this.db = new DashboardDatabase(); - this.db.initialize(); + if (isBrowser) { + // Only initialize Dexie in browser environments + this.db = new DashboardDatabase(); + this.db.initialize(); + } else { + // Use a mock database in non-browser environments + console.log("Running in Node.js environment, using mock database"); + this.db = new MockDashboardDatabase() as any; + } } public static getInstance(): DexieService { @@ -96,40 +125,58 @@ export class DexieService { // Get the database instance public getDB(): DashboardDatabase { - return this.db; + if (!isBrowser) { + console.warn( + "Attempting to access IndexedDB in a non-browser environment", + ); + } + return this.db as DashboardDatabase; } // Update the last sync timestamp for a collection public async updateLastSync(collection: string): Promise { - await this.db.syncInfo.update(collection, { lastSync: Date.now() }); + if (!isBrowser) return; + await (this.db as DashboardDatabase).syncInfo.update(collection, { + lastSync: Date.now(), + }); } // Get the last sync timestamp for a collection public async getLastSync(collection: string): Promise { - const info = await this.db.syncInfo.get(collection); + if (!isBrowser) return 0; + const info = await (this.db as DashboardDatabase).syncInfo.get(collection); return info?.lastSync || 0; } // Clear all data (useful for logout) public async clearAllData(): Promise { - await this.db.users.clear(); - await this.db.events.clear(); - await this.db.eventRequests.clear(); - await this.db.logs.clear(); - await this.db.officers.clear(); - await this.db.reimbursements.clear(); - await this.db.receipts.clear(); - await this.db.sponsors.clear(); - await this.db.offlineChanges.clear(); - + if (!isBrowser) return; + + const db = this.db as DashboardDatabase; + await db.users.clear(); + await db.events.clear(); + await db.eventRequests.clear(); + await db.logs.clear(); + await db.officers.clear(); + await db.reimbursements.clear(); + await db.receipts.clear(); + await db.sponsors.clear(); + await db.offlineChanges.clear(); + // Reset sync timestamps const collections = [ - 'users', 'events', 'event_request', 'logs', - 'officers', 'reimbursement', 'receipts', 'sponsors' + "users", + "events", + "event_request", + "logs", + "officers", + "reimbursement", + "receipts", + "sponsors", ]; - + for (const collection of collections) { - await this.db.syncInfo.update(collection, { lastSync: 0 }); + await db.syncInfo.update(collection, { lastSync: 0 }); } } -} \ No newline at end of file +} diff --git a/src/scripts/pocketbase/Get.ts b/src/scripts/pocketbase/Get.ts index 2a2df3e..2791d3d 100644 --- a/src/scripts/pocketbase/Get.ts +++ b/src/scripts/pocketbase/Get.ts @@ -137,17 +137,17 @@ export class Get { try { const pb = this.auth.getPocketBase(); - + // Handle expand parameter let expandString: string | undefined; if (options?.expand) { if (Array.isArray(options.expand)) { expandString = options.expand.join(","); - } else if (typeof options.expand === 'string') { + } else if (typeof options.expand === "string") { expandString = options.expand; } } - + const requestOptions = { ...(options?.fields && { fields: options.fields.join(",") }), ...(expandString && { expand: expandString }), @@ -190,17 +190,17 @@ export class Get { const filter = recordIds.map((id) => `id="${id}"`).join(" || "); const pb = this.auth.getPocketBase(); - + // Handle expand parameter let expandString: string | undefined; if (options?.expand) { if (Array.isArray(options.expand)) { expandString = options.expand.join(","); - } else if (typeof options.expand === 'string') { + } else if (typeof options.expand === "string") { expandString = options.expand; } } - + const requestOptions = { filter, ...(options?.fields && { fields: options.fields.join(",") }), @@ -300,17 +300,17 @@ export class Get { // but the token is still valid for API requests try { const pb = this.auth.getPocketBase(); - + // Handle expand parameter let expandString: string | undefined; if (options?.expand) { if (Array.isArray(options.expand)) { expandString = options.expand.join(","); - } else if (typeof options.expand === 'string') { + } else if (typeof options.expand === "string") { expandString = options.expand; } } - + const requestOptions = { ...(filter && { filter }), ...(sort && { sort }),