Initial commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { initializeApp, getApps, getApp } from 'firebase/app';
|
||||
import { getAuth } from 'firebase/auth';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
|
||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
|
||||
};
|
||||
|
||||
export const app =
|
||||
getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
|
||||
|
||||
export const auth = getAuth(app);
|
||||
export const db = getFirestore(app);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { initializeApp, getApps, getApp } from 'firebase/app';
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
|
||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
|
||||
};
|
||||
|
||||
export const app = !getApps().length ? initializeApp(firebaseConfig) : getApp();
|
||||
@@ -0,0 +1,287 @@
|
||||
import {
|
||||
collection,
|
||||
doc,
|
||||
getDoc,
|
||||
getDocs,
|
||||
setDoc,
|
||||
updateDoc,
|
||||
query,
|
||||
where,
|
||||
orderBy,
|
||||
Timestamp,
|
||||
addDoc,
|
||||
deleteDoc,
|
||||
} from 'firebase/firestore';
|
||||
import { db } from './firebase';
|
||||
import type {
|
||||
User,
|
||||
AgentSettings,
|
||||
CallTask,
|
||||
CallAttempt,
|
||||
CallRecord,
|
||||
CreditTransaction,
|
||||
NotificationSettings,
|
||||
BookedAppointment,
|
||||
DefaultLimits,
|
||||
Purchase,
|
||||
} from './types';
|
||||
|
||||
// Users
|
||||
export const getUser = async (uid: string): Promise<User | null> => {
|
||||
const docRef = doc(db, 'users', uid);
|
||||
const docSnap = await getDoc(docRef);
|
||||
return docSnap.exists() ? (docSnap.data() as User) : null;
|
||||
};
|
||||
|
||||
export const updateUserCredits = async (uid: string, newBalance: number) => {
|
||||
const docRef = doc(db, 'users', uid);
|
||||
await updateDoc(docRef, {
|
||||
creditBalance: newBalance,
|
||||
updatedAt: Timestamp.now(),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateUserOnboardingCompleted = async (uid: string, completed: boolean) => {
|
||||
const docRef = doc(db, 'users', uid);
|
||||
await updateDoc(docRef, {
|
||||
onboardingCompleted: completed,
|
||||
updatedAt: Timestamp.now(),
|
||||
});
|
||||
};
|
||||
|
||||
// Agent Settings
|
||||
export const getAgentSettings = async (userId: string): Promise<AgentSettings | null> => {
|
||||
const docRef = doc(db, 'agentSettings', userId);
|
||||
const docSnap = await getDoc(docRef);
|
||||
if (docSnap.exists()) {
|
||||
return docSnap.data() as AgentSettings;
|
||||
}
|
||||
// Create default if not exists
|
||||
const defaults: AgentSettings = {
|
||||
userId,
|
||||
agentName: 'HolaCompi',
|
||||
appLanguage: 'English',
|
||||
primaryCallLanguage: 'English',
|
||||
secondaryCallLanguages: [],
|
||||
tone: 'Friendly',
|
||||
agentInstructions: 'You are HolaCompi, a helpful Spanish friend who makes phone calls on behalf of users.',
|
||||
createdAt: Timestamp.now(),
|
||||
updatedAt: Timestamp.now(),
|
||||
};
|
||||
await setDoc(docRef, defaults);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
export const updateAgentSettings = async (userId: string, updates: Partial<AgentSettings>) => {
|
||||
const docRef = doc(db, 'agentSettings', userId);
|
||||
await updateDoc(docRef, {
|
||||
...updates,
|
||||
updatedAt: Timestamp.now(),
|
||||
});
|
||||
};
|
||||
|
||||
// Call Tasks
|
||||
export const createCallTask = async (task: Omit<CallTask, 'id'>): Promise<string> => {
|
||||
const docRef = await addDoc(collection(db, 'callTasks'), task);
|
||||
return docRef.id;
|
||||
};
|
||||
|
||||
export const getCallTasks = async (userId: string): Promise<CallTask[]> => {
|
||||
const q = query(
|
||||
collection(db, 'callTasks'),
|
||||
where('userId', '==', userId),
|
||||
orderBy('scheduledAt', 'desc')
|
||||
);
|
||||
const snapshot = await getDocs(q);
|
||||
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as CallTask));
|
||||
};
|
||||
|
||||
export const getCallTask = async (taskId: string): Promise<CallTask | null> => {
|
||||
const docRef = doc(db, 'callTasks', taskId);
|
||||
const docSnap = await getDoc(docRef);
|
||||
return docSnap.exists() ? ({ id: docSnap.id, ...docSnap.data() } as CallTask) : null;
|
||||
};
|
||||
|
||||
export const updateCallTask = async (taskId: string, updates: Partial<CallTask>) => {
|
||||
const docRef = doc(db, 'callTasks', taskId);
|
||||
await updateDoc(docRef, {
|
||||
...updates,
|
||||
updatedAt: Timestamp.now(),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCallTask = async (taskId: string) => {
|
||||
await deleteDoc(doc(db, 'callTasks', taskId));
|
||||
};
|
||||
|
||||
// Call Attempts
|
||||
export const createCallAttempt = async (
|
||||
taskId: string,
|
||||
attempt: Omit<CallAttempt, 'id'>
|
||||
): Promise<string> => {
|
||||
const docRef = await addDoc(
|
||||
collection(db, 'callTasks', taskId, 'attempts'),
|
||||
attempt
|
||||
);
|
||||
return docRef.id;
|
||||
};
|
||||
|
||||
export const getCallAttempts = async (taskId: string): Promise<CallAttempt[]> => {
|
||||
const q = query(
|
||||
collection(db, 'callTasks', taskId, 'attempts'),
|
||||
orderBy('startTime', 'desc')
|
||||
);
|
||||
const snapshot = await getDocs(q);
|
||||
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as CallAttempt));
|
||||
};
|
||||
|
||||
export const updateCallAttempt = async (
|
||||
taskId: string,
|
||||
attemptId: string,
|
||||
updates: Partial<CallAttempt>
|
||||
) => {
|
||||
const docRef = doc(db, 'callTasks', taskId, 'attempts', attemptId);
|
||||
await updateDoc(docRef, updates);
|
||||
};
|
||||
|
||||
// Calls
|
||||
export const createCallRecord = async (
|
||||
record: Omit<CallRecord, 'id' | 'createdAt' | 'updatedAt'>
|
||||
): Promise<string> => {
|
||||
const docRef = await addDoc(collection(db, 'calls'), {
|
||||
...record,
|
||||
createdAt: Timestamp.now(),
|
||||
updatedAt: Timestamp.now(),
|
||||
});
|
||||
return docRef.id;
|
||||
};
|
||||
|
||||
export const updateCallRecord = async (
|
||||
callId: string,
|
||||
updates: Partial<CallRecord>
|
||||
) => {
|
||||
const docRef = doc(db, 'calls', callId);
|
||||
await updateDoc(docRef, { ...updates, updatedAt: Timestamp.now() });
|
||||
};
|
||||
|
||||
export const getCallByVapiCallId = async (vapiCallId: string): Promise<CallRecord | null> => {
|
||||
const q = query(collection(db, 'calls'), where('vapiCallId', '==', vapiCallId));
|
||||
const snapshot = await getDocs(q);
|
||||
if (snapshot.empty) return null;
|
||||
const docSnap = snapshot.docs[0];
|
||||
return { id: docSnap.id, ...(docSnap.data() as CallRecord) };
|
||||
};
|
||||
|
||||
// Credit Transactions
|
||||
export const createCreditTransaction = async (
|
||||
transaction: Omit<CreditTransaction, 'id'>
|
||||
): Promise<string> => {
|
||||
const docRef = await addDoc(collection(db, 'creditLedger'), transaction);
|
||||
return docRef.id;
|
||||
};
|
||||
|
||||
export const getCreditTransactions = async (userId: string): Promise<CreditTransaction[]> => {
|
||||
const q = query(
|
||||
collection(db, 'creditLedger'),
|
||||
where('userId', '==', userId),
|
||||
orderBy('createdAt', 'desc')
|
||||
);
|
||||
const snapshot = await getDocs(q);
|
||||
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as CreditTransaction));
|
||||
};
|
||||
|
||||
// Purchases
|
||||
export const createPurchase = async (purchase: Omit<Purchase, 'id'>): Promise<string> => {
|
||||
const docRef = await addDoc(collection(db, 'purchases'), purchase);
|
||||
return docRef.id;
|
||||
};
|
||||
|
||||
export const getPurchasesByUser = async (userId: string): Promise<Purchase[]> => {
|
||||
const q = query(
|
||||
collection(db, 'purchases'),
|
||||
where('userId', '==', userId),
|
||||
orderBy('createdAt', 'desc')
|
||||
);
|
||||
const snapshot = await getDocs(q);
|
||||
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as Purchase));
|
||||
};
|
||||
|
||||
// Notification Settings
|
||||
export const getNotificationSettings = async (userId: string): Promise<NotificationSettings> => {
|
||||
const docRef = doc(db, 'notificationSettings', userId);
|
||||
const docSnap = await getDoc(docRef);
|
||||
if (docSnap.exists()) {
|
||||
return docSnap.data() as NotificationSettings;
|
||||
}
|
||||
const defaults: NotificationSettings = {
|
||||
userId,
|
||||
notifyOnSuccess: true,
|
||||
notifyOnFailure: true,
|
||||
emailSummary: false,
|
||||
autoAddToCalendar: false,
|
||||
googleCalendarConnected: false,
|
||||
iCloudCalendarConnected: false,
|
||||
createdAt: Timestamp.now(),
|
||||
updatedAt: Timestamp.now(),
|
||||
};
|
||||
await setDoc(docRef, defaults);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
export const updateNotificationSettings = async (
|
||||
userId: string,
|
||||
updates: Partial<NotificationSettings>
|
||||
) => {
|
||||
const docRef = doc(db, 'notificationSettings', userId);
|
||||
await updateDoc(docRef, {
|
||||
...updates,
|
||||
updatedAt: Timestamp.now(),
|
||||
});
|
||||
};
|
||||
|
||||
// Booked Appointments
|
||||
export const createBookedAppointment = async (
|
||||
appointment: Omit<BookedAppointment, 'id'>
|
||||
): Promise<string> => {
|
||||
const docRef = await addDoc(collection(db, 'bookedAppointments'), appointment);
|
||||
return docRef.id;
|
||||
};
|
||||
|
||||
export const getBookedAppointments = async (userId: string): Promise<BookedAppointment[]> => {
|
||||
const q = query(
|
||||
collection(db, 'bookedAppointments'),
|
||||
where('userId', '==', userId),
|
||||
orderBy('dateTime', 'asc')
|
||||
);
|
||||
const snapshot = await getDocs(q);
|
||||
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as BookedAppointment));
|
||||
};
|
||||
|
||||
// Default Limits
|
||||
export const getDefaultLimits = async (userId: string): Promise<DefaultLimits> => {
|
||||
const docRef = doc(db, 'defaultLimits', userId);
|
||||
const docSnap = await getDoc(docRef);
|
||||
if (docSnap.exists()) {
|
||||
return docSnap.data() as DefaultLimits;
|
||||
}
|
||||
const defaults: DefaultLimits = {
|
||||
userId,
|
||||
defaultMaxMinutes: 10,
|
||||
defaultMaxCredits: 10,
|
||||
allowAutoExtension: false,
|
||||
maxExtraMinutes: 0,
|
||||
defaultMaxRecalls: 2,
|
||||
minDelayBetweenRecalls: 15,
|
||||
updatedAt: Timestamp.now(),
|
||||
};
|
||||
await setDoc(docRef, defaults);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
export const updateDefaultLimits = async (userId: string, updates: Partial<DefaultLimits>) => {
|
||||
const docRef = doc(db, 'defaultLimits', userId);
|
||||
await updateDoc(docRef, {
|
||||
...updates,
|
||||
updatedAt: Timestamp.now(),
|
||||
});
|
||||
};
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import { Timestamp } from 'firebase/firestore';
|
||||
|
||||
export interface User {
|
||||
uid: string;
|
||||
email: string;
|
||||
displayName?: string;
|
||||
onboardingCompleted?: boolean;
|
||||
creditBalance: number; // in EUR/credits
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface AgentSettings {
|
||||
userId: string;
|
||||
agentName: string; // default "HolaCompi"
|
||||
appLanguage: string; // fixed "English" for v1
|
||||
primaryCallLanguage: string; // default "English"
|
||||
secondaryCallLanguages: string[]; // e.g., ["Spanish"]
|
||||
tone: 'Friendly' | 'Professional' | 'Assertive';
|
||||
agentInstructions: string; // system prompt
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface CallTask {
|
||||
id: string;
|
||||
userId: string;
|
||||
businessName: string;
|
||||
phoneNumber: string;
|
||||
preferredLanguage: string; // "English" or "Spanish"
|
||||
callGoals: string; // multi-line text
|
||||
scheduledAt: Timestamp;
|
||||
timezone?: string;
|
||||
maxMinutes: number;
|
||||
maxCredits: number; // same as maxMinutes for v1
|
||||
allowRecalls: boolean;
|
||||
maxRecalls: number;
|
||||
recallsUsed: number;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
lastResult?: string;
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface CallAttempt {
|
||||
id: string;
|
||||
taskId: string;
|
||||
startTime: Timestamp;
|
||||
endTime?: Timestamp;
|
||||
duration: number; // minutes
|
||||
creditsUsed: number;
|
||||
status: 'in_progress' | 'completed' | 'hangup_other_party' | 'failed' | 'busy';
|
||||
transcript?: string;
|
||||
summary?: string;
|
||||
vapiCallId?: string;
|
||||
createdAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface CallRecord {
|
||||
id: string;
|
||||
userId: string;
|
||||
phoneNumber: string;
|
||||
status: 'initiated' | 'started' | 'ended' | 'failed';
|
||||
vapiCallId?: string;
|
||||
assistantId?: string;
|
||||
scheduledTaskId?: string;
|
||||
startedAt?: Timestamp;
|
||||
endedAt?: Timestamp;
|
||||
durationSeconds?: number;
|
||||
creditsUsed?: number;
|
||||
recordingUrl?: string;
|
||||
transcript?: string;
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface CreditTransaction {
|
||||
id: string;
|
||||
userId: string;
|
||||
amount: number; // positive for add, negative for debit
|
||||
type: 'purchase' | 'call_debit' | 'refund';
|
||||
callTaskId?: string;
|
||||
callAttemptId?: string;
|
||||
balanceAfter: number;
|
||||
createdAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
userId: string;
|
||||
notifyOnSuccess: boolean;
|
||||
notifyOnFailure: boolean;
|
||||
emailSummary: boolean;
|
||||
autoAddToCalendar: boolean;
|
||||
googleCalendarConnected: boolean;
|
||||
iCloudCalendarConnected: boolean;
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface BookedAppointment {
|
||||
id: string;
|
||||
userId: string;
|
||||
callTaskId: string;
|
||||
title: string;
|
||||
dateTime: Timestamp;
|
||||
location?: string;
|
||||
phone?: string;
|
||||
notes?: string;
|
||||
calendarEventId?: string;
|
||||
createdAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface DefaultLimits {
|
||||
userId: string;
|
||||
defaultMaxMinutes: number;
|
||||
defaultMaxCredits: number;
|
||||
allowAutoExtension: boolean;
|
||||
maxExtraMinutes: number;
|
||||
defaultMaxRecalls: number;
|
||||
minDelayBetweenRecalls: number; // minutes
|
||||
updatedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface Purchase {
|
||||
id: string;
|
||||
userId: string;
|
||||
credits: number;
|
||||
amount: number; // EUR amount
|
||||
currency: string;
|
||||
stripeSessionId: string;
|
||||
createdAt: Timestamp;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import Vapi from '@vapi-ai/web';
|
||||
|
||||
export const vapiConfig = {
|
||||
publicKey: process.env.NEXT_PUBLIC_VAPI_PUBLIC_KEY!,
|
||||
// Vapi Assistant ID from dashboard
|
||||
assistantId: process.env.NEXT_PUBLIC_VAPI_ASSISTANT_ID,
|
||||
};
|
||||
|
||||
export function initializeVapi() {
|
||||
return new Vapi(vapiConfig.publicKey);
|
||||
}
|
||||
Reference in New Issue
Block a user