From 4131301a4f33d5cd25bdd18e3c33e812ff9fb683 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:38:36 +0100 Subject: [PATCH 01/34] Improve Header for mobile --- src/components/atomic-crm/layout/Header.tsx | 255 ++++++++++++++------ 1 file changed, 185 insertions(+), 70 deletions(-) diff --git a/src/components/atomic-crm/layout/Header.tsx b/src/components/atomic-crm/layout/Header.tsx index 71720d85..ea741be8 100644 --- a/src/components/atomic-crm/layout/Header.tsx +++ b/src/components/atomic-crm/layout/Header.tsx @@ -1,17 +1,34 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { Settings, User } from "lucide-react"; +import { Menu, Settings, User } from "lucide-react"; import { CanAccess } from "ra-core"; -import { Link, matchPath, useLocation } from "react-router"; +import { Link, type LinkProps, matchPath, useLocation } from "react-router"; import { RefreshButton } from "@/components/admin/refresh-button"; import { ThemeModeToggle } from "@/components/admin/theme-mode-toggle"; import { UserMenu } from "@/components/admin/user-menu"; import { useUserMenu } from "@/hooks/user-menu-context"; import { useConfigurationContext } from "../root/ConfigurationContext"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { Button } from "@/components/ui/button"; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { cn } from "@/lib/utils"; +import { type RefAttributes, useState } from "react"; +import { + NavigationMenu, + NavigationMenuLink, + NavigationMenuList, + navigationMenuTriggerStyle, +} from "@/components/ui/navigation-menu"; const Header = () => { - const { darkModeLogo, lightModeLogo, title } = useConfigurationContext(); const location = useLocation(); + const isMobile = useIsMobile(); let currentPath: string | boolean = "/"; if (matchPath("/", location.pathname)) { @@ -29,85 +46,183 @@ const Header = () => { return ( ); }; -const NavigationTab = ({ +const MobileHeader = ({ currentPath }: { currentPath: string | boolean }) => { + const { title } = useConfigurationContext(); + const [open, setOpen] = useState(false); + return ( +
+ + + + + + + Menu + +
+ + + + element?.focus()} + onClick={() => setOpen(false)} + /> + + + setOpen(false)} + /> + + + setOpen(false)} + /> + + + setOpen(false)} + /> + + + +
+
+
+

{title}

+ + + + + + +
+ ); +}; + +const DesktopHeader = ({ currentPath }: { currentPath: string | boolean }) => { + const { darkModeLogo, lightModeLogo, title } = useConfigurationContext(); + return ( +
+
+ + {title} + {title} +

{title}

+ +
+ +
+
+ + + + + + + + +
+
+
+ ); +}; + +const NavigationLink = ({ label, to, isActive, + ...props }: { label: string; to: string; isActive: boolean; -}) => ( - - {label} - -); +} & LinkProps & + RefAttributes) => { + return ( + + {label} + + ); +}; const UsersMenu = () => { const { onClose } = useUserMenu() ?? {}; From 9d416b2fb3b183c3567792a8affb19be78753b92 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:42:18 +0100 Subject: [PATCH 02/34] Improve Dashboard for mobile --- .../atomic-crm/dashboard/Dashboard.tsx | 26 ++++++++++++++++++- .../dashboard/DashboardActivityLog.tsx | 6 +++-- .../atomic-crm/dashboard/DealsChart.tsx | 6 +++-- .../atomic-crm/dashboard/TasksList.tsx | 19 ++++++++++---- src/components/atomic-crm/layout/Layout.tsx | 2 +- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/components/atomic-crm/dashboard/Dashboard.tsx b/src/components/atomic-crm/dashboard/Dashboard.tsx index a529b7f5..368b4b79 100644 --- a/src/components/atomic-crm/dashboard/Dashboard.tsx +++ b/src/components/atomic-crm/dashboard/Dashboard.tsx @@ -7,6 +7,8 @@ import { DealsChart } from "./DealsChart"; import { HotContacts } from "./HotContacts"; import { TasksList } from "./TasksList"; import { Welcome } from "./Welcome"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; export const Dashboard = () => { const { @@ -28,7 +30,7 @@ export const Dashboard = () => { pagination: { page: 1, perPage: 1 }, }, ); - + const isMobile = useIsMobile(); const isPending = isPendingContact || isPendingContactNotes || isPendingDeal; if (isPending) { @@ -43,6 +45,28 @@ export const Dashboard = () => { return ; } + if (isMobile) { + return ( +
+
+ {totalDeal ? : null} +
+ + + Tasks + Activity + + + + + + + + +
+ ); + } + return (
diff --git a/src/components/atomic-crm/dashboard/DashboardActivityLog.tsx b/src/components/atomic-crm/dashboard/DashboardActivityLog.tsx index 1d3ac1f7..e51aa3bc 100644 --- a/src/components/atomic-crm/dashboard/DashboardActivityLog.tsx +++ b/src/components/atomic-crm/dashboard/DashboardActivityLog.tsx @@ -2,11 +2,13 @@ import { Clock } from "lucide-react"; import { Card } from "@/components/ui/card"; import { ActivityLog } from "../activity/ActivityLog"; +import { useIsMobile } from "@/hooks/use-mobile"; export function DashboardActivityLog() { + const isMobile = useIsMobile(); return (
-
+
@@ -15,7 +17,7 @@ export function DashboardActivityLog() {
- +
); diff --git a/src/components/atomic-crm/dashboard/DealsChart.tsx b/src/components/atomic-crm/dashboard/DealsChart.tsx index c2964dcb..91f47a17 100644 --- a/src/components/atomic-crm/dashboard/DealsChart.tsx +++ b/src/components/atomic-crm/dashboard/DealsChart.tsx @@ -5,6 +5,7 @@ import { useGetList } from "ra-core"; import { memo, useMemo } from "react"; import type { Deal } from "../types"; +import { useIsMobile } from "@/hooks/use-mobile"; const multiplier = { opportunity: 0.2, @@ -21,6 +22,7 @@ const DEFAULT_LOCALE = "en-US"; const CURRENCY = "USD"; export const DealsChart = memo(() => { + const isMobile = useIsMobile(); const acceptedLanguages = navigator ? navigator.languages || [navigator.language] : [DEFAULT_LOCALE]; @@ -85,7 +87,7 @@ export const DealsChart = memo(() => { ); return (
-
+
@@ -100,7 +102,7 @@ export const DealsChart = memo(() => { keys={["won", "pending", "lost"]} colors={["#61cdbb", "#97e3d5", "#e25c3b"]} margin={{ top: 30, right: 50, bottom: 30, left: 0 }} - padding={0.3} + padding={isMobile ? 0.6 : 0.3} valueScale={{ type: "linear", min: range.min * 1.2, diff --git a/src/components/atomic-crm/dashboard/TasksList.tsx b/src/components/atomic-crm/dashboard/TasksList.tsx index 5fc82efe..9310554e 100644 --- a/src/components/atomic-crm/dashboard/TasksList.tsx +++ b/src/components/atomic-crm/dashboard/TasksList.tsx @@ -11,6 +11,7 @@ import { Card } from "@/components/ui/card"; import { AddTask } from "../tasks/AddTask"; import { TasksListEmpty } from "./TasksListEmpty"; import { TasksListFilter } from "./TasksListFilter"; +import { useIsMobile } from "@/hooks/use-mobile"; const today = new Date(); const todayDayOfWeek = getDay(today); @@ -41,9 +42,10 @@ const taskFilters = { }; export const TasksList = () => { + const isMobile = useIsMobile(); return (
-
+
@@ -57,11 +59,18 @@ export const TasksList = () => { - - {isBeforeFriday && ( - + {isMobile ? null : ( + <> + + {isBeforeFriday && ( + + )} + + )} -
diff --git a/src/components/atomic-crm/layout/Layout.tsx b/src/components/atomic-crm/layout/Layout.tsx index ef6ad33f..afe962a5 100644 --- a/src/components/atomic-crm/layout/Layout.tsx +++ b/src/components/atomic-crm/layout/Layout.tsx @@ -9,7 +9,7 @@ import Header from "./Header"; export const Layout = ({ children }: { children: ReactNode }) => ( <>
-
+
}> {children} From 4b6719d4a8ea13e24ecf9335332cc736f856ca76 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:04:56 +0100 Subject: [PATCH 03/34] Add floating create button to mobile dashboard --- .../atomic-crm/dashboard/Dashboard.tsx | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/components/atomic-crm/dashboard/Dashboard.tsx b/src/components/atomic-crm/dashboard/Dashboard.tsx index 368b4b79..bd47b5a0 100644 --- a/src/components/atomic-crm/dashboard/Dashboard.tsx +++ b/src/components/atomic-crm/dashboard/Dashboard.tsx @@ -9,6 +9,15 @@ import { TasksList } from "./TasksList"; import { Welcome } from "./Welcome"; import { useIsMobile } from "@/hooks/use-mobile"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { Link } from "react-router"; export const Dashboard = () => { const { @@ -48,9 +57,7 @@ export const Dashboard = () => { if (isMobile) { return (
-
- {totalDeal ? : null} -
+
{totalDeal ? : null}
Tasks @@ -63,6 +70,56 @@ export const Dashboard = () => { + + + + + + + + New + Deal + + + + + New + Task + + + + + New + Company + + + + + New + Contact + + + +
); } From a68ae899bc6a8fa50ebc34b41b840c8e80cbf475 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:04:59 +0100 Subject: [PATCH 04/34] Improve contact list for mobile --- .../atomic-crm/contacts/ContactList.tsx | 57 +++++++++--- .../contacts/ContactListContent.tsx | 86 ++++++++++--------- 2 files changed, 94 insertions(+), 49 deletions(-) diff --git a/src/components/atomic-crm/contacts/ContactList.tsx b/src/components/atomic-crm/contacts/ContactList.tsx index 94817df2..4d648afb 100644 --- a/src/components/atomic-crm/contacts/ContactList.tsx +++ b/src/components/atomic-crm/contacts/ContactList.tsx @@ -18,9 +18,15 @@ import { ContactImportButton } from "./ContactImportButton"; import { ContactListContent } from "./ContactListContent"; import { ContactListFilter } from "./ContactListFilter"; import { TopToolbar } from "../layout/TopToolbar"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { SearchInput } from "@/components/admin"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { Link } from "react-router"; export const ContactList = () => { const { identity } = useGetIdentity(); + const isMobile = useIsMobile(); if (!identity) return null; @@ -31,8 +37,31 @@ export const ContactList = () => { perPage={25} sort={{ field: "last_seen", order: "DESC" }} exporter={exporter} + filters={ + isMobile + ? [ + , + ] + : undefined + } > + ); }; @@ -40,6 +69,7 @@ export const ContactList = () => { const ContactListLayout = () => { const { data, isPending, filterValues } = useListContext(); const { identity } = useGetIdentity(); + const isMobile = useIsMobile(); const hasFilters = filterValues && Object.keys(filterValues).length > 0; @@ -49,25 +79,32 @@ const ContactListLayout = () => { return (
- + {isMobile ? null : }
- + {isMobile ? null : }
); }; -const ContactListActions = () => ( - - - - - - -); +const ContactListActions = () => { + const isMobile = useIsMobile(); + return ( + + + {isMobile ? null : ( + <> + + + + + )} + + ); +}; const exporter: Exporter = async (records, fetchRelatedRecords) => { const companies = await fetchRelatedRecords( diff --git a/src/components/atomic-crm/contacts/ContactListContent.tsx b/src/components/atomic-crm/contacts/ContactListContent.tsx index 5c73e707..917799c3 100644 --- a/src/components/atomic-crm/contacts/ContactListContent.tsx +++ b/src/components/atomic-crm/contacts/ContactListContent.tsx @@ -50,49 +50,57 @@ export const ContactListContent = () => { className="flex flex-row gap-4 items-center px-4 py-2 hover:bg-muted transition-colors first:rounded-t-xl last:rounded-b-xl" onClick={handleLinkClick} > - onToggleItem(contact.id)} - /> + {isSmall ? null : ( + onToggleItem(contact.id)} + /> + )} -
-
- {`${contact.first_name} ${contact.last_name ?? ""}`} +
+
+
+ {`${contact.first_name} ${contact.last_name ?? ""}`} +
+
+
+ + {contact.title} + {contact.title && contact.company_id != null && " at "} + {contact.company_id != null && ( + + + + )} + + {contact.nb_tasks ? ( + + {contact.nb_tasks} task{contact.nb_tasks > 1 ? "s" : ""} + + ) : null} +    + +
+
-
- {contact.title} - {contact.title && contact.company_id != null && " at "} - {contact.company_id != null && ( - +
- - - )} - {contact.nb_tasks - ? ` - ${contact.nb_tasks} task${ - contact.nb_tasks > 1 ? "s" : "" - }` - : ""} -    - -
-
- {contact.last_seen && ( -
-
- {!isSmall && "last activity "} - {formatRelative(contact.last_seen, now)}{" "} - + {!isSmall && "last activity "} + {formatRelative(contact.last_seen, now)}{" "} + +
-
- )} + )} +
))} From b4e6732dec100882f1a58e80fb715d49ad9693c6 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:40:41 +0100 Subject: [PATCH 05/34] Improve companies list for mobile --- .../atomic-crm/companies/CompanyList.tsx | 101 +++++++++++++++++- 1 file changed, 97 insertions(+), 4 deletions(-) diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index fb0d1ce9..04492043 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -9,9 +9,20 @@ import { TopToolbar } from "../layout/TopToolbar"; import { CompanyEmpty } from "./CompanyEmpty"; import { CompanyListFilter } from "./CompanyListFilter"; import { ImageList } from "./GridList"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { SearchInput, TextField } from "@/components/admin"; +import { Button } from "@/components/ui/button"; +import { Link } from "react-router"; +import { Plus } from "lucide-react"; +import { CompanyAvatar } from "./CompanyAvatar"; +import { RecordsIterator } from "ra-core"; +import { Card } from "@/components/ui/card"; +import { useRecordContext } from "ra-core"; +import { useReferenceManyFieldController } from "ra-core"; export const CompanyList = () => { const { identity } = useGetIdentity(); + const isMobile = useIsMobile(); if (!identity) return null; return ( { sort={{ field: "name", order: "ASC" }} actions={} pagination={} + filters={ + isMobile + ? [ + , + ] + : undefined + } > + ); }; @@ -29,26 +63,85 @@ export const CompanyList = () => { const CompanyListLayout = () => { const { data, isPending, filterValues } = useListContext(); const hasFilters = filterValues && Object.keys(filterValues).length > 0; + const isMobile = useIsMobile(); if (isPending) return null; if (!data?.length && !hasFilters) return ; return (
- + {isMobile ? null : }
- + {isMobile ? ( + + ) : ( + + )}
); }; +const CompanyMobileList = () => ( + +
    + ( +
  • + + +
    + +
    + +
    + +
    +
    +
    + +
  • + )} + /> +
+
+); +/** + * Necessary until we have a render prop on ReferenceManyCountBase + */ +const DealsCount = () => { + const record = useRecordContext(); + const { isLoading, error, total } = useReferenceManyFieldController({ + page: 1, + perPage: 1, + record, + reference: "deals", + target: "company_id", + }); + + const body = isLoading + ? "" + : error + ? "error" + : `${total} ${total === 1 ? "deal" : "deals"}`; + return {body}; +}; + const CompanyListActions = () => { + const isMobile = useIsMobile(); + return ( - - + {isMobile ? null : ( + <> + + + + )} ); }; From 55c678ed37857e498b94938b4306b32dd7dce687 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:50:13 +0100 Subject: [PATCH 06/34] Improve deals list for mobile --- .../atomic-crm/deals/DealColumn.tsx | 2 +- src/components/atomic-crm/deals/DealList.tsx | 46 +++++++++++++------ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/components/atomic-crm/deals/DealColumn.tsx b/src/components/atomic-crm/deals/DealColumn.tsx index 65502621..09d73d5a 100644 --- a/src/components/atomic-crm/deals/DealColumn.tsx +++ b/src/components/atomic-crm/deals/DealColumn.tsx @@ -16,7 +16,7 @@ export const DealColumn = ({ const { dealStages } = useConfigurationContext(); return ( -
+

{findDealLabel(dealStages, stage)} diff --git a/src/components/atomic-crm/deals/DealList.tsx b/src/components/atomic-crm/deals/DealList.tsx index 51f6c257..46aeb1b0 100644 --- a/src/components/atomic-crm/deals/DealList.tsx +++ b/src/components/atomic-crm/deals/DealList.tsx @@ -1,5 +1,5 @@ import { useGetIdentity, useListContext } from "ra-core"; -import { matchPath, useLocation } from "react-router"; +import { Link, matchPath, useLocation } from "react-router"; import { AutocompleteInput } from "@/components/admin/autocomplete-input"; import { CreateButton } from "@/components/admin/create-button"; import { ExportButton } from "@/components/admin/export-button"; @@ -18,25 +18,32 @@ import { DealEmpty } from "./DealEmpty"; import { DealListContent } from "./DealListContent"; import { DealShow } from "./DealShow"; import { OnlyMineInput } from "./OnlyMineInput"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; const DealList = () => { const { identity } = useGetIdentity(); const { dealCategories } = useConfigurationContext(); + const isMobile = useIsMobile(); if (!identity) return null; - const dealFilters = [ - , - - - , - ({ id: type, name: type }))} - />, - , - ]; + const dealFilters = []; + + if (!isMobile) { + dealFilters.push( + + + , + ({ id: type, name: type }))} + />, + , + ); + } return ( { title={false} sort={{ field: "index", order: "DESC" }} filters={dealFilters} - actions={} + actions={isMobile ? false : } pagination={null} > + ); }; From 124c2d22ab91ee8340a4a3039735d661cf559187 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:16:15 +0100 Subject: [PATCH 07/34] Formatting --- src/components/atomic-crm/companies/CompanyList.tsx | 6 +----- src/components/atomic-crm/layout/Layout.tsx | 5 ++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index 04492043..6d45c47c 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -72,11 +72,7 @@ const CompanyListLayout = () => {
{isMobile ? null : }
- {isMobile ? ( - - ) : ( - - )} + {isMobile ? : }
); diff --git a/src/components/atomic-crm/layout/Layout.tsx b/src/components/atomic-crm/layout/Layout.tsx index afe962a5..5393de7e 100644 --- a/src/components/atomic-crm/layout/Layout.tsx +++ b/src/components/atomic-crm/layout/Layout.tsx @@ -9,7 +9,10 @@ import Header from "./Header"; export const Layout = ({ children }: { children: ReactNode }) => ( <>
-
+
}> {children} From 871a9064aac7861caadfcce025e4f4aeaa4f1288 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:49:07 +0100 Subject: [PATCH 08/34] Improve contact list --- .../atomic-crm/contacts/ContactListContent.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/atomic-crm/contacts/ContactListContent.tsx b/src/components/atomic-crm/contacts/ContactListContent.tsx index 917799c3..59987803 100644 --- a/src/components/atomic-crm/contacts/ContactListContent.tsx +++ b/src/components/atomic-crm/contacts/ContactListContent.tsx @@ -58,13 +58,13 @@ export const ContactListContent = () => { /> )} -
+
{`${contact.first_name} ${contact.last_name ?? ""}`}
-
+
{contact.title} {contact.title && contact.company_id != null && " at "} @@ -79,17 +79,16 @@ export const ContactListContent = () => { )} {contact.nb_tasks ? ( - + {contact.nb_tasks} task{contact.nb_tasks > 1 ? "s" : ""} ) : null} -    - + {isSmall ? null : }
- {contact.last_seen && ( -
+ {contact.last_seen && !isSmall && ( +
Date: Wed, 26 Nov 2025 10:21:09 +0100 Subject: [PATCH 09/34] Improve contact show view for mobile --- .../atomic-crm/companies/CompanyList.tsx | 1 + .../atomic-crm/contacts/ContactAside.tsx | 50 +++-- .../atomic-crm/contacts/ContactShow.tsx | 172 ++++++++++++++---- src/components/atomic-crm/tasks/AddTask.tsx | 10 +- 4 files changed, 172 insertions(+), 61 deletions(-) diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index 6d45c47c..358c70b2 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -105,6 +105,7 @@ const CompanyMobileList = () => ( ); + /** * Necessary until we have a render prop on ReferenceManyCountBase */ diff --git a/src/components/atomic-crm/contacts/ContactAside.tsx b/src/components/atomic-crm/contacts/ContactAside.tsx index de816eaa..9aeede9a 100644 --- a/src/components/atomic-crm/contacts/ContactAside.tsx +++ b/src/components/atomic-crm/contacts/ContactAside.tsx @@ -22,7 +22,6 @@ import { ContactMergeButton } from "./ContactMergeButton"; import { ExportVCardButton } from "./ExportVCardButton"; export const ContactAside = ({ link = "edit" }: { link?: "edit" | "show" }) => { - const { contactGender } = useConfigurationContext(); const record = useRecordContext(); if (!record) return null; @@ -36,6 +35,35 @@ export const ContactAside = ({ link = "edit" }: { link?: "edit" | "show" }) => { )}
+ + + + + + + + + + {link !== "edit" && ( +
+ + +
+ )} +
+ ); +}; + +export const ContactDetails = () => { + const record = useRecordContext(); + const { contactGender } = useConfigurationContext(); + if (!record) return null; + return ( + <> @@ -129,25 +157,7 @@ export const ContactAside = ({ link = "edit" }: { link?: "edit" | "show" }) => { - - - - - - - - - {link !== "edit" && ( -
- - -
- )} -
+ ); }; diff --git a/src/components/atomic-crm/contacts/ContactShow.tsx b/src/components/atomic-crm/contacts/ContactShow.tsx index 67b3ec1a..69783cbe 100644 --- a/src/components/atomic-crm/contacts/ContactShow.tsx +++ b/src/components/atomic-crm/contacts/ContactShow.tsx @@ -8,7 +8,16 @@ import { CompanyAvatar } from "../companies/CompanyAvatar"; import { NoteCreate, NotesIterator } from "../notes"; import type { Contact } from "../types"; import { Avatar } from "./Avatar"; -import { ContactAside } from "./ContactAside"; +import { ContactAside, ContactDetails } from "./ContactAside"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { AddTask } from "../tasks/AddTask"; +import { TasksIterator } from "../tasks/TasksIterator"; +import { useRecordContext } from "ra-core"; +import { useReferenceManyFieldController } from "ra-core"; +import { Button } from "@/components/ui/button"; +import { Link } from "react-router"; +import { Edit } from "lucide-react"; export const ContactShow = () => ( @@ -18,59 +27,144 @@ export const ContactShow = () => ( const ContactShowContent = () => { const { record, isPending } = useShowContext(); + const isMobile = useIsMobile(); if (isPending || !record) return null; return (
- +
-
- {record.first_name} {record.last_name} -
-
- {record.title} - {record.title && record.company_id != null && " at "} - {record.company_id != null && ( - -   - - - )} +
+
+ {record.first_name} {record.last_name} +
+ +
+
+ {record.title} + + {record.title && record.company_id != null && " at "} + {record.company_id != null && ( + +   + + + )} +
-
- - - -
+ {isMobile ? null : ( +
+ + + +
+ )}
- - } - > - - + {isMobile ? ( + + + Notes + + + + Contact details + + + + } + > + + + + + + + + + + + + + + ) : ( + + } + > + + + )}
- + {isMobile ? null : }
); }; + +/** + * Necessary until we have a render prop on ReferenceManyCountBase + */ +const TasksCount = () => { + const record = useRecordContext(); + const { isLoading, error, total } = useReferenceManyFieldController({ + page: 1, + perPage: 1, + record, + reference: "tasks", + target: "contact_id", + }); + + const body = isLoading + ? "" + : error + ? "error" + : `${total} ${total === 1 ? "task" : "tasks"}`; + return {body}; +}; diff --git a/src/components/atomic-crm/tasks/AddTask.tsx b/src/components/atomic-crm/tasks/AddTask.tsx index d5f4e04e..9f0748ec 100644 --- a/src/components/atomic-crm/tasks/AddTask.tsx +++ b/src/components/atomic-crm/tasks/AddTask.tsx @@ -34,6 +34,8 @@ import { import { contactOptionText } from "../misc/ContactOption"; import { useConfigurationContext } from "../root/ConfigurationContext"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; export const AddTask = ({ selectContact, @@ -49,6 +51,7 @@ export const AddTask = ({ const { taskTypes } = useConfigurationContext(); const contact = useRecordContext(); const [open, setOpen] = useState(false); + const isMobile = useIsMobile(); const handleOpen = () => { setOpen(true); }; @@ -93,9 +96,12 @@ export const AddTask = ({
+ } text={contactNote.text} /> diff --git a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx index a8d60232..2e92108d 100644 --- a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx +++ b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx @@ -1,9 +1,11 @@ import { Link } from "react-router"; import type { RaRecord } from "ra-core"; -import { RelativeDate } from "../misc/RelativeDate"; import type { ActivityDealCreated } from "../types"; import { useActivityLogContext } from "./ActivityLogContext"; +import { ActivityLogHeader } from "./ActivityLogHeader"; +import { ReferenceField } from "@/components/admin"; +import { SaleName } from "../sales/SaleName"; type ActivityLogDealCreatedProps = { activity: RaRecord & ActivityDealCreated; @@ -15,27 +17,21 @@ export function ActivityLogDealCreated({ const context = useActivityLogContext(); const { deal } = activity; return ( -
-
-
-
- - Sales ID: {activity.sales_id} - {" "} - added deal {deal.name}{" "} - {context !== "company" && ( - <> - to company {activity.company_id}{" "} - - - )} -
- {context === "company" && ( - - - - )} -
-
+ } + activity={activity} + > + + + + + +  added deal {deal.name} + {context !== "company" && ( + <> +  to company {activity.company_id} + + )} + ); } diff --git a/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx b/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx index 3810498b..e1c611d3 100644 --- a/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx +++ b/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx @@ -2,11 +2,11 @@ import type { RaRecord } from "ra-core"; import { ReferenceField } from "@/components/admin/reference-field"; import { CompanyAvatar } from "../companies/CompanyAvatar"; -import { RelativeDate } from "../misc/RelativeDate"; import { SaleName } from "../sales/SaleName"; import type { ActivityDealNoteCreated } from "../types"; import { useActivityLogContext } from "./ActivityLogContext"; import { ActivityLogNote } from "./ActivityLogNote"; +import { ActivityLogHeader } from "./ActivityLogHeader"; type ActivityLogDealNoteCreatedProps = { activity: RaRecord & ActivityDealNoteCreated; @@ -20,63 +20,59 @@ export function ActivityLogDealNoteCreated({ return ( - + - + + + + } + activity={activity} + > + + - - - - - -  added a note about deal  - - {context !== "company" && ( - <> - {" at "} +  added a note about deal  + + {context !== "company" && ( + <> + {" at "} + - - {" "} - - )} - - - {context === "company" && ( - - - + source="company_id" + reference="companies" + link="show" + /> + {" "} + )} -
+ } text={dealNote.text} /> diff --git a/src/components/atomic-crm/activity/ActivityLogHeader.tsx b/src/components/atomic-crm/activity/ActivityLogHeader.tsx new file mode 100644 index 00000000..9e82fbe3 --- /dev/null +++ b/src/components/atomic-crm/activity/ActivityLogHeader.tsx @@ -0,0 +1,39 @@ +import { useIsMobile } from "@/hooks/use-mobile"; +import { RelativeDate } from "../misc/RelativeDate"; +import type { Activity } from "../types"; +import { useActivityLogContext } from "./ActivityLogContext"; + +export const ActivityLogHeader = ({ + activity, + avatar, + children, +}: { + activity: Activity; + avatar: React.ReactNode; + children: React.ReactNode; +}) => { + const context = useActivityLogContext(); + const isMobile = useIsMobile(); + + return ( +
+
+
{avatar}
+ +
+ {children} + {isMobile ? ( + <> +  at + + ) : null} +
+ {context === "company" && !isMobile && ( + + + + )} +
+
+ ); +}; diff --git a/src/components/atomic-crm/companies/CompanyAside.tsx b/src/components/atomic-crm/companies/CompanyAside.tsx index d8265379..0eb9bf77 100644 --- a/src/components/atomic-crm/companies/CompanyAside.tsx +++ b/src/components/atomic-crm/companies/CompanyAside.tsx @@ -42,7 +42,7 @@ export const CompanyAside = ({ link = "edit" }: CompanyAsideProps) => { ); }; -const CompanyInfo = ({ record }: { record: Company }) => { +export const CompanyInfo = ({ record }: { record: Company }) => { if (!record.website && !record.linkedin_url && !record.phone_number) { return null; } @@ -86,7 +86,7 @@ const CompanyInfo = ({ record }: { record: Company }) => { ); }; -const ContextInfo = ({ record }: { record: Company }) => { +export const ContextInfo = ({ record }: { record: Company }) => { if (!record.revenue && !record.id) { return null; } @@ -117,7 +117,7 @@ const ContextInfo = ({ record }: { record: Company }) => { ); }; -const AddressInfo = ({ record }: { record: Company }) => { +export const AddressInfo = ({ record }: { record: Company }) => { if (!record.address && !record.city && !record.zipcode && !record.stateAbbr) { return null; } @@ -133,7 +133,7 @@ const AddressInfo = ({ record }: { record: Company }) => { ); }; -const AdditionalInfo = ({ record }: { record: Company }) => { +export const AdditionalInfo = ({ record }: { record: Company }) => { if ( !record.created_at && !record.sales_id && diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index 358c70b2..4b46bd80 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -86,7 +86,7 @@ const CompanyMobileList = () => (
  • diff --git a/src/components/atomic-crm/companies/CompanyShow.tsx b/src/components/atomic-crm/companies/CompanyShow.tsx index 5c064050..bd572552 100644 --- a/src/components/atomic-crm/companies/CompanyShow.tsx +++ b/src/components/atomic-crm/companies/CompanyShow.tsx @@ -26,8 +26,16 @@ import { findDealLabel } from "../deals/deal"; import { Status } from "../misc/Status"; import { useConfigurationContext } from "../root/ConfigurationContext"; import type { Company, Contact, Deal } from "../types"; -import { CompanyAside } from "./CompanyAside"; +import { + AdditionalInfo, + AddressInfo, + CompanyAside, + CompanyInfo, + ContextInfo, +} from "./CompanyAside"; import { CompanyAvatar } from "./CompanyAvatar"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; export const CompanyShow = () => ( @@ -38,6 +46,7 @@ export const CompanyShow = () => ( const CompanyShowContent = () => { const { record, isPending } = useShowContext(); const navigate = useNavigate(); + const isMobile = useIsMobile(); // Get tab from URL or default to "activity" const tabMatch = useMatch("/companies/:id/show/:tab"); @@ -57,14 +66,19 @@ const CompanyShowContent = () => { return (
    - - + +
    {record.name}
    - + Activity {record.nb_contacts @@ -80,6 +94,9 @@ const CompanyShowContent = () => { : `${record.nb_deals} deals`} ) : null} + {isMobile ? ( + About + ) : null} @@ -122,17 +139,24 @@ const CompanyShowContent = () => { ) : null} + + + + + +
    - + {isMobile ? null : }
    ); }; const ContactsIterator = () => { const location = useLocation(); + const isMobile = useIsMobile(); const { data: contacts, error, isPending } = useListContext(); if (isPending || error) return null; @@ -162,11 +186,15 @@ const ContactsIterator = () => { contact.nb_tasks > 1 ? "s" : "" }` : ""} -     - + {isMobile ? null : ( + <> +     + + + )}
  • - {contact.last_seen && ( + {!isMobile && contact.last_seen && (
    last activity {formatDistance(contact.last_seen, now)} ago{" "} @@ -211,9 +239,9 @@ const DealsIterator = () => {
    -
    +
    {deal.name}
    {findDealLabel(dealStages, deal.stage)},{" "} @@ -227,7 +255,7 @@ const DealsIterator = () => { {deal.category ? `, ${deal.category}` : ""}
    -
    +
    last activity {formatDistance(deal.updated_at, now)} ago{" "}
    diff --git a/src/components/atomic-crm/contacts/ContactShow.tsx b/src/components/atomic-crm/contacts/ContactShow.tsx index 69783cbe..9569ac80 100644 --- a/src/components/atomic-crm/contacts/ContactShow.tsx +++ b/src/components/atomic-crm/contacts/ContactShow.tsx @@ -33,8 +33,8 @@ const ContactShowContent = () => { return (
    - - + +
    @@ -87,7 +87,7 @@ const ContactShowContent = () => {
    {isMobile ? ( - + Notes diff --git a/src/components/atomic-crm/dashboard/Dashboard.tsx b/src/components/atomic-crm/dashboard/Dashboard.tsx index bd47b5a0..cc6b175c 100644 --- a/src/components/atomic-crm/dashboard/Dashboard.tsx +++ b/src/components/atomic-crm/dashboard/Dashboard.tsx @@ -59,7 +59,7 @@ export const Dashboard = () => {
    {totalDeal ? : null}
    - + Tasks Activity From 1aba9d6615638c10bcbef93f49638e5a527daa30 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:26:20 +0100 Subject: [PATCH 11/34] Update registry --- registry.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/registry.json b/registry.json index a4bd0cb3..f356b4d1 100644 --- a/registry.json +++ b/registry.json @@ -634,6 +634,10 @@ "path": "src/components/atomic-crm/activity/ActivityLogIterator.tsx", "type": "registry:component" }, + { + "path": "src/components/atomic-crm/activity/ActivityLogHeader.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx", "type": "registry:component" From 304111889c867dbaf6c3520eb3d39d56c9adc622 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:51:32 +0100 Subject: [PATCH 12/34] Improve deal views for mobile --- .../atomic-crm/companies/CompanyList.tsx | 37 +-- .../atomic-crm/contacts/ContactShow.tsx | 46 ++- .../atomic-crm/deals/DealCreate.tsx | 48 +-- src/components/atomic-crm/deals/DealEdit.tsx | 58 +++- src/components/atomic-crm/deals/DealList.tsx | 24 +- src/components/atomic-crm/deals/DealShow.tsx | 276 ++++++++++++------ src/components/atomic-crm/deals/index.ts | 15 + .../atomic-crm/misc/ReferenceManyCount.tsx | 45 +++ src/components/atomic-crm/root/CRM.tsx | 10 +- 9 files changed, 369 insertions(+), 190 deletions(-) create mode 100644 src/components/atomic-crm/misc/ReferenceManyCount.tsx diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index 4b46bd80..ff5ae232 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -1,4 +1,4 @@ -import { useGetIdentity, useListContext } from "ra-core"; +import { RecordsIterator, useGetIdentity, useListContext } from "ra-core"; import { CreateButton } from "@/components/admin/create-button"; import { ExportButton } from "@/components/admin/export-button"; import { List } from "@/components/admin/list"; @@ -15,10 +15,8 @@ import { Button } from "@/components/ui/button"; import { Link } from "react-router"; import { Plus } from "lucide-react"; import { CompanyAvatar } from "./CompanyAvatar"; -import { RecordsIterator } from "ra-core"; import { Card } from "@/components/ui/card"; -import { useRecordContext } from "ra-core"; -import { useReferenceManyFieldController } from "ra-core"; +import { ReferenceManyCount } from "../misc/ReferenceManyCount"; export const CompanyList = () => { const { identity } = useGetIdentity(); @@ -94,7 +92,15 @@ const CompanyMobileList = () => (
    - + ( + <> + {total} {total === 1 ? "deal" : "deals"} + + )} + />
    @@ -106,27 +112,6 @@ const CompanyMobileList = () => ( ); -/** - * Necessary until we have a render prop on ReferenceManyCountBase - */ -const DealsCount = () => { - const record = useRecordContext(); - const { isLoading, error, total } = useReferenceManyFieldController({ - page: 1, - perPage: 1, - record, - reference: "deals", - target: "company_id", - }); - - const body = isLoading - ? "" - : error - ? "error" - : `${total} ${total === 1 ? "deal" : "deals"}`; - return {body}; -}; - const CompanyListActions = () => { const isMobile = useIsMobile(); diff --git a/src/components/atomic-crm/contacts/ContactShow.tsx b/src/components/atomic-crm/contacts/ContactShow.tsx index 9569ac80..fafbd0ab 100644 --- a/src/components/atomic-crm/contacts/ContactShow.tsx +++ b/src/components/atomic-crm/contacts/ContactShow.tsx @@ -13,11 +13,10 @@ import { useIsMobile } from "@/hooks/use-mobile"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AddTask } from "../tasks/AddTask"; import { TasksIterator } from "../tasks/TasksIterator"; -import { useRecordContext } from "ra-core"; -import { useReferenceManyFieldController } from "ra-core"; import { Button } from "@/components/ui/button"; import { Link } from "react-router"; import { Edit } from "lucide-react"; +import { ReferenceManyCount } from "../misc/ReferenceManyCount"; export const ContactShow = () => ( @@ -88,9 +87,27 @@ const ContactShowContent = () => { {isMobile ? ( - Notes + + ( + <> + {total?.toString()} {total === 1 ? "note" : "notes"} + + )} + /> + - + ( + <> + {total?.toString()} {total === 1 ? "task" : "tasks"} + + )} + /> Contact details @@ -147,24 +164,3 @@ const ContactShowContent = () => {
    ); }; - -/** - * Necessary until we have a render prop on ReferenceManyCountBase - */ -const TasksCount = () => { - const record = useRecordContext(); - const { isLoading, error, total } = useReferenceManyFieldController({ - page: 1, - perPage: 1, - record, - reference: "tasks", - target: "contact_id", - }); - - const body = isLoading - ? "" - : error - ? "error" - : `${total} ${total === 1 ? "task" : "tasks"}`; - return {body}; -}; diff --git a/src/components/atomic-crm/deals/DealCreate.tsx b/src/components/atomic-crm/deals/DealCreate.tsx index 323b6033..a4ca50ab 100644 --- a/src/components/atomic-crm/deals/DealCreate.tsx +++ b/src/components/atomic-crm/deals/DealCreate.tsx @@ -7,7 +7,7 @@ import { useRedirect, type GetListResult, } from "ra-core"; -import { Create } from "@/components/admin/create"; +import { Create, CreateProps } from "@/components/admin/create"; import { SaveButton } from "@/components/admin/form"; import { FormToolbar } from "@/components/admin/simple-form"; import { Dialog, DialogContent } from "@/components/ui/dialog"; @@ -16,14 +16,9 @@ import type { Deal } from "../types"; import { DealInputs } from "./DealInputs"; export const DealCreate = ({ open }: { open: boolean }) => { - const redirect = useRedirect(); const dataProvider = useDataProvider(); + const redirect = useRedirect(); const { data: allDeals } = useListContext(); - - const handleClose = () => { - redirect("/deals"); - }; - const queryClient = useQueryClient(); const onSuccess = async (deal: Deal) => { @@ -70,26 +65,35 @@ export const DealCreate = ({ open }: { open: boolean }) => { redirect("/deals"); }; - const { identity } = useGetIdentity(); + const handleClose = () => { + redirect("/deals"); + }; return ( handleClose()}> - -
    - - - - - -
    +
    ); }; + +export const DealCreatePage = (props: Partial) => { + const { identity } = useGetIdentity(); + return ( + +
    + + + + + +
    + ); +}; diff --git a/src/components/atomic-crm/deals/DealEdit.tsx b/src/components/atomic-crm/deals/DealEdit.tsx index f61beb63..812df221 100644 --- a/src/components/atomic-crm/deals/DealEdit.tsx +++ b/src/components/atomic-crm/deals/DealEdit.tsx @@ -55,27 +55,57 @@ export const DealEdit = ({ open, id }: { open: boolean; id?: string }) => { ); }; -function EditHeader() { +export const DealEditPage = () => { + const redirect = useRedirect(); + const notify = useNotify(); + return ( + { + notify("Deal updated"); + redirect(`show`, undefined, undefined, undefined, { + _scrollToTop: false, + }); + }, + }} + > + +
    + + + +
    + ); +}; + +function EditTitle() { const deal = useRecordContext(); if (!deal) { return null; } return ( - -
    -
    - - - -

    Edit {deal.name} deal

    -
    -
    - -
    +
    +
    + + + +

    Edit {deal.name} deal

    +
    + +
    +
    + ); +} + +function EditHeader() { + return ( + + ); } diff --git a/src/components/atomic-crm/deals/DealList.tsx b/src/components/atomic-crm/deals/DealList.tsx index 46aeb1b0..f1ca7adb 100644 --- a/src/components/atomic-crm/deals/DealList.tsx +++ b/src/components/atomic-crm/deals/DealList.tsx @@ -73,9 +73,12 @@ const DealList = () => { const DealLayout = () => { const location = useLocation(); - const matchCreate = matchPath("/deals/create", location.pathname); - const matchShow = matchPath("/deals/:id/show", location.pathname); - const matchEdit = matchPath("/deals/:id", location.pathname); + const isMobile = useIsMobile(); + const matchCreate = + !isMobile && matchPath("/deals/create", location.pathname); + const matchShow = + !isMobile && matchPath("/deals/:id/show", location.pathname); + const matchEdit = !isMobile && matchPath("/deals/:id", location.pathname); const { data, isPending, filterValues } = useListContext(); const hasFilters = filterValues && Object.keys(filterValues).length > 0; @@ -85,7 +88,10 @@ const DealLayout = () => { return ( <> - + @@ -96,8 +102,14 @@ const DealLayout = () => { - - + +
    ); }; diff --git a/src/components/atomic-crm/deals/DealShow.tsx b/src/components/atomic-crm/deals/DealShow.tsx index 7c0f02fc..63652679 100644 --- a/src/components/atomic-crm/deals/DealShow.tsx +++ b/src/components/atomic-crm/deals/DealShow.tsx @@ -1,6 +1,6 @@ import { useMutation } from "@tanstack/react-query"; import { format, isValid } from "date-fns"; -import { Archive, ArchiveRestore } from "lucide-react"; +import { Archive, ArchiveRestore, Edit } from "lucide-react"; import { ShowBase, useDataProvider, @@ -27,6 +27,10 @@ import { useConfigurationContext } from "../root/ConfigurationContext"; import type { Deal } from "../types"; import { ContactList } from "./ContactList"; import { findDealLabel } from "./deal"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ReferenceManyCount } from "../misc/ReferenceManyCount"; +import { Link } from "react-router"; export const DealShow = ({ open, id }: { open: boolean; id?: string }) => { const redirect = useRedirect(); @@ -37,18 +41,20 @@ export const DealShow = ({ open, id }: { open: boolean; id?: string }) => { return ( !open && handleClose()}> - {id ? ( - - - - ) : null} + {id ? : null} ); }; +export const DealShowPage = ({ id }: { id?: string }) => ( + + + +); + const DealShowContent = () => { - const { dealStages } = useConfigurationContext(); + const isMobile = useIsMobile(); const record = useRecordContext(); if (!record) return null; @@ -67,112 +73,190 @@ const DealShowContent = () => {

    {record.name}

    +
    -
    - {record.archived_at ? ( - <> - - - - ) : ( - <> - - - - )} -
    -
    - -
    -
    - - Expected closing date - -
    - - {isValid(new Date(record.expected_closing_date)) - ? format(new Date(record.expected_closing_date), "PP") - : "Invalid date"} - - {new Date(record.expected_closing_date) < new Date() ? ( - Past - ) : null} -
    -
    - -
    - - Budget - - - {record.amount.toLocaleString("en-US", { - notation: "compact", - style: "currency", - currency: "USD", - currencyDisplay: "narrowSymbol", - minimumSignificantDigits: 3, - })} - -
    - - {record.category && ( -
    - - Category - - {record.category} + {isMobile ? null : ( +
    + {record.archived_at ? ( + <> + + + + ) : ( + <> + + + + )}
    )} - -
    - - Stage - - - {findDealLabel(dealStages, record.stage)} - -
    - {!!record.contact_ids?.length && ( -
    -
    - - Contacts - + {isMobile ? ( + + + + ( + <> + {total?.toString()} {total === 1 ? "note" : "notes"} + + )} + /> + + + ( + <> + {total?.toString()}{" "} + {total === 1 ? "contact" : "contacts"} + + )} + /> + + About + + + } + > + + + + + + + + + + ) : ( + <> + + {!!record.contact_ids?.length && ( +
    +
    + + Contacts + + + + +
    +
    + )} + + {record.description && ( +
    + + Description + +

    {record.description}

    +
    + )} + +
    + + } + > + +
    -
    + )} +
    +
    + + ); +}; - {record.description && ( -
    - - Description - -

    {record.description}

    -
    - )} +const DealDetails = () => { + const { dealStages } = useConfigurationContext(); + const record = useRecordContext(); -
    - - } - > - - -
    + if (!record) return null; + return ( +
    +
    + + Expected closing date + +
    + + {isValid(new Date(record.expected_closing_date)) + ? format(new Date(record.expected_closing_date), "PP") + : "Invalid date"} + + {new Date(record.expected_closing_date) < new Date() ? ( + Past + ) : null}
    - + +
    + + Budget + + + {record.amount.toLocaleString("en-US", { + notation: "compact", + style: "currency", + currency: "USD", + currencyDisplay: "narrowSymbol", + minimumSignificantDigits: 3, + })} + +
    + + {record.category && ( +
    + + Category + + {record.category} +
    + )} + +
    + + Stage + + + {findDealLabel(dealStages, record.stage)} + +
    +
    ); }; diff --git a/src/components/atomic-crm/deals/index.ts b/src/components/atomic-crm/deals/index.ts index 82086f28..35cf4b55 100644 --- a/src/components/atomic-crm/deals/index.ts +++ b/src/components/atomic-crm/deals/index.ts @@ -1,6 +1,21 @@ import * as React from "react"; + const DealList = React.lazy(() => import("./DealList")); +const DealEdit = React.lazy(() => + import("./DealEdit").then(({ DealEditPage }) => ({ default: DealEditPage })), +); +const DealShow = React.lazy(() => + import("./DealShow").then(({ DealShowPage }) => ({ default: DealShowPage })), +); +const DealCreate = React.lazy(() => + import("./DealCreate").then(({ DealCreatePage }) => ({ + default: DealCreatePage, + })), +); export default { list: DealList, + edit: DealEdit, + show: DealShow, + create: DealCreate, }; diff --git a/src/components/atomic-crm/misc/ReferenceManyCount.tsx b/src/components/atomic-crm/misc/ReferenceManyCount.tsx new file mode 100644 index 00000000..07cfd5d2 --- /dev/null +++ b/src/components/atomic-crm/misc/ReferenceManyCount.tsx @@ -0,0 +1,45 @@ +import { + ReferenceManyCountBaseProps, + useTimeout, + useReferenceManyFieldController, +} from "ra-core"; + +export const ReferenceManyCount = ( + props: ReferenceManyCountBaseProps & { + render: (total: number | undefined) => React.ReactNode; + }, +) => { + const { loading, error, offline, render, timeout = 1000, ...rest } = props; + const oneSecondHasPassed = useTimeout(timeout); + + const { + isPaused, + isPending, + error: fetchError, + total, + } = useReferenceManyFieldController({ + ...rest, + page: 1, + perPage: 1, + }); + const shouldRenderLoading = + isPending && !isPaused && loading !== undefined && loading !== false; + const shouldRenderOffline = + isPending && isPaused && offline !== undefined && offline !== false; + const shouldRenderError = + !isPending && fetchError && error !== undefined && error !== false; + + return ( + <> + {shouldRenderLoading + ? oneSecondHasPassed + ? loading + : null + : shouldRenderOffline + ? offline + : shouldRenderError + ? error + : render(total)} + + ); +}; diff --git a/src/components/atomic-crm/root/CRM.tsx b/src/components/atomic-crm/root/CRM.tsx index 6a87ee16..ef70a318 100644 --- a/src/components/atomic-crm/root/CRM.tsx +++ b/src/components/atomic-crm/root/CRM.tsx @@ -39,6 +39,7 @@ import { } from "./defaultConfiguration"; import { i18nProvider } from "./i18nProvider"; import { StartPage } from "../login/StartPage.tsx"; +import { useIsMobile } from "@/hooks/use-mobile.ts"; export type CRMProps = { dataProvider?: DataProvider; @@ -102,6 +103,7 @@ export const CRM = ({ disableTelemetry, ...rest }: CRMProps) => { + const isMobile = useIsMobile(); useEffect(() => { if ( disableTelemetry || @@ -153,7 +155,13 @@ export const CRM = ({ } /> - + From 993f358b68584e396d666adf75cd15a1cf67503c Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:57:51 +0100 Subject: [PATCH 13/34] Update registry --- registry.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/registry.json b/registry.json index f356b4d1..f56fec91 100644 --- a/registry.json +++ b/registry.json @@ -338,6 +338,10 @@ "path": "src/components/atomic-crm/misc/RelativeDate.tsx", "type": "registry:component" }, + { + "path": "src/components/atomic-crm/misc/ReferenceManyCount.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/misc/ImageEditorField.tsx", "type": "registry:component" From f346bd866f0562780bb9a8ca14552275abef8cb1 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:36:49 +0100 Subject: [PATCH 14/34] Introduce infinite loading for contact list --- .../atomic-crm/contacts/ContactList.tsx | 53 ++--- .../atomic-crm/misc/InfinitePagination.tsx | 117 +++++++++++ src/components/atomic-crm/misc/SearchForm.tsx | 0 src/components/ui/item.tsx | 193 ++++++++++++++++++ src/components/ui/separator.tsx | 2 +- 5 files changed, 339 insertions(+), 26 deletions(-) create mode 100644 src/components/atomic-crm/misc/InfinitePagination.tsx create mode 100644 src/components/atomic-crm/misc/SearchForm.tsx create mode 100644 src/components/ui/item.tsx diff --git a/src/components/atomic-crm/contacts/ContactList.tsx b/src/components/atomic-crm/contacts/ContactList.tsx index 4d648afb..106d33d7 100644 --- a/src/components/atomic-crm/contacts/ContactList.tsx +++ b/src/components/atomic-crm/contacts/ContactList.tsx @@ -8,7 +8,7 @@ import { import { BulkActionsToolbar } from "@/components/admin/bulk-actions-toolbar"; import { CreateButton } from "@/components/admin/create-button"; import { ExportButton } from "@/components/admin/export-button"; -import { List } from "@/components/admin/list"; +import { List, ListView } from "@/components/admin/list"; import { SortButton } from "@/components/admin/sort-button"; import { Card } from "@/components/ui/card"; @@ -19,10 +19,11 @@ import { ContactListContent } from "./ContactListContent"; import { ContactListFilter } from "./ContactListFilter"; import { TopToolbar } from "../layout/TopToolbar"; import { useIsMobile } from "@/hooks/use-mobile"; -import { SearchInput } from "@/components/admin"; import { Button } from "@/components/ui/button"; import { Plus } from "lucide-react"; import { Link } from "react-router"; +import { InfiniteListBase } from "ra-core"; +import { InfinitePagination } from "../misc/InfinitePagination"; export const ContactList = () => { const { identity } = useGetIdentity(); @@ -30,6 +31,31 @@ export const ContactList = () => { if (!identity) return null; + if (isMobile) { + return ( + + } actions={false}> + + + + + ); + } + return ( { perPage={25} sort={{ field: "last_seen", order: "DESC" }} exporter={exporter} - filters={ - isMobile - ? [ - , - ] - : undefined - } > - ); }; diff --git a/src/components/atomic-crm/misc/InfinitePagination.tsx b/src/components/atomic-crm/misc/InfinitePagination.tsx new file mode 100644 index 00000000..77c3fe77 --- /dev/null +++ b/src/components/atomic-crm/misc/InfinitePagination.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; +import { useEffect, useRef } from "react"; +import { + useInfinitePaginationContext, + useListContext, + useEvent, +} from "ra-core"; +import { Item, ItemContent, ItemMedia, ItemTitle } from "@/components/ui/item"; +import { Spinner } from "@/components/admin/spinner"; + +/** + * A pagination component that loads more results when the user scrolls to the bottom of the list. + * + * Used as the default pagination component in the component. + * + * @example + * import { InfiniteList, InfinitePagination, Datagrid, TextField } from 'react-admin'; + * + * const PostList = () => ( + * }> + * + * + * + * + * + * ); + */ +export const InfinitePagination = ({ + offline = null, + options = defaultOptions, +}: InfinitePaginationProps) => { + const { isPaused, isPending } = useListContext(); + const { fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfinitePaginationContext(); + + if (!fetchNextPage) { + throw new Error( + "InfinitePagination must be used inside an InfinitePaginationContext, usually created by . You cannot use it as child of a component.", + ); + } + + const [hasRequestedNextPage, setHasRequestedNextPage] = React.useState(false); + const observerElem = useRef(null); + const handleObserver = useEvent<[IntersectionObserverEntry[]], void>( + (entries) => { + const [target] = entries; + if (target.isIntersecting && hasNextPage && !isFetchingNextPage) { + setHasRequestedNextPage(true); + fetchNextPage(); + } + }, + ); + + useEffect(() => { + // Whenever the query is unpaused, reset the requested next page state + if (!isPaused) { + setHasRequestedNextPage(false); + } + }, [isPaused]); + + useEffect(() => { + const element = observerElem.current; + if (!element) return; + const observer = new IntersectionObserver(handleObserver, options); + observer.observe(element); + return () => observer.unobserve(element); + }, [ + fetchNextPage, + hasNextPage, + handleObserver, + options, + isPending, + isFetchingNextPage, + ]); + + if (isPending) return null; + + const showOffline = + isPaused && + hasNextPage && + hasRequestedNextPage && + offline !== false && + offline !== undefined; + + return ( +
    + {showOffline ? ( + offline + ) : isFetchingNextPage && hasNextPage ? ( + + + + + + Loading... + + + ) : ( + + + + + +   + + + )} +
    + ); +}; + +const defaultOptions = { threshold: 0 }; + +export interface InfinitePaginationProps { + offline?: React.ReactNode; + options?: IntersectionObserverInit; +} diff --git a/src/components/atomic-crm/misc/SearchForm.tsx b/src/components/atomic-crm/misc/SearchForm.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/ui/item.tsx b/src/components/ui/item.tsx new file mode 100644 index 00000000..d97de21e --- /dev/null +++ b/src/components/ui/item.tsx @@ -0,0 +1,193 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" + +function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function ItemSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const itemVariants = cva( + "group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", + { + variants: { + variant: { + default: "bg-transparent", + outline: "border-border", + muted: "bg-muted/50", + }, + size: { + default: "p-4 gap-4 ", + sm: "py-3 px-4 gap-2.5", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Item({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"div"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "div" + return ( + + ) +} + +const itemMediaVariants = cva( + "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5", + { + variants: { + variant: { + default: "bg-transparent", + icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4", + image: + "size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function ItemMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
    + ) +} + +function ItemContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +

    a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...props} + /> + ) +} + +function ItemActions({ className, ...props }: React.ComponentProps<"div">) { + return ( +

    + ) +} + +function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +export { + Item, + ItemMedia, + ItemContent, + ItemActions, + ItemGroup, + ItemSeparator, + ItemTitle, + ItemDescription, + ItemHeader, + ItemFooter, +} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx index 3cf4f89b..bb3ad74c 100644 --- a/src/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -11,7 +11,7 @@ function Separator({ }: React.ComponentProps) { return ( Date: Mon, 1 Dec 2025 10:02:55 +0100 Subject: [PATCH 15/34] Fix active menu in mobile navigation --- registry.json | 8 ++++++++ src/components/atomic-crm/layout/Header.tsx | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/registry.json b/registry.json index f56fec91..057e0848 100644 --- a/registry.json +++ b/registry.json @@ -334,6 +334,10 @@ "path": "src/components/atomic-crm/misc/Status.tsx", "type": "registry:component" }, + { + "path": "src/components/atomic-crm/misc/SearchForm.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/misc/RelativeDate.tsx", "type": "registry:component" @@ -342,6 +346,10 @@ "path": "src/components/atomic-crm/misc/ReferenceManyCount.tsx", "type": "registry:component" }, + { + "path": "src/components/atomic-crm/misc/InfinitePagination.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/misc/ImageEditorField.tsx", "type": "registry:component" diff --git a/src/components/atomic-crm/layout/Header.tsx b/src/components/atomic-crm/layout/Header.tsx index ea741be8..94d10f75 100644 --- a/src/components/atomic-crm/layout/Header.tsx +++ b/src/components/atomic-crm/layout/Header.tsx @@ -59,6 +59,7 @@ const Header = () => { const MobileHeader = ({ currentPath }: { currentPath: string | boolean }) => { const { title } = useConfigurationContext(); const [open, setOpen] = useState(false); + console.log({ currentPath }); return (
    @@ -77,18 +78,20 @@ const MobileHeader = ({ currentPath }: { currentPath: string | boolean }) => { element?.focus()} onClick={() => setOpen(false)} + ref={(element) => element?.focus()} /> { { ) => { + console.log({ props }); return ( Date: Mon, 1 Dec 2025 10:25:38 +0100 Subject: [PATCH 16/34] Improve company list for mobile --- .../atomic-crm/companies/CompanyList.tsx | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index ff5ae232..a1f5725c 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -1,7 +1,12 @@ -import { RecordsIterator, useGetIdentity, useListContext } from "ra-core"; +import { + InfiniteListBase, + RecordsIterator, + useGetIdentity, + useListContext, +} from "ra-core"; import { CreateButton } from "@/components/admin/create-button"; import { ExportButton } from "@/components/admin/export-button"; -import { List } from "@/components/admin/list"; +import { List, ListView } from "@/components/admin/list"; import { ListPagination } from "@/components/admin/list-pagination"; import { SortButton } from "@/components/admin/sort-button"; @@ -17,11 +22,33 @@ import { Plus } from "lucide-react"; import { CompanyAvatar } from "./CompanyAvatar"; import { Card } from "@/components/ui/card"; import { ReferenceManyCount } from "../misc/ReferenceManyCount"; +import { InfinitePagination } from "../misc/InfinitePagination"; export const CompanyList = () => { const { identity } = useGetIdentity(); const isMobile = useIsMobile(); if (!identity) return null; + + if (isMobile) { + return ( + + } actions={false}> + + + + + ); + } return ( { sort={{ field: "name", order: "ASC" }} actions={} pagination={} - filters={ - isMobile - ? [ - , - ] - : undefined - } > - ); }; From c6373f2282d312d82fe4be30cd5eb916230f7519 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:27:57 +0100 Subject: [PATCH 17/34] Cleanup --- src/components/atomic-crm/companies/CompanyList.tsx | 12 ++++++------ src/components/atomic-crm/contacts/ContactList.tsx | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index a1f5725c..c80555d6 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -9,18 +9,18 @@ import { ExportButton } from "@/components/admin/export-button"; import { List, ListView } from "@/components/admin/list"; import { ListPagination } from "@/components/admin/list-pagination"; import { SortButton } from "@/components/admin/sort-button"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { TextField } from "@/components/admin"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Link } from "react-router"; +import { Plus } from "lucide-react"; import { TopToolbar } from "../layout/TopToolbar"; import { CompanyEmpty } from "./CompanyEmpty"; import { CompanyListFilter } from "./CompanyListFilter"; import { ImageList } from "./GridList"; -import { useIsMobile } from "@/hooks/use-mobile"; -import { SearchInput, TextField } from "@/components/admin"; -import { Button } from "@/components/ui/button"; -import { Link } from "react-router"; -import { Plus } from "lucide-react"; import { CompanyAvatar } from "./CompanyAvatar"; -import { Card } from "@/components/ui/card"; import { ReferenceManyCount } from "../misc/ReferenceManyCount"; import { InfinitePagination } from "../misc/InfinitePagination"; diff --git a/src/components/atomic-crm/contacts/ContactList.tsx b/src/components/atomic-crm/contacts/ContactList.tsx index 106d33d7..1ab36259 100644 --- a/src/components/atomic-crm/contacts/ContactList.tsx +++ b/src/components/atomic-crm/contacts/ContactList.tsx @@ -1,6 +1,7 @@ import jsonExport from "jsonexport/dist"; import { downloadCSV, + InfiniteListBase, useGetIdentity, useListContext, type Exporter, @@ -11,6 +12,10 @@ import { ExportButton } from "@/components/admin/export-button"; import { List, ListView } from "@/components/admin/list"; import { SortButton } from "@/components/admin/sort-button"; import { Card } from "@/components/ui/card"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; +import { Link } from "react-router"; import type { Company, Contact, Sale, Tag } from "../types"; import { ContactEmpty } from "./ContactEmpty"; @@ -18,11 +23,6 @@ import { ContactImportButton } from "./ContactImportButton"; import { ContactListContent } from "./ContactListContent"; import { ContactListFilter } from "./ContactListFilter"; import { TopToolbar } from "../layout/TopToolbar"; -import { useIsMobile } from "@/hooks/use-mobile"; -import { Button } from "@/components/ui/button"; -import { Plus } from "lucide-react"; -import { Link } from "react-router"; -import { InfiniteListBase } from "ra-core"; import { InfinitePagination } from "../misc/InfinitePagination"; export const ContactList = () => { From c51327ca5f7f8256250d2d590ea7cfb3e2ff9fa4 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:10:00 +0100 Subject: [PATCH 18/34] Cleanup --- src/components/atomic-crm/layout/Header.tsx | 2 -- src/components/atomic-crm/misc/InfinitePagination.tsx | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/components/atomic-crm/layout/Header.tsx b/src/components/atomic-crm/layout/Header.tsx index 94d10f75..59c6f959 100644 --- a/src/components/atomic-crm/layout/Header.tsx +++ b/src/components/atomic-crm/layout/Header.tsx @@ -59,7 +59,6 @@ const Header = () => { const MobileHeader = ({ currentPath }: { currentPath: string | boolean }) => { const { title } = useConfigurationContext(); const [open, setOpen] = useState(false); - console.log({ currentPath }); return (
    @@ -212,7 +211,6 @@ const NavigationLink = ({ isActive: boolean; } & LinkProps & RefAttributes) => { - console.log({ props }); return ( ) : ( - - -   From 8a470efbe088951551d1e9aea1177f6d4f013d8e Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:10:37 +0100 Subject: [PATCH 19/34] Improve filtering on mobile (contacts and companies) --- registry.json | 4 + .../atomic-crm/companies/CompanyList.tsx | 1 + .../companies/CompanyListFilter.tsx | 12 +-- .../atomic-crm/contacts/ContactList.tsx | 1 + .../atomic-crm/contacts/ContactListFilter.tsx | 12 +-- .../atomic-crm/misc/ResponsiveFilters.tsx | 80 +++++++++++++++++++ 6 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 src/components/atomic-crm/misc/ResponsiveFilters.tsx diff --git a/registry.json b/registry.json index 057e0848..95659461 100644 --- a/registry.json +++ b/registry.json @@ -338,6 +338,10 @@ "path": "src/components/atomic-crm/misc/SearchForm.tsx", "type": "registry:component" }, + { + "path": "src/components/atomic-crm/misc/ResponsiveFilters.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/misc/RelativeDate.tsx", "type": "registry:component" diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index c80555d6..9eb35590 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -33,6 +33,7 @@ export const CompanyList = () => { return ( } actions={false}> +

    -
    {children}
    +
    + {children} +
    ); From 16f47dc0d820cea6b3f548f9f335f2b0ed792de8 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:40:39 +0100 Subject: [PATCH 21/34] Update lock file --- package-lock.json | 57 +++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5f95cde9..7423fff7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -170,7 +170,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -771,7 +770,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1794,6 +1792,7 @@ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1845,7 +1844,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -2082,7 +2080,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -3842,7 +3839,8 @@ "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@supabase/auth-js": { "version": "2.71.1", @@ -4414,6 +4412,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4434,6 +4433,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -4443,7 +4443,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@testing-library/jest-dom": { "version": "6.8.0", @@ -4471,6 +4472,7 @@ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12", "npm": ">=6" @@ -4512,7 +4514,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4707,7 +4710,6 @@ "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4718,7 +4720,6 @@ "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -4791,7 +4792,6 @@ "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", @@ -5173,7 +5173,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5353,6 +5352,7 @@ "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "open": "^8.0.4" }, @@ -5425,7 +5425,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -5445,7 +5444,8 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/bytes": { "version": "3.1.2", @@ -6354,7 +6354,6 @@ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -6396,6 +6395,7 @@ "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.3.4" }, @@ -6438,7 +6438,6 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8322,6 +8321,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9292,7 +9292,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9309,6 +9308,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9324,6 +9324,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9336,7 +9337,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/pretty-ms": { "version": "9.3.0", @@ -9478,7 +9480,6 @@ "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-5.13.1.tgz", "integrity": "sha512-7pjERvKNtFaeTg2OvZS2uKUQt3G4OnZFfZLKrGpc4dzLzWj7CRwG+7YCcc/Q69yw2fhA7eJCBJtTfJl+EEncCw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/react-query": "^5.83.0", "date-fns": "^3.6.0", @@ -9615,7 +9616,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9637,7 +9637,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -9679,7 +9678,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9782,7 +9780,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -9884,8 +9881,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/require-directory": { "version": "2.1.1", @@ -9946,7 +9942,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz", "integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10246,7 +10241,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -10428,6 +10422,7 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10485,6 +10480,7 @@ "integrity": "sha512-Sm+qP3iGb/QKx/jTYdfE0mIeTmA2HF+5k9fD70S9oOJq3F9UdW8MLgs+5PE+E/xAfDjZU4OWAKEOyA6EYIvQHg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -10521,6 +10517,7 @@ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -10783,6 +10780,7 @@ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -10801,7 +10799,8 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/tiny-invariant": { "version": "1.3.3", @@ -10861,7 +10860,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11085,7 +11083,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11300,7 +11297,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz", "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -11431,7 +11427,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, From ced4abd1ebf4b666d5d189c35709fde1e0acd8a8 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:45:05 +0100 Subject: [PATCH 22/34] Formatting --- .../atomic-crm/activity/ActivityLogDealCreated.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx index 2e92108d..1d19ae03 100644 --- a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx +++ b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx @@ -27,11 +27,7 @@ export function ActivityLogDealCreated({  added deal {deal.name} - {context !== "company" && ( - <> -  to company {activity.company_id} - - )} + {context !== "company" && <> to company {activity.company_id}} ); } From b800b39cb8f1e89eb19581af075b52504d55e003 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:04:23 +0100 Subject: [PATCH 23/34] Improve deal edition actions --- .../atomic-crm/deals/ArchiveButton.tsx | 56 ++++++++++ src/components/atomic-crm/deals/DealEdit.tsx | 42 ++++++- src/components/atomic-crm/deals/DealShow.tsx | 103 ++---------------- .../atomic-crm/deals/UnarchiveButton.tsx | 60 ++++++++++ 4 files changed, 167 insertions(+), 94 deletions(-) create mode 100644 src/components/atomic-crm/deals/ArchiveButton.tsx create mode 100644 src/components/atomic-crm/deals/UnarchiveButton.tsx diff --git a/src/components/atomic-crm/deals/ArchiveButton.tsx b/src/components/atomic-crm/deals/ArchiveButton.tsx new file mode 100644 index 00000000..1b03c8bb --- /dev/null +++ b/src/components/atomic-crm/deals/ArchiveButton.tsx @@ -0,0 +1,56 @@ +import type { ComponentProps, MouseEvent } from "react"; +import { Archive } from "lucide-react"; +import { + useNotify, + useRecordContext, + useRedirect, + useRefresh, + useUpdate, +} from "ra-core"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import type { Deal } from "../types"; + +export const ArchiveButton = ({ + record: _recordProp, + className, + onClick, + ...props +}: { record?: Deal } & ComponentProps) => { + const redirect = useRedirect(); + const notify = useNotify(); + const refresh = useRefresh(); + const record = useRecordContext(props); + const [update] = useUpdate(undefined, undefined, { + onSuccess: () => { + redirect("list", "deals"); + notify("Deal archived", { type: "info", undoable: false }); + refresh(); + }, + onError: () => { + notify("Error: deal not archived", { type: "error" }); + }, + }); + const handleClick = (event: MouseEvent) => { + update("deals", { + id: record?.id, + data: { archived_at: new Date().toISOString() }, + previousData: record, + }); + if (onClick) onClick(event); + }; + + return ( + + ); +}; diff --git a/src/components/atomic-crm/deals/DealEdit.tsx b/src/components/atomic-crm/deals/DealEdit.tsx index 812df221..74ace94c 100644 --- a/src/components/atomic-crm/deals/DealEdit.tsx +++ b/src/components/atomic-crm/deals/DealEdit.tsx @@ -16,6 +16,16 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { CompanyAvatar } from "../companies/CompanyAvatar"; import type { Deal } from "../types"; import { DealInputs } from "./DealInputs"; +import { ArchiveButton } from "./ArchiveButton"; +import { UnarchiveButton } from "./UnarchiveButton"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { MoreVertical } from "lucide-react"; export const DealEdit = ({ open, id }: { open: boolean; id?: string }) => { const redirect = useRedirect(); @@ -81,6 +91,8 @@ export const DealEditPage = () => { function EditTitle() { const deal = useRecordContext(); + const isMobile = useIsMobile(); + if (!deal) { return null; } @@ -92,6 +104,30 @@ function EditTitle() {

    Edit {deal.name} deal

    + {isMobile ? ( + + + + + + + + + {deal.archived_at ? ( + + + + ) : ( + + + + )} + + + ) : null}
    ); - -const ArchiveButton = ({ record }: { record: Deal }) => { - const [update] = useUpdate(); - const redirect = useRedirect(); - const notify = useNotify(); - const refresh = useRefresh(); - const handleClick = () => { - update( - "deals", - { - id: record.id, - data: { archived_at: new Date().toISOString() }, - previousData: record, - }, - { - onSuccess: () => { - redirect("list", "deals"); - notify("Deal archived", { type: "info", undoable: false }); - refresh(); - }, - onError: () => { - notify("Error: deal not archived", { type: "error" }); - }, - }, - ); - }; - - return ( - - ); -}; - -const UnarchiveButton = ({ record }: { record: Deal }) => { - const dataProvider = useDataProvider(); - const redirect = useRedirect(); - const notify = useNotify(); - const refresh = useRefresh(); - - const { mutate } = useMutation({ - mutationFn: () => dataProvider.unarchiveDeal(record), - onSuccess: () => { - redirect("list", "deals"); - notify("Deal unarchived", { - type: "info", - undoable: false, - }); - refresh(); - }, - onError: () => { - notify("Error: deal not unarchived", { type: "error" }); - }, - }); - - const handleClick = () => { - mutate(); - }; - - return ( - - ); -}; diff --git a/src/components/atomic-crm/deals/UnarchiveButton.tsx b/src/components/atomic-crm/deals/UnarchiveButton.tsx new file mode 100644 index 00000000..963f0a95 --- /dev/null +++ b/src/components/atomic-crm/deals/UnarchiveButton.tsx @@ -0,0 +1,60 @@ +import type { ComponentProps, MouseEvent } from "react"; +import { ArchiveRestore } from "lucide-react"; +import { useMutation } from "@tanstack/react-query"; +import { + useDataProvider, + useNotify, + useRecordContext, + useRedirect, + useRefresh, +} from "ra-core"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import type { Deal } from "../types"; + +export const UnarchiveButton = ({ + record: _recordProp, + className, + onClick, + ...props +}: { record?: Deal } & ComponentProps) => { + const dataProvider = useDataProvider(); + const redirect = useRedirect(); + const notify = useNotify(); + const refresh = useRefresh(); + const record = useRecordContext(props); + + const { mutate } = useMutation({ + mutationFn: () => dataProvider.unarchiveDeal(record), + onSuccess: () => { + redirect("list", "deals"); + notify("Deal unarchived", { + type: "info", + undoable: false, + }); + refresh(); + }, + onError: () => { + notify("Error: deal not unarchived", { type: "error" }); + }, + }); + + const handleClick = (event: MouseEvent) => { + mutate(); + if (onClick) onClick(event); + }; + + return ( + + ); +}; From 68294e33771f881e0f4f30097f7b223169172e69 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:13:31 +0100 Subject: [PATCH 24/34] Add contact status on mobile contact list --- registry.json | 8 ++++++++ src/components/atomic-crm/contacts/ContactListContent.tsx | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/registry.json b/registry.json index 5b8d21c2..d8236ba9 100644 --- a/registry.json +++ b/registry.json @@ -416,6 +416,10 @@ "path": "src/components/atomic-crm/deals/deal.ts", "type": "registry:component" }, + { + "path": "src/components/atomic-crm/deals/UnarchiveButton.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/deals/OnlyMineInput.tsx", "type": "registry:component" @@ -464,6 +468,10 @@ "path": "src/components/atomic-crm/deals/ContactList.tsx", "type": "registry:component" }, + { + "path": "src/components/atomic-crm/deals/ArchiveButton.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/dashboard/Welcome.tsx", "type": "registry:component" diff --git a/src/components/atomic-crm/contacts/ContactListContent.tsx b/src/components/atomic-crm/contacts/ContactListContent.tsx index 59987803..237ca9f3 100644 --- a/src/components/atomic-crm/contacts/ContactListContent.tsx +++ b/src/components/atomic-crm/contacts/ContactListContent.tsx @@ -60,8 +60,11 @@ export const ContactListContent = () => {
    -
    - {`${contact.first_name} ${contact.last_name ?? ""}`} +
    +
    + {`${contact.first_name} ${contact.last_name ?? ""}`} +
    + {isSmall ? : null}
    From 988282d8fa97c69bf43c7cd65a0f8498b911677f Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 2 Dec 2025 09:38:07 +0100 Subject: [PATCH 25/34] Improve company list --- .../atomic-crm/companies/CompanyList.tsx | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index 9eb35590..9f01630b 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -93,21 +93,25 @@ const CompanyMobileList = () => ( >
    - -
    - -
    - ( - <> - {total} {total === 1 ? "deal" : "deals"} - - )} - /> -
    +
    + + ( + + {total} {total === 1 ? "deal" : "deals"} + + )} + />
    +
    From aa422445230f7a094073c3b1d8c3a87b18794b0f Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:26:28 +0100 Subject: [PATCH 26/34] Remove create task entry in dashboard menu --- src/components/atomic-crm/dashboard/Dashboard.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/atomic-crm/dashboard/Dashboard.tsx b/src/components/atomic-crm/dashboard/Dashboard.tsx index cc6b175c..38afd6de 100644 --- a/src/components/atomic-crm/dashboard/Dashboard.tsx +++ b/src/components/atomic-crm/dashboard/Dashboard.tsx @@ -91,15 +91,6 @@ export const Dashboard = () => { Deal - - - New - Task - - Date: Thu, 4 Dec 2025 14:59:18 +0100 Subject: [PATCH 27/34] Remove card wrapper in mobile views --- src/components/atomic-crm/companies/CompanyCreate.tsx | 5 +++-- src/components/atomic-crm/contacts/ContactCreate.tsx | 4 ++-- src/components/atomic-crm/contacts/ContactEdit.tsx | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/atomic-crm/companies/CompanyCreate.tsx b/src/components/atomic-crm/companies/CompanyCreate.tsx index b2b35084..5aa05f88 100644 --- a/src/components/atomic-crm/companies/CompanyCreate.tsx +++ b/src/components/atomic-crm/companies/CompanyCreate.tsx @@ -8,6 +8,7 @@ import { CompanyInputs } from "./CompanyInputs"; export const CompanyCreate = () => { const { identity } = useGetIdentity(); + return ( {
    - - + +
    diff --git a/src/components/atomic-crm/contacts/ContactCreate.tsx b/src/components/atomic-crm/contacts/ContactCreate.tsx index c26031f8..f3c6b3b2 100644 --- a/src/components/atomic-crm/contacts/ContactCreate.tsx +++ b/src/components/atomic-crm/contacts/ContactCreate.tsx @@ -20,8 +20,8 @@ export const ContactCreate = () => {
    - - + + diff --git a/src/components/atomic-crm/contacts/ContactEdit.tsx b/src/components/atomic-crm/contacts/ContactEdit.tsx index fe5e4d0d..9630d352 100644 --- a/src/components/atomic-crm/contacts/ContactEdit.tsx +++ b/src/components/atomic-crm/contacts/ContactEdit.tsx @@ -18,8 +18,8 @@ const ContactEditContent = () => { return (
    - - + + From 2e27359d9d50bc53e681d6a527e012702258fddd Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:06:59 +0100 Subject: [PATCH 28/34] Add an apply button to the filter drawer --- src/components/atomic-crm/misc/ResponsiveFilters.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/atomic-crm/misc/ResponsiveFilters.tsx b/src/components/atomic-crm/misc/ResponsiveFilters.tsx index 5d7deb9c..55725cea 100644 --- a/src/components/atomic-crm/misc/ResponsiveFilters.tsx +++ b/src/components/atomic-crm/misc/ResponsiveFilters.tsx @@ -3,13 +3,16 @@ import { FilterLiveForm } from "ra-core"; import { SearchInput, type SearchInputProps } from "@/components/admin"; import { Drawer, + DrawerClose, DrawerContent, + DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; const TIMEOUT_DURATION_IN_SECONDS = 0.5; @@ -59,6 +62,11 @@ export const ResponsiveFilters = ({ Filters {children} + + + + + ); From 2f08cb27cadf2ce1d5b4b6d27273efdf96465886 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:23:30 +0100 Subject: [PATCH 29/34] Make filter toggle buttons bigger on mobile --- .../atomic-crm/companies/CompanyListFilter.tsx | 3 +++ .../atomic-crm/contacts/ContactListFilter.tsx | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/components/atomic-crm/companies/CompanyListFilter.tsx b/src/components/atomic-crm/companies/CompanyListFilter.tsx index a3e5fe06..2af69fd7 100644 --- a/src/components/atomic-crm/companies/CompanyListFilter.tsx +++ b/src/components/atomic-crm/companies/CompanyListFilter.tsx @@ -24,6 +24,7 @@ export const CompanyListFilter = () => { className="w-auto md:w-full justify-between" label={isMobile ? size.shortName : size.name} value={{ size: size.id }} + size={isMobile ? "lg" : undefined} /> ))} @@ -34,6 +35,7 @@ export const CompanyListFilter = () => { className="w-auto md:w-full justify-between" label={sector.name} value={{ sector: sector.id }} + size={isMobile ? "lg" : undefined} /> ))} @@ -46,6 +48,7 @@ export const CompanyListFilter = () => { className="w-full justify-between" label={"Me"} value={{ sales_id: identity?.id }} + size={isMobile ? "lg" : undefined} /> diff --git a/src/components/atomic-crm/contacts/ContactListFilter.tsx b/src/components/atomic-crm/contacts/ContactListFilter.tsx index 891a401a..c1c38445 100644 --- a/src/components/atomic-crm/contacts/ContactListFilter.tsx +++ b/src/components/atomic-crm/contacts/ContactListFilter.tsx @@ -8,9 +8,11 @@ import { FilterCategory } from "../filters/FilterCategory"; import { Status } from "../misc/Status"; import { useConfigurationContext } from "../root/ConfigurationContext"; import { ResponsiveFilters } from "../misc/ResponsiveFilters"; +import { useIsMobile } from "@/hooks/use-mobile"; export const ContactListFilter = () => { const { noteStatuses } = useConfigurationContext(); + const isMobile = useIsMobile(); const { identity } = useGetIdentity(); const { data } = useGetList("tags", { pagination: { page: 1, perPage: 10 }, @@ -27,6 +29,7 @@ export const ContactListFilter = () => { "last_seen@gte": endOfYesterday().toISOString(), "last_seen@lte": undefined, }} + size={isMobile ? "lg" : undefined} /> { "last_seen@gte": startOfWeek(new Date()).toISOString(), "last_seen@lte": undefined, }} + size={isMobile ? "lg" : undefined} /> { "last_seen@gte": undefined, "last_seen@lte": startOfWeek(new Date()).toISOString(), }} + size={isMobile ? "lg" : undefined} /> { "last_seen@gte": undefined, "last_seen@lte": startOfMonth(new Date()).toISOString(), }} + size={isMobile ? "lg" : undefined} /> { 1, ).toISOString(), }} + size={isMobile ? "lg" : undefined} /> @@ -76,6 +83,7 @@ export const ContactListFilter = () => { } value={{ status: status.value }} + size={isMobile ? "lg" : undefined} /> ))} @@ -98,6 +106,7 @@ export const ContactListFilter = () => { } value={{ "tags@cs": `{${record.id}}` }} + size={isMobile ? "lg" : undefined} /> ))} @@ -107,6 +116,7 @@ export const ContactListFilter = () => { className="w-full justify-between" label={"With pending tasks"} value={{ "nb_tasks@gt": 0 }} + size={isMobile ? "lg" : undefined} /> @@ -115,6 +125,7 @@ export const ContactListFilter = () => { className="w-full justify-between" label={"Me"} value={{ sales_id: identity?.id }} + size={isMobile ? "lg" : undefined} /> From 055d208913c7ba0ea0e8757b6fa152d4d89a9bbc Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:48:24 +0100 Subject: [PATCH 30/34] Improve Activity log design --- src/components/atomic-crm/activity/ActivityLogHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/atomic-crm/activity/ActivityLogHeader.tsx b/src/components/atomic-crm/activity/ActivityLogHeader.tsx index 9e82fbe3..d68a5eea 100644 --- a/src/components/atomic-crm/activity/ActivityLogHeader.tsx +++ b/src/components/atomic-crm/activity/ActivityLogHeader.tsx @@ -24,7 +24,7 @@ export const ActivityLogHeader = ({ {children} {isMobile ? ( <> -  at +  - ) : null}
    From a9857ad49a4e8661c0ad89e3b9061b3e8cc88d26 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:55:31 +0100 Subject: [PATCH 31/34] Improve contact notes --- .../atomic-crm/contacts/ContactShow.tsx | 19 +- src/components/atomic-crm/notes/Note.tsx | 167 +++++++++++------- 2 files changed, 118 insertions(+), 68 deletions(-) diff --git a/src/components/atomic-crm/contacts/ContactShow.tsx b/src/components/atomic-crm/contacts/ContactShow.tsx index fafbd0ab..23a37b84 100644 --- a/src/components/atomic-crm/contacts/ContactShow.tsx +++ b/src/components/atomic-crm/contacts/ContactShow.tsx @@ -33,7 +33,7 @@ const ContactShowContent = () => {
    - +
    @@ -85,7 +85,14 @@ const ContactShowContent = () => { )}
    {isMobile ? ( - +
    + +
    + ) : null} + {isMobile ? ( + { target="contact_id" reference="contactNotes" sort={{ field: "date", order: "DESC" }} - empty={ - - } + empty={} > diff --git a/src/components/atomic-crm/notes/Note.tsx b/src/components/atomic-crm/notes/Note.tsx index 7005aa1b..c2bdacf6 100644 --- a/src/components/atomic-crm/notes/Note.tsx +++ b/src/components/atomic-crm/notes/Note.tsx @@ -1,4 +1,4 @@ -import { CircleX, Edit, Save, Trash2 } from "lucide-react"; +import { CircleX, Edit, MoreVertical, Save, Trash2 } from "lucide-react"; import { Form, useDelete, @@ -17,6 +17,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useIsMobile } from "@/hooks/use-mobile"; import { CompanyAvatar } from "../companies/CompanyAvatar"; import { Avatar } from "../contacts/Avatar"; @@ -26,6 +27,12 @@ import { SaleName } from "../sales/SaleName"; import type { ContactNote, DealNote } from "../types"; import { NoteAttachments } from "./NoteAttachments"; import { NoteInputs } from "./NoteInputs"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; export const Note = ({ showStatus, @@ -39,6 +46,7 @@ export const Note = ({ const [isEditing, setEditing] = useState(false); const resource = useResourceContext(); const notify = useNotify(); + const isMobile = useIsMobile(); const [update, { isPending }] = useUpdate(); @@ -84,67 +92,108 @@ export const Note = ({ onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} > -
    - {resource === "contactNote" ? ( - - ) : ( - - - - )} -
    - - } /> - {" "} - added a note{" "} - {showStatus && note.status && ( - +
    +
    +
    + {resource === "contactNote" ? ( + + ) : ( + + + + )} +
    + + } /> + {" "} + added a note{" "} + {showStatus && note.status && ( + + )} +
    +
    + {isMobile ? ( + + + Actions + + + + + + + + + + + + ) : ( + + + + + + + +

    Edit note

    +
    +
    +
    + + + + + + +

    Delete note

    +
    +
    +
    +
    )}
    - - - - - - - -

    Edit note

    -
    -
    -
    - - - - - - -

    Delete note

    -
    -
    -
    -
    - +
    From 17b25ff382e53d7150f4f28ea75e6581487b86fb Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:00:03 +0100 Subject: [PATCH 32/34] Improve company about section --- src/components/atomic-crm/companies/CompanyShow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/atomic-crm/companies/CompanyShow.tsx b/src/components/atomic-crm/companies/CompanyShow.tsx index bd572552..8a46683a 100644 --- a/src/components/atomic-crm/companies/CompanyShow.tsx +++ b/src/components/atomic-crm/companies/CompanyShow.tsx @@ -98,7 +98,7 @@ const CompanyShowContent = () => { About ) : null} - + @@ -139,7 +139,7 @@ const CompanyShowContent = () => { ) : null} - + From 74ccebeea4ce6d1b02c2a37b3c06a6719b238f58 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:01:10 +0100 Subject: [PATCH 33/34] Make deal list content scrollable --- src/components/atomic-crm/deals/DealList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/atomic-crm/deals/DealList.tsx b/src/components/atomic-crm/deals/DealList.tsx index f1ca7adb..85a2e021 100644 --- a/src/components/atomic-crm/deals/DealList.tsx +++ b/src/components/atomic-crm/deals/DealList.tsx @@ -98,7 +98,7 @@ const DealLayout = () => { ); return ( -
    +
    From 090293a11611d6a596a0c760b04f55b3e6e7089c Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:07:28 +0100 Subject: [PATCH 34/34] Fix deal forms button placement --- .../atomic-crm/deals/DealCreate.tsx | 34 +++++++++++-------- src/components/atomic-crm/deals/DealEdit.tsx | 6 ++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/components/atomic-crm/deals/DealCreate.tsx b/src/components/atomic-crm/deals/DealCreate.tsx index a4ca50ab..31f0edd9 100644 --- a/src/components/atomic-crm/deals/DealCreate.tsx +++ b/src/components/atomic-crm/deals/DealCreate.tsx @@ -7,7 +7,7 @@ import { useRedirect, type GetListResult, } from "ra-core"; -import { Create, CreateProps } from "@/components/admin/create"; +import { Create, type CreateProps } from "@/components/admin/create"; import { SaveButton } from "@/components/admin/form"; import { FormToolbar } from "@/components/admin/simple-form"; import { Dialog, DialogContent } from "@/components/ui/dialog"; @@ -81,19 +81,23 @@ export const DealCreate = ({ open }: { open: boolean }) => { export const DealCreatePage = (props: Partial) => { const { identity } = useGetIdentity(); return ( - - - - - - - - +
    + +
    + + +
    + +
    +
    + +
    +
    ); }; diff --git a/src/components/atomic-crm/deals/DealEdit.tsx b/src/components/atomic-crm/deals/DealEdit.tsx index 74ace94c..6daf058d 100644 --- a/src/components/atomic-crm/deals/DealEdit.tsx +++ b/src/components/atomic-crm/deals/DealEdit.tsx @@ -153,9 +153,11 @@ function EditToolbar() { return ( -
    +
    {isMobile ? null : } - +
    + +
    );