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/dist/assets/DealList-hFgyoSCd.js +59 -0
- package/dist/assets/{DealList-C_0DbLKA.js.map → DealList-hFgyoSCd.js.map} +1 -1
- package/dist/assets/{index-CdC59W53.js → index-BzM6--R_.js} +51 -51
- package/dist/assets/{index-CdC59W53.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 -1
- 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/providers/supabase/supabase.ts +23 -26
- package/src/components/atomic-crm/root/CRM.tsx +27 -27
- package/src/components/atomic-crm/root/DatabaseHealthCheck.tsx +4 -4
- package/src/components/atomic-crm/sales/SalesList.tsx +66 -9
- package/dist/assets/DealList-C_0DbLKA.js +0 -59
- package/dist/assets/index-C0zU8xwx.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtimex-crm",
|
|
3
|
-
"version": "0.5.
|
|
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
|
@@ -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
|
<>
|
|
@@ -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,15 +1,15 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
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
|
-
<
|
|
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
|
+
};
|