realtimex-crm 0.5.3 → 0.5.7

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.3",
3
+ "version": "0.5.7",
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",
package/src/App.tsx CHANGED
@@ -32,7 +32,6 @@ const App = () => {
32
32
  const [needsSetup, setNeedsSetup] = useState<boolean>(() => {
33
33
  // Check immediately on mount
34
34
  const configured = isSupabaseConfigured();
35
- console.log("[App] Supabase configured:", configured);
36
35
  return !configured;
37
36
  });
38
37
 
@@ -4,6 +4,8 @@ import { ExportButton } from "@/components/admin/export-button";
4
4
  import { List } from "@/components/admin/list";
5
5
  import { ListPagination } from "@/components/admin/list-pagination";
6
6
  import { SortButton } from "@/components/admin/sort-button";
7
+ import { Skeleton } from "@/components/ui/skeleton";
8
+ import { Card } from "@/components/ui/card";
7
9
 
8
10
  import { TopToolbar } from "../layout/TopToolbar";
9
11
  import { CompanyEmpty } from "./CompanyEmpty";
@@ -11,8 +13,6 @@ import { CompanyListFilter } from "./CompanyListFilter";
11
13
  import { ImageList } from "./GridList";
12
14
 
13
15
  export const CompanyList = () => {
14
- const { identity } = useGetIdentity();
15
- if (!identity) return null;
16
16
  return (
17
17
  <List
18
18
  title={false}
@@ -28,9 +28,32 @@ export const CompanyList = () => {
28
28
 
29
29
  const CompanyListLayout = () => {
30
30
  const { data, isPending, filterValues } = useListContext();
31
+ const { identity } = useGetIdentity();
31
32
  const hasFilters = filterValues && Object.keys(filterValues).length > 0;
32
33
 
33
- if (isPending) return null;
34
+ // Show loading skeleton while identity or data is loading
35
+ if (!identity || isPending) {
36
+ return (
37
+ <div className="w-full flex flex-row gap-8">
38
+ <div className="w-64">
39
+ <Skeleton className="h-96 w-full" />
40
+ </div>
41
+ <div className="flex flex-col flex-1 gap-4">
42
+ <Card className="p-4">
43
+ <div className="grid grid-cols-3 gap-4">
44
+ <Skeleton className="h-48 w-full" />
45
+ <Skeleton className="h-48 w-full" />
46
+ <Skeleton className="h-48 w-full" />
47
+ <Skeleton className="h-48 w-full" />
48
+ <Skeleton className="h-48 w-full" />
49
+ <Skeleton className="h-48 w-full" />
50
+ </div>
51
+ </Card>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
34
57
  if (!data?.length && !hasFilters) return <CompanyEmpty />;
35
58
 
36
59
  return (
@@ -11,6 +11,7 @@ import { ExportButton } from "@/components/admin/export-button";
11
11
  import { List } from "@/components/admin/list";
12
12
  import { SortButton } from "@/components/admin/sort-button";
13
13
  import { Card } from "@/components/ui/card";
14
+ import { Skeleton } from "@/components/ui/skeleton";
14
15
 
15
16
  import type { Company, Contact, Sale, Tag } from "../types";
16
17
  import { ContactEmpty } from "./ContactEmpty";
@@ -20,10 +21,6 @@ import { ContactListFilter } from "./ContactListFilter";
20
21
  import { TopToolbar } from "../layout/TopToolbar";
21
22
 
22
23
  export const ContactList = () => {
23
- const { identity } = useGetIdentity();
24
-
25
- if (!identity) return null;
26
-
27
24
  return (
28
25
  <List
29
26
  title={false}
@@ -43,7 +40,27 @@ const ContactListLayout = () => {
43
40
 
44
41
  const hasFilters = filterValues && Object.keys(filterValues).length > 0;
45
42
 
46
- if (!identity || isPending) return null;
43
+ // Show loading skeleton while identity or data is loading
44
+ if (!identity || isPending) {
45
+ return (
46
+ <div className="flex flex-row gap-8">
47
+ <div className="w-64">
48
+ <Skeleton className="h-96 w-full" />
49
+ </div>
50
+ <div className="w-full flex flex-col gap-4">
51
+ <Card className="p-4">
52
+ <div className="space-y-3">
53
+ <Skeleton className="h-12 w-full" />
54
+ <Skeleton className="h-12 w-full" />
55
+ <Skeleton className="h-12 w-full" />
56
+ <Skeleton className="h-12 w-full" />
57
+ <Skeleton className="h-12 w-full" />
58
+ </div>
59
+ </Card>
60
+ </div>
61
+ </div>
62
+ );
63
+ }
47
64
 
48
65
  if (!data?.length && !hasFilters) return <ContactEmpty />;
49
66
 
@@ -8,6 +8,8 @@ import { ReferenceInput } from "@/components/admin/reference-input";
8
8
  import { FilterButton } from "@/components/admin/filter-form";
9
9
  import { SearchInput } from "@/components/admin/search-input";
10
10
  import { SelectInput } from "@/components/admin/select-input";
11
+ import { Skeleton } from "@/components/ui/skeleton";
12
+ import { Card } from "@/components/ui/card";
11
13
 
12
14
  import { useConfigurationContext } from "../root/ConfigurationContext";
13
15
  import { TopToolbar } from "../layout/TopToolbar";
@@ -20,11 +22,8 @@ import { DealShow } from "./DealShow";
20
22
  import { OnlyMineInput } from "./OnlyMineInput";
21
23
 
22
24
  const DealList = () => {
23
- const { identity } = useGetIdentity();
24
25
  const { dealCategories } = useConfigurationContext();
25
26
 
26
- if (!identity) return null;
27
-
28
27
  const dealFilters = [
29
28
  <SearchInput source="q" alwaysOn />,
30
29
  <ReferenceInput source="company_id" reference="companies">
@@ -60,9 +59,25 @@ const DealLayout = () => {
60
59
  const matchEdit = matchPath("/deals/:id", location.pathname);
61
60
 
62
61
  const { data, isPending, filterValues } = useListContext();
62
+ const { identity } = useGetIdentity();
63
63
  const hasFilters = filterValues && Object.keys(filterValues).length > 0;
64
64
 
65
- if (isPending) return null;
65
+ // Show loading skeleton while identity or data is loading
66
+ if (!identity || isPending) {
67
+ return (
68
+ <div className="w-full">
69
+ <Card className="p-4">
70
+ <div className="flex gap-4">
71
+ <Skeleton className="h-96 w-1/4" />
72
+ <Skeleton className="h-96 w-1/4" />
73
+ <Skeleton className="h-96 w-1/4" />
74
+ <Skeleton className="h-96 w-1/4" />
75
+ </div>
76
+ </Card>
77
+ </div>
78
+ );
79
+ }
80
+
66
81
  if (!data?.length && !hasFilters)
67
82
  return (
68
83
  <>
@@ -1,34 +1,31 @@
1
1
  import { createClient, type SupabaseClient } from "@supabase/supabase-js";
2
2
  import { getSupabaseConfig } from "@/lib/supabase-config";
3
3
 
4
- // Lazy initialization - create client on first access
5
- let supabaseInstance: SupabaseClient | null = null;
4
+ // Create client immediately to ensure session restoration happens early
5
+ // This is critical for auth session persistence across page refreshes
6
+ function createSupabaseClient(): SupabaseClient {
7
+ const config = getSupabaseConfig();
6
8
 
7
- function getSupabaseClient(): SupabaseClient {
8
- if (!supabaseInstance) {
9
- const config = getSupabaseConfig();
10
-
11
- if (!config) {
12
- // Return a placeholder client that will never be used
13
- // (App.tsx will show setup wizard before this is accessed)
14
- console.warn("[Supabase] No configuration found, using placeholder");
15
- supabaseInstance = createClient(
16
- "https://placeholder.supabase.co",
17
- "placeholder-key",
18
- );
19
- } else {
20
- supabaseInstance = createClient(config.url, config.anonKey);
21
- }
9
+ if (!config) {
10
+ // Return a placeholder client that will never be used
11
+ // (App.tsx will show setup wizard before this is accessed)
12
+ console.warn("[Supabase] No configuration found, using placeholder");
13
+ return createClient(
14
+ "https://placeholder.supabase.co",
15
+ "placeholder-key",
16
+ );
22
17
  }
23
18
 
24
- return supabaseInstance;
19
+ return createClient(config.url, config.anonKey, {
20
+ auth: {
21
+ // Ensure session is persisted and restored from localStorage
22
+ persistSession: true,
23
+ autoRefreshToken: true,
24
+ detectSessionInUrl: true,
25
+ },
26
+ });
25
27
  }
26
28
 
27
- // Export as a getter proxy to support lazy initialization
28
- export const supabase = new Proxy({} as SupabaseClient, {
29
- get(_target, prop) {
30
- const client = getSupabaseClient();
31
- const value = client[prop as keyof SupabaseClient];
32
- return typeof value === "function" ? value.bind(client) : value;
33
- },
34
- });
29
+ // Create client immediately on module load (not lazy!)
30
+ // This ensures session restoration from localStorage happens before any auth checks
31
+ export const supabase = createSupabaseClient();
@@ -131,32 +131,32 @@ export const CRM = ({
131
131
  taskTypes={taskTypes}
132
132
  title={title}
133
133
  >
134
- <Admin
135
- dataProvider={dataProvider}
136
- authProvider={authProvider}
137
- store={localStorageStore(undefined, "CRM")}
138
- layout={Layout}
139
- loginPage={StartPage}
140
- i18nProvider={i18nProvider}
141
- dashboard={Dashboard}
142
- requireAuth
143
- disableTelemetry
144
- {...rest}
145
- >
146
- <CustomRoutes noLayout>
147
- <Route path={SignupPage.path} element={<SignupPage />} />
148
- <Route path={SetPasswordPage.path} element={<SetPasswordPage />} />
149
- <Route
150
- path={ForgotPasswordPage.path}
151
- element={<ForgotPasswordPage />}
152
- />
153
- </CustomRoutes>
134
+ <DatabaseHealthCheck dataProvider={dataProvider}>
135
+ <Admin
136
+ dataProvider={dataProvider}
137
+ authProvider={authProvider}
138
+ store={localStorageStore(undefined, "CRM")}
139
+ layout={Layout}
140
+ loginPage={StartPage}
141
+ i18nProvider={i18nProvider}
142
+ dashboard={Dashboard}
143
+ requireAuth
144
+ disableTelemetry
145
+ {...rest}
146
+ >
147
+ <CustomRoutes noLayout>
148
+ <Route path={SignupPage.path} element={<SignupPage />} />
149
+ <Route path={SetPasswordPage.path} element={<SetPasswordPage />} />
150
+ <Route
151
+ path={ForgotPasswordPage.path}
152
+ element={<ForgotPasswordPage />}
153
+ />
154
+ </CustomRoutes>
154
155
 
155
- <CustomRoutes>
156
- <Route path={SettingsPage.path} element={<SettingsPage />} />
157
- <Route path={DatabasePage.path} element={<DatabasePage />} />
158
- </CustomRoutes>
159
- <DatabaseHealthCheck>
156
+ <CustomRoutes>
157
+ <Route path={SettingsPage.path} element={<SettingsPage />} />
158
+ <Route path={DatabasePage.path} element={<DatabasePage />} />
159
+ </CustomRoutes>
160
160
  <Resource name="deals" {...deals} />
161
161
  <Resource name="contacts" {...contacts} />
162
162
  <Resource name="companies" {...companies} />
@@ -165,8 +165,8 @@ export const CRM = ({
165
165
  <Resource name="tasks" />
166
166
  <Resource name="sales" {...sales} />
167
167
  <Resource name="tags" />
168
- </DatabaseHealthCheck>
169
- </Admin>
168
+ </Admin>
169
+ </DatabaseHealthCheck>
170
170
  </ConfigurationProvider>
171
171
  );
172
172
  };
@@ -1,15 +1,15 @@
1
1
  import { useEffect, useState } from "react";
2
- import { useDataProvider } from "ra-core";
2
+ import type { DataProvider } from "ra-core";
3
3
  import { checkDatabaseHealth, DatabaseHealthStatus } from "@/lib/database-health-check";
4
4
  import { getSupabaseConfig } from "@/lib/supabase-config";
5
5
  import { DatabaseSetupGuide } from "../setup/DatabaseSetupGuide";
6
6
 
7
7
  interface DatabaseHealthCheckProps {
8
8
  children: React.ReactNode;
9
+ dataProvider: DataProvider;
9
10
  }
10
11
 
11
- export function DatabaseHealthCheck({ children }: DatabaseHealthCheckProps) {
12
- const dataProvider = useDataProvider();
12
+ export function DatabaseHealthCheck({ children, dataProvider }: DatabaseHealthCheckProps) {
13
13
  const [healthStatus, setHealthStatus] = useState<DatabaseHealthStatus | null>(null);
14
14
  const [isChecking, setIsChecking] = useState(true);
15
15
 
@@ -24,7 +24,7 @@ export function DatabaseHealthCheck({ children }: DatabaseHealthCheckProps) {
24
24
  setIsChecking(false);
25
25
  }
26
26
  } catch (error) {
27
- console.error("Failed to check database health:", error);
27
+ console.error("[DatabaseHealthCheck] Failed to check database health:", error);
28
28
  if (!cancelled) {
29
29
  setIsChecking(false);
30
30
  }
@@ -1,12 +1,15 @@
1
- import { useRecordContext } from "ra-core";
1
+ import { useGetIdentity, useListContext, useRecordContext } from "ra-core";
2
2
  import { CreateButton } from "@/components/admin/create-button";
3
3
  import { DataTable } from "@/components/admin/data-table";
4
4
  import { ExportButton } from "@/components/admin/export-button";
5
5
  import { List } from "@/components/admin/list";
6
6
  import { SearchInput } from "@/components/admin/search-input";
7
7
  import { Badge } from "@/components/ui/badge";
8
+ import { Card } from "@/components/ui/card";
9
+ import { Skeleton } from "@/components/ui/skeleton";
8
10
 
9
11
  import { TopToolbar } from "../layout/TopToolbar";
12
+ import useAppBarHeight from "../misc/useAppBarHeight";
10
13
 
11
14
  const SalesListActions = () => (
12
15
  <TopToolbar>
@@ -49,14 +52,68 @@ export function SalesList() {
49
52
  actions={<SalesListActions />}
50
53
  sort={{ field: "first_name", order: "ASC" }}
51
54
  >
52
- <DataTable>
53
- <DataTable.Col source="first_name" />
54
- <DataTable.Col source="last_name" />
55
- <DataTable.Col source="email" />
56
- <DataTable.Col label={false}>
57
- <OptionsField />
58
- </DataTable.Col>
59
- </DataTable>
55
+ <SalesListLayout />
60
56
  </List>
61
57
  );
62
58
  }
59
+
60
+ const SalesListLayout = () => {
61
+ const { data, isPending, filterValues } = useListContext();
62
+ const { identity } = useGetIdentity();
63
+
64
+ const hasFilters = filterValues && Object.keys(filterValues).length > 0;
65
+
66
+ // Show loading skeleton while identity or data is loading
67
+ if (!identity || isPending) {
68
+ return (
69
+ <Card className="p-4">
70
+ <div className="space-y-3">
71
+ <Skeleton className="h-12 w-full" />
72
+ <Skeleton className="h-12 w-full" />
73
+ <Skeleton className="h-12 w-full" />
74
+ <Skeleton className="h-12 w-full" />
75
+ <Skeleton className="h-12 w-full" />
76
+ </div>
77
+ </Card>
78
+ );
79
+ }
80
+
81
+ // Show empty state when no data
82
+ if (!data?.length && !hasFilters) {
83
+ return <SalesEmpty />;
84
+ }
85
+
86
+ return (
87
+ <DataTable>
88
+ <DataTable.Col source="first_name" />
89
+ <DataTable.Col source="last_name" />
90
+ <DataTable.Col source="email" />
91
+ <DataTable.Col label={false}>
92
+ <OptionsField />
93
+ </DataTable.Col>
94
+ </DataTable>
95
+ );
96
+ };
97
+
98
+ const SalesEmpty = () => {
99
+ const appbarHeight = useAppBarHeight();
100
+ return (
101
+ <div
102
+ className="flex flex-col justify-center items-center gap-3"
103
+ style={{
104
+ height: `calc(100dvh - ${appbarHeight}px)`,
105
+ }}
106
+ >
107
+ <img src="./img/empty.svg" alt="No users found" />
108
+ <div className="flex flex-col gap-0 items-center">
109
+ <h6 className="text-lg font-bold">No users found</h6>
110
+ <p className="text-sm text-muted-foreground text-center mb-4">
111
+ It seems your user list is empty.
112
+ </p>
113
+ </div>
114
+ <div className="flex flex-row gap-2">
115
+ <CreateButton label="New user" />
116
+ </div>
117
+ </div>
118
+ );
119
+ };