diff --git a/apps/api/src/db/models/beta-applicant.model.ts b/apps/api/src/db/models/beta-applicant.model.ts index 43885d2..19f6385 100644 --- a/apps/api/src/db/models/beta-applicant.model.ts +++ b/apps/api/src/db/models/beta-applicant.model.ts @@ -1,6 +1,7 @@ import mongoose, { Schema, Document } from 'mongoose'; -export interface BetaApplicantDocument extends Document { +export interface BetaApplicantDocument extends Document { + _id: string; email: string; platform: 'win' | 'mac-arm' | 'mac-intel' | 'linux'; motivation: string; @@ -9,6 +10,10 @@ export interface BetaApplicantDocument extends Document { const BetaApplicantSchema = new Schema( { + _id: { + type: String, + default: () => new mongoose.Types.ObjectId().toString() + }, email: { type: String, required: true, diff --git a/apps/api/src/db/models/user.model.ts b/apps/api/src/db/models/user.model.ts index 171a1bb..180d8b5 100644 --- a/apps/api/src/db/models/user.model.ts +++ b/apps/api/src/db/models/user.model.ts @@ -9,7 +9,8 @@ import { } from '../plugins/index.js'; export interface UserDocument - extends mongoose.Document, SoftDeleteDocument, AuditDocument, TimestampDocument { + extends mongoose.Document, SoftDeleteDocument, AuditDocument, TimestampDocument { + _id: string; email: string; passwordHash?: string | null; name: string; @@ -25,6 +26,10 @@ export interface UserDocument const userSchema = new Schema( { + _id: { + type: String, + default: () => new mongoose.Types.ObjectId().toString() + }, email: { type: String, required: true, diff --git a/apps/api/src/db/models/workspace.model.ts b/apps/api/src/db/models/workspace.model.ts index 997d33b..153e5d3 100644 --- a/apps/api/src/db/models/workspace.model.ts +++ b/apps/api/src/db/models/workspace.model.ts @@ -21,7 +21,8 @@ export interface WorkspaceMember { } export interface WorkspaceDocument - extends mongoose.Document, SoftDeleteDocument, AuditDocument, TimestampDocument { + extends mongoose.Document, SoftDeleteDocument, AuditDocument, TimestampDocument { + _id: string; name: string; slug: string; ownerId: string; @@ -39,6 +40,10 @@ export interface WorkspaceDocument const workspaceSchema = new Schema( { + _id: { + type: String, + default: () => new mongoose.Types.ObjectId().toString() + }, name: { type: String, required: true, diff --git a/apps/api/src/db/plugins/workspace.ts b/apps/api/src/db/plugins/workspace.ts index 4ea61c3..b138a53 100644 --- a/apps/api/src/db/plugins/workspace.ts +++ b/apps/api/src/db/plugins/workspace.ts @@ -1,4 +1,4 @@ -import type { Schema } from 'mongoose'; +import mongoose, { type Schema } from 'mongoose'; export interface WorkspaceScopedDocument { workspaceId: string; @@ -6,6 +6,10 @@ export interface WorkspaceScopedDocument { export function workspacePlugin(schema: Schema) { schema.add({ + _id: { + type: String, + default: () => new mongoose.Types.ObjectId().toString() + }, workspaceId: { type: String, required: true, diff --git a/apps/api/src/repositories/base/base.repository.ts b/apps/api/src/repositories/base/base.repository.ts index d25c542..0070a72 100644 --- a/apps/api/src/repositories/base/base.repository.ts +++ b/apps/api/src/repositories/base/base.repository.ts @@ -8,12 +8,14 @@ import { type FilterQuery = any; -export class BaseRepository { +export class BaseRepository> { constructor( protected model: Model, protected workspaceId?: string ) {} + + /** * Translates mongoose exceptions to domain errors. */ @@ -86,6 +88,9 @@ export class BaseRepository { public async create(data: Partial | any, session?: ClientSession): Promise { try { const payload = this.workspaceId ? { ...data, workspaceId: this.workspaceId } : data; + if (payload.id && !payload._id) { + payload._id = payload.id; + } const doc = new this.model(payload); const saveOptions = session ? { session } : {}; diff --git a/apps/api/src/services/outreach/outreach.service.ts b/apps/api/src/services/outreach/outreach.service.ts index eb80098..3d41acb 100644 --- a/apps/api/src/services/outreach/outreach.service.ts +++ b/apps/api/src/services/outreach/outreach.service.ts @@ -16,7 +16,8 @@ export class OutreachService { * Encrypts SMTP App Password and saves the email account to the workspace database. */ public async createEmailAccount(data: any): Promise { - const encrypted = encrypt(data.password); + const rawPassword = data.password || data.smtpPassword || data.imapPassword || ''; + const encrypted = encrypt(rawPassword); if (data.isDefault) { await EmailAccountModel.updateMany({ workspaceId: this.workspaceId } as any, { @@ -25,6 +26,7 @@ export class OutreachService { } const account = new EmailAccountModel({ + _id: data.id || data._id || undefined, workspaceId: this.workspaceId as any, name: data.name, email: data.email, @@ -93,6 +95,7 @@ export class OutreachService { const variables = Array.from(new Set([...bodyVars, ...subjectVars])); const template = new EmailTemplateModel({ + _id: data.id || data._id || undefined, workspaceId: new mongoose.Types.ObjectId(this.workspaceId), name: data.name, subject: data.subject, diff --git a/apps/desktop/src/main/database/repositories/local-crm.ts b/apps/desktop/src/main/database/repositories/local-crm.ts index 1b51d6d..e2e6d56 100644 --- a/apps/desktop/src/main/database/repositories/local-crm.ts +++ b/apps/desktop/src/main/database/repositories/local-crm.ts @@ -131,7 +131,21 @@ export const LocalCRMRepository = { const operation = existing ? 'UPDATE' : 'CREATE'; // 2. Perform write on target table - db.prepare(query).run(...params); + if (existing) { + const updateColumns = columns.filter((c) => c !== 'id'); + const setClause = updateColumns.map((c) => `${c} = ?`).join(', '); + const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE id = ?`; + const updateParams = updateColumns.map((col) => { + const val = record[col]; + if (val instanceof Date) return val.toISOString(); + if (typeof val === 'object' && val !== null) return JSON.stringify(val); + return val; + }); + updateParams.push(record.id); + db.prepare(updateQuery).run(...updateParams); + } else { + db.prepare(query).run(...params); + } // 3. Queue offline mutation task if this is a syncable crm table const syncableTables = [ @@ -218,7 +232,12 @@ export const LocalCRMRepository = { const placeholders = columns.map(() => '?').join(', '); const query = `INSERT OR REPLACE INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`; + const updateColumns = columns.filter((c) => c !== 'id'); + const setClause = updateColumns.map((c) => `${c} = ?`).join(', '); + const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE id = ?`; + const statement = db.prepare(query); + const updateStatement = db.prepare(updateQuery); const checkStmt = db.prepare(`SELECT * FROM ${tableName} WHERE id = ?`); const insertSyncQueue = db.prepare(` INSERT INTO sync_queue (id, workspaceId, entityType, entityId, operation, payload, version, retryCount, lastError, createdAt, updatedAt) @@ -237,17 +256,31 @@ export const LocalCRMRepository = { const transaction = db.transaction((list: any[]) => { for (const item of list) { - const params = columns.map((col) => { - const val = item[col]; - if (val instanceof Date) return val.toISOString(); - if (typeof val === 'object' && val !== null) return JSON.stringify(val); - return val; - }); - const existing = checkStmt.get(item.id) as any; const operation = existing ? 'UPDATE' : 'CREATE'; - statement.run(...params); + if (existing) { + // Prevent remote sync from overwriting local changes still queued to upload + if (existing.syncStatus === 'pending') { + continue; + } + const updateParams = updateColumns.map((col) => { + const val = item[col]; + if (val instanceof Date) return val.toISOString(); + if (typeof val === 'object' && val !== null) return JSON.stringify(val); + return val; + }); + updateParams.push(item.id); + updateStatement.run(...updateParams); + } else { + const params = columns.map((col) => { + const val = item[col]; + if (val instanceof Date) return val.toISOString(); + if (typeof val === 'object' && val !== null) return JSON.stringify(val); + return val; + }); + statement.run(...params); + } if (!skipQueue && syncableTables.includes(tableName)) { insertSyncQueue.run( diff --git a/apps/desktop/src/main/services/sync-engine.ts b/apps/desktop/src/main/services/sync-engine.ts index 05d1c3d..5d23381 100644 --- a/apps/desktop/src/main/services/sync-engine.ts +++ b/apps/desktop/src/main/services/sync-engine.ts @@ -128,11 +128,18 @@ export class SyncEngine { try { const campaigns = await this.sdk.campaigns.list(); if (campaigns && campaigns.length) { - const records = campaigns.map((c: any) => ({ - ...c, - workspaceId: this.workspaceId, - syncStatus: 'synced' - })); + const records = campaigns.map((c: any) => { + let status = c.status; + if (status) { + status = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); + } + return { + ...c, + status, + workspaceId: this.workspaceId, + syncStatus: 'synced' + }; + }); await LocalCRMRepository.saveMany('campaigns', records, true); // skipQueue = true } } catch (e) { @@ -328,6 +335,11 @@ export class SyncEngine { try { const payload = JSON.parse(item.payload || '{}'); + // Map local campaign status casing to server uppercase requirements + if (item.entityType === 'campaigns' && payload.status) { + payload.status = payload.status.toUpperCase(); + } + // Resolve SDK module dynamically const clientModule = this.resolveSdkModule(item.entityType); if (!clientModule) { diff --git a/apps/desktop/src/renderer/components/auth/animated-bg.tsx b/apps/desktop/src/renderer/components/auth/animated-bg.tsx index c11eda8..baca74c 100644 --- a/apps/desktop/src/renderer/components/auth/animated-bg.tsx +++ b/apps/desktop/src/renderer/components/auth/animated-bg.tsx @@ -171,10 +171,10 @@ export function AnimatedBackground() { className="pointer-events-none absolute inset-0 overflow-hidden bg-[#000000]" > {/* 1. Noise texture — filmic grain for surface depth */} -
+
{/* 2. Scanlines — subtle CRT depth effect */} -
+
{/* 3. Interactive constellation canvas */} {/* Form column */} -
+
{/* Subtle background pulsing glows behind the glassmorphic card */} -
-
+
+
-
+
{/* Top-left decorative dots */} { return val; }; -export const objectIdField = z.string().regex(/^[0-9a-fA-F]{24}$/, { - message: 'Invalid ObjectId format' -}); +export const objectIdField = z.string().min(1, 'ID is required'); // Nullable variant that accepts empty strings (coerces them to null) export const objectIdFieldNullable = z.preprocess( emptyToNull, - z - .string() - .regex(/^[0-9a-fA-F]{24}$/, { message: 'Invalid ObjectId format' }) - .nullable() + z.string().nullable() ); export const emailField = z.string().email({ message: 'Invalid email address' });