diff --git a/modules/web/pnpm-lock.yaml b/modules/web/pnpm-lock.yaml index ff99a4a2c..847fb89c8 100644 --- a/modules/web/pnpm-lock.yaml +++ b/modules/web/pnpm-lock.yaml @@ -682,6 +682,9 @@ importers: vite: specifier: ^5.4.14 version: 5.4.14(@types/node@22.13.11)(sass@1.86.0) + vite-plugin-checker: + specifier: ^0.8.0 + version: 0.8.0(eslint@8.57.1)(typescript@5.8.2)(vite@5.4.14) vite-plugin-svgr: specifier: ^4.3.0 version: 4.3.0(typescript@5.8.2)(vite@5.4.14) @@ -717,6 +720,15 @@ packages: js-tokens: 4.0.0 picocolors: 1.1.1 + /@babel/code-frame@7.27.1: + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + dev: true + /@babel/compat-data@7.26.8: resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} engines: {node: '>=6.9.0'} @@ -903,6 +915,11 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-identifier@7.27.1: + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-option@7.25.9: resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} @@ -7973,6 +7990,13 @@ packages: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + /ansi-escapes@7.0.0: resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} engines: {node: '>=18'} @@ -8800,6 +8824,11 @@ packages: engines: {node: '>= 10'} dev: true + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: true + /commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -15377,6 +15406,11 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -15695,6 +15729,59 @@ packages: engines: {node: '>= 0.8'} dev: true + /vite-plugin-checker@0.8.0(eslint@8.57.1)(typescript@5.8.2)(vite@5.4.14): + resolution: {integrity: sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==} + engines: {node: '>=14.16'} + peerDependencies: + '@biomejs/biome': '>=1.7' + eslint: '>=7' + meow: ^9.0.0 + optionator: ^0.9.1 + stylelint: '>=13' + typescript: '*' + vite: '>=2.0.0' + vls: '*' + vti: '*' + vue-tsc: ~2.1.6 + peerDependenciesMeta: + '@biomejs/biome': + optional: true + eslint: + optional: true + meow: + optional: true + optionator: + optional: true + stylelint: + optional: true + typescript: + optional: true + vls: + optional: true + vti: + optional: true + vue-tsc: + optional: true + dependencies: + '@babel/code-frame': 7.27.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + chokidar: 3.6.0 + commander: 8.3.0 + eslint: 8.57.1 + fast-glob: 3.3.3 + fs-extra: 11.3.0 + npm-run-path: 4.0.1 + strip-ansi: 6.0.1 + tiny-invariant: 1.3.3 + typescript: 5.8.2 + vite: 5.4.14(@types/node@22.13.11)(sass@1.86.0) + vscode-languageclient: 7.0.0 + vscode-languageserver: 7.0.0 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + dev: true + /vite-plugin-dts@3.9.1(@types/node@22.13.11)(typescript@5.8.2)(vite@5.4.14): resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -15792,6 +15879,46 @@ packages: fsevents: 2.3.3 dev: true + /vscode-jsonrpc@6.0.0: + resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} + engines: {node: '>=8.0.0 || >=10.0.0'} + dev: true + + /vscode-languageclient@7.0.0: + resolution: {integrity: sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==} + engines: {vscode: ^1.52.0} + dependencies: + minimatch: 3.1.2 + semver: 7.7.1 + vscode-languageserver-protocol: 3.16.0 + dev: true + + /vscode-languageserver-protocol@3.16.0: + resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==} + dependencies: + vscode-jsonrpc: 6.0.0 + vscode-languageserver-types: 3.16.0 + dev: true + + /vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + dev: true + + /vscode-languageserver-types@3.16.0: + resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} + dev: true + + /vscode-languageserver@7.0.0: + resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} + hasBin: true + dependencies: + vscode-languageserver-protocol: 3.16.0 + dev: true + + /vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + dev: true + /vue-template-compiler@2.7.16: resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} dependencies: diff --git a/modules/web/web-app/components/ButtonTabs.tsx b/modules/web/web-app/components/ButtonTabs.tsx new file mode 100644 index 000000000..749a88643 --- /dev/null +++ b/modules/web/web-app/components/ButtonTabs.tsx @@ -0,0 +1,25 @@ +import { Button, ButtonProps, cn } from "@ui/index" +import { PropsWithChildren } from "react" + +interface ButtonTabsProps extends Omit, PropsWithChildren { + active?: boolean +} + +export const ButtonTabs = ({ children, active = false, ...props }: ButtonTabsProps) => { + const { className, ...rest } = props + + return ( + + ) +} \ No newline at end of file diff --git a/modules/web/web-app/components/PageHeading/PageHeading.styled.ts b/modules/web/web-app/components/PageHeading/PageHeading.styled.ts deleted file mode 100644 index 32790c67a..000000000 --- a/modules/web/web-app/components/PageHeading/PageHeading.styled.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { colors, fontSizes, fontWeights, spaces } from '@md/foundation' -import { styled } from '@stitches/react' - -export const StyledPageHeading = styled('h1', { - fontSize: fontSizes.fontSize6, - fontWeight: fontWeights.bold, - lineHeight: 1, -}) - -export const Count = styled('span', { - display: 'inline-block', - fontSize: fontSizes.fontSize4, - fontWeight: fontWeights.medium, - lineHeight: 1, - color: colors.neutral9, - marginLeft: spaces.space3, -}) diff --git a/modules/web/web-app/components/PageHeading/PageHeading.tsx b/modules/web/web-app/components/PageHeading/PageHeading.tsx index 0f556b102..92ff9277c 100644 --- a/modules/web/web-app/components/PageHeading/PageHeading.tsx +++ b/modules/web/web-app/components/PageHeading/PageHeading.tsx @@ -1,5 +1,3 @@ -import { Count, StyledPageHeading } from '@/components/PageHeading/PageHeading.styled' - import type { FunctionComponent, ReactNode } from 'react' interface PageHeadingProps { @@ -9,10 +7,14 @@ interface PageHeadingProps { const PageHeading: FunctionComponent = ({ children, count }) => { return ( - +

{children} - {count !== undefined && count >= 0 && ({count})} - + {count !== undefined && count >= 0 && ( + + ({count}) + + )} +

) } diff --git a/modules/web/web-app/components/layouts/PageLayout.tsx b/modules/web/web-app/components/layouts/PageLayout.tsx new file mode 100644 index 000000000..bc8268f2e --- /dev/null +++ b/modules/web/web-app/components/layouts/PageLayout.tsx @@ -0,0 +1,76 @@ +import { Flex } from '@ui/index' +import { PropsWithChildren, ReactNode } from 'react' +import { useSearchParams } from 'react-router-dom' + +import { ButtonTabs } from '@/components/ButtonTabs' + +interface TabConfig { + key: string + label: string +} + +interface PageLayoutProps extends PropsWithChildren { + imgLink: 'customers' | 'invoices' | 'subscriptions' + title: string + tabs?: TabConfig[] + customTabs?: ReactNode + actions?: ReactNode +} + +export const PageLayout = ({ imgLink, title, children, tabs, customTabs, actions }: PageLayoutProps) => { + const [searchParams, setSearchParams] = useSearchParams() + const currentTab = searchParams.get('tab') || (tabs?.[0]?.key ?? 'all') + + const updateTab = (tab: string) => { + const newSearchParams = new URLSearchParams(searchParams) + + if (tab === (tabs?.[0]?.key ?? 'all')) { + newSearchParams.delete('tab') + } else { + newSearchParams.set('tab', tab.toLowerCase()) + } + setSearchParams(newSearchParams) + } + + const renderTabs = () => { + if (customTabs) { + return customTabs + } + + if (tabs && tabs.length > 0) { + return ( + + {tabs.map((tab) => ( + updateTab(tab.key)} + > + {tab.label} + + ))} + + ) + } + + return null + } + + return
+
+ + + + {title} +
{title}
+ {renderTabs()} +
+ {actions && + {actions} + } +
+ {children} +
+
+
+} \ No newline at end of file diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/NavigationBar.tsx b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/NavigationBar.tsx deleted file mode 100644 index 5e609ee57..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/NavigationBar.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { FunctionComponent } from 'react' - -import Footer from './components/Footer' -import Header from './components/Header' -import Items from './components/Items' - -export const NavigationBar: FunctionComponent = () => { - return ( - - ) -} diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/Footer.styled.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/Footer.styled.ts deleted file mode 100644 index 99b39aa21..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/Footer.styled.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { radius, spaces } from '@md/foundation' -import { styled } from '@stitches/react' - -export const StyledFooter = styled('footer', { - width: '100%', -}) - -export const Avatar = styled('img', { - borderRadius: radius.round, -}) - -export const AvatarTrigger = styled('li', { - width: `calc(100% - ${spaces.space5} * 2)`, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - position: 'relative', - margin: `0 ${spaces.space5}`, - padding: `${spaces.space4} 0`, - borderRadius: radius.radius3, - backgroundColor: 'transparent', - transition: 'background-color 0.2s ease-in-out', -}) diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/Footer.tsx b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/Footer.tsx deleted file mode 100644 index a0d5e5251..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/Footer.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { SettingsIcon } from '@md/icons' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuTrigger, - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@md/ui' -import { LogOutIcon, TerminalIcon, UserCircle2Icon } from 'lucide-react' -import { Link } from 'react-router-dom' - -import { StyledItems as Items } from '../Items/Items.styled' -import Item from '../Items/components/Item/Item' - -import { AvatarTrigger, StyledFooter } from './Footer.styled' - -import type { FunctionComponent, ReactNode } from 'react' - -const Footer: FunctionComponent = () => { - return ( - - - } /> - } /> - - - - ) -} - -const UserPreferenceTooltip = ({ children }: { children: ReactNode }) => { - return ( - - {children} - Account - - ) -} - -export const FooterAccountDropdown: FunctionComponent = () => { - return ( -
  • - - - - - - - - - - - - - Logout - - - - - -
  • - ) -} - -export default Footer diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/index.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/index.ts deleted file mode 100644 index 5d06e9b71..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Footer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Footer' diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/Header.styled.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/Header.styled.ts deleted file mode 100644 index 187339d0c..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/Header.styled.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { styled } from '@stitches/react' - -export const StyledHeader = styled('header', {}) diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/Header.tsx b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/Header.tsx deleted file mode 100644 index b174e47b2..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/Header.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { LogoSymbol } from '@md/foundation' -import { Link } from 'react-router-dom' - -import { useTheme } from 'providers/ThemeProvider' - -import { StyledHeader } from './Header.styled' - -import type { FunctionComponent } from 'react' - -const Header: FunctionComponent = () => { - const { isDarkMode } = useTheme() - - return ( - - - - - - ) -} - -export default Header diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/index.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/index.ts deleted file mode 100644 index 6f8c57905..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Header/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Header' diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.data.tsx b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.data.tsx deleted file mode 100644 index 4e611cde5..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.data.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { BillingIcon, Catalog2Icon, CustomersIcon, HomeIcon, ReportsIcon } from '@md/icons' -import { LightbulbIcon } from 'lucide-react' - -import { NavigationItemType } from './components/Item/Item.types' - -export const NAVIGATION_ITEMS: NavigationItemType[] = [ - { - label: 'Home', - to: '.', - end: true, - icon: , - divider: true, - }, - - { - label: 'Product catalog', // metrics - to: 'plans', - icon: , - }, - { - label: 'Billing', - to: 'subscriptions', - icon: , - }, - { - label: 'Customers', - to: 'customers', - icon: , - divider: true, - }, - { - label: 'Growth', - to: 'growth', - icon: , - }, - { - label: 'Reports', - to: 'reports', - icon: , - }, -] diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.styled.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.styled.ts deleted file mode 100644 index 56e911431..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.styled.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { colors, spaces } from '@md/foundation' -import { styled } from '@stitches/react' - -const NAVIGATION_BAR_WIDTH = 55 - -export const StyledItems = styled('ul', { - maxWidth: NAVIGATION_BAR_WIDTH, - width: '100%', - display: 'flex', - flexDirection: 'column', - gap: spaces.space4, -}) - -export const ItemDivider = styled('hr', { - width: `calc(100% - ${spaces.space5} * 2)`, - height: 1, - border: 'none', - background: `linear-gradient(81deg, rgba(246,202,220,0) 0%, ${colors.neutral6} 49%, rgba(255,255,255,0) 100%)`, - marginLeft: spaces.space5, -}) diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.tsx b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.tsx deleted file mode 100644 index 717ce61b8..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/Items.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Fragment, FunctionComponent } from 'react' - -import { NAVIGATION_ITEMS } from './Items.data' -import { ItemDivider, StyledItems } from './Items.styled' -import Item from './components/Item' - -const Items: FunctionComponent = () => { - return ( - - {NAVIGATION_ITEMS.map(item => ( - - - {item.divider && } - - ))} - - ) -} - -export default Items diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.hooks.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.hooks.ts deleted file mode 100644 index 71046e156..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.hooks.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const onClick = () => { - const activeLink = document.querySelector('aside ul li a.active') - activeLink?.setAttribute('data-exit', 'true') - - setTimeout(() => { - activeLink?.removeAttribute('data-exit') - }, 300) -} diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.styled.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.styled.ts deleted file mode 100644 index 4733b5910..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.styled.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { radius, spaces } from '@md/foundation' -import { keyframes, styled } from '@stitches/react' -import { NavLink } from 'react-router-dom' - -const ActiveStateKeyframe = keyframes({ - '0%': { - transform: 'translateX(-4px)', - opacity: 0, - }, - '100%': { - transform: 'translateX(0)', - opacity: 1, - }, -}) - -export const ItemLink = styled(NavLink, { - position: 'relative', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - margin: `0 ${spaces.space5}`, - padding: `${spaces.space4} 0`, - borderRadius: radius.radius3, - backgroundColor: 'transparent', - transition: 'background-color 0.2s ease-in-out', - - '&.active::before, &[data-exit="true"]::before': { - content: '', - position: 'absolute', - right: `calc(${spaces.space5} * -1)`, - top: 0, - width: 3, - height: '100%', - borderTopLeftRadius: 1.5, - borderBottomLeftRadius: 1.5, - background: 'linear-gradient(180deg, #4F46FF -37.5%, #817AFF 100%), #7B74FF', - animation: `${ActiveStateKeyframe} 0.3s ease-in`, - transition: 'transform 0.3s ease-out, opacity 0.3s ease-out', - }, - - '&[data-exit="true"]::before': { - transform: 'translateX(-4px)', - opacity: 0, - }, -}) diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.tsx b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.tsx deleted file mode 100644 index d0e5d53a2..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Tooltip, TooltipContent, TooltipTrigger } from '@md/ui' - -import { onClick } from './Item.hooks' -import { ItemLink } from './Item.styled' -import { NavigationItemType } from './Item.types' - -import type { FunctionComponent } from 'react' - -const Item: FunctionComponent = ({ to, end, icon, label }) => { - return ( -
  • - - - - {icon} - - - {label} - -
  • - ) -} - -export default Item diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.types.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.types.ts deleted file mode 100644 index 83e11ba38..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/Item.types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { To } from 'react-router-dom' - -export type NavigationItemType = { - label: string - to: To - end?: boolean - icon: React.ReactNode - divider?: boolean -} diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/index.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/index.ts deleted file mode 100644 index 748b336b1..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/components/Item/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Item' diff --git a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/index.ts b/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/index.ts deleted file mode 100644 index 65af26b77..000000000 --- a/modules/web/web-app/components/layouts/TenantLayout/NavigationBar/components/Items/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Items' diff --git a/modules/web/web-app/components/table/CustomTable/components/Pagination/Pagination.styled.ts b/modules/web/web-app/components/table/CustomTable/components/Pagination/Pagination.styled.ts deleted file mode 100644 index 1952437bf..000000000 --- a/modules/web/web-app/components/table/CustomTable/components/Pagination/Pagination.styled.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { colors, fontSizes, fontWeights, spaces } from '@md/foundation' -import { styled } from '@stitches/react' - -export const CountInfo = styled('div', { - fontSize: fontSizes.fontSize2, - color: colors.neutral11, - display: 'flex', - alignItems: 'center', - gap: spaces.space2, - lineHeight: 1, - - '& > span': { - fontWeight: fontWeights.medium, - }, -}) diff --git a/modules/web/web-app/features/auth/shared.styled.ts b/modules/web/web-app/features/auth/shared.styled.ts deleted file mode 100644 index acf3b2768..000000000 --- a/modules/web/web-app/features/auth/shared.styled.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { styled } from '@stitches/react' - -export const StyledContainer = styled('div', { - height: '100%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}) diff --git a/modules/web/web-app/features/customers/cards/address/AddressCard.tsx b/modules/web/web-app/features/customers/cards/address/AddressCard.tsx index e1f6337bc..92b6c32dc 100644 --- a/modules/web/web-app/features/customers/cards/address/AddressCard.tsx +++ b/modules/web/web-app/features/customers/cards/address/AddressCard.tsx @@ -68,4 +68,4 @@ export const AddressCard = ({ customer, className }: Props) => { /> ) -} +} \ No newline at end of file diff --git a/modules/web/web-app/features/customers/cards/address/EditAddressModal.tsx b/modules/web/web-app/features/customers/cards/address/EditAddressModal.tsx index 38c6c5180..13585b4c9 100644 --- a/modules/web/web-app/features/customers/cards/address/EditAddressModal.tsx +++ b/modules/web/web-app/features/customers/cards/address/EditAddressModal.tsx @@ -119,4 +119,4 @@ export const EditAddressModal = ({ customer, ...props }: Props) => { ) -} +} \ No newline at end of file diff --git a/modules/web/web-app/features/customers/cards/address/schema.ts b/modules/web/web-app/features/customers/cards/address/schema.ts index f2badec5e..4fe142d2c 100644 --- a/modules/web/web-app/features/customers/cards/address/schema.ts +++ b/modules/web/web-app/features/customers/cards/address/schema.ts @@ -1,18 +1,18 @@ import { z } from 'zod' const addressSchema = z.object({ - line1: z.string().optional(), - line2: z.string().optional(), - city: z.string().optional(), - country: z.string().optional(), - state: z.string().optional(), - zipcode: z.string().optional(), + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + country: z.string().optional(), + state: z.string().optional(), + zipcode: z.string().optional(), }) const shippingAddressSchema = z.object({ - address: addressSchema.optional(), - sameAsBilling: z.boolean(), + address: addressSchema.optional(), + sameAsBilling: z.boolean(), }) export const addressesSchema = z.object({ - billing_address: addressSchema.optional(), - shipping_address: shippingAddressSchema.optional(), -}) + billing_address: addressSchema.optional(), + shipping_address: shippingAddressSchema.optional(), +}) \ No newline at end of file diff --git a/modules/web/web-app/features/customers/cards/balance/BalanceCard.tsx b/modules/web/web-app/features/customers/cards/balance/BalanceCard.tsx deleted file mode 100644 index e0d356e0b..000000000 --- a/modules/web/web-app/features/customers/cards/balance/BalanceCard.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ComponentProps, useState } from 'react' - -import { Property } from '@/components/Property' -import { PageSection } from '@/components/layouts/shared/PageSection' -import { CardAction } from '@/features/customers/cards/CardAction' -import { EditBalanceModal } from '@/features/customers/cards/balance/EditBalanceModal' -import { Customer } from '@/rpc/api/customers/v1/models_pb' - -type Props = Pick, 'className'> & { - customer: Customer -} -export const BalanceCard = ({ customer, className }: Props) => { - const [editModalVisible, setEditModalVisible] = useState(false) - - return ( - setEditModalVisible(true)} />, - }} - > -
    -
    - -
    -
    - - setEditModalVisible(false)} - /> -
    - ) -} diff --git a/modules/web/web-app/features/customers/cards/balance/EditBalanceModal.tsx b/modules/web/web-app/features/customers/cards/balance/EditBalanceModal.tsx deleted file mode 100644 index 633d9474c..000000000 --- a/modules/web/web-app/features/customers/cards/balance/EditBalanceModal.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query' -import { Form, InputFormField, Modal } from '@md/ui' -import { useQueryClient } from '@tanstack/react-query' -import { ComponentProps } from 'react' -import { toast } from 'sonner' -import { z } from 'zod' - -import { balanceSchema } from '@/features/customers/cards/balance/schema' -import { useZodForm } from '@/hooks/useZodForm' -import { - getCustomerById, - patchCustomer, -} from '@/rpc/api/customers/v1/customers-CustomersService_connectquery' -import { Customer } from '@/rpc/api/customers/v1/models_pb' - -type Props = Pick, 'visible' | 'onCancel'> & { - customer: Customer -} - -export const EditBalanceModal = ({ customer, ...props }: Props) => { - const queryClient = useQueryClient() - const patchCustomerMutation = useMutation(patchCustomer, { - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: createConnectQueryKey(getCustomerById, { id: customer.id }), - }) - }, - }) - - const methods = useZodForm({ - mode: 'onChange', - schema: balanceSchema, - defaultValues: { - balanceValueCents: Number(customer.balanceValueCents), - }, - }) - - const onSubmit = async (data: z.infer) => { - console.log('data', data) - await patchCustomerMutation.mutateAsync({ - customer: { - id: customer.id, - balanceValueCents: BigInt(data.balanceValueCents), - }, - }) - toast.success('Balance updated') - props.onCancel?.() - } - - return ( - Edit balance} - {...props} - onConfirm={() => methods.handleSubmit(onSubmit)()} - > - -
    - -
    - -
    -
    - -
    -
    - ) -} diff --git a/modules/web/web-app/features/customers/cards/balance/schema.ts b/modules/web/web-app/features/customers/cards/balance/schema.ts deleted file mode 100644 index e22b228a3..000000000 --- a/modules/web/web-app/features/customers/cards/balance/schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod' - -const CURRENCIES = ['EUR', 'USD'] as const -export const balanceSchema = z.object({ - balanceValueCents: z.number().min(0, 'Must be positive'), - balanceCurrency: z.enum(CURRENCIES, { - errorMap: () => ({ message: "Expecting 'EUR' or 'USD'" }), - }), -}) diff --git a/modules/web/web-app/features/customers/cards/customer/CustomerCard.tsx b/modules/web/web-app/features/customers/cards/customer/CustomerCard.tsx deleted file mode 100644 index 635a7f69a..000000000 --- a/modules/web/web-app/features/customers/cards/customer/CustomerCard.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import dayjs from 'dayjs' -import { ComponentProps, useState } from 'react' - -import { Property } from '@/components/Property' -import { PageSection } from '@/components/layouts/shared/PageSection' -import { CardAction } from '@/features/customers/cards/CardAction' -import { EditCustomerModal } from '@/features/customers/cards/customer/EditCustomerModal' -import { Customer } from '@/rpc/api/customers/v1/models_pb' - -type Props = Pick, 'className'> & { - customer: Customer -} - -export const CustomerCard = ({ customer, className }: Props) => { - const [editModalVisible, setEditModalVisible] = useState(false) - - return ( - setEditModalVisible(true)} />, - }} - > -
    -
    - - - -
    -
    - - {/* TODO */} - - -
    -
    - - setEditModalVisible(false)} - /> -
    - ) -} diff --git a/modules/web/web-app/features/customers/cards/customer/EditCustomerModal.tsx b/modules/web/web-app/features/customers/cards/customer/EditCustomerModal.tsx deleted file mode 100644 index a867c0d41..000000000 --- a/modules/web/web-app/features/customers/cards/customer/EditCustomerModal.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query' -import { Form, InputFormField, Modal } from '@md/ui' -import { useQueryClient } from '@tanstack/react-query' -import { ComponentProps } from 'react' -import { toast } from 'sonner' -import { z } from 'zod' - -import { customerSchema } from '@/features/customers/cards/customer/schema' -import { useZodForm } from '@/hooks/useZodForm' -import { - getCustomerById, - patchCustomer, -} from '@/rpc/api/customers/v1/customers-CustomersService_connectquery' -import { Customer } from '@/rpc/api/customers/v1/models_pb' - -type Props = Pick, 'visible' | 'onCancel'> & { - customer: Customer -} - -export const EditCustomerModal = ({ customer, ...props }: Props) => { - const queryClient = useQueryClient() - const patchCustomerMutation = useMutation(patchCustomer, { - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: createConnectQueryKey(getCustomerById, { id: customer.id }), - }) - }, - }) - - const methods = useZodForm({ - mode: 'onChange', - schema: customerSchema, - defaultValues: customer, - }) - - const onSubmit = async (data: z.infer) => { - await patchCustomerMutation.mutateAsync({ - customer: { - id: customer.id, - name: data.name, - alias: data.alias, - billingEmail: data.email, - // TODO allow multiple - invoicingEmails: data.invoicingEmail ? { emails: [data.invoicingEmail] } : undefined, - phone: data.phone, - }, - }) - toast.success('Address updated') - props.onCancel?.() - } - - return ( - Edit customer} - {...props} - onConfirm={() => methods.handleSubmit(onSubmit)()} - > - -
    - -
    -

    Customer details

    - - - - - -
    -
    - -
    -
    - ) -} diff --git a/modules/web/web-app/features/customers/cards/customer/schema.ts b/modules/web/web-app/features/customers/cards/customer/schema.ts deleted file mode 100644 index 552ace5ac..000000000 --- a/modules/web/web-app/features/customers/cards/customer/schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod' - -export const customerSchema = z.object({ - name: z.string().min(3, 'Required'), - alias: z.string().optional(), - email: z.string().optional(), - invoicingEmail: z.string().optional(), - phone: z.string().optional(), -}) diff --git a/modules/web/web-app/features/customers/headers/CustomerHeader.tsx b/modules/web/web-app/features/customers/headers/CustomerHeader.tsx deleted file mode 100644 index 284ecf888..000000000 --- a/modules/web/web-app/features/customers/headers/CustomerHeader.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { spaces } from '@md/foundation' -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, - Input, - Flex as NewFlex, - Separator, - cn, -} from '@md/ui' -import { Flex } from '@ui/components/legacy' -import { Check, ChevronDown, ChevronRight } from 'lucide-react' -import { FunctionComponent, useState } from 'react' -import { useNavigate } from 'react-router-dom' - -import { Loader } from '@/features/auth/components/Loader' -import { useBasePath } from '@/hooks/useBasePath' -import { useDebounceValue } from '@/hooks/useDebounce' -import { useQuery } from '@/lib/connectrpc' -import { listCustomers } from '@/rpc/api/customers/v1/customers-CustomersService_connectquery' -import { ListCustomerRequest_SortBy } from '@/rpc/api/customers/v1/customers_pb' - -interface CustomerHeaderProps { - name?: string - setEditPanelVisible: (visible: boolean) => void - setShowIncoice: () => void -} - -export const CustomerHeader: FunctionComponent = ({ - name, - setEditPanelVisible, - setShowIncoice, -}) => { - const basePath = useBasePath() - const navigate = useNavigate() - - const [search, setSearch] = useState('') - - const debouncedSearch = useDebounceValue(search, 400) - - const pageIndex = 0 - const pageSize = 20 - - const customersQuery = useQuery( - listCustomers, - { - pagination: { - limit: pageSize, - offset: pageIndex * pageSize, - }, - search: debouncedSearch.length > 0 ? debouncedSearch : undefined, - sortBy: ListCustomerRequest_SortBy.NAME_ASC, - }, - {} - ) - - const data = customersQuery.data?.customers ?? [] - const isLoading = customersQuery.isLoading - - return ( - <> - - - - customer logo -
    navigate('..')} - > - Customers -
    - -
    {name}
    - - - - - - - - setSearch(e.target.value)} - autoFocus - placeholder="Search..." - className="h-7 w-full bg-transparent focus-visible:shadow-none outline-none border-none" - /> - - {isLoading ? ( - - ) : ( - data.map(customer => ( - navigate(`${basePath}/customers/${customer.id}`)} - key={customer.id} - className={cn(customer.name === name && 'bg-accent', 'mt-1 cursor-pointer')} - > - - {customer.name} - {customer.name === name && } - - - )) - )} - - -
    - - - - - - - - Assign subscription - Charge one time payment - Create Invoice - Create quote - - Add balance - setEditPanelVisible(true)}> - Edit customer details - - - Archive customer - - - -
    -
    - -
    -
    - - ) -} diff --git a/modules/web/web-app/features/customers/headers/CustomersHeader.tsx b/modules/web/web-app/features/customers/headers/CustomersHeader.tsx deleted file mode 100644 index bb219174f..000000000 --- a/modules/web/web-app/features/customers/headers/CustomersHeader.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { spaces } from '@md/foundation' -import { SearchIcon } from '@md/icons' -import { Button, ButtonProps, InputWithIcon, Flex as NewFlex, Separator, cn } from '@md/ui' -import { Flex } from '@ui/components/legacy' -import { ListFilter } from 'lucide-react' -import { FunctionComponent, PropsWithChildren, useState } from 'react' -import { useSearchParams } from 'react-router-dom' - -import { CustomersExportModal } from '@/features/customers/modals/CustomersExportModal' - -interface CustomersHeaderProps { - setEditPanelVisible: (visible: boolean) => void - setSearch: (search: string) => void - search: string -} - -export const CustomersHeader: FunctionComponent = ({ - setEditPanelVisible, - setSearch, - search, -}) => { - const [searchParams, setSearchParams] = useSearchParams() - const currentTab = searchParams.get('tab') || 'all' - - const [visible, setVisible] = useState(false) - - const updateTab = (tab: string) => { - const newSearchParams = new URLSearchParams(searchParams) - - if (tab === 'all') newSearchParams.delete('tab') - else { - newSearchParams.set('tab', tab.toLowerCase()) - } - setSearchParams(newSearchParams) - } - - return ( - <> - - - - customer logo -
    Customers
    - - updateTab('all')}> - All - - updateTab('active')}> - Active - - updateTab('inactive')}> - Inactive - - updateTab('archived')}> - Archived - - -
    - - - - -
    -
    - -
    - - } - width="fit-content" - value={search} - onChange={e => setSearch(e.target.value)} - /> - - -
    - - - ) -} - -interface ButtonTabsProps extends Omit, PropsWithChildren { - active?: boolean -} - -const ButtonTabs = ({ children, active = false, ...props }: ButtonTabsProps) => { - const { className, ...rest } = props - - return ( - - ) -} diff --git a/modules/web/web-app/features/customers/headers/index.ts b/modules/web/web-app/features/customers/headers/index.ts deleted file mode 100644 index 0fd719ec3..000000000 --- a/modules/web/web-app/features/customers/headers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { CustomerHeader } from './CustomerHeader' -export { CustomersHeader } from './CustomersHeader' diff --git a/modules/web/web-app/features/customers/index.tsx b/modules/web/web-app/features/customers/index.tsx index 50f1363ba..d0875b3be 100644 --- a/modules/web/web-app/features/customers/index.tsx +++ b/modules/web/web-app/features/customers/index.tsx @@ -1,3 +1,4 @@ export { CustomersEditPanel } from './CustomersEditPanel' -export { CustomerHeader, CustomersHeader } from './headers' +export { CustomerDetailsPanel, CustomerOverviewPanel } from './panels' export { CustomersTable } from './table/CustomersTable' + diff --git a/modules/web/web-app/features/customers/panels/CustomerDetailsPanel.tsx b/modules/web/web-app/features/customers/panels/CustomerDetailsPanel.tsx new file mode 100644 index 000000000..9063ff899 --- /dev/null +++ b/modules/web/web-app/features/customers/panels/CustomerDetailsPanel.tsx @@ -0,0 +1,49 @@ +import { Flex, Separator } from '@md/ui' + +import { Customer } from '@/rpc/api/customers/v1/models_pb' + +interface CustomerDetailsPanelProps { + customer: Customer +} + +export const CustomerDetailsPanel = ({ customer }: CustomerDetailsPanelProps) => { + return ( + + +
    {customer.name}
    +
    {customer.alias}
    + + + + + +
    Address
    +
    {customer.billingAddress?.city}
    +
    + + +
    + + +
    Integrations
    + + {/* TODO */} + +
    + + +
    Payment
    + + + +
    +
    + ) +} + +const FlexDetails = ({ title, value }: { title: string; value?: string }) => ( + +
    {title}
    +
    {value ?? 'N/A'}
    +
    +) \ No newline at end of file diff --git a/modules/web/web-app/features/customers/panels/CustomerOverviewPanel.tsx b/modules/web/web-app/features/customers/panels/CustomerOverviewPanel.tsx new file mode 100644 index 000000000..bcf6dfeee --- /dev/null +++ b/modules/web/web-app/features/customers/panels/CustomerOverviewPanel.tsx @@ -0,0 +1,55 @@ +import { Card, Flex } from '@md/ui' +import { ChevronDown, Plus } from 'lucide-react' + +import { InvoicesCard } from '@/features/customers/cards/InvoicesCard' +import { SubscriptionsCard } from '@/features/customers/cards/SubscriptionsCard' +import { Customer } from '@/rpc/api/customers/v1/models_pb' + +interface CustomerOverviewPanelProps { + customer: Customer + onCreateInvoice: () => void +} + +export const CustomerOverviewPanel = ({ customer, onCreateInvoice }: CustomerOverviewPanelProps) => { + return ( + +
    Overview
    +
    + + +
    + +
    Subscriptions
    + + Assign subscription + +
    +
    + +
    + +
    Invoices
    + + Create invoice + +
    +
    + +
    +
    + ) +} + +const OverviewCard = ({ title, value }: { title: string; value?: number }) => ( + + +
    {title}
    + +
    +
    € {value}
    +
    +) \ No newline at end of file diff --git a/modules/web/web-app/features/customers/panels/index.ts b/modules/web/web-app/features/customers/panels/index.ts new file mode 100644 index 000000000..53e4d5ccd --- /dev/null +++ b/modules/web/web-app/features/customers/panels/index.ts @@ -0,0 +1,3 @@ +export { CustomerDetailsPanel } from './CustomerDetailsPanel' +export { CustomerOverviewPanel } from './CustomerOverviewPanel' + diff --git a/modules/web/web-app/features/invoices/InvoicesHeader.tsx b/modules/web/web-app/features/invoices/InvoicesHeader.tsx deleted file mode 100644 index f88116b89..000000000 --- a/modules/web/web-app/features/invoices/InvoicesHeader.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { spaces } from '@md/foundation' -import { PlusIcon, SearchIcon } from '@md/icons' -import { Button, InputWithIcon } from '@md/ui' -import { Flex } from '@ui/components/legacy' -import { RefreshCwIcon } from 'lucide-react' -import { FunctionComponent } from 'react' - -import PageHeading from '@/components/PageHeading/PageHeading' -import { FilterDropdown } from '@/features/invoices/FilterDropdown' -import { InvoicesSearch } from '@/features/invoices/types' - -type InvoicesProps = { - count: number - isLoading: boolean - refetch: () => void - setEditPanelVisible: (visible: boolean) => void - setSearch: (search: InvoicesSearch) => void - search: InvoicesSearch -} - -export const InvoicesHeader: FunctionComponent = ({ - count, - isLoading, - refetch, - setEditPanelVisible, - setSearch, - search, -}) => { - return ( - - - Invoices - - - - - - - } - width="fit-content" - value={search.text} - onChange={e => setSearch({ ...search, text: e.target.value })} - /> - - setSearch({ ...search, status: value })} - /> - - - ) -} diff --git a/modules/web/web-app/features/invoices/index.tsx b/modules/web/web-app/features/invoices/index.tsx index c2b82037f..2db40b97b 100644 --- a/modules/web/web-app/features/invoices/index.tsx +++ b/modules/web/web-app/features/invoices/index.tsx @@ -1,2 +1,2 @@ -export { InvoicesHeader } from './InvoicesHeader' -export { InvoicesTable } from './InvoicesTable' +export { InvoicesTable } from './InvoicesTable'; + diff --git a/modules/web/web-app/features/subscriptions/SubscriptionsHeader.tsx b/modules/web/web-app/features/subscriptions/SubscriptionsHeader.tsx deleted file mode 100644 index 27117f75d..000000000 --- a/modules/web/web-app/features/subscriptions/SubscriptionsHeader.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { colors, spaces } from '@md/foundation' -import { PlusIcon, SearchIcon } from '@md/icons' -import { Button, InputWithIcon } from '@ui/components' -import { Flex } from '@ui/components/legacy' -import { RefreshCwIcon } from 'lucide-react' -import { FunctionComponent } from 'react' -import { Link } from 'react-router-dom' - -import PageHeading from '@/components/PageHeading/PageHeading' - -interface SubscriptionsProps { - count: number - isLoading: boolean - refetch: () => void -} - -export const SubscriptionsHeader: FunctionComponent = ({ - count, - isLoading, - refetch, -}) => { - return ( - - - Subscriptions - - - - - - - - } - width="fit-content" - disabled - /> - - - - ) -} diff --git a/modules/web/web-app/features/subscriptions/index.tsx b/modules/web/web-app/features/subscriptions/index.tsx index 7dba11cab..247ac46b2 100644 --- a/modules/web/web-app/features/subscriptions/index.tsx +++ b/modules/web/web-app/features/subscriptions/index.tsx @@ -1,2 +1,2 @@ -export { SubscriptionsHeader } from './SubscriptionsHeader' -export { SubscriptionsTable } from './SubscriptionsTable' +export { SubscriptionsTable } from './SubscriptionsTable'; + diff --git a/modules/web/web-app/package.json b/modules/web/web-app/package.json index e6bc9bd24..7b110cb57 100644 --- a/modules/web/web-app/package.json +++ b/modules/web/web-app/package.json @@ -87,6 +87,7 @@ "tailwindcss-radix": "^2.9.0", "typescript": "^5.8.2", "vite": "^5.4.14", + "vite-plugin-checker": "^0.8.0", "vite-plugin-svgr": "^4.3.0", "vite-tsconfig-paths": "^4.3.2" }, @@ -101,4 +102,4 @@ "generate:proto": "buf generate --template=buf.gen.yaml ../../..", "postinstall": "pnpm run generate:proto" } -} +} \ No newline at end of file diff --git a/modules/web/web-app/pages/tenants/billing/index.tsx b/modules/web/web-app/pages/tenants/billing/index.tsx index 467b3c6a3..ab5430fc8 100644 --- a/modules/web/web-app/pages/tenants/billing/index.tsx +++ b/modules/web/web-app/pages/tenants/billing/index.tsx @@ -1,7 +1,7 @@ import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query' import { useQueryClient } from '@tanstack/react-query' import { FunctionComponent } from 'react' -import { Navigate, Outlet } from 'react-router-dom' +import { Navigate } from 'react-router-dom' import { TenantPageLayout } from '@/components/layouts' import ProductEmptyState from '@/features/productCatalog/ProductEmptyState' @@ -14,14 +14,6 @@ export const Billing: FunctionComponent = () => { return } -export const BillingOutlet: FunctionComponent = () => { - return ( - - - - ) -} - export const FamilyCreationModalPage = () => { const queryClient = useQueryClient() diff --git a/modules/web/web-app/pages/tenants/customer/customer.tsx b/modules/web/web-app/pages/tenants/customer/customer.tsx index 0339d7f0f..ea1c077bf 100644 --- a/modules/web/web-app/pages/tenants/customer/customer.tsx +++ b/modules/web/web-app/pages/tenants/customer/customer.tsx @@ -1,21 +1,42 @@ -import { Card, Flex, Separator, Skeleton } from '@md/ui' -import { ChevronDown, Plus } from 'lucide-react' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + Flex, + Input, + Separator, + Skeleton, + cn, +} from '@md/ui' +import { Check, ChevronDown, ChevronRight } from 'lucide-react' import { Fragment, useState } from 'react' +import { useNavigate } from 'react-router-dom' -import { TenantPageLayout } from '@/components/layouts' -import { CustomerHeader, CustomersEditPanel } from '@/features/customers' -import { InvoicesCard } from '@/features/customers/cards/InvoicesCard' -import { SubscriptionsCard } from '@/features/customers/cards/SubscriptionsCard' +import { PageLayout } from '@/components/layouts/PageLayout' +import { Loader } from '@/features/auth/components/Loader' +import { CustomersEditPanel } from '@/features/customers' import { CustomerInvoiceModal } from '@/features/customers/modals/CustomerInvoiceModal' +import { CustomerDetailsPanel, CustomerOverviewPanel } from '@/features/customers/panels' +import { useBasePath } from '@/hooks/useBasePath' +import { useDebounceValue } from '@/hooks/useDebounce' import { useQuery } from '@/lib/connectrpc' -import { getCustomerById } from '@/rpc/api/customers/v1/customers-CustomersService_connectquery' +import { getCustomerById, listCustomers } from '@/rpc/api/customers/v1/customers-CustomersService_connectquery' +import { ListCustomerRequest_SortBy } from '@/rpc/api/customers/v1/customers_pb' import { useTypedParams } from '@/utils/params' export const Customer = () => { const { customerId } = useTypedParams<{ customerId: string }>() + const basePath = useBasePath() + const navigate = useNavigate() const [editPanelVisible, setEditPanelVisible] = useState(false) const [createInvoiceVisible, setCreateInvoiceVisible] = useState(false) + const [search, setSearch] = useState('') + + const debouncedSearch = useDebounceValue(search, 400) const customerQuery = useQuery( getCustomerById, @@ -25,88 +46,120 @@ export const Customer = () => { { enabled: Boolean(customerId) } ) + const customersQuery = useQuery( + listCustomers, + { + pagination: { + limit: 20, + offset: 0, + }, + search: debouncedSearch.length > 0 ? debouncedSearch : undefined, + sortBy: ListCustomerRequest_SortBy.NAME_ASC, + }, + {} + ) + const data = customerQuery.data?.customer const isLoading = customerQuery.isLoading + const customersList = customersQuery.data?.customers ?? [] + const isCustomersLoading = customersQuery.isLoading return ( - - - setCreateInvoiceVisible(true)} - /> - {isLoading || !data ? ( - <> - - - - ) : ( - - -
    Overview
    -
    - - -
    - -
    Subscriptions
    - - Assign subscription - -
    -
    - -
    - -
    Invoices
    - setCreateInvoiceVisible(true)} - > - Create invoice - -
    -
    - -
    -
    - - -
    {data.name}
    -
    {data.alias}
    - - - - - -
    Address
    -
    {data.billingAddress?.city}
    -
    - - -
    - - -
    Integrations
    - - {/* TODO */} - -
    - - -
    Payment
    - - - -
    + +
    navigate('..')} + > + Customers +
    + +
    {data?.name || data?.alias}
    + + + + -
    - )} -
    -
    + + + setSearch(e.target.value)} + autoFocus + placeholder="Search..." + className="h-7 w-full bg-transparent focus-visible:shadow-none outline-none border-none" + /> + + {isCustomersLoading ? ( + + ) : ( + customersList.map(customer => ( + navigate(`${basePath}/customers/${customer.id}`)} + key={customer.id} + className={cn(customer.name === data?.name && 'bg-accent', 'mt-1 cursor-pointer')} + > + + {customer.name} + {customer.name === data?.name && } + + + )) + )} + + + } + actions={<> + + + + + + + Assign subscription + Charge one time payment + setCreateInvoiceVisible(true)}>Create Invoice + Create quote + + Add balance + setEditPanelVisible(true)}> + Edit customer details + + + Archive customer + + + } + > +
    + +
    + {isLoading || !data ? ( + <> + + + + ) : ( +
    + setCreateInvoiceVisible(true)} + /> + +
    + )} + setEditPanelVisible(false)} @@ -115,20 +168,3 @@ export const Customer = () => {
    ) } - -const OverviewCard = ({ title, value }: { title: string; value?: number }) => ( - - -
    {title}
    - -
    -
    € {value}
    -
    -) - -const FlexDetails = ({ title, value }: { title: string; value?: string }) => ( - -
    {title}
    -
    {value ?? 'N/A'}
    -
    -) diff --git a/modules/web/web-app/pages/tenants/customer/customers.tsx b/modules/web/web-app/pages/tenants/customer/customers.tsx index c416543da..eedb36e4e 100644 --- a/modules/web/web-app/pages/tenants/customer/customers.tsx +++ b/modules/web/web-app/pages/tenants/customer/customers.tsx @@ -1,9 +1,13 @@ -import { Button, Flex } from '@ui/index' +import { SearchIcon } from '@md/icons' +import { Button, InputWithIcon, Separator } from '@md/ui' +import { Flex } from '@ui/index' +import { ListFilter } from 'lucide-react' import { Fragment, FunctionComponent, useState } from 'react' import { EmptyState } from '@/components/empty-state/EmptyState' -import { TenantPageLayout } from '@/components/layouts' -import { CustomersEditPanel, CustomersHeader, CustomersTable } from '@/features/customers' +import { PageLayout } from '@/components/layouts/PageLayout' +import { CustomersEditPanel, CustomersTable } from '@/features/customers' +import { CustomersExportModal } from '@/features/customers/modals/CustomersExportModal' import { useDebounceValue } from '@/hooks/useDebounce' import { useQuery } from '@/lib/connectrpc' import { listCustomers } from '@/rpc/api/customers/v1/customers-CustomersService_connectquery' @@ -14,6 +18,7 @@ import type { PaginationState } from '@tanstack/react-table' export const Customers: FunctionComponent = () => { const [createPanelVisible, setCreatePanelVisible] = useState(false) const [search, setSearch] = useState('') + const [exportModalVisible, setExportModalVisible] = useState(false) const debouncedSearch = useDebounceValue(search, 400) @@ -43,39 +48,70 @@ export const Customers: FunctionComponent = () => { return ( - - - + + + } + > +
    + +
    + + } + width="fit-content" + value={search} + onChange={e => setSearch(e.target.value)} /> - {isEmpty ? ( - setCreatePanelVisible(true)}> - New customer - - } - /> - ) : ( - - )} + -
    + {isEmpty ? ( + setCreatePanelVisible(true)}> + New customer + + } + /> + ) : ( + + )} + setCreatePanelVisible(false)} /> +
    ) } diff --git a/modules/web/web-app/pages/tenants/invoice/Invoices.tsx b/modules/web/web-app/pages/tenants/invoice/Invoices.tsx index 916cc7401..c110238a7 100644 --- a/modules/web/web-app/pages/tenants/invoice/Invoices.tsx +++ b/modules/web/web-app/pages/tenants/invoice/Invoices.tsx @@ -1,8 +1,12 @@ -import { spaces } from '@md/foundation' -import { Flex } from '@ui/components/legacy' +import { SearchIcon } from '@md/icons' +import { Button, InputWithIcon } from '@md/ui' +import { Flex } from '@ui/index' +import { RefreshCwIcon } from 'lucide-react' import { Fragment, useState } from 'react' -import { InvoicesHeader, InvoicesTable } from '@/features/invoices' +import { PageLayout } from '@/components/layouts/PageLayout' +import { InvoicesTable } from '@/features/invoices' +import { FilterDropdown } from '@/features/invoices/FilterDropdown' import { InvoicesSearch } from '@/features/invoices/types' import { useDebounceValue } from '@/hooks/useDebounce' import { useQuery } from '@/lib/connectrpc' @@ -12,7 +16,7 @@ import { ListInvoicesRequest_SortBy } from '@/rpc/api/invoices/v1/invoices_pb' import type { PaginationState } from '@tanstack/react-table' export const Invoices = () => { - const [, setEditPanelVisible] = useState(false) + const [_, setEditPanelVisible] = useState(false) const [search, setSearch] = useState({}) const debouncedSearch = useDebounceValue(search, 400) @@ -44,17 +48,40 @@ export const Invoices = () => { invoicesQuery.refetch() } + const tabs = [ + { key: 'active', label: 'Active' }, + { key: 'expired', label: 'Expired' }, + { key: 'cancelled', label: 'Cancelled' } + ] + return ( - - + setEditPanelVisible(true)} size="sm"> + New invoice + + } + > + + } + width="fit-content" + value={search.text} + onChange={e => setSearch({ ...search, text: e.target.value })} + /> + + setSearch({ ...search, status: value })} + /> + { setPagination={setPagination} isLoading={isLoading} /> - + ) } diff --git a/modules/web/web-app/pages/tenants/subscription/subscriptions.tsx b/modules/web/web-app/pages/tenants/subscription/subscriptions.tsx index d9c74b9a7..4ab9fba99 100644 --- a/modules/web/web-app/pages/tenants/subscription/subscriptions.tsx +++ b/modules/web/web-app/pages/tenants/subscription/subscriptions.tsx @@ -1,8 +1,12 @@ -import { spaces } from '@md/foundation' -import { Flex } from '@ui/components/legacy' +import { SearchIcon } from '@md/icons' +import { Button, InputWithIcon } from '@ui/components' +import { Flex } from '@ui/index' +import { RefreshCwIcon } from 'lucide-react' import { useState } from 'react' +import { Link } from 'react-router-dom' -import { SubscriptionsHeader, SubscriptionsTable } from '@/features/subscriptions' +import { PageLayout } from '@/components/layouts/PageLayout' +import { SubscriptionsTable } from '@/features/subscriptions' import { useQuery } from '@/lib/connectrpc' import { listSubscriptions } from '@/rpc/api/subscriptions/v1/subscriptions-SubscriptionsService_connectquery' @@ -33,9 +37,36 @@ export const Subscriptions = () => { subscriptionsQuery.refetch() } + const tabs = [ + { key: 'active', label: 'Active' }, + { key: 'expired', label: 'Expired' }, + { key: 'cancelled', label: 'Cancelled' } + ] + return ( - - + + + + } + > + + } + width="fit-content" + disabled + /> + + { setPagination={setPagination} isLoading={isLoading} /> - + ) } diff --git a/modules/web/web-app/public/header/customer.svg b/modules/web/web-app/public/header/customers.svg similarity index 100% rename from modules/web/web-app/public/header/customer.svg rename to modules/web/web-app/public/header/customers.svg diff --git a/modules/web/web-app/public/header/invoices.svg b/modules/web/web-app/public/header/invoices.svg new file mode 100644 index 000000000..c77a5aba2 --- /dev/null +++ b/modules/web/web-app/public/header/invoices.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/web/web-app/public/header/subscriptions.svg b/modules/web/web-app/public/header/subscriptions.svg new file mode 100644 index 000000000..42d4d72bb --- /dev/null +++ b/modules/web/web-app/public/header/subscriptions.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/web/web-app/router/tenant/billing.tsx b/modules/web/web-app/router/tenant/billing.tsx index ba19f62ca..1315beeb7 100644 --- a/modules/web/web-app/router/tenant/billing.tsx +++ b/modules/web/web-app/router/tenant/billing.tsx @@ -1,14 +1,14 @@ -import { RouteObject } from 'react-router-dom' +import { Outlet, RouteObject } from 'react-router-dom' import { NotImplemented } from '@/features/NotImplemented' -import { Billing, BillingOutlet } from '@/pages/tenants/billing' +import { Billing } from '@/pages/tenants/billing' import { Invoice, Invoices } from '@/pages/tenants/invoice' import { Subscriptions } from '@/pages/tenants/subscription' import { Subscription } from '@/pages/tenants/subscription/subscription' import { SubscriptionCreate } from '@/pages/tenants/subscription/subscriptionCreate' export const billingRoutes: RouteObject = { - element: , + element: , children: [ { index: true, diff --git a/modules/web/web-app/vite.config.ts b/modules/web/web-app/vite.config.ts index 4af7dbc76..4405ce5c4 100644 --- a/modules/web/web-app/vite.config.ts +++ b/modules/web/web-app/vite.config.ts @@ -1,6 +1,7 @@ import basicSsl from '@vitejs/plugin-basic-ssl' import react from '@vitejs/plugin-react' import { UserConfigExport, defineConfig } from 'vite' +import checker from 'vite-plugin-checker' import svgr from 'vite-plugin-svgr' import tsconfigPaths from 'vite-tsconfig-paths' @@ -22,7 +23,14 @@ export default defineConfig(({ mode }) => { }, }, envDir: '../../../', - plugins: [react(), tsconfigPaths(), svgr()], + plugins: [ + react(), + tsconfigPaths(), + svgr(), + checker({ + typescript: true, + }), + ], } return localSsl ? {