diff --git a/package.json b/package.json
index 3260796..5c82ae7 100755
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"react-day-picker": "8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.2",
+ "recharts": "^2.13.3",
"server-only": "^0.0.1",
"sonner": "^1.5.0",
"tailwind-merge": "^2.4.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 136cfe0..fcd5445 100755
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -113,6 +113,9 @@ importers:
react-hook-form:
specifier: ^7.52.2
version: 7.52.2(react@18.3.1)
+ recharts:
+ specifier: ^2.13.3
+ version: 2.13.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
server-only:
specifier: ^0.0.1
version: 0.0.1
@@ -1489,6 +1492,33 @@ packages:
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+ '@types/d3-array@3.2.1':
+ resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.0':
+ resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==}
+
+ '@types/d3-scale@4.0.8':
+ resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
+
+ '@types/d3-shape@3.1.6':
+ resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
'@types/eslint@8.56.11':
resolution: {integrity: sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==}
@@ -1829,6 +1859,50 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.0:
+ resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
d@1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'}
@@ -1868,6 +1942,9 @@ packages:
supports-color:
optional: true
+ decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
deep-equal@2.2.3:
resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
engines: {node: '>= 0.4'}
@@ -1911,6 +1988,9 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
+ dom-helpers@5.2.1:
+ resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+
dotenv@16.4.5:
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'}
@@ -2208,12 +2288,19 @@ packages:
event-emitter@0.3.5:
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
+ eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
ext@1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ fast-equals@5.0.1:
+ resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
+ engines: {node: '>=6.0.0'}
+
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@@ -2393,6 +2480,10 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@@ -2629,6 +2720,9 @@ packages:
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
+ lodash@4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -3053,6 +3147,9 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-is@18.3.1:
+ resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
react-remove-scroll-bar@2.3.6:
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
@@ -3093,6 +3190,12 @@ packages:
'@types/react':
optional: true
+ react-smooth@4.0.1:
+ resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@@ -3103,6 +3206,12 @@ packages:
'@types/react':
optional: true
+ react-transition-group@4.4.5:
+ resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
+ peerDependencies:
+ react: '>=16.6.0'
+ react-dom: '>=16.6.0'
+
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -3114,6 +3223,16 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
+ recharts-scale@0.4.5:
+ resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
+
+ recharts@2.13.3:
+ resolution: {integrity: sha512-YDZ9dOfK9t3ycwxgKbrnDlRC4BHdjlY73fet3a0C1+qGMjXVZe6+VXmpOIIhzkje5MMEL8AN4hLIe4AMskBzlA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ react: ^16.0.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
+
reflect.getprototypeof@1.0.6:
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
engines: {node: '>= 0.4'}
@@ -3340,6 +3459,9 @@ packages:
resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==}
engines: {node: '>=0.12'}
+ tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -3451,6 +3573,9 @@ packages:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
+ victory-vendor@36.9.2:
+ resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+
which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
@@ -4582,6 +4707,30 @@ snapshots:
'@types/cookie@0.6.0': {}
+ '@types/d3-array@3.2.1': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.0': {}
+
+ '@types/d3-scale@4.0.8':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-shape@3.1.6':
+ dependencies:
+ '@types/d3-path': 3.1.0
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
'@types/eslint@8.56.11':
dependencies:
'@types/estree': 1.0.5
@@ -4975,6 +5124,44 @@ snapshots:
csstype@3.1.3: {}
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-color@3.1.0: {}
+
+ d3-ease@3.0.1: {}
+
+ d3-format@3.1.0: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-path@3.1.0: {}
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.0
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-timer@3.0.1: {}
+
d@1.0.2:
dependencies:
es5-ext: 0.10.64
@@ -5010,6 +5197,8 @@ snapshots:
dependencies:
ms: 2.1.2
+ decimal.js-light@2.5.1: {}
+
deep-equal@2.2.3:
dependencies:
array-buffer-byte-length: 1.0.1
@@ -5069,6 +5258,11 @@ snapshots:
dependencies:
esutils: 2.0.3
+ dom-helpers@5.2.1:
+ dependencies:
+ '@babel/runtime': 7.25.6
+ csstype: 3.1.3
+
dotenv@16.4.5: {}
dreamopt@0.8.0:
@@ -5550,12 +5744,16 @@ snapshots:
d: 1.0.2
es5-ext: 0.10.64
+ eventemitter3@4.0.7: {}
+
ext@1.7.0:
dependencies:
type: 2.7.3
fast-deep-equal@3.1.3: {}
+ fast-equals@5.0.1: {}
+
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -5759,6 +5957,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.0.6
+ internmap@2.0.3: {}
+
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
@@ -5992,6 +6192,8 @@ snapshots:
lodash.throttle@4.1.1: {}
+ lodash@4.17.21: {}
+
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
@@ -6345,6 +6547,8 @@ snapshots:
react-is@16.13.1: {}
+ react-is@18.3.1: {}
+
react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1):
dependencies:
react: 18.3.1
@@ -6386,6 +6590,14 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.3
+ react-smooth@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ fast-equals: 5.0.1
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+
react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1):
dependencies:
get-nonce: 1.0.1
@@ -6395,6 +6607,15 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.3
+ react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.25.6
+ dom-helpers: 5.2.1
+ loose-envify: 1.4.0
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
react@18.3.1:
dependencies:
loose-envify: 1.4.0
@@ -6407,6 +6628,23 @@ snapshots:
dependencies:
picomatch: 2.3.1
+ recharts-scale@0.4.5:
+ dependencies:
+ decimal.js-light: 2.5.1
+
+ recharts@2.13.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ clsx: 2.1.1
+ eventemitter3: 4.0.7
+ lodash: 4.17.21
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-is: 18.3.1
+ react-smooth: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ recharts-scale: 0.4.5
+ tiny-invariant: 1.3.3
+ victory-vendor: 36.9.2
+
reflect.getprototypeof@1.0.6:
dependencies:
call-bind: 1.0.7
@@ -6674,6 +6912,8 @@ snapshots:
es5-ext: 0.10.64
next-tick: 1.1.0
+ tiny-invariant@1.3.3: {}
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -6801,6 +7041,23 @@ snapshots:
- '@types/react'
- '@types/react-dom'
+ victory-vendor@36.9.2:
+ dependencies:
+ '@types/d3-array': 3.2.1
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.8
+ '@types/d3-shape': 3.1.6
+ '@types/d3-time': 3.0.4
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
which-boxed-primitive@1.0.2:
dependencies:
is-bigint: 1.0.4
diff --git a/src/app/account/billtracker/page.tsx b/src/app/account/billtracker/page.tsx
old mode 100755
new mode 100644
index f22f4df..c31bdd6
--- a/src/app/account/billtracker/page.tsx
+++ b/src/app/account/billtracker/page.tsx
@@ -1,19 +1,148 @@
-"use server"
-import { auth } from "~/auth"
-import BreadCrumbBillTracker from "~/components/portal/home/breadcrumb/BreadCrumbBillTracker"
-import BillTrackerCalendar from "~/components/portal/billtracker/BillTrackerCalendar"
+'use client'
-export default async function HomePage() {
- const session = await auth()
- if (!session?.user) return <>>
- return (
-
-
- < BillTrackerCalendar />
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Calendar } from "~/components/ui/calendar"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
+import { Input } from "~/components/ui/input"
+import { Label } from "~/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
+import { Plus, DollarSign } from 'lucide-react'
+
+// Mock data for bills
+const initialBills = [
+ { id: 1, name: 'Electricity', amount: 80, dueDate: new Date(2023, 6, 15), category: 'Utilities' },
+ { id: 2, name: 'Internet', amount: 60, dueDate: new Date(2023, 6, 20), category: 'Utilities' },
+ { id: 3, name: 'Water', amount: 40, dueDate: new Date(2023, 6, 25), category: 'Utilities' },
+]
+
+export default function BillTrackerPage() {
+ const [bills, setBills] = useState(initialBills)
+ const [selectedDate, setSelectedDate] = useState
(new Date())
+ const [isDialogOpen, setIsDialogOpen] = useState(false)
+
+ const handleAddBill = (event: React.FormEvent) => {
+ event.preventDefault()
+ const formData = new FormData(event.currentTarget)
+ const newBill = {
+ id: bills.length + 1,
+ name: formData.get('billName') as string,
+ amount: Number(formData.get('amount')),
+ dueDate: selectedDate as Date,
+ category: formData.get('category') as string,
+ }
+ setBills([...bills, newBill])
+ setIsDialogOpen(false)
+ }
+
+ const getDayContent = (day: Date | undefined) => {
+ if (!day) return null;
+ const dayBills = bills.filter(bill =>
+ bill.dueDate.getDate() === day.getDate() &&
+ bill.dueDate.getMonth() === day.getMonth() &&
+ bill.dueDate.getFullYear() === day.getFullYear()
+ )
+ return dayBills.length > 0 ? (
+
- );
+ ) : null
+ }
+
+ return (
+
+
+
+
+
Bill Tracker
+
+
+
+
+
+
+ getDayContent(date),
+ }}
+ />
+
+
+
+
+
+
+ )
}
+
diff --git a/src/app/account/billtracker/page.tsx.bak b/src/app/account/billtracker/page.tsx.bak
new file mode 100755
index 0000000..f22f4df
--- /dev/null
+++ b/src/app/account/billtracker/page.tsx.bak
@@ -0,0 +1,19 @@
+"use server"
+import { auth } from "~/auth"
+import BreadCrumbBillTracker from "~/components/portal/home/breadcrumb/BreadCrumbBillTracker"
+import BillTrackerCalendar from "~/components/portal/billtracker/BillTrackerCalendar"
+
+export default async function HomePage() {
+ const session = await auth()
+ if (!session?.user) return <>>
+ return (
+
+
+ < BillTrackerCalendar />
+
+ );
+}
diff --git a/src/app/account/documents/page.tsx b/src/app/account/documents/page.tsx
index e69de29..d56e85e 100644
--- a/src/app/account/documents/page.tsx
+++ b/src/app/account/documents/page.tsx
@@ -0,0 +1,63 @@
+import { Button } from "~/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { FileText, Download, Upload } from 'lucide-react'
+
+// Mock data for documents
+const documents = [
+ { id: 1, name: 'Lease Agreement', type: 'PDF', size: '2.5 MB', date: '2023-01-15' },
+ { id: 2, name: 'Move-in Checklist', type: 'DOCX', size: '1.2 MB', date: '2023-01-15' },
+ { id: 3, name: 'Rent Payment Receipt - June 2023', type: 'PDF', size: '0.5 MB', date: '2023-06-01' },
+ { id: 4, name: 'Property Rules and Regulations', type: 'PDF', size: '1.8 MB', date: '2023-01-15' },
+]
+
+export default function DocumentsPage() {
+ return (
+
+
+
+
+ Documents
+
+
+
+
+
+
+
+ Name
+ Type
+ Size
+ Date
+ Action
+
+
+
+ {documents.map((doc) => (
+
+
+
+
+ {doc.name}
+
+
+ {doc.type}
+ {doc.size}
+ {doc.date}
+
+
+
+
+ ))}
+
+
+
+
+
+ )
+}
+
diff --git a/src/app/account/messages/page.tsx b/src/app/account/messages/page.tsx
index e69de29..de40d15 100644
--- a/src/app/account/messages/page.tsx
+++ b/src/app/account/messages/page.tsx
@@ -0,0 +1,70 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Input } from "~/components/ui/input"
+import { Avatar, AvatarFallback } from "~/components/ui/avatar"
+
+// Mock data for messages
+const initialMessages = [
+ { id: 1, sender: 'Property Manager', content: 'Your maintenance request has been received and scheduled for next Tuesday.', timestamp: '2023-06-20T10:30:00Z' },
+ { id: 2, sender: 'Tenant', content: 'Thank you for the quick response. I'll make sure to be available on Tuesday.', timestamp: '2023-06-20T11:15:00Z' },
+ { id: 3, sender: 'Property Manager', content: 'Great! The maintenance team will arrive between 9 AM and 12 PM. Please ensure they have access to the affected area.', timestamp: '2023-06-20T14:00:00Z' },
+]
+
+export default function MessagesPage() {
+ const [messages, setMessages] = useState(initialMessages)
+ const [newMessage, setNewMessage] = useState('')
+
+ const handleSendMessage = (e: React.FormEvent) => {
+ e.preventDefault()
+ if (newMessage.trim()) {
+ const message = {
+ id: messages.length + 1,
+ sender: 'Tenant',
+ content: newMessage,
+ timestamp: new Date().toISOString(),
+ }
+ setMessages([...messages, message])
+ setNewMessage('')
+ }
+ }
+
+ return (
+
+
+
+ Messages
+
+
+
+ {messages.map((message) => (
+
+
+
+ {message.sender[0]}
+
+
+
{message.sender}
+
{message.content}
+
{new Date(message.timestamp).toLocaleString()}
+
+
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/src/app/account/payments/page.tsx b/src/app/account/payments/page.tsx
index e69de29..14ec53f 100644
--- a/src/app/account/payments/page.tsx
+++ b/src/app/account/payments/page.tsx
@@ -0,0 +1,80 @@
+import { Button } from "~/components/ui/button"
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { CreditCard, DollarSign } from 'lucide-react'
+
+// This would typically come from your database
+const paymentHistory = [
+ { id: 1, date: '2023-06-01', amount: 1200, status: 'Paid' },
+ { id: 2, date: '2023-05-01', amount: 1200, status: 'Paid' },
+ { id: 3, date: '2023-04-01', amount: 1200, status: 'Paid' },
+]
+
+export default function PaymentsPage() {
+ return (
+
+
+
+ Current Balance
+
+
+ $1,200.00
+ Due on July 1, 2023
+
+
+
+
+
+
+
+
+ Payment Methods
+ Manage your payment methods
+
+
+
+
+
+
+
Visa ending in 1234
+
Expires 12/2025
+
+
+
+
+
+
+
+
+
+
+
+
+ Payment History
+
+
+
+
+
+ Date
+ Amount
+ Status
+
+
+
+ {paymentHistory.map((payment) => (
+
+ {payment.date}
+ ${payment.amount.toFixed(2)}
+ {payment.status}
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/src/app/account/workorders/page.tsx b/src/app/account/workorders/page.tsx
index e69de29..cb65c53 100644
--- a/src/app/account/workorders/page.tsx
+++ b/src/app/account/workorders/page.tsx
@@ -0,0 +1,135 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
+import { Input } from "~/components/ui/input"
+import { Label } from "~/components/ui/label"
+import { Textarea } from "~/components/ui/textarea"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
+import { PenToolIcon as Tool, Plus } from 'lucide-react'
+
+// This would typically come from your database
+const workOrders = [
+ { id: 1, date: '2023-06-15', issue: 'Leaky faucet', status: 'In Progress' },
+ { id: 2, date: '2023-06-10', issue: 'Broken AC', status: 'Scheduled' },
+ { id: 3, date: '2023-05-28', issue: 'Clogged drain', status: 'Completed' },
+]
+
+export default function WorkOrdersPage() {
+ const [isDialogOpen, setIsDialogOpen] = useState(false)
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault()
+ // Here you would typically send the form data to your server
+ setIsDialogOpen(false)
+ }
+
+ return (
+
+
+
+
+
Work Orders
+
+
+
+
+
+
+
+ Date
+ Issue
+ Status
+ Action
+
+
+
+ {workOrders.map((order) => (
+
+ {order.date}
+ {order.issue}
+ {order.status}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Maintenance Tips
+ Keep your living space in top condition with these tips
+
+
+
+ - Regularly clean or replace HVAC filters
+ - Check and clean gutters seasonally
+ - Test smoke and carbon monoxide detectors monthly
+ - Inspect plumbing fixtures for leaks
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/src/app/admin/documents/page.tsx b/src/app/admin/documents/page.tsx
new file mode 100644
index 0000000..8120952
--- /dev/null
+++ b/src/app/admin/documents/page.tsx
@@ -0,0 +1,223 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Input } from "~/components/ui/input"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
+import { Label } from "~/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
+import { Badge } from "~/components/ui/badge"
+import { Search, Plus, FileText, Download, Trash2, Share2, Upload } from 'lucide-react'
+
+// Mock data for documents
+const documents = [
+ { id: 1, name: "Lease Agreement - John Doe", type: "PDF", size: "2.5 MB", date: "2023-07-01", status: "Active" },
+ { id: 2, name: "Rent Receipt - Jane Smith", type: "PDF", size: "1.2 MB", date: "2023-07-02", status: "Archived" },
+ { id: 3, name: "Maintenance Report - Riverside Condos", type: "DOCX", size: "3.7 MB", date: "2023-07-03", status: "Active" },
+ { id: 4, name: "Property Rules - Sunset Apartments", type: "PDF", size: "1.8 MB", date: "2023-07-04", status: "Active" },
+ // Add more mock data as needed
+]
+
+export default function DocumentsPage() {
+ const [searchTerm, setSearchTerm] = useState("")
+ const [selectedStatus, setSelectedStatus] = useState("All")
+
+ const filteredDocuments = documents.filter(doc =>
+ doc.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
+ (selectedStatus === "All" || doc.status === selectedStatus)
+ )
+
+ return (
+
+
+
+
+
Documents
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-[300px]"
+ />
+
+
+
+
+
+
+
+ Name
+ Type
+ Size
+ Date
+ Status
+ Actions
+
+
+
+ {filteredDocuments.map((doc) => (
+
+ {doc.name}
+ {doc.type}
+ {doc.size}
+ {doc.date}
+
+
+ {doc.status}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Document Statistics
+
+
+
+
+ Overview
+ Usage
+
+
+
+
+
+
+ Total Documents
+
+
+
+
+ {documents.length}
+
+
+
+
+
+ Active Documents
+
+
+
+
+
+ {documents.filter(doc => doc.status === 'Active').length}
+
+
+
+
+
+
+ Total Storage Used
+
+
+
+
+ 9.2 MB
+
+
+
+
+
+ Most Common Type
+
+
+
+
+ PDF
+
+
+
+
+
+ Document usage statistics and charts will be displayed here.
+
+
+
+
+
+ )
+}
+
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
new file mode 100644
index 0000000..bebcc6c
--- /dev/null
+++ b/src/app/admin/layout.tsx
@@ -0,0 +1,62 @@
+import Link from 'next/link'
+import { Button } from "~/components/ui/button"
+import { Card, CardContent } from "~/components/ui/card"
+import { CreditCard, FileText, MessageSquare, PenToolIcon as Tool, BarChart, Users, Home } from 'lucide-react'
+
+export default function AdminLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
Admin Dashboard
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/src/app/admin/messages/page.tsx b/src/app/admin/messages/page.tsx
new file mode 100644
index 0000000..adaf520
--- /dev/null
+++ b/src/app/admin/messages/page.tsx
@@ -0,0 +1,249 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Input } from "~/components/ui/input"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
+import { Label } from "~/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
+import { Textarea } from "~/components/ui/textarea"
+import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar"
+import { Badge } from "~/components/ui/badge"
+import { Search, Plus, MessageSquare, Users, ArrowUpRight } from 'lucide-react'
+
+// Mock data for messages
+const messages = [
+ { id: 1, tenant: "John Doe", property: "Sunset Apartments, Apt 4B", subject: "Maintenance Request", date: "2023-07-05", status: "Unread" },
+ { id: 2, tenant: "Jane Smith", property: "Oakwood Residences, Apt 2A", subject: "Rent Inquiry", date: "2023-07-04", status: "Read" },
+ { id: 3, tenant: "Bob Johnson", property: "Riverside Condos, Apt 3C", subject: "Lease Renewal", date: "2023-07-03", status: "Replied" },
+ { id: 4, tenant: "Alice Brown", property: "Sunset Apartments, Apt 2C", subject: "Noise Complaint", date: "2023-07-02", status: "Unread" },
+ // Add more mock data as needed
+]
+
+export default function MessagesPage() {
+ const [searchTerm, setSearchTerm] = useState("")
+ const [selectedStatus, setSelectedStatus] = useState("All")
+ const [selectedMessage, setSelectedMessage] = useState(null)
+
+ const filteredMessages = messages.filter(message =>
+ (message.tenant.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ message.property.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ message.subject.toLowerCase().includes(searchTerm.toLowerCase())) &&
+ (selectedStatus === "All" || message.status === selectedStatus)
+ )
+
+ return (
+
+
+
+
+
Messages
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-[300px]"
+ />
+
+
+
+
+
+
+
+ Tenant
+ Property
+ Subject
+ Date
+ Status
+ Actions
+
+
+
+ {filteredMessages.map((message) => (
+
+ {message.tenant}
+ {message.property}
+ {message.subject}
+ {message.date}
+
+
+ {message.status}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {selectedMessage && (
+
+
+
+ Message Details
+
+
+
+
+
+
+
+
+ {selectedMessage.tenant[0]}
+
+
+
{selectedMessage.tenant}
+
{selectedMessage.property}
+
+
+
+
Subject: {selectedMessage.subject}
+
Received on {selectedMessage.date}
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ Message Statistics
+
+
+
+
+
+
+ Total Messages
+
+
+
+
+ {messages.length}
+
+
+
+
+
+ Unread Messages
+
+
+
+
+
+ {messages.filter(m => m.status === 'Unread').length}
+
+
+
+
+
+
+ Response Rate
+
+
+
+
+
+ {Math.round((messages.filter(m => m.status === 'Replied').length / messages.length) * 100)}%
+
+
+
+
+
+
+ Avg. Response Time
+
+
+
+
+ 4.2 hours
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
new file mode 100644
index 0000000..9b30c03
--- /dev/null
+++ b/src/app/admin/page.tsx
@@ -0,0 +1,174 @@
+'use client'
+
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Button } from "~/components/ui/button"
+import { ChartContainer, ChartTooltip, ChartTooltipContent } from "~/components/ui/chart"
+import { Bar, BarChart, Line, LineChart, ResponsiveContainer, XAxis, YAxis } from "recharts"
+import { CreditCard, Users, Home, Wrench, AlertTriangle } from 'lucide-react'
+
+// Mock data for charts
+const rentData = [
+ { month: "Jan", collected: 95 },
+ { month: "Feb", collected: 98 },
+ { month: "Mar", collected: 92 },
+ { month: "Apr", collected: 97 },
+ { month: "May", collected: 99 },
+ { month: "Jun", collected: 94 },
+]
+
+const occupancyData = [
+ { month: "Jan", rate: 92 },
+ { month: "Feb", rate: 94 },
+ { month: "Mar", rate: 96 },
+ { month: "Apr", rate: 95 },
+ { month: "May", rate: 97 },
+ { month: "Jun", rate: 98 },
+]
+
+export default function AdminDashboard() {
+ return (
+
+
+
+
+ Total Revenue
+
+
+
+ $24,560
+ +20.1% from last month
+
+
+
+
+ Occupancy Rate
+
+
+
+ 98%
+ +2% from last month
+
+
+
+
+ Total Properties
+
+
+
+ 45
+ +3 new this month
+
+
+
+
+ Active Work Orders
+
+
+
+ 12
+ -5 from last week
+
+
+
+
+
+
+
+ Rent Collection Rate
+
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+
+ Occupancy Trend
+
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+
+
+
+ Recent Activities
+
+
+
+
+
+
New work order: Leaky faucet at 123 Main St, Apt 4B
+
+
+
+ New tenant: John Doe moved into 456 Elm St, Apt 2A
+
+
+
+ Rent payment received: $1,200 from Jane Smith, 789 Oak Rd, Apt 3C
+
+
+
+
+
+
+
+ Upcoming Lease Renewals
+
+
+
+
+
+
Sarah Johnson
+
123 Main St, Apt 2B
+
+
+
Expires in 30 days
+
+
+
+
+
+
Michael Brown
+
456 Elm St, Apt 1A
+
+
+
Expires in 45 days
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/admin/payments/page.tsx b/src/app/admin/payments/page.tsx
new file mode 100644
index 0000000..9fed1d0
--- /dev/null
+++ b/src/app/admin/payments/page.tsx
@@ -0,0 +1,269 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Input } from "~/components/ui/input"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
+import { Label } from "~/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
+import { Badge } from "~/components/ui/badge"
+import { Search, Plus, FileText, DollarSign, CreditCard, Calendar } from 'lucide-react'
+import { ChartContainer, ChartTooltip, ChartTooltipContent } from "~/components/ui/chart"
+import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"
+
+// Mock data for payments
+const payments = [
+ { id: 1, tenant: "John Doe", property: "Sunset Apartments, Apt 4B", amount: 1200, date: "2023-07-01", status: "Paid" },
+ { id: 2, tenant: "Jane Smith", property: "Oakwood Residences, Apt 2A", amount: 1500, date: "2023-07-02", status: "Paid" },
+ { id: 3, tenant: "Bob Johnson", property: "Riverside Condos, Apt 3C", amount: 1800, date: "2023-07-05", status: "Pending" },
+ { id: 4, tenant: "Alice Brown", property: "Sunset Apartments, Apt 2C", amount: 1100, date: "2023-07-03", status: "Late" },
+ // Add more mock data as needed
+]
+
+// Mock data for payment chart
+const paymentChartData = [
+ { month: "Jan", amount: 45000 },
+ { month: "Feb", amount: 42000 },
+ { month: "Mar", amount: 47000 },
+ { month: "Apr", amount: 44000 },
+ { month: "May", amount: 46000 },
+ { month: "Jun", amount: 48000 },
+]
+
+export default function PaymentsPage() {
+ const [searchTerm, setSearchTerm] = useState("")
+ const [selectedStatus, setSelectedStatus] = useState("All")
+
+ const filteredPayments = payments.filter(payment =>
+ (payment.tenant.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ payment.property.toLowerCase().includes(searchTerm.toLowerCase())) &&
+ (selectedStatus === "All" || payment.status === selectedStatus)
+ )
+
+ return (
+
+
+
+
+
Payments
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-[300px]"
+ />
+
+
+
+
+
+
+
+ Tenant
+ Property
+ Amount
+ Date
+ Status
+ Actions
+
+
+
+ {filteredPayments.map((payment) => (
+
+ {payment.tenant}
+ {payment.property}
+ ${payment.amount}
+ {payment.date}
+
+
+ {payment.status}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Payment Statistics
+
+
+
+
+ Overview
+ Trends
+
+
+
+
+
+
+ Total Collected
+
+
+
+
+ $45,600
+ +20.1% from last month
+
+
+
+
+
+ Pending Payments
+
+
+
+
+ $3,800
+ 5 payments pending
+
+
+
+
+
+ Late Payments
+
+
+
+
+ $1,200
+ 2 payments overdue
+
+
+
+
+
+ Collection Rate
+
+
+
+
+ 98%
+ +2% from last month
+
+
+
+
+
+
+
+ Monthly Payment Trends
+
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/admin/properties/page.tsx b/src/app/admin/properties/page.tsx
new file mode 100644
index 0000000..da691db
--- /dev/null
+++ b/src/app/admin/properties/page.tsx
@@ -0,0 +1,218 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Input } from "~/components/ui/input"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
+import { Label } from "~/components/ui/label"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
+import { Badge } from "~/components/ui/badge"
+import { Search, Plus, Edit, Trash2, Home, DollarSign, Users } from 'lucide-react'
+
+// Mock data for properties
+const properties = [
+ { id: 1, name: "Sunset Apartments", address: "123 Main St", units: 20, occupancy: 18, rentRange: "$1000 - $1500" },
+ { id: 2, name: "Oakwood Residences", address: "456 Elm St", units: 15, occupancy: 14, rentRange: "$1200 - $1800" },
+ { id: 3, name: "Riverside Condos", address: "789 Oak Rd", units: 30, occupancy: 28, rentRange: "$1500 - $2200" },
+ // Add more mock data as needed
+]
+
+export default function PropertiesPage() {
+ const [searchTerm, setSearchTerm] = useState("")
+
+ const filteredProperties = properties.filter(property =>
+ property.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ property.address.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+
+ return (
+
+
+
+
+
Properties
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-[300px]"
+ />
+
+
+
+
+
+
+ Name
+ Address
+ Units
+ Occupancy
+ Rent Range
+ Actions
+
+
+
+ {filteredProperties.map((property) => (
+
+ {property.name}
+ {property.address}
+ {property.units}
+
+
+ {property.occupancy}/{property.units}
+
+
+ {property.rentRange}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Property Statistics
+
+
+
+
+ Overview
+ Financial
+ Maintenance
+
+
+
+
+
+
+ Total Properties
+
+
+
+
+ {properties.length}
+
+
+
+
+
+ Total Units
+
+
+
+
+
+ {properties.reduce((sum, p) => sum + p.units, 0)}
+
+
+
+
+
+
+ Occupancy Rate
+
+
+
+
+
+ {Math.round(
+ (properties.reduce((sum, p) => sum + p.occupancy, 0) /
+ properties.reduce((sum, p) => sum + p.units, 0)) * 100
+ )}%
+
+
+
+
+
+
+ Avg. Rent
+
+
+
+
+ $1,450
+
+
+
+
+
+ Detailed financial information and statistics will be displayed here.
+
+
+ Property maintenance history and upcoming tasks will be displayed here.
+
+
+
+
+
+ )
+}
+
diff --git a/src/app/admin/tenants/page.tsx b/src/app/admin/tenants/page.tsx
new file mode 100644
index 0000000..6245605
--- /dev/null
+++ b/src/app/admin/tenants/page.tsx
@@ -0,0 +1,236 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Input } from "~/components/ui/input"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
+import { Label } from "~/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
+import { Badge } from "~/components/ui/badge"
+import { Search, Plus, Edit, Trash2, Mail, Phone } from 'lucide-react'
+
+// Mock data for tenants
+const tenants = [
+ { id: 1, name: "John Doe", email: "john@example.com", phone: "123-456-7890", property: "123 Main St, Apt 4B", leaseEnd: "2023-12-31", status: "Active" },
+ { id: 2, name: "Jane Smith", email: "jane@example.com", phone: "098-765-4321", property: "456 Elm St, Apt 2A", leaseEnd: "2024-03-15", status: "Active" },
+ { id: 3, name: "Bob Johnson", email: "bob@example.com", phone: "555-123-4567", property: "789 Oak Rd, Apt 3C", leaseEnd: "2023-09-30", status: "Notice Given" },
+ // Add more mock data as needed
+]
+
+export default function TenantsPage() {
+ const [searchTerm, setSearchTerm] = useState("")
+ const [selectedStatus, setSelectedStatus] = useState("All")
+
+ const filteredTenants = tenants.filter(tenant =>
+ (tenant.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ tenant.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ tenant.property.toLowerCase().includes(searchTerm.toLowerCase())) &&
+ (selectedStatus === "All" || tenant.status === selectedStatus)
+ )
+
+ return (
+
+
+
+
+
Tenants
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-[300px]"
+ />
+
+
+
+
+
+
+
+ Name
+ Property
+ Lease End
+ Status
+ Actions
+
+
+
+ {filteredTenants.map((tenant) => (
+
+ {tenant.name}
+ {tenant.property}
+ {tenant.leaseEnd}
+
+
+ {tenant.status}
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Tenant Statistics
+
+
+
+
+ Overview
+ Leases
+ Payments
+
+
+
+
+
+
+ Total Tenants
+
+
+
+ {tenants.length}
+
+
+
+
+
+ Active Leases
+
+
+
+
+ {tenants.filter(t => t.status === 'Active').length}
+
+
+
+
+
+
+ Expiring Soon
+
+
+
+ 2
+
+
+
+
+
+ Vacant Units
+
+
+
+ 3
+
+
+
+
+
+ Detailed lease information and statistics will be displayed here.
+
+
+ Tenant payment history and statistics will be displayed here.
+
+
+
+
+
+ )
+}
diff --git a/src/app/admin/workorders/page.tsx b/src/app/admin/workorders/page.tsx
new file mode 100644
index 0000000..69fe3e8
--- /dev/null
+++ b/src/app/admin/workorders/page.tsx
@@ -0,0 +1,298 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from "~/components/ui/button"
+import { Input } from "~/components/ui/input"
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog"
+import { Label } from "~/components/ui/label"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"
+import { Badge } from "~/components/ui/badge"
+import { Textarea } from "~/components/ui/textarea"
+import { Search, Plus, Wrench, Clock, CheckCircle, AlertTriangle } from 'lucide-react'
+import { ChartContainer, ChartTooltip, ChartTooltipContent } from "~/components/ui/chart"
+import { Pie, PieChart, ResponsiveContainer, Cell } from "recharts"
+
+// Mock data for work orders
+const workOrders = [
+ { id: 1, property: "Sunset Apartments, Apt 4B", issue: "Leaky faucet", tenant: "John Doe", date: "2023-07-01", status: "Open" },
+ { id: 2, property: "Oakwood Residences, Apt 2A", issue: "Broken AC", tenant: "Jane Smith", date: "2023-07-02", status: "In Progress" },
+ { id: 3, property: "Riverside Condos, Apt 3C", issue: "Clogged drain", tenant: "Bob Johnson", date: "2023-07-03", status: "Completed" },
+ { id: 4, property: "Sunset Apartments, Apt 2C", issue: "Electrical issue", tenant: "Alice Brown", date: "2023-07-04", status: "Open" },
+ // Add more mock data as needed
+]
+
+// Mock data for work order status chart
+const statusChartData = [
+ { name: "Open", value: 5 },
+ { name: "In Progress", value: 3 },
+ { name: "Completed", value: 8 },
+]
+
+const COLORS = ['#0088FE', '#00C49F', '#FFBB28']
+
+export default function WorkOrdersPage() {
+ const [searchTerm, setSearchTerm] = useState("")
+ const [selectedStatus, setSelectedStatus] = useState("All")
+
+ const filteredWorkOrders = workOrders.filter(order =>
+ (order.property.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ order.issue.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ order.tenant.toLowerCase().includes(searchTerm.toLowerCase())) &&
+ (selectedStatus === "All" || order.status === selectedStatus)
+ )
+
+ return (
+
+
+
+
+
Work Orders
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-[300px]"
+ />
+
+
+
+
+
+
+
+ Property
+ Issue
+ Tenant
+ Date
+ Status
+ Actions
+
+
+
+ {filteredWorkOrders.map((order) => (
+
+ {order.property}
+ {order.issue}
+ {order.tenant}
+ {order.date}
+
+
+ {order.status}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Work Order Statistics
+
+
+
+
+ Overview
+ Trends
+
+
+
+
+
+
+ Total Work Orders
+
+
+
+
+ {workOrders.length}
+
+
+
+
+
+ Open Work Orders
+
+
+
+
+
+ {workOrders.filter(order => order.status === 'Open').length}
+
+
+
+
+
+
+ In Progress
+
+
+
+
+
+ {workOrders.filter(order => order.status === 'In Progress').length}
+
+
+
+
+
+
+ Completed
+
+
+
+
+
+ {workOrders.filter(order => order.status === 'Completed').length}
+
+
+
+
+
+
+
+
+ Work Order Status Distribution
+
+
+
+
+
+
+ {statusChartData.map((entry, index) => (
+ |
+ ))}
+
+ } />
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
new file mode 100644
index 0000000..9b213de
--- /dev/null
+++ b/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "~/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..36bc437
--- /dev/null
+++ b/src/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "~/lib/utils"
+import { buttonVariants } from "~/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx
new file mode 100644
index 0000000..921e9fc
--- /dev/null
+++ b/src/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "~/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+