Add more pages using v0

This commit is contained in:
Gabriel Brown 2024-11-30 12:48:48 -06:00
parent 6ff62ca0a6
commit d3b792dc1d
19 changed files with 2966 additions and 16 deletions

View File

@ -52,6 +52,7 @@
"react-day-picker": "8.10.1", "react-day-picker": "8.10.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.52.2", "react-hook-form": "^7.52.2",
"recharts": "^2.13.3",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"sonner": "^1.5.0", "sonner": "^1.5.0",
"tailwind-merge": "^2.4.0", "tailwind-merge": "^2.4.0",

257
pnpm-lock.yaml generated
View File

@ -113,6 +113,9 @@ importers:
react-hook-form: react-hook-form:
specifier: ^7.52.2 specifier: ^7.52.2
version: 7.52.2(react@18.3.1) 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: server-only:
specifier: ^0.0.1 specifier: ^0.0.1
version: 0.0.1 version: 0.0.1
@ -1489,6 +1492,33 @@ packages:
'@types/cookie@0.6.0': '@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} 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': '@types/eslint@8.56.11':
resolution: {integrity: sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==} resolution: {integrity: sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==}
@ -1829,6 +1859,50 @@ packages:
csstype@3.1.3: csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 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: d@1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@ -1868,6 +1942,9 @@ packages:
supports-color: supports-color:
optional: true optional: true
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
deep-equal@2.2.3: deep-equal@2.2.3:
resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1911,6 +1988,9 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dotenv@16.4.5: dotenv@16.4.5:
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -2208,12 +2288,19 @@ packages:
event-emitter@0.3.5: event-emitter@0.3.5:
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
ext@1.7.0: ext@1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 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: fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
@ -2393,6 +2480,10 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
invariant@2.2.4: invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@ -2629,6 +2720,9 @@ packages:
lodash.throttle@4.1.1: lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
loose-envify@1.4.0: loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true hasBin: true
@ -3053,6 +3147,9 @@ packages:
react-is@16.13.1: react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 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: react-remove-scroll-bar@2.3.6:
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3093,6 +3190,12 @@ packages:
'@types/react': '@types/react':
optional: true 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: react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3103,6 +3206,12 @@ packages:
'@types/react': '@types/react':
optional: true 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: react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -3114,6 +3223,16 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} 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: reflect.getprototypeof@1.0.6:
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3340,6 +3459,9 @@ packages:
resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@ -3451,6 +3573,9 @@ packages:
react: ^16.8 || ^17.0 || ^18.0 react: ^16.8 || ^17.0 || ^18.0
react-dom: ^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: which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
@ -4582,6 +4707,30 @@ snapshots:
'@types/cookie@0.6.0': {} '@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': '@types/eslint@8.56.11':
dependencies: dependencies:
'@types/estree': 1.0.5 '@types/estree': 1.0.5
@ -4975,6 +5124,44 @@ snapshots:
csstype@3.1.3: {} 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: d@1.0.2:
dependencies: dependencies:
es5-ext: 0.10.64 es5-ext: 0.10.64
@ -5010,6 +5197,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
decimal.js-light@2.5.1: {}
deep-equal@2.2.3: deep-equal@2.2.3:
dependencies: dependencies:
array-buffer-byte-length: 1.0.1 array-buffer-byte-length: 1.0.1
@ -5069,6 +5258,11 @@ snapshots:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.25.6
csstype: 3.1.3
dotenv@16.4.5: {} dotenv@16.4.5: {}
dreamopt@0.8.0: dreamopt@0.8.0:
@ -5550,12 +5744,16 @@ snapshots:
d: 1.0.2 d: 1.0.2
es5-ext: 0.10.64 es5-ext: 0.10.64
eventemitter3@4.0.7: {}
ext@1.7.0: ext@1.7.0:
dependencies: dependencies:
type: 2.7.3 type: 2.7.3
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-equals@5.0.1: {}
fast-glob@3.3.2: fast-glob@3.3.2:
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@ -5759,6 +5957,8 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
side-channel: 1.0.6 side-channel: 1.0.6
internmap@2.0.3: {}
invariant@2.2.4: invariant@2.2.4:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
@ -5992,6 +6192,8 @@ snapshots:
lodash.throttle@4.1.1: {} lodash.throttle@4.1.1: {}
lodash@4.17.21: {}
loose-envify@1.4.0: loose-envify@1.4.0:
dependencies: dependencies:
js-tokens: 4.0.0 js-tokens: 4.0.0
@ -6345,6 +6547,8 @@ snapshots:
react-is@16.13.1: {} 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): react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@ -6386,6 +6590,14 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.3 '@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): react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1):
dependencies: dependencies:
get-nonce: 1.0.1 get-nonce: 1.0.1
@ -6395,6 +6607,15 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.3 '@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: react@18.3.1:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
@ -6407,6 +6628,23 @@ snapshots:
dependencies: dependencies:
picomatch: 2.3.1 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: reflect.getprototypeof@1.0.6:
dependencies: dependencies:
call-bind: 1.0.7 call-bind: 1.0.7
@ -6674,6 +6912,8 @@ snapshots:
es5-ext: 0.10.64 es5-ext: 0.10.64
next-tick: 1.1.0 next-tick: 1.1.0
tiny-invariant@1.3.3: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
@ -6801,6 +7041,23 @@ snapshots:
- '@types/react' - '@types/react'
- '@types/react-dom' - '@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: which-boxed-primitive@1.0.2:
dependencies: dependencies:
is-bigint: 1.0.4 is-bigint: 1.0.4

155
src/app/account/billtracker/page.tsx Executable file → Normal file
View File

@ -1,19 +1,148 @@
"use server" 'use client'
import { auth } from "~/auth"
import BreadCrumbBillTracker from "~/components/portal/home/breadcrumb/BreadCrumbBillTracker" import { useState } from 'react'
import BillTrackerCalendar from "~/components/portal/billtracker/BillTrackerCalendar" 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<Date | undefined>(new Date())
const [isDialogOpen, setIsDialogOpen] = useState(false)
const handleAddBill = (event: React.FormEvent<HTMLFormElement>) => {
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 ? (
<div className="w-full h-full flex items-center justify-center">
<div className="h-2 w-2 bg-primary rounded-full" />
</div>
) : null
}
export default async function HomePage() {
const session = await auth()
if (!session?.user) return <></>
return ( return (
<div className="w-2/3 flex flex-col p-6"> <div className="space-y-8">
<div className="flex flex-row"> <Card>
<div className=""> <CardHeader>
<BreadCrumbBillTracker /> <div className="flex justify-between items-center">
<CardTitle className="text-2xl">Bill Tracker</CardTitle>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Add Bill
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleAddBill}>
<DialogHeader>
<DialogTitle>Add New Bill</DialogTitle>
<DialogDescription>
Enter the details of the bill you want to track.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="billName" className="text-right">
Bill Name
</Label>
<Input id="billName" name="billName" className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="amount" className="text-right">
Amount
</Label>
<Input id="amount" name="amount" type="number" className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="category" className="text-right">
Category
</Label>
<Select name="category" required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Utilities">Utilities</SelectItem>
<SelectItem value="Rent">Rent</SelectItem>
<SelectItem value="Insurance">Insurance</SelectItem>
<SelectItem value="Subscriptions">Subscriptions</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
</div> </div>
</div> </div>
< BillTrackerCalendar /> <DialogFooter>
<Button type="submit">Add Bill</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div> </div>
); </CardHeader>
<CardContent>
<div className="flex space-x-4">
<div className="flex-1">
<Calendar
mode="single"
selected={selectedDate}
onSelect={setSelectedDate}
className="rounded-md border"
components={{
DayContent: ({ date }) => getDayContent(date),
}}
/>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold mb-4">Upcoming Bills</h3>
<ul className="space-y-2">
{bills.map((bill) => (
<li key={bill.id} className="flex justify-between items-center p-2 bg-muted rounded-md">
<div>
<p className="font-medium">{bill.name}</p>
<p className="text-sm text-muted-foreground">{bill.dueDate.toLocaleDateString()}</p>
</div>
<div className="flex items-center">
<DollarSign className="h-4 w-4 mr-1 text-primary" />
<span className="font-semibold">{bill.amount.toFixed(2)}</span>
</div>
</li>
))}
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
)
} }

View File

@ -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 (
<div className="w-2/3 flex flex-col p-6">
<div className="flex flex-row">
<div className="">
<BreadCrumbBillTracker />
</div>
</div>
< BillTrackerCalendar />
</div>
);
}

View File

@ -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 (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Documents</CardTitle>
<Button>
<Upload className="mr-2 h-4 w-4" /> Upload Document
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Size</TableHead>
<TableHead>Date</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{documents.map((doc) => (
<TableRow key={doc.id}>
<TableCell className="font-medium">
<div className="flex items-center">
<FileText className="mr-2 h-4 w-4" />
{doc.name}
</div>
</TableCell>
<TableCell>{doc.type}</TableCell>
<TableCell>{doc.size}</TableCell>
<TableCell>{doc.date}</TableCell>
<TableCell>
<Button variant="ghost" size="sm">
<Download className="mr-2 h-4 w-4" /> Download
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}

View File

@ -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&apos;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 (
<div className="space-y-8">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Messages</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{messages.map((message) => (
<div key={message.id} className={`flex ${message.sender === 'Tenant' ? 'justify-end' : 'justify-start'}`}>
<div className={`flex ${message.sender === 'Tenant' ? 'flex-row-reverse' : 'flex-row'} items-start space-x-2`}>
<Avatar className="w-10 h-10">
<AvatarFallback>{message.sender[0]}</AvatarFallback>
</Avatar>
<div className={`rounded-lg p-3 ${message.sender === 'Tenant' ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
<p className="text-sm font-medium">{message.sender}</p>
<p className="mt-1">{message.content}</p>
<p className="mt-1 text-xs opacity-70">{new Date(message.timestamp).toLocaleString()}</p>
</div>
</div>
</div>
))}
</div>
<form onSubmit={handleSendMessage} className="mt-4 flex space-x-2">
<Input
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type your message..."
className="flex-grow"
/>
<Button type="submit">Send</Button>
</form>
</CardContent>
</Card>
</div>
)
}

View File

@ -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 (
<div className="space-y-8">
<Card>
<CardHeader>
<CardTitle className="text-2xl">Current Balance</CardTitle>
</CardHeader>
<CardContent>
<div className="text-4xl font-bold">$1,200.00</div>
<p className="text-muted-foreground mt-2">Due on July 1, 2023</p>
</CardContent>
<CardFooter>
<Button className="w-full">
<DollarSign className="mr-2 h-4 w-4" /> Make a Payment
</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>Payment Methods</CardTitle>
<CardDescription>Manage your payment methods</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center">
<CreditCard className="mr-2 h-4 w-4" />
<div>
<p className="font-medium">Visa ending in 1234</p>
<p className="text-sm text-muted-foreground">Expires 12/2025</p>
</div>
</div>
<Button variant="outline">Edit</Button>
</div>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">Add Payment Method</Button>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>Payment History</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paymentHistory.map((payment) => (
<TableRow key={payment.id}>
<TableCell>{payment.date}</TableCell>
<TableCell>${payment.amount.toFixed(2)}</TableCell>
<TableCell>{payment.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}

View File

@ -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<HTMLFormElement>) => {
event.preventDefault()
// Here you would typically send the form data to your server
setIsDialogOpen(false)
}
return (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Work Orders</CardTitle>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> New Work Order
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Create New Work Order</DialogTitle>
<DialogDescription>
Describe the issue you're experiencing. We'll get on it as soon as possible.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="issue" className="text-right">
Issue
</Label>
<Input id="issue" className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="location" className="text-right">
Location
</Label>
<Select required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="kitchen">Kitchen</SelectItem>
<SelectItem value="bathroom">Bathroom</SelectItem>
<SelectItem value="bedroom">Bedroom</SelectItem>
<SelectItem value="livingroom">Living Room</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Textarea id="description" className="col-span-3" required />
</div>
</div>
<DialogFooter>
<Button type="submit">Submit Work Order</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Issue</TableHead>
<TableHead>Status</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workOrders.map((order) => (
<TableRow key={order.id}>
<TableCell>{order.date}</TableCell>
<TableCell>{order.issue}</TableCell>
<TableCell>{order.status}</TableCell>
<TableCell>
<Button variant="outline" size="sm">View Details</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Maintenance Tips</CardTitle>
<CardDescription>Keep your living space in top condition with these tips</CardDescription>
</CardHeader>
<CardContent>
<ul className="list-disc pl-4 space-y-2">
<li>Regularly clean or replace HVAC filters</li>
<li>Check and clean gutters seasonally</li>
<li>Test smoke and carbon monoxide detectors monthly</li>
<li>Inspect plumbing fixtures for leaks</li>
</ul>
</CardContent>
<CardFooter>
<Button variant="link" className="w-full">View More Tips</Button>
</CardFooter>
</Card>
</div>
)
}

View File

@ -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 (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Documents</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Upload Document
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Upload New Document</DialogTitle>
<DialogDescription>
Upload a new document and assign it to tenants or properties.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="document" className="text-right">
Document
</Label>
<Input id="document" type="file" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="assign" className="text-right">
Assign To
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select recipients" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Tenants</SelectItem>
<SelectItem value="sunset">Sunset Apartments</SelectItem>
<SelectItem value="oakwood">Oakwood Residences</SelectItem>
<SelectItem value="riverside">Riverside Condos</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit">Upload Document</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search documents..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Archived">Archived</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Size</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredDocuments.map((doc) => (
<TableRow key={doc.id}>
<TableCell className="font-medium">{doc.name}</TableCell>
<TableCell>{doc.type}</TableCell>
<TableCell>{doc.size}</TableCell>
<TableCell>{doc.date}</TableCell>
<TableCell>
<Badge variant={doc.status === 'Active' ? 'default' : 'secondary'}>
{doc.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button variant="ghost" size="icon">
<Download className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Share2 className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Document Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="usage">Usage</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Documents
</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{documents.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Documents
</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{documents.filter(doc => doc.status === 'Active').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Storage Used
</CardTitle>
<Upload className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">9.2 MB</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Most Common Type
</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">PDF</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="usage">
<p>Document usage statistics and charts will be displayed here.</p>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

62
src/app/admin/layout.tsx Normal file
View File

@ -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 (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8 text-primary">Admin Dashboard</h1>
<div className="flex flex-col md:flex-row gap-8">
<Card className="w-full md:w-64 h-fit">
<CardContent className="p-4">
<nav className="space-y-2">
<Link href="/admin" passHref>
<Button variant="ghost" className="w-full justify-start">
<BarChart className="mr-2 h-4 w-4" /> Dashboard
</Button>
</Link>
<Link href="/admin/tenants" passHref>
<Button variant="ghost" className="w-full justify-start">
<Users className="mr-2 h-4 w-4" /> Tenants
</Button>
</Link>
<Link href="/admin/properties" passHref>
<Button variant="ghost" className="w-full justify-start">
<Home className="mr-2 h-4 w-4" /> Properties
</Button>
</Link>
<Link href="/admin/payments" passHref>
<Button variant="ghost" className="w-full justify-start">
<CreditCard className="mr-2 h-4 w-4" /> Payments
</Button>
</Link>
<Link href="/admin/workorders" passHref>
<Button variant="ghost" className="w-full justify-start">
<Tool className="mr-2 h-4 w-4" /> Work Orders
</Button>
</Link>
<Link href="/admin/messages" passHref>
<Button variant="ghost" className="w-full justify-start">
<MessageSquare className="mr-2 h-4 w-4" /> Messages
</Button>
</Link>
<Link href="/admin/documents" passHref>
<Button variant="ghost" className="w-full justify-start">
<FileText className="mr-2 h-4 w-4" /> Documents
</Button>
</Link>
</nav>
</CardContent>
</Card>
<main className="flex-1">
{children}
</main>
</div>
</div>
)
}

View File

@ -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<typeof messages[0] | null>(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 (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Messages</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> New Message
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Send New Message</DialogTitle>
<DialogDescription>
Compose a new message to send to a tenant or multiple tenants.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="recipients" className="text-right">
To
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select recipients" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Tenants</SelectItem>
<SelectItem value="sunset">Sunset Apartments</SelectItem>
<SelectItem value="oakwood">Oakwood Residences</SelectItem>
<SelectItem value="riverside">Riverside Condos</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="subject" className="text-right">
Subject
</Label>
<Input id="subject" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="message" className="text-right">
Message
</Label>
<Textarea id="message" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Send Message</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search messages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Unread">Unread</SelectItem>
<SelectItem value="Read">Read</SelectItem>
<SelectItem value="Replied">Replied</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Tenant</TableHead>
<TableHead>Property</TableHead>
<TableHead>Subject</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredMessages.map((message) => (
<TableRow key={message.id}>
<TableCell>{message.tenant}</TableCell>
<TableCell>{message.property}</TableCell>
<TableCell>{message.subject}</TableCell>
<TableCell>{message.date}</TableCell>
<TableCell>
<Badge variant={
message.status === 'Unread' ? 'default' :
message.status === 'Read' ? 'secondary' :
'outline'
}>
{message.status}
</Badge>
</TableCell>
<TableCell>
<Button variant="ghost" size="icon" onClick={() => setSelectedMessage(message)}>
<MessageSquare className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{selectedMessage && (
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Message Details</CardTitle>
<Button variant="outline" onClick={() => setSelectedMessage(null)}>Close</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center space-x-4">
<Avatar>
<AvatarImage src="/placeholder-avatar.jpg" alt={selectedMessage.tenant} />
<AvatarFallback>{selectedMessage.tenant[0]}</AvatarFallback>
</Avatar>
<div>
<h3 className="font-semibold">{selectedMessage.tenant}</h3>
<p className="text-sm text-muted-foreground">{selectedMessage.property}</p>
</div>
</div>
<div>
<h4 className="font-semibold">Subject: {selectedMessage.subject}</h4>
<p className="text-sm text-muted-foreground">Received on {selectedMessage.date}</p>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<Textarea placeholder="Type your reply here..." className="mt-4" />
<div className="flex justify-end space-x-2">
<Button variant="outline">Save Draft</Button>
<Button>Send Reply</Button>
</div>
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>Message Statistics</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Messages
</CardTitle>
<MessageSquare className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{messages.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Unread Messages
</CardTitle>
<ArrowUpRight className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{messages.filter(m => m.status === 'Unread').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Response Rate
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{Math.round((messages.filter(m => m.status === 'Replied').length / messages.length) * 100)}%
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Avg. Response Time
</CardTitle>
<MessageSquare className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">4.2 hours</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</div>
)
}

174
src/app/admin/page.tsx Normal file
View File

@ -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 (
<div className="space-y-8">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<CreditCard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$24,560</div>
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Occupancy Rate</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">98%</div>
<p className="text-xs text-muted-foreground">+2% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Properties</CardTitle>
<Home className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">45</div>
<p className="text-xs text-muted-foreground">+3 new this month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Work Orders</CardTitle>
<Wrench className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12</div>
<p className="text-xs text-muted-foreground">-5 from last week</p>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Rent Collection Rate</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={{
collected: {
label: "Collected",
color: "hsl(var(--chart-1))",
},
}} className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={rentData}>
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="collected" fill="var(--color-collected)" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Occupancy Trend</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={{
rate: {
label: "Occupancy Rate",
color: "hsl(var(--chart-2))",
},
}} className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={occupancyData}>
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Line type="monotone" dataKey="rate" stroke="var(--color-rate)" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Recent Activities</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center">
<AlertTriangle className="h-4 w-4 text-yellow-500 mr-2" />
<span className="text-sm">New work order: Leaky faucet at 123 Main St, Apt 4B</span>
</div>
<div className="flex items-center">
<Users className="h-4 w-4 text-green-500 mr-2" />
<span className="text-sm">New tenant: John Doe moved into 456 Elm St, Apt 2A</span>
</div>
<div className="flex items-center">
<CreditCard className="h-4 w-4 text-blue-500 mr-2" />
<span className="text-sm">Rent payment received: $1,200 from Jane Smith, 789 Oak Rd, Apt 3C</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Upcoming Lease Renewals</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div>
<p className="font-medium">Sarah Johnson</p>
<p className="text-sm text-muted-foreground">123 Main St, Apt 2B</p>
</div>
<div className="text-right">
<p className="font-medium">Expires in 30 days</p>
<Button size="sm">Send Renewal</Button>
</div>
</div>
<div className="flex justify-between items-center">
<div>
<p className="font-medium">Michael Brown</p>
<p className="text-sm text-muted-foreground">456 Elm St, Apt 1A</p>
</div>
<div className="text-right">
<p className="font-medium">Expires in 45 days</p>
<Button size="sm">Send Renewal</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@ -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 (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Payments</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Record Payment
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Record New Payment</DialogTitle>
<DialogDescription>
Enter the details of the new payment. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tenant" className="text-right">
Tenant
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select tenant" />
</SelectTrigger>
<SelectContent>
<SelectItem value="john-doe">John Doe</SelectItem>
<SelectItem value="jane-smith">Jane Smith</SelectItem>
<SelectItem value="bob-johnson">Bob Johnson</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="amount" className="text-right">
Amount
</Label>
<Input id="amount" type="number" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="date" className="text-right">
Date
</Label>
<Input id="date" type="date" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="method" className="text-right">
Payment Method
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="credit-card">Credit Card</SelectItem>
<SelectItem value="bank-transfer">Bank Transfer</SelectItem>
<SelectItem value="cash">Cash</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit">Save Payment</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search payments..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Paid">Paid</SelectItem>
<SelectItem value="Pending">Pending</SelectItem>
<SelectItem value="Late">Late</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Tenant</TableHead>
<TableHead>Property</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredPayments.map((payment) => (
<TableRow key={payment.id}>
<TableCell>{payment.tenant}</TableCell>
<TableCell>{payment.property}</TableCell>
<TableCell>${payment.amount}</TableCell>
<TableCell>{payment.date}</TableCell>
<TableCell>
<Badge variant={
payment.status === 'Paid' ? 'default' :
payment.status === 'Pending' ? 'secondary' :
'destructive'
}>
{payment.status}
</Badge>
</TableCell>
<TableCell>
<Button variant="ghost" size="icon">
<FileText className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Payment Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="trends">Trends</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Collected
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,600</div>
<p className="text-xs text-muted-foreground">+20.1% from last month</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Pending Payments
</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$3,800</div>
<p className="text-xs text-muted-foreground">5 payments pending</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Late Payments
</CardTitle>
<CreditCard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$1,200</div>
<p className="text-xs text-muted-foreground">2 payments overdue</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Collection Rate
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">98%</div>
<p className="text-xs text-muted-foreground">+2% from last month</p>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="trends">
<Card>
<CardHeader>
<CardTitle>Monthly Payment Trends</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={{
amount: {
label: "Amount",
color: "hsl(var(--chart-1))",
},
}} className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={paymentChartData}>
<XAxis dataKey="month" />
<YAxis />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="amount" fill="var(--color-amount)" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View File

@ -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 (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Properties</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Add Property
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Property</DialogTitle>
<DialogDescription>
Enter the details of the new property. Click save when you&apos;re done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="address" className="text-right">
Address
</Label>
<Input id="address" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="units" className="text-right">
Total Units
</Label>
<Input id="units" type="number" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rentMin" className="text-right">
Min Rent
</Label>
<Input id="rentMin" type="number" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rentMax" className="text-right">
Max Rent
</Label>
<Input id="rentMax" type="number" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save Property</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search properties..."placeholder="Search properties..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Address</TableHead>
<TableHead>Units</TableHead>
<TableHead>Occupancy</TableHead>
<TableHead>Rent Range</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredProperties.map((property) => (
<TableRow key={property.id}>
<TableCell>{property.name}</TableCell>
<TableCell>{property.address}</TableCell>
<TableCell>{property.units}</TableCell>
<TableCell>
<Badge variant={property.occupancy === property.units ? 'default' : 'secondary'}>
{property.occupancy}/{property.units}
</Badge>
</TableCell>
<TableCell>{property.rentRange}</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Property Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="financial">Financial</TabsTrigger>
<TabsTrigger value="maintenance">Maintenance</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Properties
</CardTitle>
<Home className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{properties.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Units
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{properties.reduce((sum, p) => sum + p.units, 0)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Occupancy Rate
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{Math.round(
(properties.reduce((sum, p) => sum + p.occupancy, 0) /
properties.reduce((sum, p) => sum + p.units, 0)) * 100
)}%
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Avg. Rent
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$1,450</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="financial">
<p>Detailed financial information and statistics will be displayed here.</p>
</TabsContent>
<TabsContent value="maintenance">
<p>Property maintenance history and upcoming tasks will be displayed here.</p>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View File

@ -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 (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Tenants</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Add Tenant
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Tenant</DialogTitle>
<DialogDescription>
Enter the details of the new tenant. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Name
</Label>
<Input id="name" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="email" className="text-right">
Email
</Label>
<Input id="email" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="phone" className="text-right">
Phone
</Label>
<Input id="phone" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="property" className="text-right">
Property
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select property" />
</SelectTrigger>
<SelectContent>
<SelectItem value="123 Main St, Apt 4B">123 Main St, Apt 4B</SelectItem>
<SelectItem value="456 Elm St, Apt 2A">456 Elm St, Apt 2A</SelectItem>
<SelectItem value="789 Oak Rd, Apt 3C">789 Oak Rd, Apt 3C</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="leaseEnd" className="text-right">
Lease End Date
</Label>
<Input id="leaseEnd" type="date" className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit">Save Tenant</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search tenants..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Notice Given">Notice Given</SelectItem>
<SelectItem value="Inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Property</TableHead>
<TableHead>Lease End</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTenants.map((tenant) => (
<TableRow key={tenant.id}>
<TableCell>{tenant.name}</TableCell>
<TableCell>{tenant.property}</TableCell>
<TableCell>{tenant.leaseEnd}</TableCell>
<TableCell>
<Badge variant={tenant.status === 'Active' ? 'default' : 'secondary'}>
{tenant.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button variant="ghost" size="icon">
<Mail className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Phone className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Tenant Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="leases">Leases</TabsTrigger>
<TabsTrigger value="payments">Payments</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Tenants
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{tenants.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Active Leases
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{tenants.filter(t => t.status === 'Active').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Expiring Soon
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">2</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Vacant Units
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">3</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="leases">
<p>Detailed lease information and statistics will be displayed here.</p>
</TabsContent>
<TabsContent value="payments">
<p>Tenant payment history and statistics will be displayed here.</p>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View File

@ -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 (
<div className="space-y-8">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-2xl">Work Orders</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" /> Create Work Order
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Work Order</DialogTitle>
<DialogDescription>
Enter the details of the new work order. Click save when you're done.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="property" className="text-right">
Property
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select property" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sunset-apartments">Sunset Apartments</SelectItem>
<SelectItem value="oakwood-residences">Oakwood Residences</SelectItem>
<SelectItem value="riverside-condos">Riverside Condos</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tenant" className="text-right">
Tenant
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select tenant" />
</SelectTrigger>
<SelectContent>
<SelectItem value="john-doe">John Doe</SelectItem>
<SelectItem value="jane-smith">Jane Smith</SelectItem>
<SelectItem value="bob-johnson">Bob Johnson</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="issue" className="text-right">
Issue
</Label>
<Input id="issue" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Textarea id="description" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="priority" className="text-right">
Priority
</Label>
<Select>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit">Create Work Order</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-2">
<Input
placeholder="Search work orders..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-[300px]"
/>
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Select value={selectedStatus} onValueChange={setSelectedStatus}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Statuses</SelectItem>
<SelectItem value="Open">Open</SelectItem>
<SelectItem value="In Progress">In Progress</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Property</TableHead>
<TableHead>Issue</TableHead>
<TableHead>Tenant</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredWorkOrders.map((order) => (
<TableRow key={order.id}>
<TableCell>{order.property}</TableCell>
<TableCell>{order.issue}</TableCell>
<TableCell>{order.tenant}</TableCell>
<TableCell>{order.date}</TableCell>
<TableCell>
<Badge variant={
order.status === 'Open' ? 'default' :
order.status === 'In Progress' ? 'secondary' :
'success'
}>
{order.status}
</Badge>
</TableCell>
<TableCell>
<Button variant="ghost" size="icon">
<Wrench className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Work Order Statistics</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="trends">Trends</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Work Orders
</CardTitle>
<Wrench className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{workOrders.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Open Work Orders
</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{workOrders.filter(order => order.status === 'Open').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
In Progress
</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{workOrders.filter(order => order.status === 'In Progress').length}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Completed
</CardTitle>
<CheckCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{workOrders.filter(order => order.status === 'Completed').length}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="trends">
<Card>
<CardHeader>
<CardTitle>Work Order Status Distribution</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={{
status: {
label: "Status",
color: "hsl(var(--chart-1))",
},
}} className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={statusChartData}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{statusChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<ChartTooltip content={<ChartTooltipContent />} />
</PieChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View File

@ -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<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -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<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

365
src/components/ui/chart.tsx Normal file
View File

@ -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<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
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 (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
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 (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}