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" }, diff --git a/registry.json b/registry.json index 246f9355..d8236ba9 100644 --- a/registry.json +++ b/registry.json @@ -336,10 +336,26 @@ "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/ResponsiveFilters.tsx", + "type": "registry:component" + }, { "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/InfinitePagination.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/misc/ImageEditorField.tsx", "type": "registry:component" @@ -400,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" @@ -448,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" @@ -632,6 +656,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" diff --git a/src/components/atomic-crm/activity/ActivityLogCompanyCreated.tsx b/src/components/atomic-crm/activity/ActivityLogCompanyCreated.tsx index bf2fe98c..3c2712d1 100644 --- a/src/components/atomic-crm/activity/ActivityLogCompanyCreated.tsx +++ b/src/components/atomic-crm/activity/ActivityLogCompanyCreated.tsx @@ -2,10 +2,9 @@ import { Link } from "react-router"; import { ReferenceField } from "@/components/admin/reference-field"; import { CompanyAvatar } from "../companies/CompanyAvatar"; -import { RelativeDate } from "../misc/RelativeDate"; import { SaleName } from "../sales/SaleName"; import type { ActivityCompanyCreated } from "../types"; -import { useActivityLogContext } from "./ActivityLogContext"; +import { ActivityLogHeader } from "./ActivityLogHeader"; type ActivityLogCompanyCreatedProps = { activity: ActivityCompanyCreated; @@ -14,37 +13,19 @@ type ActivityLogCompanyCreatedProps = { export function ActivityLogCompanyCreated({ activity, }: ActivityLogCompanyCreatedProps) { - const context = useActivityLogContext(); const { company } = activity; return ( -
-
- - -
- - - - - -  added company   - {company.name} - {context === "all" && ( - <> - - - )} -
- {context === "company" && ( - - - - )} -
-
+ } + activity={activity} + > + + + + + +  added company   + {company.name} + ); } diff --git a/src/components/atomic-crm/activity/ActivityLogContactCreated.tsx b/src/components/atomic-crm/activity/ActivityLogContactCreated.tsx index 90e4e41e..5f9a6348 100644 --- a/src/components/atomic-crm/activity/ActivityLogContactCreated.tsx +++ b/src/components/atomic-crm/activity/ActivityLogContactCreated.tsx @@ -2,10 +2,10 @@ import { Link } from "react-router"; import { ReferenceField } from "@/components/admin/reference-field"; import { Avatar } from "../contacts/Avatar"; -import { RelativeDate } from "../misc/RelativeDate"; import { SaleName } from "../sales/SaleName"; import type { ActivityContactCreated } from "../types"; import { useActivityLogContext } from "./ActivityLogContext"; +import { ActivityLogHeader } from "./ActivityLogHeader"; type ActivityLogContactCreatedProps = { activity: ActivityContactCreated; @@ -17,26 +17,23 @@ export function ActivityLogContactCreated({ const context = useActivityLogContext(); const { contact } = activity; return ( -
-
- - - - - -  added  - - {contact.first_name} {contact.last_name} - -   - {context !== "company" && <>to company {activity.company_id}} - - {context === "company" && ( - - - - )} -
-
+ } + activity={activity} + > + + + +  added  + + {contact.first_name} {contact.last_name} + + {context !== "company" && <> to company {activity.company_id}} + ); } diff --git a/src/components/atomic-crm/activity/ActivityLogContactNoteCreated.tsx b/src/components/atomic-crm/activity/ActivityLogContactNoteCreated.tsx index 7ec80998..5437501f 100644 --- a/src/components/atomic-crm/activity/ActivityLogContactNoteCreated.tsx +++ b/src/components/atomic-crm/activity/ActivityLogContactNoteCreated.tsx @@ -3,11 +3,10 @@ import { useRecordContext } from "ra-core"; import { ReferenceField } from "@/components/admin/reference-field"; import { TextField } from "@/components/admin/text-field"; import { Avatar } from "../contacts/Avatar"; -import { RelativeDate } from "../misc/RelativeDate"; import { SaleName } from "../sales/SaleName"; import type { ActivityContactNoteCreated, Contact } from "../types"; -import { useActivityLogContext } from "./ActivityLogContext"; import { ActivityLogNote } from "./ActivityLogNote"; +import { ActivityLogHeader } from "./ActivityLogHeader"; type ActivityLogContactNoteCreatedProps = { activity: ActivityContactNoteCreated; @@ -21,49 +20,42 @@ function ContactAvatar() { export function ActivityLogContactNoteCreated({ activity, }: ActivityLogContactNoteCreatedProps) { - const context = useActivityLogContext(); const { contactNote } = activity; return ( + + + + } + activity={activity} + > + + + +  added a note about  - + +   + - -
-
- - - - - -  added a note about -   - - - -
- - {context === "company" && ( - - - - )} -
- +
} text={contactNote.text} /> diff --git a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx index a8d60232..1d19ae03 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,17 @@ 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..d68a5eea --- /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 ? ( + <> +  - + + ) : 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/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/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index fb0d1ce9..9f01630b 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -1,18 +1,55 @@ -import { 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"; +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 { CompanyAvatar } from "./CompanyAvatar"; +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 ( { 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 = () => ( + +
    + ( +
  • + + +
    +
    + + ( + + {total} {total === 1 ? "deal" : "deals"} + + )} + /> +
    + +
    + +
  • + )} + /> +
+
+); + const CompanyListActions = () => { + const isMobile = useIsMobile(); + return ( - - + {isMobile ? null : ( + <> + + + + )} ); }; diff --git a/src/components/atomic-crm/companies/CompanyListFilter.tsx b/src/components/atomic-crm/companies/CompanyListFilter.tsx index 23f98c5c..2af69fd7 100644 --- a/src/components/atomic-crm/companies/CompanyListFilter.tsx +++ b/src/components/atomic-crm/companies/CompanyListFilter.tsx @@ -1,31 +1,30 @@ import { Building, Truck, Users } from "lucide-react"; -import { FilterLiveForm, useGetIdentity } from "ra-core"; +import { useGetIdentity } from "ra-core"; import { ToggleFilterButton } from "@/components/admin/toggle-filter-button"; -import { SearchInput } from "@/components/admin/search-input"; import { FilterCategory } from "../filters/FilterCategory"; import { useConfigurationContext } from "../root/ConfigurationContext"; import { sizes } from "./sizes"; +import { ResponsiveFilters } from "../misc/ResponsiveFilters"; +import { useIsMobile } from "@/hooks/use-mobile"; export const CompanyListFilter = () => { const { identity } = useGetIdentity(); + const isMobile = useIsMobile(); const { companySectors } = useConfigurationContext(); const sectors = companySectors.map((sector) => ({ id: sector, name: sector, })); return ( -
- - - - + } label="Size"> {sizes.map((size) => ( ))} @@ -33,9 +32,10 @@ export const CompanyListFilter = () => { } label="Sector"> {sectors.map((sector) => ( ))} @@ -48,8 +48,9 @@ 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/companies/CompanyShow.tsx b/src/components/atomic-crm/companies/CompanyShow.tsx index 5c064050..8a46683a 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,8 +94,11 @@ 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/companies/sizes.ts b/src/components/atomic-crm/companies/sizes.ts index a4495de3..471b4bf6 100644 --- a/src/components/atomic-crm/companies/sizes.ts +++ b/src/components/atomic-crm/companies/sizes.ts @@ -1,7 +1,7 @@ export const sizes = [ - { id: 1, name: "1 employee" }, - { id: 10, name: "2-9 employees" }, - { id: 50, name: "10-49 employees" }, - { id: 250, name: "50-249 employees" }, - { id: 500, name: "250 or more employees" }, + { id: 1, name: "1 employee", shortName: "1" }, + { id: 10, name: "2-9 employees", shortName: "2-9" }, + { id: 50, name: "10-49 employees", shortName: "10-49" }, + { id: 250, name: "50-249 employees", shortName: "50-249" }, + { id: 500, name: "250 or more employees", shortName: "250 or more" }, ]; 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/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 (
- - + + diff --git a/src/components/atomic-crm/contacts/ContactList.tsx b/src/components/atomic-crm/contacts/ContactList.tsx index 94817df2..8b93ef6e 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, @@ -8,9 +9,13 @@ 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"; +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,12 +23,40 @@ import { ContactImportButton } from "./ContactImportButton"; import { ContactListContent } from "./ContactListContent"; import { ContactListFilter } from "./ContactListFilter"; import { TopToolbar } from "../layout/TopToolbar"; +import { InfinitePagination } from "../misc/InfinitePagination"; export const ContactList = () => { const { identity } = useGetIdentity(); + const isMobile = useIsMobile(); if (!identity) return null; + if (isMobile) { + return ( + + } actions={false}> + + + + + + ); + } + return ( { const ContactListLayout = () => { const { data, isPending, filterValues } = useListContext(); const { identity } = useGetIdentity(); + const isMobile = useIsMobile(); const hasFilters = filterValues && Object.keys(filterValues).length > 0; @@ -49,25 +83,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..237ca9f3 100644 --- a/src/components/atomic-crm/contacts/ContactListContent.tsx +++ b/src/components/atomic-crm/contacts/ContactListContent.tsx @@ -50,49 +50,59 @@ 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 ?? ""}`} +
+ {isSmall ? : 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" : ""} + + ) : null} + {isSmall ? 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)}{" "} + +
-
- )} + )} +
))} diff --git a/src/components/atomic-crm/contacts/ContactListFilter.tsx b/src/components/atomic-crm/contacts/ContactListFilter.tsx index ce76bf1d..c1c38445 100644 --- a/src/components/atomic-crm/contacts/ContactListFilter.tsx +++ b/src/components/atomic-crm/contacts/ContactListFilter.tsx @@ -1,16 +1,18 @@ import { endOfYesterday, startOfMonth, startOfWeek, subMonths } from "date-fns"; import { CheckSquare, Clock, Tag, TrendingUp, Users } from "lucide-react"; -import { FilterLiveForm, useGetIdentity, useGetList } from "ra-core"; +import { useGetIdentity, useGetList } from "ra-core"; import { ToggleFilterButton } from "@/components/admin/toggle-filter-button"; -import { SearchInput } from "@/components/admin/search-input"; import { Badge } from "@/components/ui/badge"; 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 }, @@ -18,46 +20,46 @@ export const ContactListFilter = () => { }); return ( -
- - - - + }> { 1, ).toISOString(), }} + size={isMobile ? "lg" : undefined} /> @@ -73,13 +76,14 @@ export const ContactListFilter = () => { {noteStatuses.map((status) => ( {status.label} } value={{ status: status.value }} + size={isMobile ? "lg" : undefined} /> ))} @@ -88,7 +92,7 @@ export const ContactListFilter = () => { {data && data.map((record) => ( { } value={{ "tags@cs": `{${record.id}}` }} + size={isMobile ? "lg" : undefined} /> ))} @@ -111,6 +116,7 @@ export const ContactListFilter = () => { className="w-full justify-between" label={"With pending tasks"} value={{ "nb_tasks@gt": 0 }} + size={isMobile ? "lg" : undefined} /> @@ -119,8 +125,9 @@ export const ContactListFilter = () => { className="w-full justify-between" label={"Me"} value={{ sales_id: identity?.id }} + size={isMobile ? "lg" : undefined} /> -
+ ); }; diff --git a/src/components/atomic-crm/contacts/ContactShow.tsx b/src/components/atomic-crm/contacts/ContactShow.tsx index 67b3ec1a..23a37b84 100644 --- a/src/components/atomic-crm/contacts/ContactShow.tsx +++ b/src/components/atomic-crm/contacts/ContactShow.tsx @@ -8,7 +8,15 @@ 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 { Button } from "@/components/ui/button"; +import { Link } from "react-router"; +import { Edit } from "lucide-react"; +import { ReferenceManyCount } from "../misc/ReferenceManyCount"; export const ContactShow = () => ( @@ -18,59 +26,142 @@ 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 ? (
- - - +
-
- - } - > - - + ) : null} + {isMobile ? ( + + + + ( + <> + {total?.toString()} {total === 1 ? "note" : "notes"} + + )} + /> + + + ( + <> + {total?.toString()} {total === 1 ? "task" : "tasks"} + + )} + /> + + Contact details + + + } + > + + + + + + + + + + + + + + ) : ( + + } + > + + + )}
- + {isMobile ? null : }
); }; diff --git a/src/components/atomic-crm/dashboard/Dashboard.tsx b/src/components/atomic-crm/dashboard/Dashboard.tsx index a529b7f5..38afd6de 100644 --- a/src/components/atomic-crm/dashboard/Dashboard.tsx +++ b/src/components/atomic-crm/dashboard/Dashboard.tsx @@ -7,6 +7,17 @@ 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"; +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 { @@ -28,7 +39,7 @@ export const Dashboard = () => { pagination: { page: 1, perPage: 1 }, }, ); - + const isMobile = useIsMobile(); const isPending = isPendingContact || isPendingContactNotes || isPendingDeal; if (isPending) { @@ -43,6 +54,67 @@ export const Dashboard = () => { return ; } + if (isMobile) { + return ( +
+
{totalDeal ? : null}
+ + + Tasks + Activity + + + + + + + + + + + + + + + + New + Deal + + + + + New + Company + + + + + New + Contact + + + + +
+ ); + } + 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/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/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/DealCreate.tsx b/src/components/atomic-crm/deals/DealCreate.tsx index 323b6033..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 } 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"; @@ -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,39 @@ 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..6daf058d 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(); @@ -55,37 +65,99 @@ 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(); + const isMobile = useIsMobile(); + if (!deal) { return null; } return ( - -
-
- - - -

Edit {deal.name} deal

-
-
- -
+
+
+ + + +

Edit {deal.name} deal

+ {isMobile ? ( + + + + + + + + + {deal.archived_at ? ( + + + + ) : ( + + + + )} + + + ) : null} +
+
+
+
+ ); +} + +function EditHeader() { + return ( + + ); } function EditToolbar() { + const record = useRecordContext(); + const isMobile = useIsMobile(); + if (!record) return null; + return ( -
- - +
+ {isMobile ? null : } +
+ +
); diff --git a/src/components/atomic-crm/deals/DealList.tsx b/src/components/atomic-crm/deals/DealList.tsx index 51f6c257..85a2e021 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} > + ); }; 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; @@ -67,19 +88,28 @@ const DealLayout = () => { return ( <> - + ); return ( -
+
- - + +
); }; diff --git a/src/components/atomic-crm/deals/DealShow.tsx b/src/components/atomic-crm/deals/DealShow.tsx index 7c0f02fc..abb49a61 100644 --- a/src/components/atomic-crm/deals/DealShow.tsx +++ b/src/components/atomic-crm/deals/DealShow.tsx @@ -1,15 +1,9 @@ -import { useMutation } from "@tanstack/react-query"; import { format, isValid } from "date-fns"; -import { Archive, ArchiveRestore } from "lucide-react"; -import { - ShowBase, - useDataProvider, - useNotify, - useRecordContext, - useRedirect, - useRefresh, - useUpdate, -} from "ra-core"; +import { ShowBase, useRecordContext, useRedirect } from "ra-core"; +import { Edit } from "lucide-react"; +import { Link } from "react-router"; + +import { useIsMobile } from "@/hooks/use-mobile"; import { DeleteButton } from "@/components/admin/delete-button"; import { EditButton } from "@/components/admin/edit-button"; import { ReferenceArrayField } from "@/components/admin/reference-array-field"; @@ -19,6 +13,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Separator } from "@/components/ui/separator"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { CompanyAvatar } from "../companies/CompanyAvatar"; import { NoteCreate } from "../notes/NoteCreate"; @@ -27,6 +22,9 @@ import { useConfigurationContext } from "../root/ConfigurationContext"; import type { Deal } from "../types"; import { ContactList } from "./ContactList"; import { findDealLabel } from "./deal"; +import { ReferenceManyCount } from "../misc/ReferenceManyCount"; +import { UnarchiveButton } from "./UnarchiveButton"; +import { ArchiveButton } from "./ArchiveButton"; export const DealShow = ({ open, id }: { open: boolean; id?: string }) => { const redirect = useRedirect(); @@ -37,18 +35,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,194 +67,195 @@ 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 ArchivedTitle = () => ( -
-

Archived Deal

-
-); - -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" }); - }, - }, - ); - }; +const DealDetails = () => { + const { dealStages } = useConfigurationContext(); + const record = useRecordContext(); + if (!record) return null; return ( - - ); -}; - -const UnarchiveButton = ({ record }: { record: Deal }) => { - const dataProvider = useDataProvider(); - const redirect = useRedirect(); - const notify = useNotify(); - const refresh = useRefresh(); +
+
+ + 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} +
+
- 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" }); - }, - }); +
+ + Budget + + + {record.amount.toLocaleString("en-US", { + notation: "compact", + style: "currency", + currency: "USD", + currencyDisplay: "narrowSymbol", + minimumSignificantDigits: 3, + })} + +
- const handleClick = () => { - mutate(); - }; + {record.category && ( +
+ + Category + + {record.category} +
+ )} - return ( - +
+ + Stage + + + {findDealLabel(dealStages, record.stage)} + +
+
); }; + +const ArchivedTitle = () => ( +
+

Archived Deal

+
+); 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 ( + + ); +}; 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/filters/FilterCategory.tsx b/src/components/atomic-crm/filters/FilterCategory.tsx index 64d6e54f..e958fc24 100644 --- a/src/components/atomic-crm/filters/FilterCategory.tsx +++ b/src/components/atomic-crm/filters/FilterCategory.tsx @@ -15,6 +15,8 @@ export const FilterCategory = ({ {icon}

-
{children}
+
+ {children} +
); diff --git a/src/components/atomic-crm/layout/Header.tsx b/src/components/atomic-crm/layout/Header.tsx index 71720d85..59c6f959 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,189 @@ const Header = () => { return ( ); }; -const NavigationTab = ({ +const MobileHeader = ({ currentPath }: { currentPath: string | boolean }) => { + const { title } = useConfigurationContext(); + const [open, setOpen] = useState(false); + return ( +
+ + + + + + + Menu + +
+ + + + setOpen(false)} + ref={(element) => element?.focus()} + /> + + + 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, + className, + ...props }: { label: string; to: string; isActive: boolean; -}) => ( - - {label} - -); +} & LinkProps & + RefAttributes) => { + return ( + + {label} + + ); +}; const UsersMenu = () => { const { onClose } = useUserMenu() ?? {}; diff --git a/src/components/atomic-crm/layout/Layout.tsx b/src/components/atomic-crm/layout/Layout.tsx index ef6ad33f..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} diff --git a/src/components/atomic-crm/misc/InfinitePagination.tsx b/src/components/atomic-crm/misc/InfinitePagination.tsx new file mode 100644 index 00000000..0fff5a37 --- /dev/null +++ b/src/components/atomic-crm/misc/InfinitePagination.tsx @@ -0,0 +1,114 @@ +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/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/misc/ResponsiveFilters.tsx b/src/components/atomic-crm/misc/ResponsiveFilters.tsx new file mode 100644 index 00000000..55725cea --- /dev/null +++ b/src/components/atomic-crm/misc/ResponsiveFilters.tsx @@ -0,0 +1,88 @@ +import { useRef, useState } from "react"; +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; + +export const ResponsiveFilters = ({ + children, + searchInput, +}: { + children: React.ReactNode; + searchInput: Partial; +}) => { + const [open, setOpen] = useState(false); + const { source = "q", className, ...otherSearchInputProps } = searchInput; + const isMobile = useIsMobile(); + const dialogContentRef = useRef(null); + + if (isMobile) { + return ( + setOpen(false)} modal={false}> + + + { + setOpen(true); + setTimeout(() => { + if (!dialogContentRef.current) return; + const input = event.target as HTMLInputElement; + dialogContentRef.current.style.top = + input.getBoundingClientRect().bottom + + input.clientTop + + 7 + + "px"; + }, TIMEOUT_DURATION_IN_SECONDS); + }} + onKeyDown={() => setOpen(false)} + className={cn("pb-2", className)} + {...otherSearchInputProps} + /> + + + + + Filters + + {children} + + + + + + + + ); + } + + return ( +
+ + setOpen(false)} + {...otherSearchInputProps} + /> + + + {children} +
+ ); +}; 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/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

-
-
-
-
- +
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 = ({ } /> - + 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 = ({