realtimex-crm 0.5.1 → 0.5.5
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/README.md +3 -20
- package/dist/assets/DealList-hFgyoSCd.js +59 -0
- package/dist/assets/{DealList-90RCBW_j.js.map → DealList-hFgyoSCd.js.map} +1 -1
- package/dist/assets/{index-CTPFYcZP.js → index-BzM6--R_.js} +54 -57
- package/dist/assets/{index-CTPFYcZP.js.map → index-BzM6--R_.js.map} +1 -1
- package/dist/assets/index-C5TuuS9H.css +1 -0
- package/dist/index.html +1 -1
- package/dist/stats.html +1 -1
- package/package.json +1 -3
- package/src/App.tsx +0 -1
- package/src/components/atomic-crm/companies/CompanyList.tsx +26 -3
- package/src/components/atomic-crm/contacts/ContactList.tsx +22 -5
- package/src/components/atomic-crm/deals/DealList.tsx +19 -4
- package/src/components/atomic-crm/login/StartPage.tsx +5 -109
- package/src/components/atomic-crm/providers/supabase/supabase.ts +23 -26
- package/src/components/atomic-crm/root/CRM.tsx +27 -27
- package/src/components/atomic-crm/root/DatabaseHealthCheck.tsx +6 -4
- package/src/components/atomic-crm/sales/SalesList.tsx +66 -9
- package/src/components/atomic-crm/setup/DatabaseSetupGuide.tsx +51 -98
- package/src/lib/database-health-check.ts +18 -27
- package/dist/assets/DealList-90RCBW_j.js +0 -59
- package/dist/assets/index-DbCBhMSJ.css +0 -1
- package/dist/edge-functions/README.md +0 -151
- package/dist/edge-functions/updatePassword.ts +0 -84
- package/dist/edge-functions/users.ts +0 -240
- package/dist/setup.sql +0 -976
- package/public/edge-functions/README.md +0 -151
- package/public/edge-functions/updatePassword.ts +0 -84
- package/public/edge-functions/users.ts +0 -240
- package/public/setup.sql +0 -976
- package/supabase/migrations/README.md +0 -98
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtimex-crm",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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",
|
|
@@ -65,8 +65,6 @@
|
|
|
65
65
|
"prettier": "prettier --config ./.prettierrc.json --check \"**/*.{js,json,ts,tsx,css,md,html}\"",
|
|
66
66
|
"registry:build": "npx shadcn build",
|
|
67
67
|
"registry:gen": "node ./scripts/generate-registry.mjs",
|
|
68
|
-
"setup:gen": "cat supabase/migrations/*.sql > public/setup.sql && echo 'Generated public/setup.sql from migrations'",
|
|
69
|
-
"db:migrate": "node ./scripts/auto-migrate.mjs",
|
|
70
68
|
"ghpages:deploy": "node ./scripts/ghpages-deploy.mjs",
|
|
71
69
|
"supabase:remote:init": "node ./scripts/supabase-remote-init.mjs",
|
|
72
70
|
"prepare": "husky"
|
package/src/App.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
<>
|
|
@@ -2,130 +2,26 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
2
2
|
import { useDataProvider } from "ra-core";
|
|
3
3
|
import { Navigate } from "react-router-dom";
|
|
4
4
|
import { LoginPage } from "@/components/admin/login-page";
|
|
5
|
-
import { checkDatabaseHealth } from "@/lib/database-health-check";
|
|
6
5
|
|
|
7
6
|
import type { CrmDataProvider } from "../providers/types";
|
|
8
7
|
import { LoginSkeleton } from "./LoginSkeleton";
|
|
9
8
|
|
|
10
9
|
export const StartPage = () => {
|
|
11
10
|
const dataProvider = useDataProvider<CrmDataProvider>();
|
|
12
|
-
|
|
13
|
-
// First check database health
|
|
14
|
-
const {
|
|
15
|
-
data: healthStatus,
|
|
16
|
-
error: healthError,
|
|
17
|
-
isPending: isCheckingHealth,
|
|
18
|
-
} = useQuery({
|
|
19
|
-
queryKey: ["database-health"],
|
|
20
|
-
queryFn: checkDatabaseHealth,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Then check if initialized (only if database is healthy)
|
|
24
11
|
const {
|
|
25
12
|
data: isInitialized,
|
|
26
|
-
error
|
|
27
|
-
isPending
|
|
13
|
+
error,
|
|
14
|
+
isPending,
|
|
28
15
|
} = useQuery({
|
|
29
16
|
queryKey: ["init"],
|
|
30
17
|
queryFn: async () => {
|
|
31
18
|
return dataProvider.isInitialized();
|
|
32
19
|
},
|
|
33
|
-
enabled: healthStatus?.isHealthy === true,
|
|
34
20
|
});
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
// Show error if schema is missing
|
|
40
|
-
if (healthStatus && !healthStatus.isHealthy) {
|
|
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
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Show login page if there's an error or already initialized
|
|
127
|
-
if (healthError || initError || isInitialized) return <LoginPage />;
|
|
22
|
+
if (isPending) return <LoginSkeleton />;
|
|
23
|
+
if (error) return <LoginPage />;
|
|
24
|
+
if (isInitialized) return <LoginPage />;
|
|
128
25
|
|
|
129
|
-
// Not initialized yet, go to signup
|
|
130
26
|
return <Navigate to="/sign-up" />;
|
|
131
27
|
};
|
|
@@ -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
|
-
//
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
</
|
|
169
|
-
</
|
|
168
|
+
</Admin>
|
|
169
|
+
</DatabaseHealthCheck>
|
|
170
170
|
</ConfigurationProvider>
|
|
171
171
|
);
|
|
172
172
|
};
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import type { DataProvider } from "ra-core";
|
|
2
3
|
import { checkDatabaseHealth, DatabaseHealthStatus } from "@/lib/database-health-check";
|
|
3
4
|
import { getSupabaseConfig } from "@/lib/supabase-config";
|
|
4
5
|
import { DatabaseSetupGuide } from "../setup/DatabaseSetupGuide";
|
|
5
6
|
|
|
6
7
|
interface DatabaseHealthCheckProps {
|
|
7
8
|
children: React.ReactNode;
|
|
9
|
+
dataProvider: DataProvider;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export function DatabaseHealthCheck({ children }: DatabaseHealthCheckProps) {
|
|
12
|
+
export function DatabaseHealthCheck({ children, dataProvider }: DatabaseHealthCheckProps) {
|
|
11
13
|
const [healthStatus, setHealthStatus] = useState<DatabaseHealthStatus | null>(null);
|
|
12
14
|
const [isChecking, setIsChecking] = useState(true);
|
|
13
15
|
|
|
@@ -16,13 +18,13 @@ export function DatabaseHealthCheck({ children }: DatabaseHealthCheckProps) {
|
|
|
16
18
|
|
|
17
19
|
async function checkHealth() {
|
|
18
20
|
try {
|
|
19
|
-
const status = await checkDatabaseHealth();
|
|
21
|
+
const status = await checkDatabaseHealth(dataProvider);
|
|
20
22
|
if (!cancelled) {
|
|
21
23
|
setHealthStatus(status);
|
|
22
24
|
setIsChecking(false);
|
|
23
25
|
}
|
|
24
26
|
} catch (error) {
|
|
25
|
-
console.error("Failed to check database health:", error);
|
|
27
|
+
console.error("[DatabaseHealthCheck] Failed to check database health:", error);
|
|
26
28
|
if (!cancelled) {
|
|
27
29
|
setIsChecking(false);
|
|
28
30
|
}
|
|
@@ -34,7 +36,7 @@ export function DatabaseHealthCheck({ children }: DatabaseHealthCheckProps) {
|
|
|
34
36
|
return () => {
|
|
35
37
|
cancelled = true;
|
|
36
38
|
};
|
|
37
|
-
}, []);
|
|
39
|
+
}, [dataProvider]);
|
|
38
40
|
|
|
39
41
|
// Show loading state
|
|
40
42
|
if (isChecking) {
|
|
@@ -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
|
-
<
|
|
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
|
+
};
|