feat: Introduce core scraping types, Google Maps scraper, and dashboard pages for business and task management.
Browse files- app/api/businesses/route.ts +5 -0
- app/dashboard/businesses/page.tsx +6 -1
- app/dashboard/page.tsx +6 -1
- app/dashboard/tasks/page.tsx +11 -3
- components/active-task-card.tsx +2 -0
- lib/auth.ts +25 -10
- lib/queue.ts +6 -3
- lib/scrapers/google-maps.ts +1 -0
- lib/scrapers/types.ts +1 -0
app/api/businesses/route.ts
CHANGED
|
@@ -37,12 +37,17 @@ export async function GET(request: Request) {
|
|
| 37 |
conditions.push(eq(businesses.emailStatus, status));
|
| 38 |
}
|
| 39 |
|
|
|
|
|
|
|
|
|
|
| 40 |
// Get total count
|
| 41 |
const [{ count }] = await db
|
| 42 |
.select({ count: sql<number>`count(*)` })
|
| 43 |
.from(businesses)
|
| 44 |
.where(and(...conditions));
|
| 45 |
|
|
|
|
|
|
|
| 46 |
const totalPages = Math.ceil(count / limit);
|
| 47 |
|
| 48 |
const results = await db
|
|
|
|
| 37 |
conditions.push(eq(businesses.emailStatus, status));
|
| 38 |
}
|
| 39 |
|
| 40 |
+
console.log(`🔍 Fetching businesses for UserID: ${userId}`);
|
| 41 |
+
console.log(` Filters - Category: ${category}, Status: ${status}, Page: ${page}, Limit: ${limit}`);
|
| 42 |
+
|
| 43 |
// Get total count
|
| 44 |
const [{ count }] = await db
|
| 45 |
.select({ count: sql<number>`count(*)` })
|
| 46 |
.from(businesses)
|
| 47 |
.where(and(...conditions));
|
| 48 |
|
| 49 |
+
console.log(` Found ${count} total businesses matching criteria`);
|
| 50 |
+
|
| 51 |
const totalPages = Math.ceil(count / limit);
|
| 52 |
|
| 53 |
const results = await db
|
app/dashboard/businesses/page.tsx
CHANGED
|
@@ -71,7 +71,12 @@ export default function BusinessesPage() {
|
|
| 71 |
const handleSendEmail = async (business: Business) => {
|
| 72 |
const toastId = toast.loading(`Sending email to ${business.name}...`);
|
| 73 |
try {
|
| 74 |
-
await sendEmailApi("/api/email/send", { businessId: business.id });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
toast.success(`Email sent to ${business.email}`, { id: toastId });
|
| 76 |
|
| 77 |
// Update local state
|
|
|
|
| 71 |
const handleSendEmail = async (business: Business) => {
|
| 72 |
const toastId = toast.loading(`Sending email to ${business.name}...`);
|
| 73 |
try {
|
| 74 |
+
const result = await sendEmailApi("/api/email/send", { businessId: business.id });
|
| 75 |
+
|
| 76 |
+
if (!result) {
|
| 77 |
+
throw new Error("Failed to send email");
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
toast.success(`Email sent to ${business.email}`, { id: toastId });
|
| 81 |
|
| 82 |
// Update local state
|
app/dashboard/page.tsx
CHANGED
|
@@ -151,7 +151,12 @@ export default function DashboardPage() {
|
|
| 151 |
const handleSendEmail = async (business: Business) => {
|
| 152 |
const toastId = toast.loading(`Sending email to ${business.name}...`);
|
| 153 |
try {
|
| 154 |
-
await sendEmailApi("/api/email/send", { businessId: business.id });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
toast.success(`Email sent to ${business.email}`, { id: toastId });
|
| 156 |
|
| 157 |
setBusinesses(prev => prev.map(b =>
|
|
|
|
| 151 |
const handleSendEmail = async (business: Business) => {
|
| 152 |
const toastId = toast.loading(`Sending email to ${business.name}...`);
|
| 153 |
try {
|
| 154 |
+
const result = await sendEmailApi("/api/email/send", { businessId: business.id });
|
| 155 |
+
|
| 156 |
+
if (!result) {
|
| 157 |
+
throw new Error("Failed to send email");
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
toast.success(`Email sent to ${business.email}`, { id: toastId });
|
| 161 |
|
| 162 |
setBusinesses(prev => prev.map(b =>
|
app/dashboard/tasks/page.tsx
CHANGED
|
@@ -83,6 +83,8 @@ export default function TasksPage() {
|
|
| 83 |
if (result) {
|
| 84 |
toast.success(`Task ${action}d successfully`);
|
| 85 |
await fetchTasks();
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
} catch (error) {
|
| 88 |
toast.error(`Failed to ${action} task`);
|
|
@@ -126,6 +128,8 @@ export default function TasksPage() {
|
|
| 126 |
toast.success(`Priority updated to ${priority}`);
|
| 127 |
// No need to fetch if optimistic update matches, but safe to fetch
|
| 128 |
// await fetchTasks();
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
} catch (error) {
|
| 131 |
toast.error("Failed to update priority");
|
|
@@ -143,9 +147,13 @@ export default function TasksPage() {
|
|
| 143 |
if (!confirmDeleteId) return;
|
| 144 |
|
| 145 |
try {
|
| 146 |
-
await deleteTask(`/api/tasks?id=${confirmDeleteId.id}&type=${confirmDeleteId.type}`);
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
} catch (error) {
|
| 150 |
toast.error("Failed to delete task");
|
| 151 |
console.error("Error deleting task:", error);
|
|
|
|
| 83 |
if (result) {
|
| 84 |
toast.success(`Task ${action}d successfully`);
|
| 85 |
await fetchTasks();
|
| 86 |
+
} else {
|
| 87 |
+
throw new Error("API returned null");
|
| 88 |
}
|
| 89 |
} catch (error) {
|
| 90 |
toast.error(`Failed to ${action} task`);
|
|
|
|
| 128 |
toast.success(`Priority updated to ${priority}`);
|
| 129 |
// No need to fetch if optimistic update matches, but safe to fetch
|
| 130 |
// await fetchTasks();
|
| 131 |
+
} else {
|
| 132 |
+
throw new Error("API returned null");
|
| 133 |
}
|
| 134 |
} catch (error) {
|
| 135 |
toast.error("Failed to update priority");
|
|
|
|
| 147 |
if (!confirmDeleteId) return;
|
| 148 |
|
| 149 |
try {
|
| 150 |
+
const result = await deleteTask(`/api/tasks?id=${confirmDeleteId.id}&type=${confirmDeleteId.type}`);
|
| 151 |
+
if (result !== null) {
|
| 152 |
+
toast.success("Task deleted successfully");
|
| 153 |
+
await fetchTasks();
|
| 154 |
+
} else {
|
| 155 |
+
throw new Error("Failed to delete task");
|
| 156 |
+
}
|
| 157 |
} catch (error) {
|
| 158 |
toast.error("Failed to delete task");
|
| 159 |
console.error("Error deleting task:", error);
|
components/active-task-card.tsx
CHANGED
|
@@ -39,6 +39,8 @@ export function ActiveTaskCard({
|
|
| 39 |
if (result) {
|
| 40 |
toast.success(`Task ${action}d successfully`);
|
| 41 |
onStatusChange?.();
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
} catch (error) {
|
| 44 |
toast.error(`Failed to ${action} task`);
|
|
|
|
| 39 |
if (result) {
|
| 40 |
toast.success(`Task ${action}d successfully`);
|
| 41 |
onStatusChange?.();
|
| 42 |
+
} else {
|
| 43 |
+
throw new Error("Failed to control task");
|
| 44 |
}
|
| 45 |
} catch (error) {
|
| 46 |
toast.error(`Failed to ${action} task`);
|
lib/auth.ts
CHANGED
|
@@ -94,6 +94,8 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
| 94 |
.set({
|
| 95 |
accessToken: account?.access_token,
|
| 96 |
refreshToken: account?.refresh_token,
|
|
|
|
|
|
|
| 97 |
updatedAt: new Date(),
|
| 98 |
})
|
| 99 |
.where(eq(users.id, existingUser.id));
|
|
@@ -115,21 +117,34 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
| 115 |
}
|
| 116 |
},
|
| 117 |
async session({ session, token }) {
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
const dbUser = await db.query.users.findFirst({
|
| 124 |
where: eq(users.email, session.user.email),
|
| 125 |
});
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
}
|
| 132 |
-
}
|
| 133 |
}
|
| 134 |
return session;
|
| 135 |
},
|
|
|
|
| 94 |
.set({
|
| 95 |
accessToken: account?.access_token,
|
| 96 |
refreshToken: account?.refresh_token,
|
| 97 |
+
name: user.name,
|
| 98 |
+
image: user.image,
|
| 99 |
updatedAt: new Date(),
|
| 100 |
})
|
| 101 |
.where(eq(users.id, existingUser.id));
|
|
|
|
| 117 |
}
|
| 118 |
},
|
| 119 |
async session({ session, token }) {
|
| 120 |
+
// console.log("🔐 Session Callback - Token:", JSON.stringify(token, null, 2));
|
| 121 |
+
|
| 122 |
+
// Always prioritize DB lookup for logged in users
|
| 123 |
+
if (session.user && session.user.email) {
|
| 124 |
+
try {
|
| 125 |
const dbUser = await db.query.users.findFirst({
|
| 126 |
where: eq(users.email, session.user.email),
|
| 127 |
});
|
| 128 |
|
| 129 |
+
console.log("👤 DB User found:", dbUser ? dbUser.id : "null");
|
| 130 |
+
|
| 131 |
+
if (dbUser) {
|
| 132 |
+
// Use Database Truth (which is synced from Google on login)
|
| 133 |
+
session.user.id = dbUser.id;
|
| 134 |
+
session.user.name = dbUser.name || session.user.name;
|
| 135 |
+
session.user.image = dbUser.image || session.user.image;
|
| 136 |
+
session.user.role = "user";
|
| 137 |
+
session.user.accessToken = dbUser.accessToken || undefined;
|
| 138 |
+
} else {
|
| 139 |
+
// Only use admin fallback if NOT found in DB (unlikely for Google login)
|
| 140 |
+
if (token.role === "admin") {
|
| 141 |
+
session.user.role = "admin";
|
| 142 |
+
session.user.id = "admin-user";
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
} catch (error) {
|
| 146 |
+
console.error("Error seeking DB user in session:", error);
|
| 147 |
}
|
|
|
|
| 148 |
}
|
| 149 |
return session;
|
| 150 |
},
|
lib/queue.ts
CHANGED
|
@@ -222,15 +222,18 @@ export const scrapingWorker = new Worker(
|
|
| 222 |
...b,
|
| 223 |
userId,
|
| 224 |
category: b.category || "Unknown",
|
| 225 |
-
emailStatus: null,
|
| 226 |
}));
|
| 227 |
|
| 228 |
// Use INSERT ON CONFLICT DO NOTHING approach
|
| 229 |
try {
|
| 230 |
await db.insert(businesses).values(businessesToInsert).onConflictDoNothing();
|
| 231 |
-
} catch {
|
|
|
|
| 232 |
// Fallback if onConflictDoNothing is not supported by driver or schema setup
|
| 233 |
-
await db.insert(businesses).values(businessesToInsert).catch(() => {
|
|
|
|
|
|
|
| 234 |
}
|
| 235 |
|
| 236 |
totalFound += results.length;
|
|
|
|
| 222 |
...b,
|
| 223 |
userId,
|
| 224 |
category: b.category || "Unknown",
|
| 225 |
+
emailStatus: b.emailStatus || null,
|
| 226 |
}));
|
| 227 |
|
| 228 |
// Use INSERT ON CONFLICT DO NOTHING approach
|
| 229 |
try {
|
| 230 |
await db.insert(businesses).values(businessesToInsert).onConflictDoNothing();
|
| 231 |
+
} catch (e: any) {
|
| 232 |
+
console.error(" ❌ Failed to insert businesses (onConflictDoNothing):", e.message);
|
| 233 |
// Fallback if onConflictDoNothing is not supported by driver or schema setup
|
| 234 |
+
await db.insert(businesses).values(businessesToInsert).catch((err) => {
|
| 235 |
+
console.error(" ❌ Fallback insert also failed:", err.message);
|
| 236 |
+
});
|
| 237 |
}
|
| 238 |
|
| 239 |
totalFound += results.length;
|
lib/scrapers/google-maps.ts
CHANGED
|
@@ -30,6 +30,7 @@ export const googleMapsScraper: ScraperSource = {
|
|
| 30 |
imageUrl: undefined, // Not available from scraper
|
| 31 |
source: "google-maps",
|
| 32 |
sourceUrl: business.website || undefined,
|
|
|
|
| 33 |
}));
|
| 34 |
|
| 35 |
console.log(`✅ Google Maps: Found ${businesses.length} businesses`);
|
|
|
|
| 30 |
imageUrl: undefined, // Not available from scraper
|
| 31 |
source: "google-maps",
|
| 32 |
sourceUrl: business.website || undefined,
|
| 33 |
+
emailStatus: business.emailStatus || "pending",
|
| 34 |
}));
|
| 35 |
|
| 36 |
console.log(`✅ Google Maps: Found ${businesses.length} businesses`);
|
lib/scrapers/types.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface BusinessData {
|
|
| 28 |
};
|
| 29 |
source: string; // Source where this data was found
|
| 30 |
sourceUrl?: string; // Original listing URL
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
export interface ScraperSource {
|
|
|
|
| 28 |
};
|
| 29 |
source: string; // Source where this data was found
|
| 30 |
sourceUrl?: string; // Original listing URL
|
| 31 |
+
emailStatus?: string | null;
|
| 32 |
}
|
| 33 |
|
| 34 |
export interface ScraperSource {
|