diff --git a/bun.lock b/bun.lock index 4c0845a..3cbbc3d 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "@libsql/client": "^0.14.0", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", @@ -20,10 +21,12 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", "@react-pdf/renderer": "^4.3.0", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.69.0", + "@tanstack/react-table": "^8.21.3", "@trpc/client": "^11.0.0", "@trpc/react-query": "^11.0.0", "@trpc/server": "^11.0.0", @@ -37,7 +40,7 @@ "file-saver": "^2.0.5", "lucide": "^0.525.0", "lucide-react": "^0.525.0", - "next": "^15.2.3", + "next": "^15.4.1", "next-auth": "5.0.0-beta.25", "react": "^19.0.0", "react-day-picker": "^9.8.0", @@ -77,6 +80,8 @@ "trustedDependencies": [ "@tailwindcss/oxide", "better-sqlite3", + "esbuild", + "sharp", "unrs-resolver", ], "packages": { @@ -192,47 +197,49 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.0" }, "os": "darwin", "cpu": "x64" }, "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.0" }, "os": "linux", "cpu": "arm" }, "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.0" }, "os": "linux", "cpu": "ppc64" }, "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.0" }, "os": "linux", "cpu": "s390x" }, "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.2", "", { "dependencies": { "@emnapi/runtime": "^1.4.3" }, "cpu": "none" }, "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.3", "", { "dependencies": { "@emnapi/runtime": "^1.4.4" }, "cpu": "none" }, "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -272,25 +279,25 @@ "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], - "@next/env": ["@next/env@15.3.5", "", {}, "sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g=="], + "@next/env": ["@next/env@15.4.1", "", {}, "sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.3.5", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-BZwWPGfp9po/rAnJcwUBaM+yT/+yTWIkWdyDwc74G9jcfTrNrmsHe+hXHljV066YNdVs8cxROxX5IgMQGX190w=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -314,6 +321,8 @@ "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -360,6 +369,8 @@ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], @@ -412,8 +423,6 @@ "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="], - "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@t3-oss/env-core": ["@t3-oss/env-core@0.12.0", "", { "peerDependencies": { "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0" }, "optionalPeers": ["typescript", "valibot", "zod"] }, "sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw=="], @@ -454,6 +463,10 @@ "@tanstack/react-query": ["@tanstack/react-query@5.82.0", "", { "dependencies": { "@tanstack/query-core": "5.82.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-mnk8/ofKEthFeMdhV1dV8YXRf+9HqvXAcciXkoo755d/ocfWq7N/Y9jGOzS3h7ZW9dDGwSIhs3/HANWUBsyqYg=="], + "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], + + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@trpc/client": ["@trpc/client@11.4.3", "", { "peerDependencies": { "@trpc/server": "11.4.3", "typescript": ">=5.7.2" } }, "sha512-i2suttUCfColktXT8bqex5kHW5jpT15nwUh0hGSDiW1keN621kSUQKcLJ095blqQAUgB+lsmgSqSMmB4L9shQQ=="], "@trpc/react-query": ["@trpc/react-query@11.4.3", "", { "peerDependencies": { "@tanstack/react-query": "^5.80.3", "@trpc/client": "11.4.3", "@trpc/server": "11.4.3", "react": ">=18.2.0", "react-dom": ">=18.2.0", "typescript": ">=5.7.2" } }, "sha512-z+jhAiOBD22NNhHtvF0iFp9hO36YFA7M8AiUu/XtNmMxyLd3Y9/d1SMjMwlTdnGqxEGPo41VEWBrdhDUGtUuHg=="], @@ -614,8 +627,6 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -1034,7 +1045,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "next": ["next@15.3.5", "", { "dependencies": { "@next/env": "15.3.5", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.5", "@next/swc-darwin-x64": "15.3.5", "@next/swc-linux-arm64-gnu": "15.3.5", "@next/swc-linux-arm64-musl": "15.3.5", "@next/swc-linux-x64-gnu": "15.3.5", "@next/swc-linux-x64-musl": "15.3.5", "@next/swc-win32-arm64-msvc": "15.3.5", "@next/swc-win32-x64-msvc": "15.3.5", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RkazLBMMDJSJ4XZQ81kolSpwiCt907l0xcgcpF4xC2Vml6QVcPNXW0NQRwQ80FFtSn7UM52XN0anaw8TEJXaiw=="], + "next": ["next@15.4.1", "", { "dependencies": { "@next/env": "15.4.1", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.1", "@next/swc-darwin-x64": "15.4.1", "@next/swc-linux-arm64-gnu": "15.4.1", "@next/swc-linux-arm64-musl": "15.4.1", "@next/swc-linux-x64-gnu": "15.4.1", "@next/swc-linux-x64-musl": "15.4.1", "@next/swc-win32-arm64-msvc": "15.4.1", "@next/swc-win32-x64-msvc": "15.4.1", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw=="], "next-auth": ["next-auth@5.0.0-beta.25", "", { "dependencies": { "@auth/core": "0.37.2" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog=="], @@ -1180,7 +1191,7 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - "sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="], + "sharp": ["sharp@0.34.3", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3", "@img/sharp-libvips-darwin-arm64": "1.2.0", "@img/sharp-libvips-darwin-x64": "1.2.0", "@img/sharp-libvips-linux-arm": "1.2.0", "@img/sharp-libvips-linux-arm64": "1.2.0", "@img/sharp-libvips-linux-ppc64": "1.2.0", "@img/sharp-libvips-linux-s390x": "1.2.0", "@img/sharp-libvips-linux-x64": "1.2.0", "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", "@img/sharp-libvips-linuxmusl-x64": "1.2.0", "@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm64": "0.34.3", "@img/sharp-linux-ppc64": "0.34.3", "@img/sharp-linux-s390x": "0.34.3", "@img/sharp-linux-x64": "0.34.3", "@img/sharp-linuxmusl-arm64": "0.34.3", "@img/sharp-linuxmusl-x64": "0.34.3", "@img/sharp-wasm32": "0.34.3", "@img/sharp-win32-arm64": "0.34.3", "@img/sharp-win32-ia32": "0.34.3", "@img/sharp-win32-x64": "0.34.3" } }, "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1214,8 +1225,6 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], - "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], diff --git a/docs/RESPONSIVE_TABLE_EXAMPLES.md b/docs/RESPONSIVE_TABLE_EXAMPLES.md new file mode 100644 index 0000000..abdbc4b --- /dev/null +++ b/docs/RESPONSIVE_TABLE_EXAMPLES.md @@ -0,0 +1,138 @@ +# Responsive Table Examples + +This document shows how tables adapt across different screen sizes in the beenvoice application. + +## Mobile View (< 640px) + +### Invoices Table +- **Visible**: Invoice number, client name, amount, status, actions +- **Hidden**: Issue date, due date (shown on detail view) +- **Features**: Compact spacing, smaller buttons, simplified pagination + +### Clients Table +- **Visible**: Name with email, actions +- **Hidden**: Phone, address, created date +- **Icon**: Hidden on mobile to save space + +### Businesses Table +- **Visible**: Name with email, actions +- **Hidden**: Phone, address, tax ID, website +- **Icon**: Hidden on mobile to save space + +## Tablet View (640px - 1024px) + +### Invoices Table +- **Added**: Issue date column +- **Still Hidden**: Due date (less critical than issue date) +- **Features**: Search bar expands, column visibility toggle appears + +### Clients Table +- **Added**: Phone column, client icon +- **Still Hidden**: Address, created date +- **Features**: Better spacing, full search functionality + +### Businesses Table +- **Added**: Phone column, business icon +- **Still Hidden**: Address, tax ID +- **Features**: Website links become visible + +## Desktop View (> 1024px) + +### All Tables +- **Full Features**: All columns visible +- **Enhanced**: + - Full pagination controls with page size selector + - Column visibility toggle + - Advanced filters + - Comfortable spacing + - All metadata visible + +## Code Examples + +### Responsive Column Definition +```tsx +// Hide on mobile, show on tablet and up +{ + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.original.phone || "—"} + ), +} + +// Hide on mobile and tablet, show on desktop +{ + id: "address", + header: "Address", + cell: ({ row }) => ( + {formatAddress(row.original)} + ), +} +``` + +### Responsive Cell Content +```tsx +// Icon hidden on mobile +
+
+ +
+
+

{client.name}

+

+ {client.email || "—"} +

+
+
+``` + +### Responsive Actions +```tsx +// Compact action buttons that work on all screen sizes +
+ + +
+``` + +## Filter Bar Behavior + +### Mobile +- Search input takes full width +- Filter dropdowns stack vertically +- Column visibility hidden +- Clear filters button visible when filters active + +### Tablet+ +- Search input limited to max-width +- Filter dropdowns in horizontal row +- Column visibility toggle appears +- All controls in single row + +## Pagination Behavior + +### Mobile +- Simplified page indicator (1/5 format) +- Compact button spacing +- Page size selector with smaller text + +### Desktop +- Full "Page 1 of 5" text +- Comfortable button spacing +- First/Last page buttons visible +- Entries count with detailed information + +## Best Practices + +1. **Priority Content**: Always show the most important data on mobile +2. **Progressive Enhancement**: Add columns as screen size increases +3. **Touch Targets**: Maintain 44px minimum touch targets on mobile +4. **Text Truncation**: Use `truncate` class for long text in narrow columns +5. **Icon Usage**: Hide decorative icons on mobile, keep functional ones +6. **Testing**: Always test at 375px (iPhone SE), 768px (iPad), and 1440px (Desktop) \ No newline at end of file diff --git a/docs/UI_UNIFORMITY_GUIDE.md b/docs/UI_UNIFORMITY_GUIDE.md new file mode 100644 index 0000000..5194361 --- /dev/null +++ b/docs/UI_UNIFORMITY_GUIDE.md @@ -0,0 +1,324 @@ +# UI Uniformity Guide for beenvoice + +## Overview + +This guide documents the unified component system implemented across the beenvoice application to ensure consistent UI/UX patterns. The system follows a hierarchical approach where: + +1. **CSS Variables** (in `globals.css`) define the design tokens +2. **UI Components** (in `components/ui`) consume these variables +3. **Pages** use components with minimal additional styling + +## Design System Principles + +### 1. Variable-Based Theming +All colors, spacing, and other design tokens are defined as CSS variables in `globals.css`: +- Brand colors: `--brand-primary`, `--brand-secondary` +- Status colors: `--status-success`, `--status-warning`, `--status-error`, `--status-info` +- Semantic colors: `--background`, `--foreground`, `--muted`, etc. + +### 2. Component Composition +Complex UI patterns are built from smaller, reusable components rather than duplicating code. + +### 3. Minimal Page-Level Styling +Pages should primarily compose pre-built components and avoid custom Tailwind classes where possible. + +## Core Unified Components + +### Page Layout Components + +#### `PageContent` +Wraps page content with consistent spacing: +```tsx + + {/* Page sections */} + +``` + +#### `PageSection` +Groups related content with optional title and actions: +```tsx +Action} +> + {/* Section content */} + +``` + +#### `PageGrid` +Responsive grid layout with preset column options: +```tsx + + {/* Grid items */} + +``` + +### Data Display Components + +#### `DataTable` +Unified table component using @tanstack/react-table with floating card design: +```tsx +import { ColumnDef } from "@tanstack/react-table"; +import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table"; +import { PageSection } from "~/components/ui/page-layout"; + +const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const name = row.getValue("name") as string; + return
{name}
; + } + }, + { + id: "actions", + cell: ({ row }) => { + const item = row.original; + return ( + + ); + } + } +]; + +const filterableColumns = [ + { + id: "status", + title: "Status", + options: [ + { label: "Active", value: "active" }, + { label: "Inactive", value: "inactive" } + ] + } +]; + +// Wrap in PageSection for title/description + + + +``` + +Features: +- **Floating Card Design**: Three separate cards for filter bar, table content, and pagination +- **Filter Bar Card**: Minimal padding (p-3) with global search and column filters +- **Table Content Card**: Clean borders with overflow handling +- **Pagination Card**: Compact controls with page size selector +- **Responsive Design**: Mobile-optimized with hidden columns on smaller screens +- **Tight Appearance**: Compact spacing with smaller action buttons +- **Sorting**: Visual indicators with proper arrow directions +- **Column Visibility**: Toggle columns (hidden on mobile) +- **Dark Mode**: Consistent styling across light/dark themes +- **Loading States**: DataTableSkeleton component with matching card structure + +#### `StatsCard` +Displays statistics with consistent styling: +```tsx + +``` + +#### `QuickActionCard` +Interactive cards for navigation or actions: +```tsx + + +
+ + +``` + +### Feedback Components + +#### `EmptyState` +Consistent empty state displays: +```tsx +} + title="No invoices yet" + description="Create your first invoice to get started" + action={} +/> +``` + +## Component Variants + +### Color Variants +Most components support these variants: +- `default` - Uses default theme colors +- `success` - Green color scheme for positive states +- `warning` - Orange/amber for warnings +- `error` - Red for errors or destructive actions +- `info` - Blue for informational content + +### Size Variants +- `sm` - Small size +- `default` - Normal size +- `lg` - Large size + +## Usage Examples + +### Standard Page Structure +```tsx +export default function ExamplePage() { + return ( + + + + + + + + + + + + + + + + ); +} +``` + +### Consistent Button Usage +```tsx +// Primary actions + + +// Secondary actions + + +// Destructive actions + + +// Icon-only actions + +``` + +## Styling Guidelines + +### Do's +- ✅ Use predefined color variables from globals.css +- ✅ Compose existing UI components +- ✅ Use semantic variant names (success, error, etc.) +- ✅ Follow the established spacing patterns +- ✅ Use the PageLayout components for structure + +### Don'ts +- ❌ Add custom colors directly in components +- ❌ Create one-off table or card implementations +- ❌ Override component styles with important flags +- ❌ Use arbitrary spacing values +- ❌ Mix different UI patterns on the same page + +## Migration Checklist + +When updating a page to use the unified system: + +1. Replace custom tables with `DataTable` using @tanstack/react-table ColumnDef +2. Replace statistics displays with `StatsCard` +3. Replace action cards with `QuickActionCard` +4. Wrap content in `PageContent` and `PageSection` +5. Use `PageGrid` for responsive layouts +6. Replace custom empty states with `EmptyState` +7. Update buttons to use the `brand` variant for primary actions +8. Remove page-specific color classes +9. Use `DataTableColumnHeader` for sortable column headers +10. Use `DataTableSkeleton` for loading states + +## Color System Reference + +### Brand Colors +- Primary: Green (`#16a34a` / `oklch(0.646 0.222 164.25)`) +- Secondary: Teal/cyan shades +- Gradients: Use `bg-brand-gradient` class + +### Status Colors +- Success: Green shades +- Warning: Amber/orange shades +- Error: Red shades +- Info: Blue shades + +### Semantic Colors +- Background: White/dark gray +- Foreground: Black/white text +- Muted: Gray shades for secondary content +- Border: Light gray borders + +## Component Documentation + +For detailed component APIs and props, refer to: +- `/src/components/ui/data-table.tsx` - TanStack Table-based data table with sorting, filtering, and pagination +- `/src/components/ui/stats-card.tsx` - Statistics display cards +- `/src/components/ui/quick-action-card.tsx` - Interactive action cards +- `/src/components/ui/page-layout.tsx` - Page structure components + +### DataTable Props +- `columns`: ColumnDef array from @tanstack/react-table +- `data`: Array of data to display +- `searchPlaceholder?`: Placeholder text for search input +- `showColumnVisibility?`: Show/hide column visibility toggle (default: true) +- `showPagination?`: Show/hide pagination controls (default: true) +- `showSearch?`: Show/hide search input (default: true) +- `pageSize?`: Number of items per page (default: 10) +- `filterableColumns?`: Array of column filters with options + +Note: `title` and `description` should be provided via the wrapping `PageSection` component for consistent spacing and typography. + +### Responsive Table Guidelines +- Use `hidden sm:flex` classes for icons in table cells +- Use `hidden md:inline` for less important columns on mobile +- Use `min-w-0` and `truncate` for text that might overflow +- Keep action buttons small with `h-8 w-8 p-0` sizing +- Test tables at all breakpoints (mobile, tablet, desktop) + +## Future Considerations + +1. **Form Components**: Create unified form field components +2. **Modal Patterns**: Standardize modal and dialog usage +3. **Loading States**: Create consistent skeleton loaders +4. **Animation**: Define standard transition patterns +5. **Icons**: Establish icon usage guidelines + +## Maintenance + +To maintain UI consistency: +1. Always check for existing components before creating new ones +2. Update this guide when adding new unified components +3. Review PRs for adherence to these patterns +4. Refactor pages that deviate from the system \ No newline at end of file diff --git a/docs/breadcrumbs-guide.md b/docs/breadcrumbs-guide.md new file mode 100644 index 0000000..63dfbc3 --- /dev/null +++ b/docs/breadcrumbs-guide.md @@ -0,0 +1,198 @@ +# Dynamic Breadcrumbs Guide + +## Overview + +The breadcrumb system in beenvoice automatically generates navigation trails based on the current URL path. It features intelligent pluralization, proper capitalization, and dynamic resource name fetching. + +## Key Features + +### 1. Automatic Pluralization + +The breadcrumb system intelligently handles singular and plural forms: + +- **List pages** (e.g., `/dashboard/businesses`) → "Businesses" +- **Detail pages** (e.g., `/dashboard/businesses/[id]`) → "Business" +- **New pages** (e.g., `/dashboard/businesses/new`) → "Business" (singular context) + +### 2. Smart Capitalization + +All route segments are automatically capitalized: +- `businesses` → "Businesses" +- `clients` → "Clients" +- `invoices` → "Invoices" + +### 3. Dynamic Resource Names + +Instead of showing UUIDs, breadcrumbs fetch and display actual resource names: +- `/dashboard/clients/123e4567-e89b-12d3-a456-426614174000` → "Dashboard / Clients / John Doe" +- `/dashboard/invoices/987fcdeb-51a2-43f1-b321-123456789abc` → "Dashboard / Invoices / INV-2024-001" + +### 4. Context-Aware Labels + +Special pages are handled intelligently: +- **Edit pages**: Show the resource name instead of "Edit" as the last breadcrumb +- **New pages**: Show "New" as the last breadcrumb +- **Import/Export pages**: Show appropriate action labels + +## Implementation Details + +### Pluralization Rules + +The system uses a comprehensive pluralization utility (`src/lib/pluralize.ts`) that handles: + +```typescript +// Common business terms +business → businesses +client → clients +invoice → invoices +category → categories +company → companies + +// General rules +- Words ending in 's', 'ss', 'sh', 'ch', 'x', 'z' → add 'es' +- Words ending in consonant + 'y' → change to 'ies' +- Words ending in 'f' or 'fe' → change to 'ves' +- Default → add 's' +``` + +### Resource Fetching + +The breadcrumbs automatically detect resource IDs and fetch the appropriate data: + +```typescript +// Detects UUID patterns in the URL +const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + +// Fetches data based on resource type +- Clients: Shows client name +- Invoices: Shows invoice number or formatted date +- Businesses: Shows business name +``` + +### Loading States + +While fetching resource data, breadcrumbs show loading skeletons: +```tsx + +``` + +## Usage Examples + +### Basic List Page +**URL**: `/dashboard/clients` +**Breadcrumbs**: Dashboard / Clients + +### Resource Detail Page +**URL**: `/dashboard/clients/550e8400-e29b-41d4-a716-446655440000` +**Breadcrumbs**: Dashboard / Clients / Jane Smith + +### Resource Edit Page +**URL**: `/dashboard/businesses/550e8400-e29b-41d4-a716-446655440000/edit` +**Breadcrumbs**: Dashboard / Businesses / Acme Corp +*(Note: "Edit" is hidden when showing the resource name)* + +### New Resource Page +**URL**: `/dashboard/invoices/new` +**Breadcrumbs**: Dashboard / Invoices / New + +### Nested Resources +**URL**: `/dashboard/clients/550e8400-e29b-41d4-a716-446655440000/invoices` +**Breadcrumbs**: Dashboard / Clients / John Doe / Invoices + +## Customization + +### Adding New Resource Types + +To add a new resource type, update the pluralization rules: + +```typescript +// In src/lib/pluralize.ts +const PLURALIZATION_RULES = { + // ... existing rules + product: { singular: "Product", plural: "Products" }, + service: { singular: "Service", plural: "Services" }, +}; +``` + +### Custom Resource Labels + +For resources that need custom display logic, add to the breadcrumb component: + +```typescript +// For invoices, show invoice number instead of ID +if (prevSegment === "invoices") { + label = invoice.invoiceNumber || format(new Date(invoice.issueDate), "MMM dd, yyyy"); +} +``` + +### Special Segments + +Add new special segments to the `SPECIAL_SEGMENTS` object: + +```typescript +const SPECIAL_SEGMENTS = { + new: "New", + edit: "Edit", + import: "Import", + export: "Export", + duplicate: "Duplicate", + archive: "Archive", +}; +``` + +## Best Practices + +1. **Consistent Naming**: Use consistent URL patterns across your app + - List pages: `/dashboard/[resource]` + - Detail pages: `/dashboard/[resource]/[id]` + - Actions: `/dashboard/[resource]/[id]/[action]` + +2. **Resource Fetching**: Only fetch data when needed + - Check resource type before enabling queries + - Use proper loading states + +3. **Error Handling**: Handle cases where resources don't exist + - Show fallback text or maintain UUID display + - Don't break the breadcrumb trail + +4. **Performance**: Breadcrumb queries are lightweight + - Only fetch minimal data (id, name) + - Use React Query caching effectively + +## API Integration + +The breadcrumb component integrates with tRPC routers: + +```typescript +// Each resource router should have a getById method +getById: protectedProcedure + .input(z.object({ id: z.string() })) + .query(async ({ ctx, input }) => { + // Return resource with at least id and name/title + }) +``` + +## Accessibility + +- Breadcrumbs use semantic HTML with proper ARIA labels +- Each segment is a link except the current page +- Proper keyboard navigation support +- Screen reader friendly with role="navigation" + +## Responsive Design + +- Breadcrumbs wrap on smaller screens +- Font sizes adjust: `text-sm sm:text-base` +- Separators scale appropriately +- Loading skeletons match text size + +## Migration from Static Breadcrumbs + +If migrating from hardcoded breadcrumbs: + +1. Remove static breadcrumb definitions +2. Ensure URLs follow consistent patterns +3. Add getById methods to resource routers +4. Update imports to use `DashboardBreadcrumbs` + +The dynamic system will automatically generate appropriate breadcrumbs based on the URL structure. \ No newline at end of file diff --git a/docs/data-table-improvements.md b/docs/data-table-improvements.md new file mode 100644 index 0000000..41d6d4f --- /dev/null +++ b/docs/data-table-improvements.md @@ -0,0 +1,154 @@ +# Data Table Improvements Summary + +## Overview + +The data table component has been significantly improved to address padding, scaling, and responsiveness issues. The tables now provide a cleaner, more compact appearance while maintaining excellent usability across all device sizes. + +## Key Improvements Made + +### 1. Tighter, More Consistent Padding + +**Before:** +- Inconsistent padding across different table sections +- Excessive vertical padding making tables feel loose +- Cards had default py-6 padding that was too spacious + +**After:** +- Table cells: `py-1.5` (mobile) / `py-2` (desktop) - reduced from `py-2.5` / `py-3` +- Table headers: `h-9` (mobile) / `h-10` (desktop) - reduced from `h-10` / `h-12` +- Filter/pagination cards: `py-2` with `px-3` horizontal padding +- Table card: `p-0` to wrap content tightly + +### 2. Improved Responsive Column Handling + +**Before:** +```tsx +// Cells would hide but headers remained visible +cell: ({ row }) => ( + {row.original.phone} +), +``` + +**After:** +```tsx +// Both header and cell hide together +cell: ({ row }) => row.original.phone || "—", +meta: { + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", +}, +``` + +### 3. Better Small Card Appearance + +- Filter card: Compact `py-2` padding with proper horizontal spacing +- Pagination card: Matching `py-2` padding for consistency +- Content aligned properly within smaller card boundaries +- Removed excessive gaps between elements +- Search box now has consistent padding without extra bottom spacing on mobile + +### 4. Responsive Font Sizing + +- Base text: `text-xs` on mobile, `text-sm` on desktop +- Consistent scaling across all table elements +- Better readability on small screens without wasting space + +## Visual Comparison + +### Table Density +- **Before**: ~60px per row with excessive padding +- **After**: ~40px per row with comfortable but efficient spacing + +### Card Heights +- **Filter Card**: Reduced from ~80px to ~56px +- **Pagination Card**: Reduced from ~72px to ~48px +- **Table Card**: Now wraps content exactly with no extra space +- **Pagination Layout**: Entry count and pagination controls now stay on the same line on mobile + +## Implementation Examples + +### Responsive Column Definition +```tsx +const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.name, + // Always visible + }, + { + accessorKey: "email", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.email, + meta: { + // Hidden on mobile, visible on tablets and up + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => formatDate(row.getValue("createdAt")), + meta: { + // Only visible on large screens + headerClassName: "hidden lg:table-cell", + cellClassName: "hidden lg:table-cell", + }, + }, +]; +``` + +### Page Header Actions +Page headers now properly position action buttons to the right on all screen sizes: + +```tsx + + + +``` + +### Breakpoint Reference +- `sm`: 640px and up +- `md`: 768px and up +- `lg`: 1024px and up +- `xl`: 1280px and up + +## Benefits + +1. **More Data Visible**: Tighter spacing allows more rows to be visible without scrolling +2. **Professional Appearance**: Clean, compact design suitable for business applications +3. **Better Mobile UX**: Properly hidden columns prevent layout breaking +4. **Consistent Styling**: All table instances now follow the same spacing rules +5. **Performance**: CSS-only solution with no JavaScript overhead +6. **Improved Mobile Layout**: Pagination controls stay inline with entry count on mobile +7. **Consistent Header Actions**: Action buttons properly positioned to the right + +## Migration Checklist + +- [x] Update column definitions to use `meta` properties +- [x] Remove inline responsive classes from cell content +- [x] Test on actual mobile devices +- [x] Verify touch targets remain accessible (min 44x44px) +- [x] Check that critical data remains visible on small screens + +## Best Practices Going Forward + +1. **Column Priority**: Always keep the most important 2-3 columns visible on mobile +2. **Content Density**: Use the tighter spacing for data tables, looser spacing for content lists +3. **Responsive Testing**: Test at 320px, 768px, and 1024px minimum +4. **Accessibility**: Ensure interactive elements maintain proper touch targets despite tighter spacing \ No newline at end of file diff --git a/docs/data-table-responsive-guide.md b/docs/data-table-responsive-guide.md new file mode 100644 index 0000000..b8bc32f --- /dev/null +++ b/docs/data-table-responsive-guide.md @@ -0,0 +1,246 @@ +# Data Table Responsive Design Guide + +## Overview + +The data table component has been updated to provide better responsive behavior, consistent padding, and proper scaling across different screen sizes. + +## Key Improvements + +### 1. Consistent Padding +- Uniform padding across all table elements +- Responsive padding that scales with screen size +- Cards now have consistent spacing (p-3 on mobile, p-4 on desktop) + +### 2. Proper Responsive Column Hiding +- Columns now properly hide both headers and cells on smaller screens +- Uses `meta` properties for clean column visibility control +- No more orphaned headers on mobile devices + +### 3. Better Scaling +- Font sizes adapt to screen size (text-xs on mobile, text-sm on desktop) +- Button sizes and spacing adjust appropriately +- Pagination controls are optimized for touch devices + +## Using Responsive Columns + +### Basic Column Definition + +```tsx +const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.name, + // Always visible on all screen sizes + }, + { + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.phone || "—", + meta: { + // Hidden on mobile, visible on md screens and up + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, + }, + { + accessorKey: "address", + header: "Address", + cell: ({ row }) => formatAddress(row.original), + meta: { + // Hidden on mobile and tablet, visible on lg screens and up + headerClassName: "hidden lg:table-cell", + cellClassName: "hidden lg:table-cell", + }, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => formatDate(row.getValue("createdAt")), + meta: { + // Only visible on xl screens and up + headerClassName: "hidden xl:table-cell", + cellClassName: "hidden xl:table-cell", + }, + }, +]; +``` + +### Responsive Breakpoints + +- **Always visible**: No meta properties needed +- **md and up** (768px+): `hidden md:table-cell` +- **lg and up** (1024px+): `hidden lg:table-cell` +- **xl and up** (1280px+): `hidden xl:table-cell` + +## Complex Cell Content + +For cells with complex content that should partially hide on mobile: + +```tsx +{ + accessorKey: "client", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const client = row.original; + return ( +
+ {/* Icon hidden on mobile, shown on sm screens */} +
+ +
+
+

{client.name}

+ {/* Secondary info can be hidden on very small screens if needed */} +

+ {client.email || "—"} +

+
+
+ ); + }, +} +``` + +## Best Practices + +### 1. Priority-Based Column Hiding +- Always show the most important columns (e.g., name, status, primary action) +- Hide supplementary information first (e.g., dates, secondary details) +- Consider hiding decorative elements (icons) on mobile while keeping text + +### 2. Mobile-First Design +- Ensure at least 2-3 columns are visible on mobile +- Test on actual devices, not just browser dev tools +- Consider the minimum viable information for each row + +### 3. Touch-Friendly Actions +- Action buttons should be at least 44x44px on mobile +- Use appropriate spacing between interactive elements +- Consider grouping actions in a dropdown on mobile + +### 4. Performance +- The responsive system uses CSS classes, so there's no JavaScript overhead +- Column visibility is handled by Tailwind's responsive utilities +- No re-renders needed when resizing + +## Migration Guide + +If you have existing data tables, update them as follows: + +### Before: +```tsx +{ + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + {row.original.phone || "—"} + ), +} +``` + +### After: +```tsx +{ + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.phone || "—", + meta: { + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, +} +``` + +## Common Patterns + +### Status Columns +Always visible, use color and icons to convey information efficiently: + +```tsx +{ + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => , +} +``` + +### Date Columns +Often hidden on mobile, show relative dates when space is limited: + +```tsx +{ + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date; + return ( + <> + {/* Full date on larger screens */} + {formatDate(date)} + {/* Relative date on mobile */} + {formatRelativeDate(date)} + + ); + }, +} +``` + +### Action Columns +Keep actions accessible but space-efficient: + +```tsx +{ + id: "actions", + cell: ({ row }) => { + const item = row.original; + return ( +
+ {/* Show individual buttons on larger screens */} +
+ + +
+ {/* Dropdown menu on mobile */} +
+ +
+
+ ); + }, +} +``` + +## Testing Checklist + +- [ ] Table is readable on 320px wide screens +- [ ] Headers and cells align properly at all breakpoints +- [ ] Touch targets are at least 44x44px on mobile +- [ ] Horizontal scrolling works smoothly when needed +- [ ] Critical information is always visible +- [ ] Loading states work correctly +- [ ] Empty states are responsive +- [ ] Pagination controls are touch-friendly + +## Accessibility Notes + +- Hidden columns are properly hidden from screen readers +- Table remains navigable with keyboard at all screen sizes +- Sort controls are accessible on mobile +- Focus indicators are visible on all interactive elements \ No newline at end of file diff --git a/docs/forms-guide.md b/docs/forms-guide.md new file mode 100644 index 0000000..8a0a6bd --- /dev/null +++ b/docs/forms-guide.md @@ -0,0 +1,279 @@ +# Forms Improvement Guide + +## Overview + +The business and client creation/editing forms have been significantly improved with better organization, shared components, enhanced validation, and improved user experience. + +## Key Improvements + +### 1. Shared Components & Utilities + +#### Address Form Component (`src/components/ui/address-form.tsx`) +A reusable address form component that handles: +- Country-aware formatting (US ZIP codes, Canadian postal codes) +- State dropdown for US addresses, text input for other countries +- Popular countries listed first in country dropdown +- Automatic field adjustments based on country selection + +```tsx + +``` + +#### Form Constants & Utilities (`src/lib/form-constants.ts`) +Centralized location for: +- US states list with proper formatting +- All countries with ISO codes +- Popular countries for quick selection +- Format functions for phone, postal codes, tax IDs, and URLs +- Validation utilities and messages + +### 2. Enhanced Form Validation + +#### Real-time Validation +- Errors clear as soon as user starts typing +- Field-specific validation messages +- Visual feedback with red borders on invalid fields + +#### Smart Validation Rules +- Email: Proper email format checking +- Phone: US phone number format validation +- Address: Required fields only if any address field is filled +- URL: Automatic https:// prefix addition + +```typescript +// Example validation +if (formData.email && !isValidEmail(formData.email)) { + newErrors.email = VALIDATION_MESSAGES.email; +} +``` + +### 3. Better Form Organization + +#### Card-based Sections +Forms are now organized into logical sections using cards: +- **Basic Information**: Core fields like name, tax ID +- **Contact Information**: Email, phone, website +- **Address**: Complete address form with smart country handling +- **Settings**: Business-specific settings like default business flag + +#### Consistent Layout +- Maximum width container for better readability +- Responsive grid layouts that stack on mobile +- Proper spacing between sections +- Clear visual hierarchy + +### 4. Improved User Experience + +#### Loading States +- Skeleton loader while fetching data in edit mode +- Disabled form fields during submission +- Loading spinner in submit button + +#### Unsaved Changes Warning +```typescript +const handleCancel = () => { + if (isDirty) { + const confirmed = window.confirm( + "You have unsaved changes. Are you sure you want to leave?" + ); + if (!confirmed) return; + } + router.push("/dashboard/businesses"); +}; +``` + +#### Smart Field Formatting +- Phone numbers: Auto-format as (555) 123-4567 +- Tax ID: Auto-format as 12-3456789 +- Postal codes: Format based on country (US vs Canadian) +- Website URLs: Auto-add https:// if missing + +### 5. Responsive Design + +#### Mobile Optimizations +- Form sections stack vertically on small screens +- Touch-friendly input sizes +- Proper button positioning +- Readable font sizes + +#### Desktop Enhancements +- Two-column layouts for related fields +- Optimal reading width +- Side-by-side form actions + +### 6. Code Reusability + +#### Shared Between Business & Client Forms +- Address form component +- Validation logic +- Format functions +- Constants (states, countries) +- Error handling patterns + +#### TypeScript Interfaces +```typescript +interface FormData { + name: string; + email: string; + phone: string; + // ... other fields +} + +interface FormErrors { + name?: string; + email?: string; + // ... validation errors +} +``` + +## Usage Examples + +### Basic Form Implementation +```tsx +export function BusinessForm({ businessId, mode }: BusinessFormProps) { + const [formData, setFormData] = useState(initialFormData); + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDirty, setIsDirty] = useState(false); + + // Handle input changes + const handleInputChange = (field: string, value: string | boolean) => { + setFormData((prev) => ({ ...prev, [field]: value })); + setIsDirty(true); + + // Clear error when user types + if (errors[field as keyof FormErrors]) { + setErrors((prev) => ({ ...prev, [field]: undefined })); + } + }; + + // Validate and submit + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + toast.error("Please correct the errors in the form"); + return; + } + + // Submit logic... + }; +} +``` + +### Field with Icon and Validation +```tsx +
+ +
+ + handleInputChange("email", e.target.value)} + placeholder={PLACEHOLDERS.email} + className={`pl-10 ${errors.email ? "border-destructive" : ""}`} + disabled={isSubmitting} + /> +
+ {errors.email && ( +

{errors.email}

+ )} +
+``` + +## Best Practices + +### 1. Form State Management +- Use controlled components for all inputs +- Track dirty state for unsaved changes warnings +- Clear errors when user corrects them +- Disable form during submission + +### 2. Validation Strategy +- Validate on submit, not on blur (less annoying) +- Clear errors immediately when user starts fixing them +- Show field-level errors below each input +- Use consistent error message format + +### 3. Accessibility +- Proper label associations with htmlFor +- Required field indicators +- Error messages linked to fields +- Keyboard navigation support +- Focus management + +### 4. Performance +- Memoize expensive computations +- Use debouncing for format functions if needed +- Lazy load country lists +- Optimize re-renders with proper state management + +## Migration Guide + +### From Old Forms +1. Replace inline state/country arrays with imported constants +2. Use `AddressForm` component instead of individual address fields +3. Apply format functions from `form-constants.ts` +4. Update validation to use shared utilities +5. Wrap sections in Card components +6. Add loading and dirty state tracking + +### Example Migration +```tsx +// Before +const US_STATES = [ + { value: "AL", label: "Alabama" }, + // ... duplicated in each form +]; + +// After +import { US_STATES, formatPhoneNumber } from "~/lib/form-constants"; +import { AddressForm } from "~/components/ui/address-form"; +``` + +## Future Enhancements + +### Planned Improvements +1. **Field-level permissions**: Disable fields based on user role +2. **Auto-save**: Save draft as user types +3. **Multi-step forms**: Break long forms into steps +4. **Conditional fields**: Show/hide fields based on other values +5. **Bulk operations**: Create multiple records at once +6. **Import from templates**: Pre-fill common business types + +### Extensibility +The form system is designed to be easily extended: +- Add new format functions to `form-constants.ts` +- Create additional shared form components +- Extend validation rules as needed +- Add new field types with consistent patterns + +## Troubleshooting + +### Common Issues + +1. **Validation not working**: Ensure field names match FormErrors interface +2. **Format function not applying**: Check that onChange uses the format function +3. **Country dropdown not searching**: Verify SearchableSelect has search enabled +4. **Address validation failing**: Check if country field affects validation rules + +### Debug Tips +- Use React DevTools to inspect form state +- Check console for validation errors +- Verify API responses match expected format +- Test with different country selections \ No newline at end of file diff --git a/package.json b/package.json index 43fc857..bfcf56c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@libsql/client": "^0.14.0", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", @@ -42,10 +43,12 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", "@react-pdf/renderer": "^4.3.0", "@t3-oss/env-nextjs": "^0.12.0", "@tanstack/react-query": "^5.69.0", + "@tanstack/react-table": "^8.21.3", "@trpc/client": "^11.0.0", "@trpc/react-query": "^11.0.0", "@trpc/server": "^11.0.0", @@ -59,7 +62,7 @@ "file-saver": "^2.0.5", "lucide": "^0.525.0", "lucide-react": "^0.525.0", - "next": "^15.2.3", + "next": "^15.4.1", "next-auth": "5.0.0-beta.25", "react": "^19.0.0", "react-day-picker": "^9.8.0", @@ -101,6 +104,8 @@ "@tailwindcss/oxide", "better-sqlite3", "core-js", + "esbuild", + "sharp", "unrs-resolver" ] } diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx index 0310c3f..c74077d 100644 --- a/src/app/auth/register/page.tsx +++ b/src/app/auth/register/page.tsx @@ -49,25 +49,25 @@ function RegisterForm() { } return ( -
+
{/* Logo and Welcome */}
-

+

Join beenvoice

-

+

Create your account to get started

{/* Registration Form */} - + - + Create Account @@ -77,7 +77,7 @@ function RegisterForm() {
- +
- +
- +
- +
-

+

Must be at least 6 characters

@@ -152,7 +152,7 @@ function RegisterForm() {
- + Already have an account?{" "} -

+

Start invoicing like a pro

-
+
✓ Free to start ✓ No credit card ✓ Cancel anytime @@ -185,17 +185,15 @@ export default function RegisterPage() { return ( +
-

+

Join beenvoice

-

- Loading... -

+

Loading...

diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index d5f923d..93c6a88 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -42,34 +42,30 @@ function SignInForm() { } return ( -
+
{/* Logo and Welcome */}
-

- Welcome back -

-

+

Welcome back

+

Sign in to your beenvoice account

{/* Sign In Form */} - + - - Sign In - + Sign In
- +
- +
- + Don't have an account?{" "} -

+

Simple invoicing for freelancers and small businesses

-
+
✓ Easy client management ✓ Professional invoices ✓ Payment tracking @@ -142,17 +138,15 @@ export default function SignInPage() { return ( +
-

+

Welcome back

-

- Loading... -

+

Loading...

diff --git a/src/app/clients/[id]/edit/page.tsx b/src/app/clients/[id]/edit/page.tsx new file mode 100644 index 0000000..7ca8cba --- /dev/null +++ b/src/app/clients/[id]/edit/page.tsx @@ -0,0 +1,44 @@ +import { auth } from "~/server/auth"; +import { HydrateClient } from "~/trpc/server"; +import { Button } from "~/components/ui/button"; +import { ClientForm } from "~/components/client-form"; +import Link from "next/link"; + +interface EditClientPageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function EditClientPage({ params }: EditClientPageProps) { + const { id } = await params; + const session = await auth(); + + if (!session?.user) { + return ( +
+
+

Access Denied

+

+ Please sign in to edit clients +

+ + + +
+
+ ); + } + + return ( + +
+
+

Edit Client

+

Update client information

+
+ +
+
+ ); +} diff --git a/src/app/clients/new/page.tsx b/src/app/clients/new/page.tsx new file mode 100644 index 0000000..3f8325a --- /dev/null +++ b/src/app/clients/new/page.tsx @@ -0,0 +1,37 @@ +import { auth } from "~/server/auth"; +import { HydrateClient } from "~/trpc/server"; +import { Button } from "~/components/ui/button"; +import { ClientForm } from "~/components/client-form"; +import Link from "next/link"; + +export default async function NewClientPage() { + const session = await auth(); + + if (!session?.user) { + return ( +
+
+

Access Denied

+

Please sign in to create clients

+ + + +
+
+ ); + } + + return ( + +
+
+

Add New Client

+

+ Create a new client profile +

+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/app/dashboard/_components/dashboard-components.tsx b/src/app/dashboard/_components/dashboard-components.tsx index e578211..60c3b5e 100644 --- a/src/app/dashboard/_components/dashboard-components.tsx +++ b/src/app/dashboard/_components/dashboard-components.tsx @@ -3,6 +3,7 @@ import { api } from "~/trpc/react"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; +import { StatusBadge, type StatusType } from "~/components/ui/status-badge"; import { Users, FileText, @@ -44,60 +45,60 @@ export function DashboardStats() { return (
- + - + Total Clients -
- +
+
-
+
{totalClients}
-

+

{totalClients > lastMonthClients ? "+" : ""} {totalClients - lastMonthClients} from last month

- + - + Total Invoices -
- +
+
-
+
{totalInvoices}
-

+

{totalInvoices > lastMonthInvoices ? "+" : ""} {totalInvoices - lastMonthInvoices} from last month

- + - + Revenue -
- +
+
-
+
${totalRevenue.toFixed(2)}
-

+

{totalRevenue > lastMonthRevenue ? "+" : ""} {( ((totalRevenue - lastMonthRevenue) / (lastMonthRevenue || 1)) * @@ -108,22 +109,20 @@ export function DashboardStats() { - + - + Pending Invoices -

- +
+
-
+
{pendingInvoices}
-

- Due this month -

+

Due this month

@@ -134,34 +133,27 @@ export function DashboardStats() { export function DashboardCards() { return (
- + - -
+ +
Manage Clients
-

+

Add new clients and manage your existing client relationships.

- - - + + + +
+ {/* Business Information Card */} +
+ + + + + Business Information + + + + {/* Basic Info */} +
+ {business.email && ( +
+
+ +
+
+

+ Email +

+

+ {business.email} +

+
+
+ )} + + {business.phone && ( +
+
+ +
+
+

+ Phone +

+

+ {business.phone} +

+
+
+ )} + + {business.website && ( +
+
+ +
+
+

+ Website +

+ + {business.website} + +
+
+ )} + + {business.taxId && ( +
+
+ +
+
+

+ Tax ID +

+

+ {business.taxId} +

+
+
+ )} +
+ + {/* Address */} + {(business.addressLine1 ?? business.city ?? business.state) && ( +
+
+
+ +
+
+

+ Address +

+
+
+
+ {business.addressLine1 &&

{business.addressLine1}

} + {business.addressLine2 &&

{business.addressLine2}

} + {(business.city ?? business.state ?? business.postalCode) && ( +

+ {[business.city, business.state, business.postalCode] + .filter(Boolean) + .join(", ")} +

+ )} + {business.country &&

{business.country}

} +
+
+ )} + + {/* Business Since */} +
+
+ +
+
+

+ Business Added +

+

+ {formatDate(business.createdAt)} +

+
+
+ + {/* Default Business Badge */} + {business.isDefault && ( +
+
+ +
+
+

+ Status +

+ + Default Business + +
+
+ )} +
+
+
+ + {/* Settings & Actions Card */} +
+ + + + + Business Settings + + + +
+

+ Default Business +

+

+ {business.isDefault ? ( + + Yes + + ) : ( + No + )} +

+
+ +
+

+ Quick Actions +

+
+ + + + + + +
+
+
+
+ + {/* Information Card */} + + + + About This Business + + + +
+

+ This business profile is used for generating invoices and + represents your company information to clients. +

+ {business.isDefault && ( +

+ This is your default business and will be automatically + selected when creating new invoices. +

+ )} +
+
+
+
+
+
+
+ ); +} diff --git a/src/app/dashboard/businesses/_components/businesses-data-table.tsx b/src/app/dashboard/businesses/_components/businesses-data-table.tsx new file mode 100644 index 0000000..ff91b85 --- /dev/null +++ b/src/app/dashboard/businesses/_components/businesses-data-table.tsx @@ -0,0 +1,238 @@ +"use client"; + +import Link from "next/link"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "~/components/ui/button"; +import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table"; +import { Building, Pencil, Trash2, ExternalLink } from "lucide-react"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { api } from "~/trpc/react"; +import { toast } from "sonner"; + +// Type for business data +interface Business { + id: string; + name: string; + email: string | null; + phone: string | null; + addressLine1: string | null; + addressLine2: string | null; + city: string | null; + state: string | null; + postalCode: string | null; + country: string | null; + website: string | null; + taxId: string | null; + logoUrl: string | null; + createdById: string; + createdAt: Date; + updatedAt: Date | null; +} + +interface BusinessesDataTableProps { + businesses: Business[]; +} + +const formatAddress = (business: Business) => { + const parts = [ + business.addressLine1, + business.addressLine2, + business.city, + business.state, + business.postalCode, + ].filter(Boolean); + return parts.join(", ") || "—"; +}; + +export function BusinessesDataTable({ + businesses: initialBusinesses, +}: BusinessesDataTableProps) { + const [businesses, setBusinesses] = useState(initialBusinesses); + const [businessToDelete, setBusinessToDelete] = useState( + null, + ); + + const utils = api.useUtils(); + + const deleteBusinessMutation = api.businesses.delete.useMutation({ + onSuccess: () => { + toast.success("Business deleted successfully"); + setBusinesses(businesses.filter((b) => b.id !== businessToDelete?.id)); + setBusinessToDelete(null); + void utils.businesses.getAll.invalidate(); + }, + onError: (error) => { + toast.error(`Failed to delete business: ${error.message}`); + }, + }); + + const handleDelete = () => { + if (!businessToDelete) return; + deleteBusinessMutation.mutate({ id: businessToDelete.id }); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const business = row.original; + return ( +
+
+ +
+
+

{business.name}

+

+ {business.email ?? "—"} +

+
+
+ ); + }, + }, + { + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.phone ?? "—", + meta: { + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, + }, + { + id: "address", + header: "Address", + cell: ({ row }) => formatAddress(row.original), + meta: { + headerClassName: "hidden lg:table-cell", + cellClassName: "hidden lg:table-cell", + }, + }, + { + accessorKey: "taxId", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.taxId ?? "—", + meta: { + headerClassName: "hidden xl:table-cell", + cellClassName: "hidden xl:table-cell", + }, + }, + { + accessorKey: "website", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const website = row.original.website; + if (!website) return "—"; + + // Add https:// if not present + const url = website.startsWith("http") ? website : `https://${website}`; + + return ( + <> + {/* Desktop: Show full URL */} + + {website} + + {/* Mobile: Show link button */} + + + ); + }, + }, + { + id: "actions", + cell: ({ row }) => { + const business = row.original; + return ( +
+ + + + +
+ ); + }, + }, + ]; + + return ( + <> + + + {/* Delete confirmation dialog */} + !open && setBusinessToDelete(null)} + > + + + Are you sure? + + This action cannot be undone. This will permanently delete the + business "{businessToDelete?.name}" and remove all associated + data. + + + + + + + + + + ); +} diff --git a/src/app/dashboard/businesses/_components/businesses-table.tsx b/src/app/dashboard/businesses/_components/businesses-table.tsx index 5853428..3e90ad6 100644 --- a/src/app/dashboard/businesses/_components/businesses-table.tsx +++ b/src/app/dashboard/businesses/_components/businesses-table.tsx @@ -1,15 +1,19 @@ "use client"; import { api } from "~/trpc/react"; -import { UniversalTable } from "~/components/ui/universal-table"; -import { TableSkeleton } from "~/components/ui/skeleton"; +import { DataTableSkeleton } from "~/components/ui/data-table"; +import { BusinessesDataTable } from "./businesses-data-table"; export function BusinessesTable() { - const { isLoading } = api.businesses.getAll.useQuery(); + const { data: businesses, isLoading } = api.businesses.getAll.useQuery(); if (isLoading) { - return ; + return ; } - return ; -} \ No newline at end of file + if (!businesses) { + return null; + } + + return ; +} diff --git a/src/app/dashboard/businesses/new/page.tsx b/src/app/dashboard/businesses/new/page.tsx index 304ccc3..5f4f857 100644 --- a/src/app/dashboard/businesses/new/page.tsx +++ b/src/app/dashboard/businesses/new/page.tsx @@ -1,5 +1,20 @@ +import Link from "next/link"; import { BusinessForm } from "~/components/business-form"; - +import { PageHeader } from "~/components/page-header"; +import { HydrateClient } from "~/trpc/server"; + export default function NewBusinessPage() { - return ; -} \ No newline at end of file + return ( +
+ + + + + +
+ ); +} diff --git a/src/app/dashboard/businesses/page.tsx b/src/app/dashboard/businesses/page.tsx index 2754c6a..6af75ee 100644 --- a/src/app/dashboard/businesses/page.tsx +++ b/src/app/dashboard/businesses/page.tsx @@ -1,35 +1,30 @@ import Link from "next/link"; - -import { api, HydrateClient } from "~/trpc/server"; +import { HydrateClient } from "~/trpc/server"; import { Button } from "~/components/ui/button"; import { Plus } from "lucide-react"; import { BusinessesTable } from "./_components/businesses-table"; +import { PageHeader } from "~/components/page-header"; +import { PageContent, PageSection } from "~/components/ui/page-layout"; export default async function BusinessesPage() { return ( -
-
-
-

- Businesses -

-

- Manage your businesses and their information. -

-
- -
+ + -
+ ); } diff --git a/src/app/dashboard/clients/[id]/edit/page.tsx b/src/app/dashboard/clients/[id]/edit/page.tsx index eee52cb..5760922 100644 --- a/src/app/dashboard/clients/[id]/edit/page.tsx +++ b/src/app/dashboard/clients/[id]/edit/page.tsx @@ -1,5 +1,7 @@ +import Link from "next/link"; import { HydrateClient } from "~/trpc/server"; import { ClientForm } from "~/components/client-form"; +import { PageHeader } from "~/components/page-header"; interface EditClientPageProps { params: Promise<{ id: string }>; @@ -10,14 +12,11 @@ export default async function EditClientPage({ params }: EditClientPageProps) { return (
-
-

- Edit Client -

-

- Update client information below. -

-
+ diff --git a/src/app/dashboard/clients/[id]/page.tsx b/src/app/dashboard/clients/[id]/page.tsx index 40f2f36..d2c1ae7 100644 --- a/src/app/dashboard/clients/[id]/page.tsx +++ b/src/app/dashboard/clients/[id]/page.tsx @@ -3,6 +3,7 @@ import { api } from "~/trpc/server"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; +import { PageHeader } from "~/components/page-header"; import Link from "next/link"; import { Edit, @@ -53,32 +54,27 @@ export default async function ClientDetailPage({ client.invoices?.filter((invoice) => invoice.status === "sent").length || 0; return ( -
+
- {/* Header */} -
-
-

- {client.name} -

-

- Client Details -

-
- - -
+
{/* Client Information Card */}
- + - + Contact Information @@ -92,10 +88,10 @@ export default async function ClientDetailPage({
-

+

Email

-

+

{client.email}

@@ -108,10 +104,10 @@ export default async function ClientDetailPage({
-

+

Phone

-

+

{client.phone}

@@ -127,12 +123,12 @@ export default async function ClientDetailPage({
-

+

Address

-
+
{client.addressLine1 &&

{client.addressLine1}

} {client.addressLine2 &&

{client.addressLine2}

} {(client.city ?? client.state ?? client.postalCode) && ( diff --git a/src/app/dashboard/clients/_components/clients-data-table.tsx b/src/app/dashboard/clients/_components/clients-data-table.tsx new file mode 100644 index 0000000..33352f2 --- /dev/null +++ b/src/app/dashboard/clients/_components/clients-data-table.tsx @@ -0,0 +1,201 @@ +"use client"; + +import Link from "next/link"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "~/components/ui/button"; +import { DataTable, DataTableColumnHeader } from "~/components/ui/data-table"; +import { UserPlus, Pencil, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { api } from "~/trpc/react"; +import { toast } from "sonner"; + +// Type for client data +interface Client { + id: string; + name: string; + email: string | null; + phone: string | null; + addressLine1: string | null; + addressLine2: string | null; + city: string | null; + state: string | null; + postalCode: string | null; + country: string | null; + createdById: string; + createdAt: Date; + updatedAt: Date | null; +} + +interface ClientsDataTableProps { + clients: Client[]; +} + +const formatAddress = (client: Client) => { + const parts = [ + client.addressLine1, + client.addressLine2, + client.city, + client.state, + client.postalCode, + ].filter(Boolean); + return parts.join(", ") || "—"; +}; + +export function ClientsDataTable({ + clients: initialClients, +}: ClientsDataTableProps) { + const [clients, setClients] = useState(initialClients); + const [clientToDelete, setClientToDelete] = useState(null); + + const utils = api.useUtils(); + + const deleteClientMutation = api.clients.delete.useMutation({ + onSuccess: () => { + toast.success("Client deleted successfully"); + setClients(clients.filter((c) => c.id !== clientToDelete?.id)); + setClientToDelete(null); + void utils.clients.getAll.invalidate(); + }, + onError: (error) => { + toast.error(`Failed to delete client: ${error.message}`); + }, + }); + + const handleDelete = () => { + if (!clientToDelete) return; + deleteClientMutation.mutate({ id: clientToDelete.id }); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const client = row.original; + return ( +
+
+ +
+
+

{client.name}

+

+ {client.email || "—"} +

+
+
+ ); + }, + }, + { + accessorKey: "phone", + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.phone || "—", + meta: { + headerClassName: "hidden md:table-cell", + cellClassName: "hidden md:table-cell", + }, + }, + { + id: "address", + header: "Address", + cell: ({ row }) => formatAddress(row.original), + meta: { + headerClassName: "hidden lg:table-cell", + cellClassName: "hidden lg:table-cell", + }, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const date = row.getValue("createdAt") as Date; + return new Intl.DateTimeFormat("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + }).format(new Date(date)); + }, + meta: { + headerClassName: "hidden xl:table-cell", + cellClassName: "hidden xl:table-cell", + }, + }, + { + id: "actions", + cell: ({ row }) => { + const client = row.original; + return ( +
+ + + + +
+ ); + }, + }, + ]; + + return ( + <> + + + {/* Delete confirmation dialog */} + !open && setClientToDelete(null)} + > + + + Are you sure? + + This action cannot be undone. This will permanently delete the + client "{clientToDelete?.name}" and remove all associated data. + + + + + + + + + + ); +} diff --git a/src/app/dashboard/clients/_components/clients-table.tsx b/src/app/dashboard/clients/_components/clients-table.tsx index 6f96cad..bd7d3e5 100644 --- a/src/app/dashboard/clients/_components/clients-table.tsx +++ b/src/app/dashboard/clients/_components/clients-table.tsx @@ -1,15 +1,19 @@ "use client"; import { api } from "~/trpc/react"; -import { UniversalTable } from "~/components/ui/universal-table"; -import { TableSkeleton } from "~/components/ui/skeleton"; +import { DataTableSkeleton } from "~/components/ui/data-table"; +import { ClientsDataTable } from "./clients-data-table"; export function ClientsTable() { - const { isLoading } = api.clients.getAll.useQuery(); + const { data: clients, isLoading } = api.clients.getAll.useQuery(); if (isLoading) { - return ; + return ; } - return ; -} \ No newline at end of file + if (!clients) { + return null; + } + + return ; +} diff --git a/src/app/dashboard/clients/new/page.tsx b/src/app/dashboard/clients/new/page.tsx index c6e98ae..b7f5518 100644 --- a/src/app/dashboard/clients/new/page.tsx +++ b/src/app/dashboard/clients/new/page.tsx @@ -1,17 +1,16 @@ +import Link from "next/link"; import { HydrateClient } from "~/trpc/server"; import { ClientForm } from "~/components/client-form"; +import { PageHeader } from "~/components/page-header"; export default async function NewClientPage() { return (
-
-

- Add Client -

-

- Enter client details below to add a new client. -

-
+ diff --git a/src/app/dashboard/clients/page.tsx b/src/app/dashboard/clients/page.tsx index 6779dff..f143eca 100644 --- a/src/app/dashboard/clients/page.tsx +++ b/src/app/dashboard/clients/page.tsx @@ -1,35 +1,30 @@ import Link from "next/link"; - -import { api, HydrateClient } from "~/trpc/server"; +import { HydrateClient } from "~/trpc/server"; import { Button } from "~/components/ui/button"; import { Plus } from "lucide-react"; import { ClientsTable } from "./_components/clients-table"; +import { PageHeader } from "~/components/page-header"; +import { PageContent, PageSection } from "~/components/ui/page-layout"; export default async function ClientsPage() { return ( -
-
-
-

- Clients -

-

- Manage your clients and their information. -

-
- -
+ + -
+ ); } diff --git a/src/app/dashboard/invoices/[id]/_components/pdf-download-button.tsx b/src/app/dashboard/invoices/[id]/_components/pdf-download-button.tsx new file mode 100644 index 0000000..5966385 --- /dev/null +++ b/src/app/dashboard/invoices/[id]/_components/pdf-download-button.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "~/components/ui/button"; +import { toast } from "sonner"; +import { generateInvoicePDF } from "~/lib/pdf-export"; +import { Download, Loader2 } from "lucide-react"; + +interface Invoice { + id: string; + invoiceNumber: string; + issueDate: Date; + dueDate: Date; + status: string; + totalAmount: number; + taxRate: number; + notes?: string | null; + business?: { + name: string; + email?: string | null; + phone?: string | null; + addressLine1?: string | null; + addressLine2?: string | null; + city?: string | null; + state?: string | null; + postalCode?: string | null; + country?: string | null; + website?: string | null; + taxId?: string | null; + } | null; + client: { + name: string; + email?: string | null; + phone?: string | null; + addressLine1?: string | null; + addressLine2?: string | null; + city?: string | null; + state?: string | null; + postalCode?: string | null; + country?: string | null; + }; + items: Array<{ + date: Date; + description: string; + hours: number; + rate: number; + amount: number; + }>; +} + +interface PDFDownloadButtonProps { + invoice: Invoice; + variant?: "button" | "menu" | "icon"; +} + +export function PDFDownloadButton({ + invoice, + variant = "button", +}: PDFDownloadButtonProps) { + const [isGenerating, setIsGenerating] = useState(false); + + const handleDownloadPDF = async () => { + if (isGenerating) return; + + setIsGenerating(true); + + try { + // Transform the invoice data to match the PDF interface + const pdfData = { + invoiceNumber: invoice.invoiceNumber, + issueDate: invoice.issueDate, + dueDate: invoice.dueDate, + status: invoice.status, + totalAmount: invoice.totalAmount, + taxRate: invoice.taxRate, + notes: invoice.notes, + business: invoice.business, + client: invoice.client, + items: invoice.items, + }; + + await generateInvoicePDF(pdfData); + + toast.success("PDF downloaded successfully"); + } catch (error) { + console.error("PDF generation error:", error); + toast.error( + error instanceof Error ? error.message : "Failed to generate PDF", + ); + } finally { + setIsGenerating(false); + } + }; + + if (variant === "menu") { + return ( + + ); + } + + if (variant === "icon") { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/app/dashboard/invoices/[id]/edit/page.tsx b/src/app/dashboard/invoices/[id]/edit/page.tsx new file mode 100644 index 0000000..8efcc96 --- /dev/null +++ b/src/app/dashboard/invoices/[id]/edit/page.tsx @@ -0,0 +1,770 @@ +"use client"; + +import React, { useState, useEffect, useRef } from "react"; +import { useRouter, useParams } from "next/navigation"; +import Link from "next/link"; +import { api } from "~/trpc/react"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { NumberInput } from "~/components/ui/number-input"; +import { Label } from "~/components/ui/label"; +import { Textarea } from "~/components/ui/textarea"; +import { PageHeader } from "~/components/page-header"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "~/components/ui/alert-dialog"; +import { Badge } from "~/components/ui/badge"; +import { Separator } from "~/components/ui/separator"; +import { DatePicker } from "~/components/ui/date-picker"; +import { FloatingActionBar } from "~/components/ui/floating-action-bar"; +import { toast } from "sonner"; +import { + ArrowLeft, + Save, + Plus, + Trash2, + FileText, + Building, + User, + Loader2, + Send, + DollarSign, + Hash, + Edit3, + Eye, +} from "lucide-react"; + +interface EditInvoicePageProps {} + +interface InvoiceItem { + id?: string; + tempId: string; + date: Date; + description: string; + hours: number; + rate: number; + amount: number; +} + +interface InvoiceFormData { + invoiceNumber: string; + businessId: string | undefined; + clientId: string; + issueDate: Date; + dueDate: Date; + notes: string; + taxRate: number; + items: InvoiceItem[]; + status: "draft" | "sent" | "paid" | "overdue"; +} + +function InvoiceItemCard({ + item, + index, + onUpdate, + onDelete, + _isLast, +}: { + item: InvoiceItem; + index: number; + onUpdate: ( + index: number, + field: keyof InvoiceItem, + value: string | number | Date, + ) => void; + onDelete: (index: number) => void; + _isLast: boolean; +}) { + const handleFieldChange = ( + field: keyof InvoiceItem, + value: string | number | Date, + ) => { + onUpdate(index, field, value); + }; + + return ( + +
+ {/* Header with item number and delete */} +
+ + Item {index + 1} + + + + + + + + Delete Item + + Are you sure you want to delete this line item? This action + cannot be undone. + + + + Cancel + onDelete(index)} + className="bg-red-600 hover:bg-red-700" + > + Delete + + + + +
+ + {/* Description */} +