realtimex-crm 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "realtimex-crm",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "RealTimeX CRM - A full-featured CRM built with React, shadcn-admin-kit, and Supabase. Fork of Atomic CRM with RealTimeX App SDK integration.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,151 @@
1
+ # Edge Functions for RealTimeX CRM
2
+
3
+ This directory contains Edge Functions that can be deployed directly from Supabase Dashboard.
4
+
5
+ ## Required Functions (Deploy via Dashboard)
6
+
7
+ These are critical for team management and can be easily deployed through the GUI:
8
+
9
+ ### 1. users.ts ⭐ **REQUIRED**
10
+ **Purpose:** Create and update CRM team members
11
+ **Required for:** Inviting new users, updating user profiles, managing permissions
12
+ **Deploy:** Copy entire `users.ts` file → Supabase Dashboard → Edge Functions → New Function → Name: `users` → Paste → Deploy
13
+
14
+ ### 2. updatePassword.ts ⭐ **REQUIRED**
15
+ **Purpose:** Reset user passwords
16
+ **Required for:** Password reset functionality
17
+ **Deploy:** Copy entire `updatePassword.ts` file → Supabase Dashboard → Edge Functions → New Function → Name: `updatePassword` → Paste → Deploy
18
+
19
+ ## Optional Functions (Deploy via CLI)
20
+
21
+ These functions have complex dependencies and are easier to deploy using Supabase CLI:
22
+
23
+ ### 3. mergeContacts
24
+ **Purpose:** Merge duplicate contact records
25
+ **Required for:** Contact deduplication feature
26
+ **Deploy via CLI:**
27
+ ```bash
28
+ cd /path/to/realtimex-crm
29
+ npx supabase functions deploy mergeContacts
30
+ ```
31
+
32
+ ### 4. postmark
33
+ **Purpose:** Inbound email integration (capture emails as notes)
34
+ **Required for:** Email forwarding feature (optional)
35
+ **Deploy via CLI:**
36
+ ```bash
37
+ npx supabase functions deploy postmark
38
+ ```
39
+
40
+ **Additional Setup for postmark:**
41
+ - Requires Postmark account and API key
42
+ - Configure webhook URL in Postmark dashboard
43
+ - Set environment variable `POSTMARK_API_KEY` in Supabase
44
+
45
+ ## Deployment Instructions
46
+
47
+ ### Method 1: Supabase Dashboard (Recommended for users, updatePassword)
48
+
49
+ 1. **Go to Supabase Dashboard**
50
+ - Open your project
51
+ - Click "Edge Functions" in the left sidebar
52
+
53
+ 2. **Create New Function**
54
+ - Click "Deploy a new function"
55
+ - Or click "+ New Edge Function"
56
+
57
+ 3. **Configure Function**
58
+ - **Function name:** Enter exactly as shown (e.g., `users`)
59
+ - **Code:** Copy entire content from `.ts` file
60
+ - Paste into the code editor
61
+
62
+ 4. **Deploy**
63
+ - Click "Deploy" button
64
+ - Wait for deployment to complete (usually 10-20 seconds)
65
+ - Status should show "Active"
66
+
67
+ 5. **Verify**
68
+ - Function should appear in the list
69
+ - Status: Active (green)
70
+ - Click function name to view logs
71
+
72
+ ### Method 2: Supabase CLI (Required for mergeContacts, postmark)
73
+
74
+ If you have the repository cloned:
75
+
76
+ ```bash
77
+ # Deploy all functions at once
78
+ npx supabase functions deploy
79
+
80
+ # Or deploy specific function
81
+ npx supabase functions deploy mergeContacts
82
+ npx supabase functions deploy postmark
83
+ ```
84
+
85
+ ## Verification
86
+
87
+ After deploying, verify functions are active:
88
+
89
+ 1. Go to Supabase Dashboard → Edge Functions
90
+ 2. You should see:
91
+ - ✅ users (Active)
92
+ - ✅ updatePassword (Active)
93
+ - ✅ mergeContacts (Active) - if deployed
94
+ - ✅ postmark (Active) - if deployed
95
+
96
+ ## What if Functions Fail?
97
+
98
+ ### Common Issues:
99
+
100
+ **1. "Function not found" errors in CRM**
101
+ - Solution: Ensure function name matches exactly (case-sensitive)
102
+ - users, updatePassword, mergeContacts, postmark (not Users or update_password)
103
+
104
+ **2. "Unauthorized" errors**
105
+ - Solution: Functions automatically use `SUPABASE_SERVICE_ROLE_KEY`
106
+ - This is set automatically by Supabase - no action needed
107
+
108
+ **3. Deploy button grayed out**
109
+ - Solution: Ensure you're the project Owner
110
+ - Only project Owners can deploy Edge Functions
111
+
112
+ **4. Function shows "Inactive"**
113
+ - Solution: Click on the function → Click "Deploy" again
114
+ - Check logs for errors
115
+
116
+ ## Testing Edge Functions
117
+
118
+ ### Test users function:
119
+ From your CRM app, try to:
120
+ 1. Go to Settings → Team
121
+ 2. Click "Invite User"
122
+ 3. Fill in details and submit
123
+ 4. If successful, function is working!
124
+
125
+ ### Test updatePassword function:
126
+ 1. Go to user profile
127
+ 2. Click "Reset Password"
128
+ 3. Check if you receive password reset email
129
+
130
+ ## CRM Features Without Edge Functions
131
+
132
+ If you skip Edge Function deployment:
133
+
134
+ **✅ Works:**
135
+ - Login/Signup (first user)
136
+ - Contacts, Companies, Deals
137
+ - Tasks & Notes
138
+ - Dashboard & Reports
139
+ - Tags & Filters
140
+
141
+ **❌ Doesn't Work:**
142
+ - Inviting additional team members
143
+ - User password reset
144
+ - Contact merging
145
+ - Email capture
146
+
147
+ ## Support
148
+
149
+ - [View full documentation](https://github.com/therealtimex/realtimex-crm/blob/main/SETUP_GUIDE.md)
150
+ - [Report issues](https://github.com/therealtimex/realtimex-crm/issues)
151
+ - [Supabase Edge Functions docs](https://supabase.com/docs/guides/functions)
@@ -0,0 +1,84 @@
1
+ // RealTimeX CRM - Update Password Edge Function
2
+ // This function handles password reset requests for CRM users
3
+ //
4
+ // To deploy:
5
+ // 1. Go to Supabase Dashboard → Edge Functions
6
+ // 2. Click "Deploy a new function"
7
+ // 3. Name it: updatePassword
8
+ // 4. Copy and paste this entire file
9
+ // 5. Click "Deploy"
10
+
11
+ import { createClient } from "jsr:@supabase/supabase-js@2";
12
+
13
+ // CORS headers for API access
14
+ const corsHeaders = {
15
+ "Access-Control-Allow-Origin": "*",
16
+ "Access-Control-Allow-Headers":
17
+ "authorization, x-client-info, apikey, content-type",
18
+ "Access-Control-Allow-Methods": "POST, PATCH, DELETE",
19
+ };
20
+
21
+ function createErrorResponse(status: number, message: string) {
22
+ return new Response(JSON.stringify({ status, message }), {
23
+ headers: { "Content-Type": "application/json", ...corsHeaders },
24
+ status,
25
+ });
26
+ }
27
+
28
+ // Initialize Supabase Admin client (uses service_role key automatically)
29
+ const supabaseAdmin = createClient(
30
+ Deno.env.get("SUPABASE_URL") ?? "",
31
+ Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
32
+ {
33
+ auth: {
34
+ autoRefreshToken: false,
35
+ persistSession: false,
36
+ },
37
+ }
38
+ );
39
+
40
+ async function updatePassword(user: any) {
41
+ const { data, error } = await supabaseAdmin.auth.resetPasswordForEmail(
42
+ user.email
43
+ );
44
+
45
+ if (!data || error) {
46
+ return createErrorResponse(500, "Internal Server Error");
47
+ }
48
+
49
+ return new Response(
50
+ JSON.stringify({
51
+ data,
52
+ }),
53
+ {
54
+ headers: { "Content-Type": "application/json", ...corsHeaders },
55
+ }
56
+ );
57
+ }
58
+
59
+ Deno.serve(async (req: Request) => {
60
+ if (req.method === "OPTIONS") {
61
+ return new Response(null, {
62
+ status: 204,
63
+ headers: corsHeaders,
64
+ });
65
+ }
66
+
67
+ const authHeader = req.headers.get("Authorization")!;
68
+ const localClient = createClient(
69
+ Deno.env.get("SUPABASE_URL") ?? "",
70
+ Deno.env.get("SUPABASE_ANON_KEY") ?? "",
71
+ { global: { headers: { Authorization: authHeader } } }
72
+ );
73
+
74
+ const { data } = await localClient.auth.getUser();
75
+ if (!data?.user) {
76
+ return createErrorResponse(401, "Unauthorized");
77
+ }
78
+
79
+ if (req.method === "PATCH") {
80
+ return updatePassword(data.user);
81
+ }
82
+
83
+ return createErrorResponse(405, "Method Not Allowed");
84
+ });
@@ -0,0 +1,240 @@
1
+ // RealTimeX CRM - Users Edge Function
2
+ // This function handles creating and updating CRM users (sales team members)
3
+ //
4
+ // To deploy:
5
+ // 1. Go to Supabase Dashboard → Edge Functions
6
+ // 2. Click "Deploy a new function"
7
+ // 3. Name it: users
8
+ // 4. Copy and paste this entire file
9
+ // 5. Click "Deploy"
10
+
11
+ import { createClient } from "jsr:@supabase/supabase-js@2";
12
+
13
+ // CORS headers for API access
14
+ const corsHeaders = {
15
+ "Access-Control-Allow-Origin": "*",
16
+ "Access-Control-Allow-Headers":
17
+ "authorization, x-client-info, apikey, content-type",
18
+ "Access-Control-Allow-Methods": "POST, PATCH, DELETE",
19
+ };
20
+
21
+ function createErrorResponse(status: number, message: string) {
22
+ return new Response(JSON.stringify({ status, message }), {
23
+ headers: { "Content-Type": "application/json", ...corsHeaders },
24
+ status,
25
+ });
26
+ }
27
+
28
+ // Initialize Supabase Admin client (uses service_role key automatically)
29
+ const supabaseAdmin = createClient(
30
+ Deno.env.get("SUPABASE_URL") ?? "",
31
+ Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
32
+ {
33
+ auth: {
34
+ autoRefreshToken: false,
35
+ persistSession: false,
36
+ },
37
+ }
38
+ );
39
+
40
+ async function updateSaleDisabled(user_id: string, disabled: boolean) {
41
+ return await supabaseAdmin
42
+ .from("sales")
43
+ .update({ disabled: disabled ?? false })
44
+ .eq("user_id", user_id);
45
+ }
46
+
47
+ async function updateSaleAdministrator(
48
+ user_id: string,
49
+ administrator: boolean
50
+ ) {
51
+ const { data: sales, error: salesError } = await supabaseAdmin
52
+ .from("sales")
53
+ .update({ administrator })
54
+ .eq("user_id", user_id)
55
+ .select("*");
56
+
57
+ if (!sales?.length || salesError) {
58
+ console.error("Error updating user:", salesError);
59
+ throw salesError ?? new Error("Failed to update sale");
60
+ }
61
+ return sales.at(0);
62
+ }
63
+
64
+ async function updateSaleAvatar(user_id: string, avatar: string) {
65
+ const { data: sales, error: salesError } = await supabaseAdmin
66
+ .from("sales")
67
+ .update({ avatar })
68
+ .eq("user_id", user_id)
69
+ .select("*");
70
+
71
+ if (!sales?.length || salesError) {
72
+ console.error("Error updating user:", salesError);
73
+ throw salesError ?? new Error("Failed to update sale");
74
+ }
75
+ return sales.at(0);
76
+ }
77
+
78
+ async function inviteUser(req: Request, currentUserSale: any) {
79
+ const { email, password, first_name, last_name, disabled, administrator } =
80
+ await req.json();
81
+
82
+ if (!currentUserSale.administrator) {
83
+ return createErrorResponse(401, "Not Authorized");
84
+ }
85
+
86
+ const { data, error: userError } = await supabaseAdmin.auth.admin.createUser({
87
+ email,
88
+ password,
89
+ user_metadata: { first_name, last_name },
90
+ });
91
+
92
+ const { error: emailError } =
93
+ await supabaseAdmin.auth.admin.inviteUserByEmail(email);
94
+
95
+ if (!data?.user || userError) {
96
+ console.error(`Error inviting user: user_error=${userError}`);
97
+ return createErrorResponse(500, "Internal Server Error");
98
+ }
99
+
100
+ if (!data?.user || userError || emailError) {
101
+ console.error(`Error inviting user, email_error=${emailError}`);
102
+ return createErrorResponse(500, "Failed to send invitation mail");
103
+ }
104
+
105
+ try {
106
+ await updateSaleDisabled(data.user.id, disabled);
107
+ const sale = await updateSaleAdministrator(data.user.id, administrator);
108
+
109
+ return new Response(
110
+ JSON.stringify({
111
+ data: sale,
112
+ }),
113
+ {
114
+ headers: { "Content-Type": "application/json", ...corsHeaders },
115
+ }
116
+ );
117
+ } catch (e) {
118
+ console.error("Error patching sale:", e);
119
+ return createErrorResponse(500, "Internal Server Error");
120
+ }
121
+ }
122
+
123
+ async function patchUser(req: Request, currentUserSale: any) {
124
+ const {
125
+ sales_id,
126
+ email,
127
+ first_name,
128
+ last_name,
129
+ avatar,
130
+ administrator,
131
+ disabled,
132
+ } = await req.json();
133
+ const { data: sale } = await supabaseAdmin
134
+ .from("sales")
135
+ .select("*")
136
+ .eq("id", sales_id)
137
+ .single();
138
+
139
+ if (!sale) {
140
+ return createErrorResponse(404, "Not Found");
141
+ }
142
+
143
+ // Users can only update their own profile unless they are an administrator
144
+ if (!currentUserSale.administrator && currentUserSale.id !== sale.id) {
145
+ return createErrorResponse(401, "Not Authorized");
146
+ }
147
+
148
+ const { data, error: userError } =
149
+ await supabaseAdmin.auth.admin.updateUserById(sale.user_id, {
150
+ email,
151
+ ban_duration: disabled ? "87600h" : "none",
152
+ user_metadata: { first_name, last_name },
153
+ });
154
+
155
+ if (!data?.user || userError) {
156
+ console.error("Error patching user:", userError);
157
+ return createErrorResponse(500, "Internal Server Error");
158
+ }
159
+
160
+ if (avatar) {
161
+ await updateSaleAvatar(data.user.id, avatar);
162
+ }
163
+
164
+ // Only administrators can update the administrator and disabled status
165
+ if (!currentUserSale.administrator) {
166
+ const { data: new_sale } = await supabaseAdmin
167
+ .from("sales")
168
+ .select("*")
169
+ .eq("id", sales_id)
170
+ .single();
171
+ return new Response(
172
+ JSON.stringify({
173
+ data: new_sale,
174
+ }),
175
+ {
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ ...corsHeaders,
179
+ },
180
+ }
181
+ );
182
+ }
183
+
184
+ try {
185
+ await updateSaleDisabled(data.user.id, disabled);
186
+ const sale = await updateSaleAdministrator(data.user.id, administrator);
187
+ return new Response(
188
+ JSON.stringify({
189
+ data: sale,
190
+ }),
191
+ {
192
+ headers: {
193
+ "Content-Type": "application/json",
194
+ ...corsHeaders,
195
+ },
196
+ }
197
+ );
198
+ } catch (e) {
199
+ console.error("Error patching sale:", e);
200
+ return createErrorResponse(500, "Internal Server Error");
201
+ }
202
+ }
203
+
204
+ Deno.serve(async (req: Request) => {
205
+ if (req.method === "OPTIONS") {
206
+ return new Response(null, {
207
+ status: 204,
208
+ headers: corsHeaders,
209
+ });
210
+ }
211
+
212
+ const authHeader = req.headers.get("Authorization")!;
213
+ const localClient = createClient(
214
+ Deno.env.get("SUPABASE_URL") ?? "",
215
+ Deno.env.get("SUPABASE_ANON_KEY") ?? "",
216
+ { global: { headers: { Authorization: authHeader } } }
217
+ );
218
+ const { data } = await localClient.auth.getUser();
219
+ if (!data?.user) {
220
+ return createErrorResponse(401, "Unauthorized");
221
+ }
222
+ const currentUserSale = await supabaseAdmin
223
+ .from("sales")
224
+ .select("*")
225
+ .eq("user_id", data.user.id)
226
+ .single();
227
+
228
+ if (!currentUserSale?.data) {
229
+ return createErrorResponse(401, "Unauthorized");
230
+ }
231
+ if (req.method === "POST") {
232
+ return inviteUser(req, currentUserSale.data);
233
+ }
234
+
235
+ if (req.method === "PATCH") {
236
+ return patchUser(req, currentUserSale.data);
237
+ }
238
+
239
+ return createErrorResponse(405, "Method Not Allowed");
240
+ });
@@ -3,8 +3,6 @@ import { useDataProvider } from "ra-core";
3
3
  import { Navigate } from "react-router-dom";
4
4
  import { LoginPage } from "@/components/admin/login-page";
5
5
  import { checkDatabaseHealth } from "@/lib/database-health-check";
6
- import { getSupabaseConfig } from "@/lib/supabase-config";
7
- import { DatabaseSetupGuide } from "../setup/DatabaseSetupGuide";
8
6
 
9
7
  import type { CrmDataProvider } from "../providers/types";
10
8
  import { LoginSkeleton } from "./LoginSkeleton";
@@ -38,17 +36,91 @@ export const StartPage = () => {
38
36
  // Show loading state
39
37
  if (isCheckingHealth || isCheckingInit) return <LoginSkeleton />;
40
38
 
41
- // Show database setup guide if schema is missing
39
+ // Show error if schema is missing
42
40
  if (healthStatus && !healthStatus.isHealthy) {
43
- const config = getSupabaseConfig();
44
- if (config) {
45
- return (
46
- <DatabaseSetupGuide
47
- missingTables={healthStatus.missingTables}
48
- supabaseUrl={config.url}
49
- />
50
- );
51
- }
41
+ return (
42
+ <div className="flex items-center justify-center min-h-screen p-4">
43
+ <div className="max-w-md w-full space-y-4">
44
+ <div className="text-center space-y-2">
45
+ <div className="flex justify-center">
46
+ <div className="rounded-full bg-destructive/10 p-3">
47
+ <svg
48
+ className="h-6 w-6 text-destructive"
49
+ fill="none"
50
+ viewBox="0 0 24 24"
51
+ stroke="currentColor"
52
+ >
53
+ <path
54
+ strokeLinecap="round"
55
+ strokeLinejoin="round"
56
+ strokeWidth={2}
57
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
58
+ />
59
+ </svg>
60
+ </div>
61
+ </div>
62
+ <h1 className="text-2xl font-bold">Database Not Configured</h1>
63
+ <p className="text-muted-foreground">
64
+ Your Supabase database is missing required tables and functions.
65
+ </p>
66
+ </div>
67
+
68
+ <div className="bg-muted p-4 rounded-lg space-y-2">
69
+ <p className="text-sm font-semibold">Missing tables:</p>
70
+ <div className="flex flex-wrap gap-2">
71
+ {healthStatus.missingTables.map((table) => (
72
+ <span
73
+ key={table}
74
+ className="px-2 py-1 bg-background text-xs rounded border"
75
+ >
76
+ {table}
77
+ </span>
78
+ ))}
79
+ </div>
80
+ </div>
81
+
82
+ <div className="space-y-3 text-sm">
83
+ <p className="font-semibold">To fix this:</p>
84
+ <ol className="list-decimal list-inside space-y-2 text-muted-foreground ml-2">
85
+ <li>
86
+ Download{" "}
87
+ <a
88
+ href="https://raw.githubusercontent.com/therealtimex/realtimex-crm/main/public/setup.sql"
89
+ target="_blank"
90
+ rel="noopener noreferrer"
91
+ className="text-primary hover:underline font-semibold"
92
+ download="setup.sql"
93
+ >
94
+ setup.sql
95
+ </a>
96
+ </li>
97
+ <li>Open your Supabase project's SQL Editor</li>
98
+ <li>Copy all contents from setup.sql and paste into the editor</li>
99
+ <li>Click "Run" to execute the setup</li>
100
+ <li>Return here and reload the page</li>
101
+ </ol>
102
+ </div>
103
+
104
+ <button
105
+ onClick={() => window.location.reload()}
106
+ className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
107
+ >
108
+ Reload Page
109
+ </button>
110
+
111
+ <div className="pt-4 border-t text-center">
112
+ <a
113
+ href="https://github.com/therealtimex/realtimex-crm#database-setup"
114
+ target="_blank"
115
+ rel="noopener noreferrer"
116
+ className="text-sm text-primary hover:underline"
117
+ >
118
+ View detailed setup guide →
119
+ </a>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ );
52
124
  }
53
125
 
54
126
  // Show login page if there's an error or already initialized