Compare commits
647 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 061eccd87f | |||
| 68055ba720 | |||
| e0505cbaaa | |||
| 8786cae3f8 | |||
| 489543ffca | |||
| 6631ad8e1f | |||
| b4cef51a91 | |||
| 46c59336f4 | |||
| c4086c8072 | |||
| 97acb7e17f | |||
| 329053bfb9 | |||
|
|
d0f8445848 | ||
| 18016888ce | |||
| bbedb8af8c | |||
| f90cc4eb9b | |||
| d2f6a5fe82 | |||
|
|
2cb0a94bbb | ||
| 80ee62fb24 | |||
| ca4375379e | |||
| 049e142845 | |||
| 3a155b6c03 | |||
| 0f35da597f | |||
| 447b5fc346 | |||
| 24e3d5ab80 | |||
| 3c6382318b | |||
| e26239ae85 | |||
| 9c94ba41fb | |||
| b6f8131b2e | |||
| bbbfaaa31d | |||
| b4c9940013 | |||
| ea73ab687b | |||
| dd9ea67d90 | |||
| 43a298723a | |||
| 789c1c3172 | |||
| 639da61802 | |||
| 7977380c16 | |||
| c346c9036f | |||
| ef7384515d | |||
| ecbcab8e31 | |||
| 9353749899 | |||
| 8c0c705dd8 | |||
| 164f738776 | |||
| 6e1dfdb79f | |||
|
|
d3b9c51952 | ||
| 0f937f12f5 | |||
| 88e4718f2e | |||
|
|
0f5ac1f1f4 | ||
|
|
8aeccb0cab | ||
| 446d1bb7ae | |||
| ebb9b77025 | |||
| 75651a6bd3 | |||
| e409e4684d | |||
| ce0a79196c | |||
| f5ebab5d01 | |||
| 58a48215da | |||
| 11618f0cfd | |||
| 002f58f24e | |||
|
|
a717837bd4 | ||
|
|
a0eb6aa1f9 | ||
|
|
357fac2d56 | ||
| d30f4b7add | |||
| 07c3219bec | |||
| da74491d55 | |||
|
|
077d147498 | ||
| 049c39984b | |||
| 4f7eeb9def | |||
| 3cdcc95656 | |||
| dc0d929eaf | |||
| f5430a1940 | |||
| e2b9ae6e0c | |||
| 99768ec9b9 | |||
| 1ce390e528 | |||
| 4d96d1961c | |||
| 18030d5d2e | |||
| 4f73da1218 | |||
| ee0a643636 | |||
| 287e71f2a9 | |||
| d198fb3d42 | |||
| 80df6116ab | |||
| f7c1ff453f | |||
| 4162e34a5f | |||
| 46bdd5c3d7 | |||
| a6056c11e4 | |||
| be4932661d | |||
| 87233a2240 | |||
| 3c4ec9cac9 | |||
| a008b18838 | |||
| c4d5c44a44 | |||
| 50f55ce8c0 | |||
| d26c963ee8 | |||
| 2c9dedb65e | |||
| 7b4874b8b1 | |||
| eda4549c36 | |||
| 4ca57c92f1 | |||
| 03abba0679 | |||
| cd824dcc63 | |||
| 8d8ba41edf | |||
| c09309a47a | |||
| 86a1b0f72c | |||
| 4f6807b28f | |||
| 4e5f221749 | |||
| 95b45c2e54 | |||
| 4402d0398f | |||
| 1972488eb0 | |||
| 73c4bc6cc7 | |||
| 006023e289 | |||
| cd7ef12085 | |||
| 52404422bd | |||
| 690c35415b | |||
| b711635afa | |||
| 795adacb57 | |||
| adcb695d85 | |||
| b60db151cb | |||
| f4cc5d4641 | |||
| b08d1d29ae | |||
| 51829dd833 | |||
| 653560ee31 | |||
| b5a798adcc | |||
| e7d5765ae1 | |||
| 6b11de24b8 | |||
| 5e5c79faff | |||
| 89e6e05e8e | |||
| b2f3bc117d | |||
| 892d0394af | |||
| 726e1ee52d | |||
| f50928fef1 | |||
| e87bb7bebf | |||
| e23f9f9318 | |||
| feeaed6244 | |||
| f68559d028 | |||
| ee94703773 | |||
| f61a765624 | |||
| c9904b18a1 | |||
| 7154a4a7ba | |||
| 175fead68a | |||
| 812f197b75 | |||
| 8a9e018031 | |||
| 285da7df05 | |||
| 9a4039680e | |||
| 186eb36c7e | |||
| 2077c1b10c | |||
| 52e333b31c | |||
| c24d49fa5b | |||
| df5510224b | |||
| 3a38affdff | |||
| 748a4ff866 | |||
| d7179665d9 | |||
| 1ab4df0b81 | |||
| 06da74e193 | |||
| 59a83e81de | |||
| 7730c0f80a | |||
| f8f2a4b05e | |||
| 5ff5995bcf | |||
| 9c1cbb0ed8 | |||
| e975d1d2b0 | |||
| 1bea2f808d | |||
| 49685a75f7 | |||
| 0bfd709f1f | |||
| 362da2fc06 | |||
| 77b697ac83 | |||
| 405fa60d61 | |||
| 045701d848 | |||
| 3b17f47efe | |||
| 7926df8987 | |||
| 92660414c9 | |||
| f226e335c2 | |||
| 89fb7554e0 | |||
| 076c36bc85 | |||
| 08cdb941a4 | |||
| d723f3e3cc | |||
| 15e08834a0 | |||
| db8499e18d | |||
|
|
8e1ab89e6c | ||
| 6046d8720f | |||
| fc0712cc78 | |||
| 3d0e05daf2 | |||
| 839963049d | |||
| ecea09afc6 | |||
| d40904b5cd | |||
| b782e4e62b | |||
| fb2b070e32 | |||
|
|
85653af39e | ||
|
|
cdcc50e93a | ||
|
|
914e0bceda | ||
|
|
7ad35bd2dc | ||
|
|
1163961b90 | ||
|
|
a2c8df16b7 | ||
|
|
a23171359f | ||
| 404f162728 | |||
| bd7368f0cf | |||
| fb7d71c86e | |||
|
|
2c1871b6e2 | ||
| f22a187148 | |||
| 28ccf8b96d | |||
| 377439495c | |||
|
|
c8a1c29cc3 | ||
| 3214d3893b | |||
| 5817f82b77 | |||
| ad63a314c9 | |||
|
|
1636ba14b6 | ||
| 0315920b0d | |||
|
|
4643feb868 | ||
| a3eff2e738 | |||
| 2e13c0cea0 | |||
| 023fdf5ee9 | |||
| 7d3cc51c8f | |||
| fe8fdafdd8 | |||
|
|
024a618473 | ||
| f194d7ca78 | |||
|
|
755fa092c2 | ||
| 8cf12d32d3 | |||
|
|
017ef65b5a | ||
| 934a857cdf | |||
| 6c4fc20870 | |||
|
|
be697f080e | ||
|
|
6235f96def | ||
|
|
4f6a6f71ed | ||
| de37b27fcc | |||
| 0b244938cf | |||
| 9cc2da6b92 | |||
|
|
08c8e54b2d | ||
| 3ee259abfd | |||
|
|
42a4bcd104 | ||
|
|
ec0d2d1ec7 | ||
| 6aac0c6366 | |||
| 1a51f16ee0 | |||
|
|
7340a5616e | ||
| dea1caafd6 | |||
| d35c820967 | |||
|
|
9c47f5ac76 | ||
|
|
aca52efc44 | ||
|
|
6848ab3586 | ||
|
|
b3547b2971 | ||
|
|
c5c5461167 | ||
| 5b88ee242e | |||
| 06a6eeff9c | |||
| 9cafc305fd | |||
|
|
6382c87d67 | ||
|
|
44b7fcbba2 | ||
| 77d106cb06 | |||
| f8cf68eb1a | |||
| 67d3cb2c7f | |||
| 8127b539e8 | |||
| ea1237942d | |||
| 9d6d7d885d | |||
| 86061e2733 | |||
| 923d323808 | |||
| 933bd6b7fd | |||
| 0c63963ddf | |||
| 51a48e07eb | |||
| 3a7621678d | |||
| 0a8b96bc3b | |||
| d3e203b575 | |||
| 02768f940a | |||
| 711c018014 | |||
| 758b44d6d6 | |||
| 5964528524 | |||
| 7c3e34e83d | |||
| 060ecf1ef2 | |||
| f0ce6d8c2d | |||
| 52b97f435a | |||
| 39312c8698 | |||
|
|
917d318566 | ||
|
|
fbcab5d097 | ||
|
|
113c8c61be | ||
|
|
a4a4831615 | ||
|
|
c2f39c40f0 | ||
| 67aadfa66f | |||
|
|
208372c782 | ||
|
|
1f389ca25c | ||
| ceafee2a80 | |||
|
|
7303c22cd3 | ||
|
|
7bddd74a6e | ||
|
|
f3fc93d6e5 | ||
|
|
eefb8de460 | ||
|
|
f623c17071 | ||
|
|
4594139f8e | ||
|
|
a860a1c247 | ||
|
|
a3fcebfa15 | ||
|
|
6c4b3ef120 | ||
|
|
d4958ec914 | ||
| 8e205ebca2 | |||
| cf274f9013 | |||
| 3c4743fa70 | |||
| 168ebe94fc | |||
|
|
94344b146f | ||
|
|
d898732a17 | ||
| 2757bc66da | |||
| b115a4b66c | |||
|
|
c38bb24e94 | ||
|
|
98584f1324 | ||
|
|
3ef6604884 | ||
|
|
044e9ba7b2 | ||
| f93f4f576b | |||
| 1b00366a1e | |||
|
|
572895efa9 | ||
|
|
c244e5742a | ||
|
|
6efa440bbc | ||
| 0e53b16d6c | |||
|
|
884f56c9a7 | ||
| 0e997071c3 | |||
| 68893b1318 | |||
| a221f2a91b | |||
| 1577ed41cd | |||
|
|
1b4069e7e4 | ||
|
|
a6f90f8477 | ||
|
|
aefa00f95d | ||
|
|
c145b3e552 | ||
|
|
c5de2fdd87 | ||
| ef3318a4ff | |||
| 39e30aa8d5 | |||
|
|
de5bbbf3f7 | ||
|
|
fba042d2b9 | ||
|
|
d85530dd2c | ||
|
|
326182aeef | ||
|
|
2172bede74 | ||
|
|
f497251873 | ||
|
|
e4e04940a9 | ||
|
|
b493357f31 | ||
|
|
4eaf98834d | ||
|
|
7cdab36f5d | ||
|
|
6831123aed | ||
|
|
f7b8648778 | ||
|
|
870fa5488a | ||
|
|
3472b4bcd7 | ||
|
|
d166ec1b4d | ||
|
|
f023077715 | ||
|
|
96e6cd6dc7 | ||
|
|
0311887185 | ||
| 27b15893a7 | |||
| 9b59c5bfe0 | |||
| 62094f2f4f | |||
|
|
8ae47ef056 | ||
|
|
d15940a5f2 | ||
| a7f2fc5b92 | |||
| 50c344091f | |||
| c69820241f | |||
|
|
26bfd3973c | ||
|
|
c0ebef7e96 | ||
|
|
cd74e5f06f | ||
|
|
bb62b5d352 | ||
|
|
f26c795d46 | ||
| d6c4be3acf | |||
| b888307ce7 | |||
| 6b15207ad0 | |||
| ce04f16f97 | |||
| 6d7524c859 | |||
| 2f705b87f7 | |||
| 37410b2f7a | |||
| b043577da0 | |||
| 4cfed522eb | |||
| 289a404c0a | |||
| 08e074607e | |||
| 8de2612b05 | |||
| fb36a00fbf | |||
| bb28a8f31d | |||
| 33bf63cb1e | |||
|
|
a50fa82a50 | ||
|
|
b9635cae68 | ||
|
|
5ecf322f0c | ||
|
|
1e7d56499b | ||
|
|
dff379c99d | ||
|
|
79261274f6 | ||
|
|
3a78021b98 | ||
|
|
5b1e0ea115 | ||
|
|
57ad824d67 | ||
|
|
6ece516cc6 | ||
| b1f574b5a4 | |||
| 31398fdcac | |||
| 12aa06f953 | |||
| 6b055c4364 | |||
| 4bd44727a5 | |||
| 6be0099048 | |||
| 9bfd3a48dc | |||
| 92b66d7995 | |||
| d28fced5b3 | |||
| fa01645d31 | |||
| 666c7e0ca6 | |||
| 39e4a3896e | |||
| 67b5da131e | |||
|
|
1010d57b79 | ||
| 3270971c7f | |||
|
|
a8b5ee361e | ||
| e68e39426b | |||
|
|
fea01ba8ec | ||
|
|
b9b74d2447 | ||
| 56111f3ac7 | |||
| da0c819da5 | |||
|
|
872e3a27f1 | ||
| 5cb32114c8 | |||
| 3631201435 | |||
| fe7e81986d | |||
| 6e21e4fd6c | |||
|
|
83eda83671 | ||
|
|
01be4a7620 | ||
|
|
872042de5a | ||
|
|
6a3730ca04 | ||
|
|
8559a46cb9 | ||
|
|
67cb178f46 | ||
| 54ff97448d | |||
| dd1502d2ce | |||
| c82a0efa93 | |||
| 43715e05a5 | |||
| 4dce16f067 | |||
| d1ff46844a | |||
| ab0dbec1af | |||
|
|
4b8e0b2868 | ||
|
|
8fa74240bc | ||
|
|
f98abf19ef | ||
|
|
6ebec31f09 | ||
|
|
f06f98a657 | ||
|
|
b6bee250df | ||
|
|
5f9d7a848c | ||
|
|
271314f990 | ||
|
|
2bc0ffd47a | ||
|
|
6f11f4ebbb | ||
| 5734ceca6e | |||
| fb7c4c66ef | |||
| 75aec048ae | |||
| aa42a53c74 | |||
| 02184d9cfd | |||
| 0695f1d120 | |||
| 0f2b94a277 | |||
| b80ffff296 | |||
| cbdec1882c | |||
| a7ba0fb4b4 | |||
| 4845aeb479 | |||
| 55b61d3908 | |||
| 808b6e3d2b | |||
| 56ffecba92 | |||
| 548d7629d3 | |||
| 1fc675a138 | |||
|
|
aeca5e4341 | ||
|
|
811edf842b | ||
|
|
a35b432b54 | ||
|
|
627ce7a987 | ||
|
|
57ac8ad2ce | ||
|
|
526650bd98 | ||
|
|
21d086a816 | ||
|
|
a6b188e0d6 | ||
|
|
bd46968b90 | ||
| ef1b7ac716 | |||
| b3ff789f1b | |||
| 3baef0e1ab | |||
| 65140d77ac | |||
| 1de6c981dc | |||
| 9ca647f0cc | |||
|
|
332f38db57 | ||
|
|
54ab737e2d | ||
|
|
155c64a729 | ||
|
|
538b4440d8 | ||
|
|
5a3229eb19 | ||
|
|
c232f193d1 | ||
| f17ad44155 | |||
| 4500b213c6 | |||
|
|
4ccfe55174 | ||
| 0173123898 | |||
|
|
9eaf64c7a9 | ||
|
|
aa30efd3b6 | ||
| 852a8a0eb2 | |||
|
|
4e887c1a73 | ||
|
|
108de589bc | ||
| 27871d96f5 | |||
| c776c579ab | |||
| 9096f4fd29 | |||
| 858d13cadc | |||
| 4d7f452a97 | |||
| ee2cab953d | |||
| 0655dfdfea | |||
|
|
c61f71d226 | ||
| de654e487b | |||
| 6d77a98d92 | |||
| 7e3d094217 | |||
|
|
7c0ab0bfae | ||
|
|
8c5348dbb6 | ||
| 3c35e05e47 | |||
|
|
77e93a38b7 | ||
|
|
6f646a7cf0 | ||
|
|
b7f97c45a5 | ||
|
|
5c9d04fc88 | ||
|
|
cbd038d8fb | ||
|
|
6c832e01f3 | ||
|
|
1d00a3b55c | ||
|
|
5823152c56 | ||
|
|
3d403ee35e | ||
|
|
230f2715ca | ||
|
|
6b3979c68f | ||
|
|
003c7ec58a | ||
|
|
5cb625a710 | ||
|
|
5a1ff67db4 | ||
|
|
870920f67e | ||
|
|
d9db1f778e | ||
|
|
bb7fbf9f78 | ||
|
|
53246a78c6 | ||
| 5a691a5ddf | |||
| 7a70d163c7 | |||
| 24a2af0bee | |||
| 67116d2c89 | |||
| 1029e48f42 | |||
| 3088573b0b | |||
| ae653027c4 | |||
| 386deff6b8 | |||
| 2057d5695b | |||
| f646c18035 | |||
| e291a0e999 | |||
| 3760d61e2e | |||
| c96c3b1dab | |||
| 9f465b25bb | |||
| fd3f49255a | |||
| 88dfdb05a8 | |||
| c15313ea5f | |||
| b2ddea8794 | |||
| 6682cdc047 | |||
| beff4c5297 | |||
| 0570758b07 | |||
| a36dee75af | |||
| 48f1a9c1ba | |||
| f3a611ad60 | |||
|
|
4725e11b89 | ||
| d8704c38f1 | |||
|
|
90628b37d9 | ||
| d839081112 | |||
| 9e3e8c835e | |||
| 35640a1a39 | |||
| 9655a77972 | |||
|
|
6a515f0edc | ||
| d3754c8018 | |||
| 6f8c0674c2 | |||
| 18407f8328 | |||
|
|
4f0abd2f26 | ||
| d51b1b0ab7 | |||
| d8e2c8c95d | |||
| 6d4c9a5e65 | |||
| 706cd94d14 | |||
| 8d3430bd75 | |||
|
|
4b9bf4dff4 | ||
|
|
124a10c619 | ||
|
|
eecb695b87 | ||
|
|
38a0242264 | ||
|
|
693829ce42 | ||
|
|
defd851dbd | ||
|
|
8b2d406433 | ||
|
|
65a8475e47 | ||
| bdd5566493 | |||
| 8f635116df | |||
| c84d817810 | |||
| 30b5041ae5 | |||
| 0294f078f9 | |||
|
|
0c75ffbf35 | ||
|
|
47bd755e72 | ||
|
|
be79de7c82 | ||
|
|
e25a02fe1f | ||
|
|
ddc8e98c19 | ||
|
|
4ea76ddab5 | ||
|
|
7bb2d98639 | ||
| b7d85053bc | |||
| dd373d3800 | |||
| 259770ce69 | |||
| ca06f6c8b3 | |||
| 83a0448219 | |||
| 86267ddddf | |||
|
|
3cc1e93c5b | ||
| cfaee96c3e | |||
|
|
cfd5a7c8cd | ||
|
|
a3e5e6701e | ||
|
|
2a871a4d41 | ||
|
|
4bd98ef06f | ||
|
|
82935303ba | ||
|
|
b7681499ae | ||
|
|
877e0cf0de | ||
| 3d6b87a7d2 | |||
| 5ef045165d | |||
| 488f67289d | |||
| 38a71a8b59 | |||
| 054094a61e | |||
| c48039eeb4 | |||
| e580ed3251 | |||
| 695c80ba9d | |||
| bdbcd75042 | |||
| 563f8ee691 | |||
| 352d7beec1 | |||
| d078af520d | |||
| 77c5f53532 | |||
| 06908ffacb | |||
| 9c3f5d4d50 | |||
| 31a11656c4 | |||
| 07cd4f6c0e | |||
|
|
3b8b1e9b97 | ||
| 8f05f22b23 | |||
| 467daa35f0 | |||
| 0f20523b3c | |||
| a9fc3e3227 | |||
| 061275ba30 | |||
| edea9ef315 | |||
| a7162682c4 | |||
| 5791ddc47d | |||
| e572d9f3cf | |||
| c48e3b8a95 | |||
| aa0950df9b | |||
| 5d3efa0af5 | |||
| 3fbb108646 | |||
| 1a0fe7f95d | |||
| f6147aa810 | |||
| 277d1dce8f | |||
| 24041f4242 | |||
|
|
26c437261f | ||
| 1bab36f727 | |||
| a727878b0c | |||
| fdd4af6746 | |||
| 54b3b1d457 | |||
| 9dba49e141 | |||
| df42f68c04 | |||
| 3cb3faa073 | |||
| c5e9baad5e | |||
| 32ce987fbc | |||
| b8fa6b730f | |||
| 8d419b0dd9 | |||
|
|
cce97b7509 | ||
| d3723b8ae5 | |||
| a254d88775 | |||
|
|
324e5d4e17 | ||
| b3a30e10d1 | |||
|
|
c4775bcbfa | ||
|
|
862ece691a | ||
|
|
13916236eb | ||
|
|
2ad097413b | ||
|
|
653ae3f233 | ||
|
|
87a4404c20 | ||
|
|
55f40572de | ||
|
|
7e832a139f | ||
| 8a278cd6e2 | |||
|
|
f5e031484e | ||
|
|
1360fa1592 | ||
|
|
998390c063 | ||
|
|
9f38ca1bcc | ||
| a3ad1dab8c | |||
|
|
177ac844ee | ||
|
|
4f02825b96 | ||
|
|
d31db865cf | ||
| 5c94880733 | |||
| 3c9a3bf1b3 | |||
| 46d7b58238 | |||
|
|
d3a69b7aea | ||
| 29153cb6ee | |||
|
|
4736b8bd3f | ||
|
|
b46705396d | ||
| 5b7c19f334 |
114
.github/workflows/deploy.yml
vendored
Normal file
114
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set image name (lowercase)
|
||||
run: |
|
||||
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "BACKEND_IMAGE=ghcr.io/${OWNER}/petshop-backend" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push backend image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: ${{ env.BACKEND_IMAGE }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set image name (lowercase)
|
||||
run: |
|
||||
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "FRONTEND_IMAGE=ghcr.io/${OWNER}/petshop-web" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3.3.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push frontend image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./web
|
||||
push: true
|
||||
tags: ${{ env.FRONTEND_IMAGE }}:latest
|
||||
no-cache: true
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-backend, build-frontend]
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Set image names (lowercase)
|
||||
run: |
|
||||
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "BACKEND_IMAGE=ghcr.io/${OWNER}/petshop-backend" >> $GITHUB_ENV
|
||||
echo "FRONTEND_IMAGE=ghcr.io/${OWNER}/petshop-web" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Deploy backend
|
||||
run: |
|
||||
az containerapp update \
|
||||
--name ${{ secrets.AZURE_BACKEND_APP_NAME }} \
|
||||
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
||||
--image ${{ env.BACKEND_IMAGE }}:latest \
|
||||
--revision-suffix r${{ github.run_number }}
|
||||
|
||||
- name: Deploy frontend
|
||||
run: |
|
||||
az containerapp update \
|
||||
--name ${{ secrets.AZURE_FRONTEND_APP_NAME }} \
|
||||
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
||||
--image ${{ env.FRONTEND_IMAGE }}:latest \
|
||||
--revision-suffix r${{ github.run_number }}
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
*.zip
|
||||
.local/
|
||||
commit-patches/
|
||||
temp_photos/
|
||||
.env
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
1570
.idea/caches/deviceStreaming.xml
generated
Normal file
1570
.idea/caches/deviceStreaming.xml
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
.idea/group-2-threaded-project-petshop.iml
generated
Normal file
9
.idea/group-2-threaded-project-petshop.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/markdown.xml
generated
Normal file
8
.idea/markdown.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
5
.idea/misc.xml
generated
Normal file
5
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/group-2-threaded-project-petshop.iml" filepath="$PROJECT_DIR$/.idea/group-2-threaded-project-petshop.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
245
README.md
245
README.md
@@ -1 +1,244 @@
|
||||
# group-2-threaded-project-petshop
|
||||
# PetShop
|
||||
|
||||
A pet store management application with a Spring Boot API serving three clients: a Next.js web app, a JavaFX desktop app, and an Android app.
|
||||
|
||||
Handles product sales, pet adoption, appointment booking, real-time chat, AI assistance, payments (Stripe), email notifications (Resend), and file storage (Azure Blob).
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| API | Java 25, Spring Boot 4, Spring Security (JWT), Hibernate |
|
||||
| Database | MySQL 8.0, Flyway migrations |
|
||||
| Web | Next.js 16, React 19, Tailwind CSS 4 |
|
||||
| Desktop | JavaFX, Maven |
|
||||
| Android | Kotlin, Hilt, Retrofit, CameraX |
|
||||
| Infra | Docker, Azure Container Apps |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
main/
|
||||
backend/ Spring Boot REST API
|
||||
web/ Next.js frontend
|
||||
desktop/ JavaFX desktop client
|
||||
android/ Android mobile app
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Java 25
|
||||
- Node.js 18+
|
||||
- Docker
|
||||
- Maven
|
||||
- Android Studio (for mobile)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Start the database
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### 2. Configure the backend
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Fill in `.env` with your keys:
|
||||
|
||||
```
|
||||
JWT_SECRET=<openssl rand -base64 32>
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
OPENROUTER_API_KEY=sk-or-v1-...
|
||||
RESEND_API_KEY=re_...
|
||||
RESEND_FROM=PetShop <no-reply@yourdomain.com>
|
||||
```
|
||||
|
||||
### 3. Run the backend
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
The API starts at `http://localhost:8080`. Flyway runs migrations and seeds data automatically on first boot.
|
||||
|
||||
### 4. Run the web frontend
|
||||
|
||||
```sh
|
||||
cd web
|
||||
cp .env.example .env.local
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The web app starts at `http://localhost:3000`.
|
||||
|
||||
### 5. Run the desktop client
|
||||
|
||||
```sh
|
||||
cd desktop
|
||||
cp connectionpetstore.properties.example connectionpetstore.properties
|
||||
mvn javafx:run
|
||||
```
|
||||
|
||||
### 6. Run the Android app
|
||||
|
||||
Open `android/` in Android Studio and run on an emulator or device.
|
||||
|
||||
## Switching Between Azure and Local Backend
|
||||
|
||||
Each client reads the backend URL from a config file. To point a client at the
|
||||
hosted Azure backend versus a local one, flip the commented lines.
|
||||
|
||||
### Web
|
||||
|
||||
Edit `web/.env.local`:
|
||||
|
||||
```
|
||||
# Local
|
||||
BACKEND_URL=http://localhost:8080
|
||||
#BACKEND_URL=https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io
|
||||
|
||||
# Azure
|
||||
#BACKEND_URL=http://localhost:8080
|
||||
BACKEND_URL=https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io
|
||||
```
|
||||
|
||||
Restart the dev server after changing.
|
||||
|
||||
### Desktop
|
||||
|
||||
Edit `desktop/src/main/resources/connectionpetstore.properties`:
|
||||
|
||||
```
|
||||
# Local
|
||||
api.baseUrl=http://localhost:8080
|
||||
#api.baseUrl=https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io
|
||||
|
||||
# Azure
|
||||
#api.baseUrl=http://localhost:8080
|
||||
api.baseUrl=https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
Edit `android/local.properties`:
|
||||
|
||||
```properties
|
||||
# Local (emulator — 10.0.2.2 maps to host's localhost)
|
||||
petstore.backend.emulatorUrl=http\://10.0.2.2\:8080/
|
||||
petstore.backend.deviceUrl=http\://192.168.x.x\:8080/
|
||||
|
||||
# Azure
|
||||
petstore.backend.emulatorUrl=https\://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/
|
||||
petstore.backend.deviceUrl=https\://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/
|
||||
```
|
||||
|
||||
Sync Gradle and re-run the app.
|
||||
|
||||
## API
|
||||
|
||||
A Postman collection is available at `backend/postman/`. Key endpoint groups:
|
||||
|
||||
- `/api/auth` -- registration, login, password reset
|
||||
- `/api/products` -- catalog and inventory
|
||||
- `/api/pets` -- listings and adoption
|
||||
- `/api/appointments` -- booking
|
||||
- `/api/cart`, `/api/sales`, `/api/refunds` -- transactions
|
||||
- `/api/chat` -- messaging and AI assistant
|
||||
- `/ws` -- WebSocket (STOMP) for real-time updates
|
||||
|
||||
## Docker (full stack)
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Starts the API and MySQL together. The web frontend has its own Dockerfile for independent deployment.
|
||||
|
||||
## Running the Web App
|
||||
|
||||
Requires Node.js 18+.
|
||||
|
||||
```sh
|
||||
cd web
|
||||
cp .env.example .env.local
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:3000`. The app proxies API calls to the backend at `http://localhost:8080` by default.
|
||||
|
||||
To point at a different backend, edit `BACKEND_URL` and `NEXT_PUBLIC_BACKEND_URL` in `.env.local`.
|
||||
|
||||
For a production build:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Running the Desktop App (JavaFX)
|
||||
|
||||
Requires IntelliJ IDEA and Java 25+.
|
||||
|
||||
1. Open the `desktop/` directory in IntelliJ.
|
||||
2. Copy `connectionpetstore.properties.example` to `connectionpetstore.properties` and edit it to match your database. The defaults expect the dev Docker database:
|
||||
|
||||
```
|
||||
url=jdbc:mysql://127.0.0.1:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
|
||||
user=petshop
|
||||
password=petshop
|
||||
```
|
||||
|
||||
3. Open **View > Tool Windows > Maven** and click **Reload All Maven Projects**.
|
||||
4. Expand **Plugins > javafx** and double-click **javafx:run**.
|
||||
|
||||
Default accounts seeded on first run:
|
||||
|
||||
| Role | Username | Password |
|
||||
|------|----------|----------|
|
||||
| Admin | `admin` | `admin123` |
|
||||
| Staff | `staff` | `staff123` |
|
||||
|
||||
## Running the Android App
|
||||
|
||||
Requires Android Studio and the Android SDK (min API 24).
|
||||
|
||||
1. Copy `local.properties.template` to `local.properties` and set `sdk.dir` to your Android SDK path.
|
||||
2. Configure the backend URLs in `local.properties`:
|
||||
|
||||
```properties
|
||||
# Emulator — 10.0.2.2 maps to the host machine's localhost
|
||||
petstore.backend.emulatorUrl=http\://10.0.2.2\:8080/
|
||||
|
||||
# Physical device — use the host machine's LAN IP
|
||||
petstore.backend.deviceUrl=http\://192.168.x.x\:8080/
|
||||
```
|
||||
|
||||
3. Open the `android/` directory in Android Studio.
|
||||
4. Sync Gradle, then run on an emulator or connected device.
|
||||
|
||||
## Running the Backend
|
||||
|
||||
Requires IntelliJ IDEA and Java 25+.
|
||||
|
||||
1. Open the `backend/` directory in IntelliJ.
|
||||
2. Copy `.env.example` to `.env` and fill in your API keys.
|
||||
3. Start the database using Docker from IntelliJ's **Services** panel, or from a terminal:
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
4. Run the `BackendApplication` main class from IntelliJ.
|
||||
|
||||
The API starts at `http://localhost:8080`. Flyway runs migrations and seeds data automatically on first boot.
|
||||
|
||||
3
android/.gitignore
vendored
3
android/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
*.iml
|
||||
nohup.out
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/*
|
||||
@@ -16,6 +17,8 @@
|
||||
/app/src/androidTest/
|
||||
/app/src/test/
|
||||
.DS_Store
|
||||
/.project
|
||||
/.settings/
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
|
||||
1
android/.idea/gradle.xml
generated
1
android/.idea/gradle.xml
generated
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
||||
1
android/.idea/misc.xml
generated
1
android/.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
|
||||
6
android/.idea/vcs.xml
generated
Normal file
6
android/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
4
android/app/.gitignore
vendored
4
android/app/.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
/build
|
||||
/nohup.out
|
||||
/.classpath
|
||||
/.project
|
||||
/.settings/
|
||||
/src/test/
|
||||
/src/androidTest/
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.navigation.safeargs)
|
||||
}
|
||||
|
||||
val localProperties = Properties().apply {
|
||||
val file = rootProject.file("local.properties")
|
||||
if (file.exists()) {
|
||||
file.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun quoted(value: String): String = "\"$value\""
|
||||
|
||||
val emulatorBackendUrl =
|
||||
(localProperties.getProperty("petstore.backend.emulatorUrl") ?: "https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/").trim()
|
||||
val deviceBackendUrl =
|
||||
(localProperties.getProperty("petstore.backend.deviceUrl") ?: "https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/").trim()
|
||||
|
||||
android {
|
||||
namespace = "com.example.petstoremobile"
|
||||
compileSdk = 36
|
||||
@@ -14,6 +32,14 @@ android {
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField("String", "EMULATOR_BACKEND_URL", quoted(emulatorBackendUrl))
|
||||
buildConfigField("String", "DEVICE_BACKEND_URL", quoted(deviceBackendUrl))
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -32,30 +58,45 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Core AndroidX & UI
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.activity)
|
||||
implementation(libs.constraintlayout)
|
||||
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||
|
||||
implementation("androidx.camera:camera-core:1.4.0")
|
||||
implementation("androidx.camera:camera-camera2:1.4.0")
|
||||
implementation("androidx.camera:camera-lifecycle:1.4.0")
|
||||
implementation("androidx.camera:camera-view:1.4.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation(libs.swiperefreshlayout)
|
||||
implementation(libs.viewpager2)
|
||||
|
||||
// Hilt Dependency Injection
|
||||
implementation(libs.hilt.android)
|
||||
annotationProcessor(libs.hilt.compiler)
|
||||
|
||||
// Navigation Component
|
||||
implementation(libs.navigation.fragment)
|
||||
implementation(libs.navigation.ui)
|
||||
|
||||
// Networking
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.gson)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging)
|
||||
|
||||
// CameraX
|
||||
implementation(libs.camera.core)
|
||||
implementation(libs.camera.camera2)
|
||||
implementation(libs.camera.lifecycle)
|
||||
implementation(libs.camera.view)
|
||||
|
||||
// Image Loading
|
||||
implementation(libs.glide)
|
||||
annotationProcessor(libs.glide.compiler)
|
||||
|
||||
// Other Third-party Libraries
|
||||
implementation("com.github.NaikSoftware:StompProtocolAndroid:1.6.6")
|
||||
implementation("io.reactivex.rxjava2:rxjava:2.2.21")
|
||||
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
|
||||
implementation("com.github.prolificinteractive:material-calendarview:2.0.1")
|
||||
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
@@ -24,10 +25,18 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.PetStoreMobile">
|
||||
|
||||
<service
|
||||
android:name=".services.ChatNotificationService"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.HomeActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activities.ForgotPasswordActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -1,13 +1,18 @@
|
||||
/*
|
||||
* Application entry point, sets up Hilt dependency injection.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
package com.example.petstoremobile;
|
||||
|
||||
import android.app.Application;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import dagger.hilt.android.HiltAndroidApp;
|
||||
|
||||
@HiltAndroidApp
|
||||
public class PetStoreApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
// Clear login data on app so when the application closes, the user is logged out and have to re-login
|
||||
TokenManager.getInstance(this).clearLoginData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Screen where the user can request a password reset email.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
package com.example.petstoremobile.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.databinding.ActivityForgotPasswordBinding;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class ForgotPasswordActivity extends AppCompatActivity {
|
||||
|
||||
@Inject
|
||||
AuthApi authApi;
|
||||
|
||||
private ActivityForgotPasswordBinding binding;
|
||||
|
||||
/**
|
||||
* Set the content view for forget password page
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
EdgeToEdge.enable(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityForgotPasswordBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.forgotPasswordRoot, (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets;
|
||||
});
|
||||
|
||||
binding.btnSubmit.setOnClickListener(v -> {
|
||||
if (!InputValidator.isValidEmail(binding.etEmail)) return;
|
||||
String email = binding.etEmail.getText().toString().trim();
|
||||
sendResetLink(email);
|
||||
});
|
||||
|
||||
binding.btnBackToLogin.setOnClickListener(v -> finish());
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to send a reset link to the given email address.
|
||||
* Calls the forgotPassword endpoint. To send the reset link
|
||||
* */
|
||||
private void sendResetLink(String email) {
|
||||
binding.btnSubmit.setEnabled(false);
|
||||
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("usernameOrEmail", email);
|
||||
|
||||
authApi.forgotPassword(body).enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||
if (binding == null) return;
|
||||
binding.btnSubmit.setEnabled(true);
|
||||
Toast.makeText(ForgotPasswordActivity.this,
|
||||
"If this email is registered, a reset link will be sent.",
|
||||
Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
if (binding == null) return;
|
||||
binding.btnSubmit.setEnabled(true);
|
||||
Toast.makeText(ForgotPasswordActivity.this,
|
||||
"Could not send reset link. Please try again.",
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,147 @@
|
||||
/*
|
||||
* Main home screen that shows the dashboard after the user logs in.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
package com.example.petstoremobile.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.navigation.ui.NavigationUI;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.fragments.ChatFragment;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.ProfileFragment;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.ActivityHomeBinding;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class HomeActivity extends AppCompatActivity {
|
||||
private ActivityHomeBinding binding;
|
||||
private NavController navController;
|
||||
|
||||
private final BroadcastReceiver forceLogoutReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Intent loginIntent = new Intent(HomeActivity.this, MainActivity.class);
|
||||
loginIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(loginIntent);
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
// Launcher to ask for notification permission
|
||||
private final ActivityResultLauncher<String> requestPermissionLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||||
if (!isGranted) {
|
||||
Log.w("HomeActivity", "Notification permission denied");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets up the home screen, initializes bottom navigation, and handles incoming navigation intents.
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
EdgeToEdge.enable(this);
|
||||
setContentView(R.layout.activity_home);
|
||||
super.onCreate(savedInstanceState);
|
||||
binding = ActivityHomeBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets;
|
||||
});
|
||||
|
||||
//get the bottom navbar from the layout
|
||||
BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
|
||||
// Initialize Navigation Component
|
||||
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.nav_host_fragment);
|
||||
if (navHostFragment != null) {
|
||||
navController = navHostFragment.getNavController();
|
||||
NavigationUI.setupWithNavController(binding.bottomNavigation, navController);
|
||||
}
|
||||
|
||||
// Load ListFragment by default only if this is a fresh start
|
||||
//load the list fragment by default if it's a fresh start
|
||||
if (savedInstanceState == null) {
|
||||
loadFragment(new ListFragment());
|
||||
bottomNav.setSelectedItemId(R.id.nav_list);
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
//when an item in the bar is selected, load the corresponding fragment
|
||||
bottomNav.setOnItemSelectedListener(item -> {
|
||||
|
||||
if (item.getItemId() == R.id.nav_list) {
|
||||
loadFragment(new ListFragment());
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.nav_chat) {
|
||||
loadFragment(new ChatFragment());
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.nav_profile) {
|
||||
loadFragment(new ProfileFragment());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
IntentFilter filter = new IntentFilter(TokenManager.ACTION_FORCE_LOGOUT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(forceLogoutReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(forceLogoutReceiver, filter);
|
||||
}
|
||||
|
||||
//helper function to load a fragment
|
||||
private void loadFragment(Fragment fragment) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment)
|
||||
.commit();
|
||||
startNotificationService();
|
||||
requestNotificationPermission();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
unregisterReceiver(forceLogoutReceiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles new intents received while the activity is already running (like notifications).
|
||||
*/
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the intent to determine if specific navigation (like opening a chat) is required.
|
||||
*/
|
||||
private void handleIntent(Intent intent) {
|
||||
if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) {
|
||||
if (binding.bottomNavigation != null) {
|
||||
// Navigate by selecting the bottom nav item.
|
||||
binding.bottomNavigation.setSelectedItemId(R.id.nav_chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the background service responsible for monitoring chat notifications.
|
||||
*/
|
||||
private void startNotificationService() {
|
||||
Intent serviceIntent = new Intent(this, ChatNotificationService.class);
|
||||
startService(serviceIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests for notification permission from the user if running on Android 13 and above.
|
||||
*/
|
||||
private void requestNotificationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
/*
|
||||
* Entry point of the app that shows the login and registration screen.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
package com.example.petstoremobile.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
@@ -13,120 +16,142 @@ import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.dtos.AuthDTO;
|
||||
import com.example.petstoremobile.databinding.ActivityMainBinding;
|
||||
import com.example.petstoremobile.viewmodels.AuthViewModel;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
//The login screen activity
|
||||
@AndroidEntryPoint
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private EditText etUser;
|
||||
private EditText etPassword;
|
||||
private Button btnLogin;
|
||||
private TextView tvLoginStatus;
|
||||
private ActivityMainBinding binding;
|
||||
private AuthViewModel viewModel;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
/**
|
||||
* Initializes the activity, sets up the UI, and checks for an existing login session.
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
EdgeToEdge.enable(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Check if user is already logged in
|
||||
if (TokenManager.getInstance(this).isLoggedIn()) {
|
||||
Intent intent = new Intent(this, HomeActivity.class);
|
||||
startActivity(intent);
|
||||
if (tokenManager.isLoggedIn()) {
|
||||
if ("CUSTOMER".equalsIgnoreCase(tokenManager.getRole())) {
|
||||
// If a customer somehow remained logged in, clear them out
|
||||
tokenManager.clearLoginData();
|
||||
} else {
|
||||
startActivity(new Intent(this, HomeActivity.class));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeToEdge.enable(this);
|
||||
setContentView(R.layout.activity_main);
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
|
||||
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
|
||||
return insets;
|
||||
});
|
||||
|
||||
//get all controls from layout
|
||||
tvLoginStatus = findViewById(R.id.tvLoginStatus);
|
||||
etUser = findViewById(R.id.etUser);
|
||||
etPassword = findViewById(R.id.etPassword);
|
||||
btnLogin = findViewById(R.id.btnLogin);
|
||||
//clear login status
|
||||
tvLoginStatus.setText("");
|
||||
binding.tvLoginStatus.setText("");
|
||||
|
||||
// Set editor action listener for password field to login on when enter is pressed
|
||||
binding.etPassword.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
|
||||
binding.btnLogin.performClick();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
//Set click listener for login button
|
||||
btnLogin.setOnClickListener(v -> {
|
||||
binding.btnLogin.setOnClickListener(v -> {
|
||||
//Get username and password from text fields
|
||||
String username = etUser.getText().toString();
|
||||
String password = etPassword.getText().toString();
|
||||
String username = binding.etUser.getText().toString();
|
||||
String password = binding.etPassword.getText().toString();
|
||||
|
||||
//check if fields are empty
|
||||
if (username.isEmpty() || password.isEmpty()) {
|
||||
Toast.makeText(this, "Please enter username and password", Toast.LENGTH_SHORT).show();
|
||||
tvLoginStatus.setText("Please enter username and password");
|
||||
binding.tvLoginStatus.setText("Please enter username and password");
|
||||
return;
|
||||
}
|
||||
|
||||
AuthApi authApi = RetrofitClient.getAuthApi(this);
|
||||
|
||||
//Call login from api and get response
|
||||
authApi.login(new AuthDTO.LoginRequest(username,password)).enqueue(new Callback<AuthDTO.LoginResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<AuthDTO.LoginResponse> call, Response<AuthDTO.LoginResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
//save login data in shared preferences
|
||||
TokenManager.getInstance(MainActivity.this).saveLoginData(
|
||||
response.body().getToken(),
|
||||
response.body().getUsername(),
|
||||
response.body().getRole()
|
||||
);
|
||||
|
||||
//fetch user id from api then login to home activity
|
||||
RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser()
|
||||
.enqueue(new Callback<AuthDTO.UserResponse>() {
|
||||
@Override
|
||||
public void onResponse(Call<AuthDTO.UserResponse> call,
|
||||
Response<AuthDTO.UserResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
TokenManager.getInstance(MainActivity.this)
|
||||
.saveUserId(response.body().getId());
|
||||
}
|
||||
|
||||
Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show();
|
||||
startActivity(new Intent(MainActivity.this, HomeActivity.class));
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<AuthDTO.UserResponse> call,
|
||||
Throwable t) {
|
||||
Log.e("MainActivity", "Failed to fetch userId", t);
|
||||
|
||||
Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show();
|
||||
startActivity(new Intent(MainActivity.this, HomeActivity.class));
|
||||
finish();
|
||||
}
|
||||
performLogin(username, password);
|
||||
});
|
||||
|
||||
// Set click listener for forgot password link
|
||||
binding.tvForgotPassword.setOnClickListener(v -> {
|
||||
startActivity(new Intent(this, ForgotPasswordActivity.class));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform login process using the AuthViewModel and handles the authentication response.
|
||||
*/
|
||||
private void performLogin(String username, String password) {
|
||||
viewModel.login(username, password).observe(this, resource -> {
|
||||
if (resource == null) return;
|
||||
|
||||
switch (resource.status) {
|
||||
case LOADING:
|
||||
UIUtils.setViewsEnabled(false, binding.btnLogin);
|
||||
binding.tvLoginStatus.setText("Logging in...");
|
||||
break;
|
||||
case SUCCESS:
|
||||
if (resource.data != null) {
|
||||
String role = resource.data.getRole();
|
||||
//Check if role is staff/admin or customer
|
||||
if ("CUSTOMER".equalsIgnoreCase(role)) {
|
||||
UIUtils.setViewsEnabled(true, binding.btnLogin);
|
||||
binding.tvLoginStatus.setText("Customers are not allowed to log in");
|
||||
Toast.makeText(this, "Access denied: Customers are not allowed to log in.", Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
|
||||
tvLoginStatus.setText("Login failed");
|
||||
tokenManager.saveLoginData(resource.data.getToken(), resource.data.getUsername(), role);
|
||||
fetchUserIdAndNavigate();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ERROR:
|
||||
UIUtils.setViewsEnabled(true, binding.btnLogin);
|
||||
binding.tvLoginStatus.setText(resource.message);
|
||||
Toast.makeText(this, resource.message, Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<AuthDTO.LoginResponse> call, Throwable t) {
|
||||
Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show();
|
||||
tvLoginStatus.setText("Login failed");
|
||||
/**
|
||||
* Retrieves the user's profile information to save their ID before navigating to the home screen.
|
||||
*/
|
||||
private void fetchUserIdAndNavigate() {
|
||||
viewModel.getMe().observe(this, resource -> {
|
||||
if (resource != null && resource.status != Resource.Status.LOADING) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
tokenManager.saveUserId(resource.data.getId());
|
||||
tokenManager.savePrimaryStoreId(resource.data.getStoreId());
|
||||
}
|
||||
Toast.makeText(this, "Login successful", Toast.LENGTH_SHORT).show();
|
||||
startActivity(new Intent(this, HomeActivity.class));
|
||||
finish();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Adapter for displaying activity log entries in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.dtos.ActivityLogDTO;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ActivityLogAdapter extends RecyclerView.Adapter<ActivityLogAdapter.ViewHolder> {
|
||||
|
||||
private static final String SEPARATOR = " | ";
|
||||
private static final SimpleDateFormat INPUT_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
|
||||
private static final SimpleDateFormat OUTPUT_FORMAT = new SimpleDateFormat("MMM d, HH:mm", Locale.getDefault());
|
||||
|
||||
private final List<ActivityLogDTO> items;
|
||||
|
||||
/**
|
||||
* Constructor for the ActivityLogAdapter.
|
||||
*/
|
||||
public ActivityLogAdapter(List<ActivityLogDTO> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout for an activity log item and creates a new ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_activity_log, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the data from ActivityLogDTO to the items in the ViewHolder.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
ActivityLogDTO log = items.get(position);
|
||||
|
||||
String activity = log.getActivity() != null ? log.getActivity() : "";
|
||||
int separatorIndex = activity.indexOf(SEPARATOR);
|
||||
if (separatorIndex >= 0) {
|
||||
holder.tvActivity.setText(activity.substring(0, separatorIndex).trim());
|
||||
holder.tvTechnical.setText(activity.substring(separatorIndex + SEPARATOR.length()).trim());
|
||||
holder.tvTechnical.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.tvActivity.setText(activity);
|
||||
holder.tvTechnical.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
String fullName = firstNonBlank(log.getFullName(), log.getFullNameSnapshot(), "Unknown");
|
||||
String username = firstNonBlank(log.getUsername(), log.getUsernameSnapshot(), "");
|
||||
holder.tvUser.setText(username.isEmpty() ? fullName : fullName + " (" + username + ")");
|
||||
|
||||
String role = firstNonBlank(log.getRole(), log.getRoleSnapshot(), "");
|
||||
String store = firstNonBlank(log.getStoreName(), log.getStoreNameSnapshot(), "");
|
||||
if (!role.isEmpty() && !store.isEmpty()) {
|
||||
holder.tvMeta.setText(store + " · " + formatRole(role));
|
||||
} else if (!role.isEmpty()) {
|
||||
holder.tvMeta.setText(formatRole(role));
|
||||
} else if (!store.isEmpty()) {
|
||||
holder.tvMeta.setText(store);
|
||||
} else {
|
||||
holder.tvMeta.setText("");
|
||||
}
|
||||
|
||||
holder.tvTimestamp.setText(formatTimestamp(log.getLogTimestamp()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of items in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() { return items.size(); }
|
||||
|
||||
/**
|
||||
* Formats the timestamp string from the API to a readable date/time format.
|
||||
*/
|
||||
private String formatTimestamp(String raw) {
|
||||
if (raw == null) return "";
|
||||
try {
|
||||
String normalized = raw.length() > 19 ? raw.substring(0, 19) : raw;
|
||||
Date date = INPUT_FORMAT.parse(normalized);
|
||||
return date != null ? OUTPUT_FORMAT.format(date) : raw.substring(0, Math.min(16, raw.length())).replace("T", " ");
|
||||
} catch (ParseException e) {
|
||||
return raw.length() >= 16 ? raw.substring(0, 16).replace("T", " ") : raw;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the Role string to be consistent
|
||||
*/
|
||||
private String formatRole(String role) {
|
||||
if (role == null) return "";
|
||||
switch (role.toUpperCase(Locale.ROOT)) {
|
||||
case "ADMIN": return "Admin";
|
||||
case "STAFF": return "Staff";
|
||||
case "CUSTOMER": return "Customer";
|
||||
default: return role;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first non-null and non-blank string from the provided arguments.
|
||||
*/
|
||||
private String firstNonBlank(String... values) {
|
||||
for (String v : values) {
|
||||
if (v != null && !v.isBlank()) return v;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder class that holds references to the UI components for an activity log item.
|
||||
*/
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvActivity, tvTechnical, tvUser, tvMeta, tvTimestamp;
|
||||
|
||||
/**
|
||||
* Initializes the ViewHolder by finding the views within the item layout.
|
||||
*/
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvActivity = itemView.findViewById(R.id.tvLogActivity);
|
||||
tvTechnical = itemView.findViewById(R.id.tvLogTechnical);
|
||||
tvUser = itemView.findViewById(R.id.tvLogUser);
|
||||
tvMeta = itemView.findViewById(R.id.tvLogMeta);
|
||||
tvTimestamp = itemView.findViewById(R.id.tvLogTimestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,153 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
/*
|
||||
* Adapter for showing adoption records in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.models.Adoption;
|
||||
import com.example.petstoremobile.databinding.ItemAdoptionBinding;
|
||||
import com.example.petstoremobile.dtos.AdoptionDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
import java.util.List;
|
||||
|
||||
public class AdoptionAdapter extends RecyclerView.Adapter<AdoptionAdapter.AdoptionViewHolder> {
|
||||
public class AdoptionAdapter extends RecyclerView.Adapter<AdoptionAdapter.AdoptionViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<Adoption> adoptionList;
|
||||
private OnAdoptionClickListener adoptionClickListener;
|
||||
private List<AdoptionDTO> adoptionList;
|
||||
private OnAdoptionClickListener listener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for adoption click on recycler view
|
||||
public interface OnAdoptionClickListener {
|
||||
void onAdoptionClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
public AdoptionAdapter(List<Adoption> adoptionList, OnAdoptionClickListener adoptionClickListener) {
|
||||
/**
|
||||
* Constructor for AdoptionAdapter.
|
||||
*/
|
||||
public AdoptionAdapter(List<AdoptionDTO> adoptionList, OnAdoptionClickListener listener) {
|
||||
this.adoptionList = adoptionList;
|
||||
this.adoptionClickListener = adoptionClickListener;
|
||||
this.listener = listener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
listener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of IDs for the currently selected adoption items.
|
||||
*/
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the selection state, deselecting all items.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
public static class AdoptionViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvAdopterName, tvPetName, tvAdoptionDate, tvAdoptionStatus;
|
||||
final ItemAdoptionBinding binding;
|
||||
|
||||
public AdoptionViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvAdopterName = v.findViewById(R.id.tvAdopterName);
|
||||
tvPetName = v.findViewById(R.id.tvAdoptionPetName);
|
||||
tvAdoptionDate = v.findViewById(R.id.tvAdoptionDate);
|
||||
tvAdoptionStatus = v.findViewById(R.id.tvAdoptionStatus);
|
||||
public AdoptionViewHolder(@NonNull ItemAdoptionBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new row view
|
||||
/**
|
||||
* Inflates the layout for an adoption item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public AdoptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_adoption, parent, false);
|
||||
return new AdoptionViewHolder(v);
|
||||
ItemAdoptionBinding binding = ItemAdoptionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new AdoptionViewHolder(binding);
|
||||
}
|
||||
|
||||
// Populate the row with adoption data
|
||||
/**
|
||||
* Binds adoption data to the UI components and handles click/long-click logic.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AdoptionViewHolder holder, int position) {
|
||||
Adoption adoption = adoptionList.get(position);
|
||||
AdoptionDTO a = adoptionList.get(position);
|
||||
ItemAdoptionBinding binding = holder.binding;
|
||||
|
||||
holder.tvAdopterName.setText(adoption.getAdopterName());
|
||||
holder.tvPetName.setText("Pet: " + adoption.getPetName());
|
||||
holder.tvAdoptionDate.setText("Date: " + adoption.getAdoptionDate());
|
||||
holder.tvAdoptionStatus.setText(adoption.getStatus());
|
||||
binding.tvAdoptionCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
|
||||
binding.tvAdoptionPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
|
||||
binding.tvAdoptionStaffName.setText("Staff: " + (a.getEmployeeName() != null ? a.getEmployeeName() : "N/A"));
|
||||
binding.tvAdoptionDate.setText("Date: " + (a.getAdoptionDate() != null ? a.getAdoptionDate() : ""));
|
||||
binding.tvAdoptionFee.setText(a.getAdoptionFee() != null ? "$" + a.getAdoptionFee() : "");
|
||||
|
||||
// Set the status color depending on adoption status
|
||||
if (adoption.getStatus().equals("Approved")) {
|
||||
holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
} else if (adoption.getStatus().equals("Pending")) {
|
||||
holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
||||
|
||||
String status = a.getAdoptionStatus() != null ? a.getAdoptionStatus() : "";
|
||||
binding.tvAdoptionStatus.setText(status);
|
||||
|
||||
switch (status) {
|
||||
case "Completed":
|
||||
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
break;
|
||||
case "Pending":
|
||||
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
||||
break;
|
||||
case "Cancelled":
|
||||
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
break;
|
||||
default:
|
||||
binding.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
|
||||
break;
|
||||
}
|
||||
|
||||
String key = String.valueOf(a.getAdoptionId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectAdoption.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectAdoption.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
binding.cbSelectAdoption.setVisibility(View.GONE);
|
||||
binding.cbSelectAdoption.setChecked(false);
|
||||
}
|
||||
|
||||
// When a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> adoptionClickListener.onAdoptionClick(position));
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
listener.onAdoptionClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of adoption items in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return adoptionList.size();
|
||||
}
|
||||
public int getItemCount() { return adoptionList.size(); }
|
||||
}
|
||||
|
||||
@@ -1,84 +1,160 @@
|
||||
/*
|
||||
* Adapter for showing appointments in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.models.Appointment;
|
||||
import com.example.petstoremobile.databinding.ItemAppointmentBinding;
|
||||
import com.example.petstoremobile.dtos.AppointmentDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
import java.util.List;
|
||||
|
||||
public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.AppointmentViewHolder> {
|
||||
public class AppointmentAdapter extends RecyclerView.Adapter<AppointmentAdapter.AppointmentViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<Appointment> appointmentList;
|
||||
private List<AppointmentDTO> appointmentList;
|
||||
private OnAppointmentClickListener appointmentClickListener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for appointment click on recycler view
|
||||
public interface OnAppointmentClickListener {
|
||||
void onAppointmentClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
public AppointmentAdapter(List<Appointment> appointmentList, OnAppointmentClickListener appointmentClickListener) {
|
||||
/**
|
||||
* Constructor for AppointmentAdapter.
|
||||
*/
|
||||
public AppointmentAdapter(List<AppointmentDTO> appointmentList,
|
||||
OnAppointmentClickListener appointmentClickListener) {
|
||||
this.appointmentList = appointmentList;
|
||||
this.appointmentClickListener = appointmentClickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
appointmentClickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of IDs for the currently selected appointment items.
|
||||
*/
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the selection state, deselecting all items.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder class that holds references to the UI components for an appointment item.
|
||||
*/
|
||||
public static class AppointmentViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvCustomerName, tvPetName, tvServiceType, tvDateTime, tvAppointmentStatus;
|
||||
private final ItemAppointmentBinding binding;
|
||||
|
||||
public AppointmentViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvCustomerName = v.findViewById(R.id.tvCustomerName);
|
||||
tvPetName = v.findViewById(R.id.tvApptPetName);
|
||||
tvServiceType = v.findViewById(R.id.tvServiceType);
|
||||
tvDateTime = v.findViewById(R.id.tvDateTime);
|
||||
tvAppointmentStatus = v.findViewById(R.id.tvAppointmentStatus);
|
||||
/**
|
||||
* Initializes the ViewHolder by finding the views within the item layout.
|
||||
*/
|
||||
public AppointmentViewHolder(@NonNull ItemAppointmentBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new row view
|
||||
/**
|
||||
* Inflates the layout for an appointment item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_appointment, parent, false);
|
||||
return new AppointmentViewHolder(v);
|
||||
ItemAppointmentBinding binding = ItemAppointmentBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new AppointmentViewHolder(binding);
|
||||
}
|
||||
|
||||
// Populate the row with appointment data
|
||||
/**
|
||||
* Binds appointment data to the UI components and handles click/long-click logic.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) {
|
||||
Appointment appointment = appointmentList.get(position);
|
||||
AppointmentDTO a = appointmentList.get(position);
|
||||
ItemAppointmentBinding binding = holder.binding;
|
||||
|
||||
holder.tvCustomerName.setText(appointment.getCustomerName());
|
||||
holder.tvPetName.setText("Pet: " + appointment.getPetName());
|
||||
holder.tvServiceType.setText(appointment.getServiceType());
|
||||
holder.tvDateTime.setText(appointment.getAppointmentDate() + " at " + appointment.getAppointmentTime());
|
||||
holder.tvAppointmentStatus.setText(appointment.getStatus());
|
||||
binding.tvCustomerName.setText(a.getCustomerName() != null ? a.getCustomerName() : "");
|
||||
binding.tvApptPetName.setText("Pet: " + (a.getPetName() != null ? a.getPetName() : ""));
|
||||
binding.tvServiceType.setText(a.getServiceType() != null ? a.getServiceType() : "");
|
||||
binding.tvStaffName.setText("Staff: " + (a.getEmployeeName() != null ? a.getEmployeeName() : "Unassigned"));
|
||||
binding.tvDateTime.setText((a.getAppointmentDate() != null ? a.getAppointmentDate() : "") +
|
||||
" at " + (a.getAppointmentTime() != null ? a.getAppointmentTime() : ""));
|
||||
|
||||
// Set the status color depending on appointment status
|
||||
switch (appointment.getStatus()) {
|
||||
case "Confirmed":
|
||||
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
String status = a.getStatus() != null ? a.getStatus() : "";
|
||||
binding.tvAppointmentStatus.setText(status);
|
||||
|
||||
switch (status.toUpperCase()) {
|
||||
case "BOOKED":
|
||||
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#2196F3")); // blue
|
||||
break;
|
||||
case "Pending":
|
||||
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
||||
case "COMPLETED":
|
||||
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); // green
|
||||
break;
|
||||
case "CANCELLED":
|
||||
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); // red
|
||||
break;
|
||||
default:
|
||||
holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
binding.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#9E9E9E")); // gray
|
||||
break;
|
||||
}
|
||||
|
||||
// When a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> appointmentClickListener.onAppointmentClick(position));
|
||||
String key = String.valueOf(a.getAppointmentId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectAppointment.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectAppointment.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectAppointment.setVisibility(View.GONE);
|
||||
binding.cbSelectAppointment.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
appointmentClickListener.onAppointmentClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of appointment items in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return appointmentList.size();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Custom array adapter that displays items with black text.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.example.petstoremobile.R;
|
||||
import java.util.List;
|
||||
|
||||
// A class that overrides the arrayAdapter so the text color changes based on theme
|
||||
// Used to make spinners have black text on white background no matter the theme
|
||||
public class BlackTextArrayAdapter<T> extends ArrayAdapter<T> {
|
||||
public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
|
||||
public BlackTextArrayAdapter(@NonNull Context context, int resource, @NonNull List<T> objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white));
|
||||
if (view instanceof TextView) {
|
||||
((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text));
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getDropDownView(position, convertView, parent);
|
||||
view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white));
|
||||
if (view instanceof TextView) {
|
||||
((TextView) view).setTextColor(ContextCompat.getColor(getContext(), R.color.spinner_text));
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
/*
|
||||
* Adapter for displaying chat conversations in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemChatBinding;
|
||||
import com.example.petstoremobile.models.Chat;
|
||||
|
||||
import java.util.List;
|
||||
@@ -22,38 +27,55 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ChatViewHolder
|
||||
void onChatClick(Chat chat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for ChatAdapter.
|
||||
*/
|
||||
public ChatAdapter(List<Chat> chatList, OnChatClickListener listener) {
|
||||
this.chatList = chatList;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout for a chat item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public ChatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat, parent, false);
|
||||
return new ChatViewHolder(view);
|
||||
ItemChatBinding binding = ItemChatBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new ChatViewHolder(binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds chat data to the UI components for a specific conversation.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ChatViewHolder holder, int position) {
|
||||
Chat chat = chatList.get(position);
|
||||
holder.tvCustomerName.setText(chat.getCustomerName());
|
||||
holder.tvLastMessage.setText(chat.getLastMessage());
|
||||
holder.binding.tvCustomerName.setText(chat.getCustomerName());
|
||||
holder.binding.tvLastMessage.setText(chat.getLastMessage());
|
||||
holder.itemView.setOnClickListener(v -> listener.onChatClick(chat));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of chat items in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return chatList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder class that holds references to the UI components for a chat item.
|
||||
*/
|
||||
public static class ChatViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvCustomerName, tvLastMessage;
|
||||
final ItemChatBinding binding;
|
||||
|
||||
public ChatViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvCustomerName = itemView.findViewById(R.id.tvCustomerName);
|
||||
tvLastMessage = itemView.findViewById(R.id.tvLastMessage);
|
||||
/**
|
||||
* Initializes the ViewHolder with the chat item's view binding.
|
||||
*/
|
||||
public ChatViewHolder(@NonNull ItemChatBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Adapter for showing coupons in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.dtos.CouponDTO;
|
||||
import com.example.petstoremobile.utils.DateTimeUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class CouponAdapter extends RecyclerView.Adapter<CouponAdapter.ViewHolder> {
|
||||
private final List<CouponDTO> coupons;
|
||||
private final OnCouponClickListener listener;
|
||||
private boolean selectionMode = false;
|
||||
private final Set<Long> selectedIds = new HashSet<>();
|
||||
|
||||
public interface OnCouponClickListener {
|
||||
void onCouponClick(CouponDTO coupon);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for CouponAdapter.
|
||||
*/
|
||||
public CouponAdapter(List<CouponDTO> coupons, OnCouponClickListener listener) {
|
||||
this.coupons = coupons;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout for a coupon item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_coupon, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds coupon data to the UI components and handles interaction logic.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
CouponDTO coupon = coupons.get(position);
|
||||
holder.tvCouponCode.setText(coupon.getCouponCode());
|
||||
|
||||
String discountText = "";
|
||||
if ("PERCENT".equals(coupon.getDiscountType())) {
|
||||
discountText = coupon.getDiscountValue().stripTrailingZeros().toPlainString() + "% OFF";
|
||||
} else {
|
||||
discountText = "$" + coupon.getDiscountValue().stripTrailingZeros().toPlainString() + " OFF";
|
||||
}
|
||||
holder.tvCouponDiscount.setText(discountText);
|
||||
|
||||
holder.tvCouponMinOrder.setText("Min order: $" + coupon.getMinOrderAmount().stripTrailingZeros().toPlainString());
|
||||
|
||||
if (coupon.getEndsAt() != null) {
|
||||
holder.tvCouponExpiry.setText("Expires: " + DateTimeUtils.extractDate(coupon.getEndsAt()));
|
||||
holder.tvCouponExpiry.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.tvCouponExpiry.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(coupon.getActive())) {
|
||||
holder.tvCouponStatus.setText("ACTIVE");
|
||||
holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.primary_dark));
|
||||
} else {
|
||||
holder.tvCouponStatus.setText("INACTIVE");
|
||||
holder.tvCouponStatus.setBackgroundTintList(ContextCompat.getColorStateList(holder.itemView.getContext(), R.color.accent_coral));
|
||||
}
|
||||
|
||||
holder.cbSelectCoupon.setVisibility(selectionMode ? View.VISIBLE : View.GONE);
|
||||
holder.cbSelectCoupon.setChecked(selectedIds.contains(coupon.getCouponId()));
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionMode) {
|
||||
toggleSelection(coupon.getCouponId());
|
||||
} else {
|
||||
listener.onCouponClick(coupon);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionMode) {
|
||||
setSelectionMode(true);
|
||||
toggleSelection(coupon.getCouponId());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
holder.cbSelectCoupon.setOnClickListener(v -> toggleSelection(coupon.getCouponId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection state of a specific coupon by its ID.
|
||||
*/
|
||||
private void toggleSelection(Long id) {
|
||||
if (selectedIds.contains(id)) {
|
||||
selectedIds.remove(id);
|
||||
} else {
|
||||
selectedIds.add(id);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
listener.onSelectionChanged(selectedIds.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables bulk selection mode.
|
||||
*/
|
||||
public void setSelectionMode(boolean selectionMode) {
|
||||
this.selectionMode = selectionMode;
|
||||
if (!selectionMode) selectedIds.clear();
|
||||
notifyDataSetChanged();
|
||||
listener.onSelectionChanged(selectedIds.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of IDs for the currently selected coupons.
|
||||
*/
|
||||
public Set<Long> getSelectedIds() {
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of coupons in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return coupons.size();
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvCouponCode, tvCouponDiscount, tvCouponMinOrder, tvCouponExpiry, tvCouponStatus;
|
||||
CheckBox cbSelectCoupon;
|
||||
|
||||
/**
|
||||
* Initializes the ViewHolder by finding the views within the item layout.
|
||||
*/
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvCouponCode = itemView.findViewById(R.id.tvCouponCode);
|
||||
tvCouponDiscount = itemView.findViewById(R.id.tvCouponDiscount);
|
||||
tvCouponMinOrder = itemView.findViewById(R.id.tvCouponMinOrder);
|
||||
tvCouponExpiry = itemView.findViewById(R.id.tvCouponExpiry);
|
||||
tvCouponStatus = itemView.findViewById(R.id.tvCouponStatus);
|
||||
cbSelectCoupon = itemView.findViewById(R.id.cbSelectCoupon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Adapter for displaying customer entries in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.UserApi;
|
||||
import com.example.petstoremobile.databinding.ItemCustomerBinding;
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CustomerAdapter extends RecyclerView.Adapter<CustomerAdapter.CustomerViewHolder> {
|
||||
|
||||
private List<CustomerDTO> list;
|
||||
private OnCustomerClickListener listener;
|
||||
private String baseUrl;
|
||||
private String token;
|
||||
|
||||
public interface OnCustomerClickListener {
|
||||
void onCustomerClick(int position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for CustomerAdapter.
|
||||
*/
|
||||
public CustomerAdapter(List<CustomerDTO> list, OnCustomerClickListener listener) {
|
||||
this.list = list;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base URL for fetching customer profile images.
|
||||
*/
|
||||
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
|
||||
|
||||
/**
|
||||
* Sets the authentication token for fetching images.
|
||||
*/
|
||||
public void setToken(String token) { this.token = token; }
|
||||
|
||||
/**
|
||||
* ViewHolder class for customer items.
|
||||
*/
|
||||
public static class CustomerViewHolder extends RecyclerView.ViewHolder {
|
||||
final ItemCustomerBinding binding;
|
||||
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public CustomerViewHolder(@NonNull ItemCustomerBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout for a customer item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public CustomerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ItemCustomerBinding binding = ItemCustomerBinding.inflate(
|
||||
LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new CustomerViewHolder(binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds customer data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull CustomerViewHolder holder, int position) {
|
||||
CustomerDTO c = list.get(position);
|
||||
ItemCustomerBinding b = holder.binding;
|
||||
|
||||
b.tvCustomerFullName.setText(c.getFullName() != null ? c.getFullName() : "");
|
||||
b.tvCustomerUsername.setText("@" + (c.getUsername() != null ? c.getUsername() : ""));
|
||||
b.tvCustomerEmail.setText(c.getEmail() != null ? c.getEmail() : "");
|
||||
|
||||
int points = c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0;
|
||||
b.tvCustomerLoyalty.setText(points + " pts");
|
||||
|
||||
boolean active = Boolean.TRUE.equals(c.getActive());
|
||||
b.tvCustomerStatus.setText(active ? "Active" : "Inactive");
|
||||
b.tvCustomerStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
|
||||
|
||||
if (baseUrl != null && c.getCustomerId() != null) {
|
||||
String imageUrl = baseUrl + String.format(UserApi.AVATAR_PATH, c.getCustomerId());
|
||||
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), b.ivCustomerProfile, imageUrl, token, R.drawable.placeholder);
|
||||
} else {
|
||||
b.ivCustomerProfile.setImageResource(R.drawable.placeholder);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onCustomerClick(position));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of customers in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() { return list.size(); }
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Adapter for displaying employee entries in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.*;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.UserApi;
|
||||
import com.example.petstoremobile.databinding.ItemEmployeeBinding;
|
||||
import com.example.petstoremobile.dtos.EmployeeDTO;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class EmployeeAdapter extends RecyclerView.Adapter<EmployeeAdapter.EmployeeViewHolder> {
|
||||
|
||||
private List<EmployeeDTO> list;
|
||||
private OnEmployeeClickListener listener;
|
||||
private String baseUrl;
|
||||
private String token;
|
||||
|
||||
public interface OnEmployeeClickListener {
|
||||
void onEmployeeClick(int position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for EmployeeAdapter.
|
||||
*/
|
||||
public EmployeeAdapter(List<EmployeeDTO> list, OnEmployeeClickListener listener) {
|
||||
this.list = list;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base URL for fetching employee profile images.
|
||||
*/
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authentication token for fetching images.
|
||||
*/
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder class for employee items.
|
||||
*/
|
||||
public static class EmployeeViewHolder extends RecyclerView.ViewHolder {
|
||||
private final ItemEmployeeBinding binding;
|
||||
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public EmployeeViewHolder(@NonNull ItemEmployeeBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout for an employee item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ItemEmployeeBinding binding = ItemEmployeeBinding.inflate(
|
||||
LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new EmployeeViewHolder(binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds employee data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull EmployeeViewHolder holder, int position) {
|
||||
EmployeeDTO e = list.get(position);
|
||||
ItemEmployeeBinding binding = holder.binding;
|
||||
|
||||
binding.tvEmployeeFullName.setText(e.getFullName() != null ? e.getFullName() : "");
|
||||
binding.tvEmployeeUsername.setText("@" + (e.getUsername() != null ? e.getUsername() : ""));
|
||||
binding.tvEmployeeEmail.setText(e.getEmail() != null ? e.getEmail() : "");
|
||||
|
||||
// Role badge
|
||||
String role = e.getRole() != null ? e.getRole() : "STAFF";
|
||||
binding.tvEmployeeRole.setText(role);
|
||||
|
||||
if ("ADMIN".equalsIgnoreCase(role)) {
|
||||
binding.tvEmployeeRole.setBackgroundColor(Color.parseColor("#1a759f"));
|
||||
} else {
|
||||
binding.tvEmployeeRole.setBackgroundColor(Color.parseColor("#577590"));
|
||||
}
|
||||
|
||||
// Status text and color
|
||||
boolean active = Boolean.TRUE.equals(e.getActive());
|
||||
binding.tvEmployeeStatus.setText(active ? "Active" : "Inactive");
|
||||
binding.tvEmployeeStatus.setTextColor(active ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
|
||||
|
||||
// Profile image
|
||||
if (baseUrl != null && e.getId() != null) {
|
||||
String imageUrl = baseUrl + String.format(UserApi.AVATAR_PATH, e.getId());
|
||||
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivEmployeeProfile, imageUrl, token, R.drawable.placeholder);
|
||||
} else {
|
||||
binding.ivEmployeeProfile.setImageResource(R.drawable.placeholder);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onEmployeeClick(position));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of employees in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() { return list.size(); }
|
||||
}
|
||||
@@ -1,77 +1,154 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
/*
|
||||
* Adapter for showing inventory items in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.models.Inventory;
|
||||
|
||||
import com.example.petstoremobile.databinding.ItemInventoryBinding;
|
||||
import com.example.petstoremobile.dtos.InventoryDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> {
|
||||
public class InventoryAdapter extends RecyclerView.Adapter<InventoryAdapter.InventoryViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<Inventory> inventoryList;
|
||||
private OnInventoryClickListener inventoryClickListener;
|
||||
private final List<InventoryDTO> inventoryList;
|
||||
private final OnInventoryClickListener clickListener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for inventory click on recycler view
|
||||
public interface OnInventoryClickListener {
|
||||
void onInventoryClick(int position);
|
||||
|
||||
void onSelectionChanged(int selectedCount);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
public InventoryAdapter(List<Inventory> inventoryList, OnInventoryClickListener inventoryClickListener) {
|
||||
/**
|
||||
* Constructor for InventoryAdapter.
|
||||
*/
|
||||
public InventoryAdapter(List<InventoryDTO> inventoryList, OnInventoryClickListener clickListener) {
|
||||
this.inventoryList = inventoryList;
|
||||
this.inventoryClickListener = inventoryClickListener;
|
||||
this.clickListener = clickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
clickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of IDs for the currently selected inventory items.
|
||||
*/
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the selection state, deselecting all items.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder class that holds references to the UI components for an inventory item.
|
||||
*/
|
||||
public static class InventoryViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvItemName, tvCategory, tvQuantity, tvUnitPrice, tvSupplier;
|
||||
final ItemInventoryBinding binding;
|
||||
|
||||
public InventoryViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvItemName = v.findViewById(R.id.tvItemName);
|
||||
tvCategory = v.findViewById(R.id.tvCategory);
|
||||
tvQuantity = v.findViewById(R.id.tvQuantity);
|
||||
tvUnitPrice = v.findViewById(R.id.tvUnitPrice);
|
||||
tvSupplier = v.findViewById(R.id.tvInvSupplier);
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public InventoryViewHolder(@NonNull ItemInventoryBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new row view
|
||||
/**
|
||||
* Inflates the layout for an inventory item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public InventoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_inventory, parent, false);
|
||||
return new InventoryViewHolder(v);
|
||||
ItemInventoryBinding binding = ItemInventoryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new InventoryViewHolder(binding);
|
||||
}
|
||||
|
||||
// Populate the row with inventory data
|
||||
/**
|
||||
* Binds inventory data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull InventoryViewHolder holder, int position) {
|
||||
Inventory inventory = inventoryList.get(position);
|
||||
InventoryDTO inv = inventoryList.get(position);
|
||||
ItemInventoryBinding binding = holder.binding;
|
||||
|
||||
holder.tvItemName.setText(inventory.getItemName());
|
||||
holder.tvCategory.setText(inventory.getCategory());
|
||||
holder.tvQuantity.setText("Qty: " + inventory.getQuantity());
|
||||
holder.tvUnitPrice.setText("$" + String.format("%.2f", inventory.getUnitPrice()));
|
||||
holder.tvSupplier.setText("Supplier: " + inventory.getSupplier());
|
||||
// Column: Product Name
|
||||
binding.tvProductName.setText(inv.getProductName() != null ? inv.getProductName() : "—");
|
||||
|
||||
// Highlight low stock items in red
|
||||
if (inventory.getQuantity() <= 5) {
|
||||
holder.tvQuantity.setTextColor(Color.parseColor("#F44336"));
|
||||
// Column: Store Name
|
||||
binding.tvInventoryStore.setText("Store: " + (inv.getStoreName() != null ? inv.getStoreName() : "—"));
|
||||
|
||||
// Column: Quantity
|
||||
int qty = inv.getQuantity() != null ? inv.getQuantity() : 0;
|
||||
binding.tvQuantity.setText("Stock: " + qty);
|
||||
|
||||
if (qty <= 5) {
|
||||
binding.tvQuantity.setTextColor(Color.parseColor("#F44336"));
|
||||
} else {
|
||||
holder.tvQuantity.setTextColor(Color.parseColor("#4CAF50"));
|
||||
binding.tvQuantity.setTextColor(Color.parseColor("#4CAF50"));
|
||||
}
|
||||
|
||||
// When a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> inventoryClickListener.onInventoryClick(position));
|
||||
String key = String.valueOf(inv.getInventoryId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectInventory.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectInventory.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectInventory.setVisibility(View.GONE);
|
||||
binding.cbSelectInventory.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
clickListener.onInventoryClick(holder.getAdapterPosition());
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of inventory items in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return inventoryList.size();
|
||||
|
||||
@@ -1,33 +1,107 @@
|
||||
/*
|
||||
* Adapter for displaying chat messages in a conversation.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemMessageReceivedBinding;
|
||||
import com.example.petstoremobile.databinding.ItemMessageSentBinding;
|
||||
import com.example.petstoremobile.models.Message;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int TYPE_SENT = 1;
|
||||
private static final int TYPE_RECEIVED = 2;
|
||||
|
||||
public interface OnAttachmentClickListener {
|
||||
void onAttachmentClick(Message message);
|
||||
}
|
||||
|
||||
private final List<Message> messages;
|
||||
private Long currentUserId;
|
||||
private Long staffId;
|
||||
private String token;
|
||||
private String baseUrl;
|
||||
private OnAttachmentClickListener attachmentClickListener;
|
||||
|
||||
/**
|
||||
* Constructor for MessageAdapter.
|
||||
*/
|
||||
public MessageAdapter(List<Message> messages, Long currentUserId) {
|
||||
this.messages = messages;
|
||||
this.currentUserId = currentUserId;
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ID for each message.
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
Message m = messages.get(position);
|
||||
return m.getId() != null ? m.getId() : position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current user's ID and refreshes the list.
|
||||
*/
|
||||
public void setCurrentUserId(Long id) {
|
||||
this.currentUserId = id;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the staff ID to identify staff messages in the UI.
|
||||
*/
|
||||
public void setStaffId(Long id) {
|
||||
this.staffId = id;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authentication token.
|
||||
*/
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base API URL.
|
||||
*/
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a listener for clicks on message attachments.
|
||||
*/
|
||||
public void setOnAttachmentClickListener(OnAttachmentClickListener listener) {
|
||||
this.attachmentClickListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a message is 'sent' or 'received' based on the sender's ID.
|
||||
*/
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
Message m = messages.get(position);
|
||||
@@ -37,42 +111,191 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
return TYPE_RECEIVED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the chat layout for a message.
|
||||
*/
|
||||
@NonNull @Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
LayoutInflater inf = LayoutInflater.from(parent.getContext());
|
||||
if (viewType == TYPE_SENT) {
|
||||
View v = inf.inflate(R.layout.item_message_sent, parent, false);
|
||||
return new SentHolder(v);
|
||||
ItemMessageSentBinding binding = ItemMessageSentBinding.inflate(inf, parent, false);
|
||||
return new SentHolder(binding);
|
||||
} else {
|
||||
View v = inf.inflate(R.layout.item_message_received, parent, false);
|
||||
return new ReceivedHolder(v);
|
||||
ItemMessageReceivedBinding binding = ItemMessageReceivedBinding.inflate(inf, parent, false);
|
||||
return new ReceivedHolder(binding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds message data to the appropriate ViewHolder.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
Message m = messages.get(position);
|
||||
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m);
|
||||
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m);
|
||||
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m, token, baseUrl, attachmentClickListener);
|
||||
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token, baseUrl, attachmentClickListener, staffId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of messages.
|
||||
*/
|
||||
@Override public int getItemCount() { return messages.size(); }
|
||||
|
||||
/**
|
||||
* ViewHolder for messages sent by the user.
|
||||
*/
|
||||
static class SentHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvMessage;
|
||||
SentHolder(View v) {
|
||||
super(v);
|
||||
tvMessage = v.findViewById(R.id.tvMessageContent); // updated
|
||||
final ItemMessageSentBinding binding;
|
||||
/**
|
||||
* Initializes the SentHolder with view binding.
|
||||
*/
|
||||
SentHolder(ItemMessageSentBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
void bind(Message m) { tvMessage.setText(m.getContent()); }
|
||||
/**
|
||||
* Binds sent message data to the bubble UI.
|
||||
*/
|
||||
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) {
|
||||
binding.tvSenderName.setText("You");
|
||||
binding.tvTimestamp.setText(formatTimestamp(m.getTimestamp()));
|
||||
|
||||
if (m.getContent() != null && !m.getContent().isEmpty()) {
|
||||
binding.tvMessageContent.setVisibility(View.VISIBLE);
|
||||
binding.tvMessageContent.setText(m.getContent());
|
||||
} else {
|
||||
binding.tvMessageContent.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
|
||||
|
||||
View.OnClickListener click = v -> {
|
||||
if (listener != null) listener.onAttachmentClick(m);
|
||||
};
|
||||
binding.ivAttachment.setOnClickListener(click);
|
||||
binding.tvAttachmentName.setOnClickListener(click);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder for messages received from others.
|
||||
*/
|
||||
static class ReceivedHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvMessage;
|
||||
ReceivedHolder(View v) {
|
||||
super(v);
|
||||
tvMessage = v.findViewById(R.id.tvMessageContent); // updated
|
||||
final ItemMessageReceivedBinding binding;
|
||||
/**
|
||||
* Initializes the ReceivedHolder with view binding.
|
||||
*/
|
||||
ReceivedHolder(ItemMessageReceivedBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
/**
|
||||
* Binds received message data to the bubble UI.
|
||||
*/
|
||||
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener, Long staffId) {
|
||||
binding.tvSenderName.setText(resolveSenderName(m, staffId));
|
||||
binding.tvTimestamp.setText(formatTimestamp(m.getTimestamp()));
|
||||
|
||||
if (m.getContent() != null && !m.getContent().isEmpty()) {
|
||||
binding.tvMessageContent.setVisibility(View.VISIBLE);
|
||||
binding.tvMessageContent.setText(m.getContent());
|
||||
} else {
|
||||
binding.tvMessageContent.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
|
||||
|
||||
View.OnClickListener click = v -> {
|
||||
if (listener != null) listener.onAttachmentClick(m);
|
||||
};
|
||||
binding.ivAttachment.setOnClickListener(click);
|
||||
binding.tvAttachmentName.setOnClickListener(click);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the display name of the sender.
|
||||
*/
|
||||
private static String resolveSenderName(Message m, Long staffId) {
|
||||
if ("BOT".equalsIgnoreCase(m.getSenderRole())) {
|
||||
return (m.getSenderDisplayName() != null && !m.getSenderDisplayName().isEmpty())
|
||||
? m.getSenderDisplayName() : "AI Bot";
|
||||
}
|
||||
if (staffId != null && staffId.equals(m.getSenderId())) {
|
||||
return "Staff";
|
||||
}
|
||||
return "Customer";
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the timestamp string into readable format.
|
||||
*/
|
||||
private static String formatTimestamp(String timestamp) {
|
||||
if (timestamp == null || timestamp.isEmpty()) return "";
|
||||
try {
|
||||
String normalized = timestamp.length() > 19 ? timestamp.substring(0, 19) : timestamp;
|
||||
SimpleDateFormat input = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
|
||||
Date date = input.parse(normalized);
|
||||
return new SimpleDateFormat("MMM d, HH:mm", Locale.getDefault()).format(date);
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logic for displaying an attachment in the chat.
|
||||
*/
|
||||
private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) {
|
||||
// Check if there's an attachment
|
||||
if (m.getAttachmentName() != null || m.getAttachmentMimeType() != null) {
|
||||
// Construct the download URL using the message ID
|
||||
String url;
|
||||
if (baseUrl != null) {
|
||||
String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
url = cleanBase + "/api/v1/chat/messages/" + m.getId() + "/attachment";
|
||||
} else {
|
||||
url = m.getAttachmentUrl();
|
||||
}
|
||||
|
||||
if (url == null) {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m.getAttachmentMimeType() != null && m.getAttachmentMimeType().startsWith("image/")) {
|
||||
iv.setVisibility(View.VISIBLE);
|
||||
tvName.setVisibility(View.GONE);
|
||||
|
||||
Object loadTarget = url;
|
||||
if (token != null) {
|
||||
loadTarget = new GlideUrl(url, new LazyHeaders.Builder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build());
|
||||
}
|
||||
|
||||
// Use a signature to prevent Glide from showing cached images instead of loading new ones
|
||||
String signatureKey = (m.getTimestamp() != null ? m.getTimestamp() : "") + m.getId();
|
||||
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
Glide.with(iv.getContext())
|
||||
.load(loadTarget)
|
||||
.signature(new ObjectKey(signatureKey))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.into(iv);
|
||||
} else {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.VISIBLE);
|
||||
tvName.setText(m.getAttachmentName() != null ? m.getAttachmentName() : "Attachment");
|
||||
}
|
||||
} else {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.GONE);
|
||||
}
|
||||
void bind(Message m) { tvMessage.setText(m.getContent()); }
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,195 @@
|
||||
/*
|
||||
* Adapter for showing pets in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.PetApi;
|
||||
import com.example.petstoremobile.databinding.ItemPetBinding;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
|
||||
public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<PetDTO> petList;
|
||||
private OnPetClickListener petClickListener;
|
||||
private String baseUrl;
|
||||
private String token;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for pet click on recycler view
|
||||
public interface OnPetClickListener {
|
||||
void onPetClick(int position);
|
||||
void onSelectionChanged(int selectedCount);
|
||||
}
|
||||
|
||||
//Constructor
|
||||
/**
|
||||
* Constructor for PetAdapter.
|
||||
*/
|
||||
public PetAdapter(List<PetDTO> petList, OnPetClickListener petClickListener) {
|
||||
this.petList = petList;
|
||||
this.petClickListener = petClickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
petClickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base URL for fetching pet images from the server.
|
||||
*/
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authentication token
|
||||
*/
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of IDs for the currently selected pet items.
|
||||
*/
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the selection state, deselecting all items.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder class that holds references to the UI components for a pet item.
|
||||
*/
|
||||
public static class PetViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus;
|
||||
private final ItemPetBinding binding;
|
||||
|
||||
public PetViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvPetName = v.findViewById(R.id.tvPetName);
|
||||
tvPetSpeciesBreed = v.findViewById(R.id.tvPetSpeciesBreed);
|
||||
tvPetAge = v.findViewById(R.id.tvPetAge);
|
||||
tvPetPrice = v.findViewById(R.id.tvPetPrice);
|
||||
tvPetStatus = v.findViewById(R.id.tvPetStatus);
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public PetViewHolder(@NonNull ItemPetBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new row view
|
||||
/**
|
||||
* Inflates the layout for a pet item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public PetViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_pet, parent, false);
|
||||
return new PetViewHolder(v);
|
||||
ItemPetBinding binding = ItemPetBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new PetViewHolder(binding);
|
||||
}
|
||||
|
||||
//populate the row with pet data
|
||||
/**
|
||||
* Binds pet data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PetViewHolder holder, int position) {
|
||||
PetDTO pet = petList.get(position);
|
||||
ItemPetBinding binding = holder.binding;
|
||||
|
||||
holder.tvPetName.setText(pet.getPetName());
|
||||
holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed());
|
||||
holder.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)");
|
||||
binding.tvPetName.setText(pet.getPetName());
|
||||
binding.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed());
|
||||
binding.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)");
|
||||
|
||||
try {
|
||||
double price = Double.parseDouble(pet.getPetPrice());
|
||||
holder.tvPetPrice.setText("$" + String.format("%.2f", price));
|
||||
} catch (Exception e) {
|
||||
holder.tvPetPrice.setText("$" + pet.getPetPrice());
|
||||
Double price = pet.getPetPrice();
|
||||
if (price != null) {
|
||||
binding.tvPetPrice.setText("$" + String.format("%.2f", price));
|
||||
} else {
|
||||
binding.tvPetPrice.setText("$0.00");
|
||||
}
|
||||
|
||||
holder.tvPetStatus.setText(pet.getPetStatus());
|
||||
binding.tvPetStatus.setText(pet.getPetStatus());
|
||||
|
||||
//Set the status color depending on availability. If available, green, otherwise red
|
||||
if (pet.getPetStatus() != null && pet.getPetStatus().equals("Available")) {
|
||||
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
//Set the status color depending on availability
|
||||
if (pet.getPetStatus() != null) {
|
||||
switch (pet.getPetStatus()) {
|
||||
case "Available":
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
||||
break;
|
||||
case "Pending":
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
||||
break;
|
||||
default:
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
||||
binding.tvPetStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
|
||||
}
|
||||
|
||||
// Load pet image using Glide
|
||||
if (baseUrl != null) {
|
||||
String imageUrl = baseUrl + String.format(PetApi.PET_IMAGE_PATH, pet.getPetId());
|
||||
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivPetProfile, imageUrl, token, R.drawable.placeholder);
|
||||
} else {
|
||||
binding.ivPetProfile.setImageResource(R.drawable.placeholder);
|
||||
}
|
||||
|
||||
String key = String.valueOf(pet.getPetId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectPet.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectPet.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectPet.setVisibility(View.GONE);
|
||||
binding.cbSelectPet.setChecked(false);
|
||||
}
|
||||
|
||||
//when a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position));
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
petClickListener.onPetClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of pet items in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return petList.size();
|
||||
|
||||
@@ -1,72 +1,108 @@
|
||||
/*
|
||||
* Adapter for displaying products in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.view.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.models.Product;
|
||||
import com.example.petstoremobile.api.ProductApi;
|
||||
import com.example.petstoremobile.databinding.ItemProductBinding;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import java.util.List;
|
||||
|
||||
public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
|
||||
|
||||
private List<Product> productList;
|
||||
private OnProductClickListener productClickListener;
|
||||
private List<ProductDTO> productList;
|
||||
private OnProductClickListener listener;
|
||||
private String baseUrl;
|
||||
private String token;
|
||||
|
||||
// Interface for product click on recycler view
|
||||
public interface OnProductClickListener {
|
||||
void onProductClick(int position);
|
||||
}
|
||||
|
||||
// Constructor
|
||||
public ProductAdapter(List<Product> productList, OnProductClickListener productClickListener) {
|
||||
/**
|
||||
* Constructor for ProductAdapter.
|
||||
*/
|
||||
public ProductAdapter(List<ProductDTO> productList, OnProductClickListener listener) {
|
||||
this.productList = productList;
|
||||
this.productClickListener = productClickListener;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
/**
|
||||
* Sets the base URL for fetching product images.
|
||||
*/
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authentication token
|
||||
*/
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder class for product items.
|
||||
*/
|
||||
public static class ProductViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvProductName, tvProductDesc, tvCategory, tvProductPrice, tvStockQuantity;
|
||||
final ItemProductBinding binding;
|
||||
|
||||
public ProductViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvProductName = v.findViewById(R.id.tvProductName);
|
||||
tvProductDesc = v.findViewById(R.id.tvProductDesc);
|
||||
tvCategory = v.findViewById(R.id.tvProductCategory);
|
||||
tvProductPrice = v.findViewById(R.id.tvProductPrice);
|
||||
tvStockQuantity = v.findViewById(R.id.tvStockQuantity);
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public ProductViewHolder(@NonNull ItemProductBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new row view
|
||||
/**
|
||||
* Inflates the layout for a product item and creates the ViewHolder.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product, parent, false);
|
||||
return new ProductViewHolder(v);
|
||||
ItemProductBinding binding = ItemProductBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new ProductViewHolder(binding);
|
||||
}
|
||||
|
||||
// Populate the row with product data
|
||||
/**
|
||||
* Binds product data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) {
|
||||
Product product = productList.get(position);
|
||||
ProductDTO p = productList.get(position);
|
||||
ItemProductBinding binding = holder.binding;
|
||||
|
||||
holder.tvProductName.setText(product.getProductName());
|
||||
holder.tvProductDesc.setText(product.getProductDesc());
|
||||
holder.tvCategory.setText(product.getCategory());
|
||||
holder.tvProductPrice.setText("$" + String.format("%.2f", product.getProductPrice()));
|
||||
holder.tvStockQuantity.setText("Stock: " + product.getStockQuantity());
|
||||
binding.tvProductName.setText(p.getProdName() != null ? p.getProdName() : "");
|
||||
binding.tvProductCategory.setText("Category: " + (p.getCategoryName() != null ? p.getCategoryName() : ""));
|
||||
binding.tvProductDesc.setText(p.getProdDesc() != null ? p.getProdDesc() : "");
|
||||
binding.tvProductPrice.setText(p.getProdPrice() != null ? "$" + p.getProdPrice() : "");
|
||||
|
||||
// When a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> productClickListener.onProductClick(position));
|
||||
// Load product image using Glide
|
||||
if (baseUrl != null) {
|
||||
String imageUrl = baseUrl + String.format(ProductApi.PRODUCT_IMAGE_PATH, p.getProdId());
|
||||
GlideUtils.loadImageWithTokenCircle(holder.itemView.getContext(), binding.ivProductImage, imageUrl, token, R.drawable.placeholder);
|
||||
} else {
|
||||
binding.ivProductImage.setImageResource(R.drawable.placeholder);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onProductClick(position));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of product items in the list.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return productList.size();
|
||||
public int getItemCount() { return productList.size(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Adapter for showing product-supplier links in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.databinding.ItemProductSupplierBinding;
|
||||
import com.example.petstoremobile.dtos.ProductSupplierDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ProductSupplierAdapter extends RecyclerView.Adapter<ProductSupplierAdapter.PSViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private final List<ProductSupplierDTO> list;
|
||||
private final OnProductSupplierClickListener listener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
public interface OnProductSupplierClickListener {
|
||||
void onProductSupplierClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for ProductSupplierAdapter.
|
||||
*/
|
||||
public ProductSupplierAdapter(List<ProductSupplierDTO> list, OnProductSupplierClickListener listener) {
|
||||
this.list = list;
|
||||
this.listener = listener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
listener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of selected keys for bulk deletion.
|
||||
*/
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all selected items and exits selection mode.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder for Product-Supplier relationship items.
|
||||
*/
|
||||
public static class PSViewHolder extends RecyclerView.ViewHolder {
|
||||
final ItemProductSupplierBinding binding;
|
||||
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public PSViewHolder(@NonNull ItemProductSupplierBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout for a Product-Supplier item.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public PSViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ItemProductSupplierBinding binding = ItemProductSupplierBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new PSViewHolder(binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds product-supplier data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PSViewHolder holder, int position) {
|
||||
ProductSupplierDTO ps = list.get(position);
|
||||
ItemProductSupplierBinding binding = holder.binding;
|
||||
|
||||
binding.tvPSProductName.setText(ps.getProductName() != null ? ps.getProductName() : "");
|
||||
binding.tvPSSupplierName.setText("Supplier: " + (ps.getSupplierName() != null ? ps.getSupplierName() : ""));
|
||||
binding.tvPSCost.setText(ps.getCost() != null ? "Cost: $" + ps.getCost() : "");
|
||||
|
||||
String key = ps.getProductId() + "-" + ps.getSupplierId();
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectProductSupplier.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectProductSupplier.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectProductSupplier.setVisibility(View.GONE);
|
||||
binding.cbSelectProductSupplier.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
listener.onProductSupplierClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() { return list.size(); }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Adapter for displaying purchase orders in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.databinding.ItemPurchaseOrderBinding;
|
||||
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
|
||||
import java.util.List;
|
||||
|
||||
public class PurchaseOrderAdapter extends RecyclerView.Adapter<PurchaseOrderAdapter.POViewHolder> {
|
||||
|
||||
private List<PurchaseOrderDTO> list;
|
||||
private OnPurchaseOrderClickListener listener;
|
||||
|
||||
public interface OnPurchaseOrderClickListener {
|
||||
void onPurchaseOrderClick(int position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for PurchaseOrderAdapter.
|
||||
*/
|
||||
public PurchaseOrderAdapter(List<PurchaseOrderDTO> list, OnPurchaseOrderClickListener listener) {
|
||||
this.list = list;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder for Purchase Order items.
|
||||
*/
|
||||
public static class POViewHolder extends RecyclerView.ViewHolder {
|
||||
final ItemPurchaseOrderBinding binding;
|
||||
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public POViewHolder(@NonNull ItemPurchaseOrderBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout for a Purchase Order item.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public POViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ItemPurchaseOrderBinding binding = ItemPurchaseOrderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new POViewHolder(binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds purchase order data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull POViewHolder holder, int position) {
|
||||
PurchaseOrderDTO po = list.get(position);
|
||||
ItemPurchaseOrderBinding binding = holder.binding;
|
||||
|
||||
binding.tvPOId.setText("PO #" + (po.getPurchaseOrderId() != null ? po.getPurchaseOrderId() : ""));
|
||||
binding.tvPOSupplier.setText("Supplier: " + (po.getSupplierName() != null ? po.getSupplierName() : ""));
|
||||
binding.tvPOStore.setText("Store: " + (po.getStoreName() != null ? po.getStoreName() : ""));
|
||||
binding.tvPODate.setText("Date: " + (po.getOrderDate() != null ? po.getOrderDate() : ""));
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onPurchaseOrderClick(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return list.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Adapter for showing sales in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemSaleBinding;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import java.util.List;
|
||||
|
||||
public class SaleAdapter extends RecyclerView.Adapter<SaleAdapter.SaleViewHolder> {
|
||||
|
||||
private List<SaleDTO> saleList;
|
||||
private OnSaleClickListener listener;
|
||||
|
||||
public interface OnSaleClickListener {
|
||||
void onSaleClick(int position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for SaleAdapter.
|
||||
*/
|
||||
public SaleAdapter(List<SaleDTO> saleList, OnSaleClickListener listener) {
|
||||
this.saleList = saleList;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder for Sale record items.
|
||||
*/
|
||||
public static class SaleViewHolder extends RecyclerView.ViewHolder {
|
||||
final ItemSaleBinding binding;
|
||||
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public SaleViewHolder(@NonNull ItemSaleBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout for a Sale record item.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public SaleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ItemSaleBinding binding = ItemSaleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new SaleViewHolder(binding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds sale transaction data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SaleViewHolder holder, int position) {
|
||||
SaleDTO s = saleList.get(position);
|
||||
ItemSaleBinding binding = holder.binding;
|
||||
|
||||
binding.tvSaleId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : ""));
|
||||
binding.tvSaleEmployee.setText("By: " + (s.getEmployeeName() != null ? s.getEmployeeName() : ""));
|
||||
if (s.getCustomerName() != null && !s.getCustomerName().isEmpty()) {
|
||||
binding.tvSaleCustomer.setText("Customer: " + s.getCustomerName());
|
||||
binding.tvSaleCustomer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.tvSaleCustomer.setVisibility(View.GONE);
|
||||
}
|
||||
binding.tvSaleDate.setText(s.getSaleDate() != null ? s.getSaleDate().substring(0, Math.min(10, s.getSaleDate().length())) : "");
|
||||
binding.tvSalePayment.setText(s.getPaymentMethod() != null ? s.getPaymentMethod() : "");
|
||||
binding.tvSaleTotal.setText(s.getTotalAmount() != null ? "$" + s.getTotalAmount() : "");
|
||||
|
||||
if (Boolean.TRUE.equals(s.getIsRefund())) {
|
||||
binding.tvSaleRefundBadge.setVisibility(View.VISIBLE);
|
||||
binding.tvSaleTotal.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_adopted));
|
||||
} else {
|
||||
binding.tvSaleRefundBadge.setVisibility(View.GONE);
|
||||
binding.tvSaleTotal.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_available));
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> listener.onSaleClick(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return saleList.size();
|
||||
}
|
||||
}
|
||||
@@ -1,64 +1,142 @@
|
||||
/*
|
||||
* Adapter for displaying services in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
|
||||
import com.example.petstoremobile.databinding.ItemServiceBinding;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ServiceAdapter extends RecyclerView.Adapter<ServiceAdapter.ServiceViewHolder> {
|
||||
/**
|
||||
* Adapter class for displaying a list of services in a RecyclerView.
|
||||
*/
|
||||
public class ServiceAdapter extends RecyclerView.Adapter<ServiceAdapter.ServiceViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<ServiceDTO> serviceList;
|
||||
private OnServiceClickListener serviceClickListener;
|
||||
private final List<ServiceDTO> serviceList;
|
||||
private final OnServiceClickListener clickListener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for service click on recycler view
|
||||
/**
|
||||
* Interface for handling clicks on service items.
|
||||
*/
|
||||
public interface OnServiceClickListener {
|
||||
void onServiceClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
//Constructor
|
||||
public ServiceAdapter(List<ServiceDTO> serviceList, OnServiceClickListener serviceClickListener) {
|
||||
/**
|
||||
* Constructor for ServiceAdapter.
|
||||
*/
|
||||
public ServiceAdapter(List<ServiceDTO> serviceList, OnServiceClickListener clickListener) {
|
||||
this.serviceList = serviceList;
|
||||
this.serviceClickListener = serviceClickListener;
|
||||
this.clickListener = clickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
clickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of selected keys for bulk deletion.
|
||||
*/
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all selected items and exits selection mode.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder for Service items.
|
||||
*/
|
||||
public static class ServiceViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvServiceName, tvServiceDesc, tvServiceDuration, tvServicePrice;
|
||||
final ItemServiceBinding binding;
|
||||
|
||||
public ServiceViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvServiceName = v.findViewById(R.id.tvServiceName);
|
||||
tvServiceDesc = v.findViewById(R.id.tvServiceDesc);
|
||||
tvServiceDuration = v.findViewById(R.id.tvServiceDuration);
|
||||
tvServicePrice = v.findViewById(R.id.tvServicePrice);
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public ServiceViewHolder(@NonNull ItemServiceBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new row view
|
||||
/**
|
||||
* Inflates the layout for a Service item.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public ServiceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_service, parent, false);
|
||||
return new ServiceViewHolder(v);
|
||||
ItemServiceBinding binding = ItemServiceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new ServiceViewHolder(binding);
|
||||
}
|
||||
|
||||
//populate the row with service data
|
||||
/**
|
||||
* Binds service data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ServiceViewHolder holder, int position) {
|
||||
ServiceDTO service = serviceList.get(position);
|
||||
ItemServiceBinding binding = holder.binding;
|
||||
|
||||
holder.tvServiceName.setText(service.getServiceName());
|
||||
holder.tvServiceDesc.setText(service.getServiceDesc());
|
||||
holder.tvServiceDuration.setText("Duration: " + service.getServiceDuration() + " min");
|
||||
holder.tvServicePrice.setText("$" + String.format("%.2f", service.getServicePrice()));
|
||||
binding.tvServiceName.setText(service.getServiceName());
|
||||
binding.tvServiceDesc.setText(service.getServiceDesc());
|
||||
binding.tvServiceDuration.setText(service.getServiceDuration() != null ? service.getServiceDuration() + " mins" : "0 mins");
|
||||
binding.tvServicePrice.setText(service.getServicePrice() != null ? "$" + String.format("%.2f", service.getServicePrice()) : "$0.00");
|
||||
|
||||
//when a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> serviceClickListener.onServiceClick(position));
|
||||
String key = String.valueOf(service.getServiceId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectService.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectService.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectService.setVisibility(View.GONE);
|
||||
binding.cbSelectService.setChecked(false);
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
clickListener.onServiceClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,64 +1,138 @@
|
||||
/*
|
||||
* Adapter for showing suppliers in a list.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
|
||||
import com.example.petstoremobile.databinding.ItemSupplierBinding;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.SelectionHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SupplierAdapter extends RecyclerView.Adapter<SupplierAdapter.SupplierViewHolder> {
|
||||
public class SupplierAdapter extends RecyclerView.Adapter<SupplierAdapter.SupplierViewHolder> implements BulkDeleteHandler.SelectableAdapter {
|
||||
|
||||
private List<SupplierDTO> supplierList;
|
||||
private OnSupplierClickListener supplierClickListener;
|
||||
private final List<SupplierDTO> supplierList;
|
||||
private final OnSupplierClickListener supplierClickListener;
|
||||
private final SelectionHelper selectionHelper;
|
||||
|
||||
// Interface for supplier click on recycler view
|
||||
public interface OnSupplierClickListener {
|
||||
void onSupplierClick(int position);
|
||||
void onSelectionChanged(int count);
|
||||
}
|
||||
|
||||
//Constructor
|
||||
/**
|
||||
* Constructor for SupplierAdapter.
|
||||
*/
|
||||
public SupplierAdapter(List<SupplierDTO> supplierList, OnSupplierClickListener supplierClickListener) {
|
||||
this.supplierList = supplierList;
|
||||
this.supplierClickListener = supplierClickListener;
|
||||
this.selectionHelper = new SelectionHelper(new SelectionHelper.SelectionListener() {
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
supplierClickListener.onSelectionChanged(count);
|
||||
}
|
||||
|
||||
// Get the controls of each row in recycler view
|
||||
@Override
|
||||
public void onSelectionModeToggle(boolean selectionMode) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of selected keys for bulk deletion.
|
||||
*/
|
||||
@Override
|
||||
public List<String> getSelectedKeys() {
|
||||
return selectionHelper.getSelectedKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all selected items and exits selection mode.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection() {
|
||||
selectionHelper.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* ViewHolder for Supplier items.
|
||||
*/
|
||||
public static class SupplierViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvSupCompany, tvSupContactName, tvSupEmail, tvSupPhone;
|
||||
final ItemSupplierBinding binding;
|
||||
|
||||
public SupplierViewHolder(@NonNull View v) {
|
||||
super(v);
|
||||
tvSupCompany = v.findViewById(R.id.tvSupCompany);
|
||||
tvSupContactName = v.findViewById(R.id.tvSupContactName);
|
||||
tvSupEmail = v.findViewById(R.id.tvSupEmail);
|
||||
tvSupPhone = v.findViewById(R.id.tvSupPhone);
|
||||
/**
|
||||
* Initializes the ViewHolder with view binding.
|
||||
*/
|
||||
public SupplierViewHolder(@NonNull ItemSupplierBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new row view
|
||||
/**
|
||||
* Inflates the layout for a Supplier item.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public SupplierViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_supplier, parent, false);
|
||||
return new SupplierViewHolder(v);
|
||||
ItemSupplierBinding binding = ItemSupplierBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new SupplierViewHolder(binding);
|
||||
}
|
||||
|
||||
//populate the row with supplier data
|
||||
/**
|
||||
* Binds supplier data to the UI components.
|
||||
*/
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SupplierViewHolder holder, int position) {
|
||||
SupplierDTO supplier = supplierList.get(position);
|
||||
ItemSupplierBinding binding = holder.binding;
|
||||
|
||||
holder.tvSupCompany.setText(supplier.getSupCompany());
|
||||
holder.tvSupContactName.setText(supplier.getSupContactFirstName() + " " + supplier.getSupContactLastName());
|
||||
holder.tvSupEmail.setText(supplier.getSupEmail());
|
||||
holder.tvSupPhone.setText(supplier.getSupPhone());
|
||||
binding.tvSupCompany.setText(supplier.getSupCompany());
|
||||
binding.tvSupContactName.setText(supplier.getSupContactFirstName() + " " + supplier.getSupContactLastName());
|
||||
binding.tvSupEmail.setText(supplier.getSupEmail());
|
||||
binding.tvSupPhone.setText(supplier.getSupPhone());
|
||||
|
||||
String key = String.valueOf(supplier.getSupId());
|
||||
|
||||
// Bulk delete selection mode
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
binding.cbSelectSupplier.setVisibility(View.VISIBLE);
|
||||
binding.cbSelectSupplier.setChecked(selectionHelper.isSelected(key));
|
||||
} else {
|
||||
binding.cbSelectSupplier.setVisibility(View.GONE);
|
||||
binding.cbSelectSupplier.setChecked(false);
|
||||
}
|
||||
|
||||
//when a row is clicked, open the detail view
|
||||
holder.itemView.setOnClickListener(v -> supplierClickListener.onSupplierClick(position));
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.toggleSelection(key);
|
||||
notifyItemChanged(position);
|
||||
} else {
|
||||
supplierClickListener.onSupplierClick(position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(v -> {
|
||||
if (!selectionHelper.isInSelectionMode()) {
|
||||
selectionHelper.startSelection(key);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Custom array adapter that displays items with white text.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.example.petstoremobile.R;
|
||||
import java.util.List;
|
||||
|
||||
// A class that overrides the arrayAdapter so the text color changes based on theme
|
||||
// Used to make spinners have white text on dark background no matter the theme
|
||||
public class WhiteTextArrayAdapter<T> extends ArrayAdapter<T> {
|
||||
public WhiteTextArrayAdapter(@NonNull Context context, int resource, @NonNull T[] objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
|
||||
public WhiteTextArrayAdapter(@NonNull Context context, int resource, @NonNull List<T> objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getView(position, convertView, parent);
|
||||
view.setBackgroundColor(Color.TRANSPARENT);
|
||||
if (view instanceof TextView) {
|
||||
((TextView) view).setTextColor(Color.WHITE);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = super.getDropDownView(position, convertView, parent);
|
||||
view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.primary_dark));
|
||||
if (view instanceof TextView) {
|
||||
((TextView) view).setTextColor(Color.WHITE);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Retrofit interface for activity log endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.ActivityLogDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls to get activity logs
|
||||
public interface ActivityLogApi {
|
||||
|
||||
// Get activity logs with filters
|
||||
@GET("api/v1/activity-logs")
|
||||
Call<List<ActivityLogDTO>> getActivityLogs(
|
||||
@Query("limit") int limit,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("role") String role,
|
||||
@Query("search") String search,
|
||||
@Query("startDate") String startDate,
|
||||
@Query("endDate") String endDate
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Retrofit interface for adoption endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.AdoptionDTO;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls for adoptions
|
||||
public interface AdoptionApi {
|
||||
|
||||
// Get all adoptions with filters
|
||||
@GET("api/v1/adoptions")
|
||||
Call<PageResponse<AdoptionDTO>> getAllAdoptions(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("status") String status,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("date") String date,
|
||||
@Query("employeeId") Long employeeId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
// Get adoption by id
|
||||
@GET("api/v1/adoptions/{id}")
|
||||
Call<AdoptionDTO> getAdoptionById(@Path("id") Long id);
|
||||
|
||||
// Create adoption
|
||||
@POST("api/v1/adoptions")
|
||||
Call<AdoptionDTO> createAdoption(@Body AdoptionDTO adoption);
|
||||
|
||||
// Update adoption
|
||||
@PUT("api/v1/adoptions/{id}")
|
||||
Call<AdoptionDTO> updateAdoption(@Path("id") Long id, @Body AdoptionDTO adoption);
|
||||
|
||||
// Delete adoption
|
||||
@DELETE("api/v1/adoptions/{id}")
|
||||
Call<Void> deleteAdoption(@Path("id") Long id);
|
||||
|
||||
// Bulk delete adoptions
|
||||
@HTTP(method = "DELETE", path = "api/v1/adoptions", hasBody = true)
|
||||
Call<Void> bulkDeleteAdoptions(@Body BulkDeleteRequest request);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Retrofit interface for appointment endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.AppointmentDTO;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls for appointments
|
||||
public interface AppointmentApi {
|
||||
|
||||
// Get all appointments with filters
|
||||
@GET("api/v1/appointments")
|
||||
Call<PageResponse<AppointmentDTO>> getAllAppointments(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("status") String status,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("date") String date,
|
||||
@Query("employeeId") Long employeeId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
// Get appointment by id
|
||||
@GET("api/v1/appointments/{id}")
|
||||
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);
|
||||
|
||||
// Create appointment
|
||||
@POST("api/v1/appointments")
|
||||
Call<AppointmentDTO> createAppointment(@Body AppointmentDTO appointment);
|
||||
|
||||
// Update appointment
|
||||
@PUT("api/v1/appointments/{id}")
|
||||
Call<AppointmentDTO> updateAppointment(@Path("id") Long id, @Body AppointmentDTO appointment);
|
||||
|
||||
// Delete appointment
|
||||
@DELETE("api/v1/appointments/{id}")
|
||||
Call<Void> deleteAppointment(@Path("id") Long id);
|
||||
|
||||
// Bulk delete appointments
|
||||
@HTTP(method = "DELETE", path = "api/v1/appointments", hasBody = true)
|
||||
Call<Void> bulkDeleteAppointments(@Body BulkDeleteRequest request);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Retrofit interface for category endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.CategoryDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls for categories
|
||||
public interface CategoryApi {
|
||||
|
||||
// Get all categories with pagination
|
||||
@GET("api/v1/categories")
|
||||
Call<PageResponse<CategoryDTO>> getAllCategories(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size);
|
||||
}
|
||||
@@ -1,23 +1,37 @@
|
||||
/*
|
||||
* Retrofit interface for chat and conversation endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.ConversationDTO;
|
||||
import com.example.petstoremobile.dtos.MessageDTO;
|
||||
import com.example.petstoremobile.dtos.UpdateConversationStatusRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
|
||||
//api calls to get conversations
|
||||
// api calls for chat conversations
|
||||
public interface ChatApi {
|
||||
|
||||
// Get all conversations
|
||||
@GET("api/v1/chat/conversations")
|
||||
Call<List<ConversationDTO>> getAllConversations();
|
||||
|
||||
// Get conversation by id
|
||||
@GET("api/v1/chat/conversations/{conversationId}")
|
||||
Call<ConversationDTO> getConversationById(@Path("conversationId") Long conversationId);
|
||||
|
||||
// Update conversation status
|
||||
@PUT("api/v1/chat/conversations/{conversationId}")
|
||||
Call<ConversationDTO> updateConversationStatus(@Path("conversationId") Long conversationId, @Body UpdateConversationStatusRequest request);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Retrofit interface for coupon endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.CouponDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
public interface CouponApi {
|
||||
|
||||
@GET("api/v1/coupons")
|
||||
Call<PageResponse<CouponDTO>> getAllCoupons(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("active") Boolean active,
|
||||
@Query("discountType") String discountType,
|
||||
@Query("sort") String sort);
|
||||
|
||||
@GET("api/v1/coupons/{id}")
|
||||
Call<CouponDTO> getCouponById(@Path("id") Long id);
|
||||
|
||||
@GET("api/v1/coupons/code/{code}")
|
||||
Call<CouponDTO> getCouponByCode(@Path("code") String code);
|
||||
|
||||
@POST("api/v1/coupons")
|
||||
Call<CouponDTO> createCoupon(@Body CouponDTO coupon);
|
||||
|
||||
@PUT("api/v1/coupons/{id}")
|
||||
Call<CouponDTO> updateCoupon(@Path("id") Long id, @Body CouponDTO coupon);
|
||||
|
||||
@DELETE("api/v1/coupons/{id}")
|
||||
Call<Void> deleteCoupon(@Path("id") Long id);
|
||||
|
||||
@HTTP(method = "DELETE", path = "api/v1/coupons", hasBody = true)
|
||||
Call<Void> bulkDeleteCoupons(@Body BulkDeleteRequest request);
|
||||
}
|
||||
@@ -1,21 +1,51 @@
|
||||
/*
|
||||
* Retrofit interface for customer endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.dtos.DropdownDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
//api calls to get customers
|
||||
// api calls for customers
|
||||
public interface CustomerApi {
|
||||
|
||||
// Get all customers with pagination
|
||||
@GET("api/v1/customers")
|
||||
Call<PageResponse<CustomerDTO>> getAllCustomers(@Query("page") int page, @Query("size") int size);
|
||||
|
||||
// Get customer by id
|
||||
@GET("api/v1/customers/{customerId}")
|
||||
Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId);
|
||||
|
||||
// Update customer
|
||||
@PUT("api/v1/customers/{customerId}")
|
||||
Call<CustomerDTO> updateCustomer(@Path("customerId") Long customerId, @Body CustomerDTO customer);
|
||||
|
||||
// Delete customer
|
||||
@DELETE("api/v1/customers/{customerId}")
|
||||
Call<Void> deleteCustomer(@Path("customerId") Long customerId);
|
||||
|
||||
// Register customer
|
||||
@POST("api/v1/auth/register")
|
||||
Call<CustomerDTO> registerCustomer(@Body CustomerDTO customer);
|
||||
|
||||
// Get customer dropdowns
|
||||
@GET("api/v1/dropdowns/customers")
|
||||
Call<List<DropdownDTO>> getCustomerDropdowns();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Retrofit interface for employee endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.EmployeeDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.*;
|
||||
|
||||
// api calls for employees
|
||||
public interface EmployeeApi {
|
||||
|
||||
// Get all employees with pagination
|
||||
@GET("api/v1/employees")
|
||||
Call<PageResponse<EmployeeDTO>> getAllEmployees(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size);
|
||||
|
||||
// Get employee by id
|
||||
@GET("api/v1/employees/{id}")
|
||||
Call<EmployeeDTO> getEmployeeById(@Path("id") Long id);
|
||||
|
||||
// Create employee
|
||||
@POST("api/v1/employees")
|
||||
Call<EmployeeDTO> createEmployee(@Body EmployeeDTO employee);
|
||||
|
||||
// Update employee
|
||||
@PUT("api/v1/employees/{id}")
|
||||
Call<EmployeeDTO> updateEmployee(@Path("id") Long id, @Body EmployeeDTO employee);
|
||||
|
||||
// Delete employee
|
||||
@DELETE("api/v1/employees/{id}")
|
||||
Call<Void> deleteEmployee(@Path("id") Long id);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Retrofit interface for inventory endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.InventoryDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls for inventory
|
||||
public interface InventoryApi {
|
||||
|
||||
// Get all inventory with filters
|
||||
@GET("api/v1/inventory")
|
||||
Call<PageResponse<InventoryDTO>> getAllInventory(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
// Get inventory by id
|
||||
@GET("api/v1/inventory/{id}")
|
||||
Call<InventoryDTO> getInventoryById(@Path("id") Long id);
|
||||
|
||||
// Create inventory
|
||||
@POST("api/v1/inventory")
|
||||
Call<InventoryDTO> createInventory(@Body InventoryDTO request);
|
||||
|
||||
// Update inventory
|
||||
@PUT("api/v1/inventory/{id}")
|
||||
Call<InventoryDTO> updateInventory(@Path("id") Long id, @Body InventoryDTO request);
|
||||
|
||||
// Delete inventory
|
||||
@DELETE("api/v1/inventory/{id}")
|
||||
Call<Void> deleteInventory(@Path("id") Long id);
|
||||
|
||||
// Bulk delete inventory
|
||||
@HTTP(method = "DELETE", path = "api/v1/inventory", hasBody = true)
|
||||
Call<Void> bulkDeleteInventory(@Body BulkDeleteRequest request);
|
||||
}
|
||||
@@ -1,20 +1,49 @@
|
||||
/*
|
||||
* Retrofit interface for message endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.MessageDTO;
|
||||
import com.example.petstoremobile.dtos.SendMessageRequest;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Part;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Streaming;
|
||||
|
||||
//api calls to get and send messages
|
||||
// api calls for messages
|
||||
public interface MessageApi {
|
||||
|
||||
// Get messages for a conversation
|
||||
@GET("api/v1/chat/conversations/{id}/messages")
|
||||
Call<List<MessageDTO>> getMessages(@Path("id") Long conversationId);
|
||||
|
||||
// Send a message
|
||||
@POST("api/v1/chat/conversations/{id}/messages")
|
||||
Call<MessageDTO> sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request);
|
||||
|
||||
// Send a message with attachment
|
||||
@Multipart
|
||||
@POST("api/v1/chat/conversations/{id}/attachments")
|
||||
Call<MessageDTO> sendMessageWithAttachment(
|
||||
@Path("id") Long conversationId,
|
||||
@Part MultipartBody.Part content,
|
||||
@Part MultipartBody.Part file
|
||||
);
|
||||
|
||||
// Download attachment
|
||||
@GET("api/v1/chat/messages/{id}/attachment")
|
||||
@Streaming
|
||||
Call<ResponseBody> downloadAttachment(@Path("id") Long messageId);
|
||||
}
|
||||
@@ -1,26 +1,70 @@
|
||||
/*
|
||||
* Retrofit interface for pet endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.DropdownDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Part;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls to CRUD pets
|
||||
public interface PetApi {
|
||||
// Get all pets
|
||||
// endpoint for downloading the pet's image file
|
||||
String PET_IMAGE_PATH = "api/v1/pets/%d/image";
|
||||
|
||||
// Get all pets with filters
|
||||
@GET("api/v1/pets")
|
||||
Call<PageResponse<PetDTO>> getAllPets(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("status") String status,
|
||||
@Query("species") String species,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("customerId") Long customerId,
|
||||
@Query("sort") String sort
|
||||
);
|
||||
|
||||
// Get pets by customer id
|
||||
@GET("api/v1/dropdowns/customers/{customerId}/pets")
|
||||
Call<List<DropdownDTO>> getCustomerPets(@Path("customerId") Long customerId);
|
||||
|
||||
// Get adoption pets
|
||||
@GET("api/v1/dropdowns/adoption-pets")
|
||||
Call<List<DropdownDTO>> getAdoptionPets(@Query("storeId") Long storeId);
|
||||
|
||||
// Get pet dropdowns
|
||||
@GET("api/v1/dropdowns/pets")
|
||||
Call<List<DropdownDTO>> getPetDropdowns();
|
||||
|
||||
// Get pet species dropdowns
|
||||
@GET("api/v1/dropdowns/pet-species")
|
||||
Call<List<DropdownDTO>> getPetSpeciesDropdowns();
|
||||
|
||||
// Get pet breeds dropdowns
|
||||
@GET("api/v1/dropdowns/pet-breeds")
|
||||
Call<List<DropdownDTO>> getPetBreedsDropdowns(@Query("species") String species);
|
||||
|
||||
// Get pet by id
|
||||
@GET("api/v1/pets/{id}")
|
||||
Call<PetDTO> getPetById(@Path("id") Long id);
|
||||
@@ -37,4 +81,16 @@ public interface PetApi {
|
||||
@DELETE("api/v1/pets/{id}")
|
||||
Call<Void> deletePet(@Path("id") Long id);
|
||||
|
||||
// Bulk delete pets
|
||||
@HTTP(method = "DELETE", path = "api/v1/pets", hasBody = true)
|
||||
Call<Void> bulkDeletePets(@Body BulkDeleteRequest request);
|
||||
|
||||
// Upload pet image
|
||||
@Multipart
|
||||
@POST("api/v1/pets/{id}/image")
|
||||
Call<Void> uploadPetImage(@Path("id") Long id, @Part MultipartBody.Part image);
|
||||
|
||||
// Delete pet image
|
||||
@DELETE("api/v1/pets/{id}/image")
|
||||
Call<Void> deletePetImage(@Path("id") Long id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Retrofit interface for product endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.DropdownDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ProductDTO;
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// api calls for products
|
||||
public interface ProductApi {
|
||||
// endpoint for downloading the product's image file
|
||||
String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image";
|
||||
|
||||
// Get all products with filters
|
||||
@GET("api/v1/products")
|
||||
Call<PageResponse<ProductDTO>> getAllProducts(
|
||||
@Query("q") String query,
|
||||
@Query("categoryId") Long categoryId,
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("sort") String sort);
|
||||
|
||||
// Get product by id
|
||||
@GET("api/v1/products/{id}")
|
||||
Call<ProductDTO> getProductById(@Path("id") Long id);
|
||||
|
||||
// Create product
|
||||
@POST("api/v1/products")
|
||||
Call<ProductDTO> createProduct(@Body ProductDTO product);
|
||||
|
||||
// Update product
|
||||
@PUT("api/v1/products/{id}")
|
||||
Call<ProductDTO> updateProduct(@Path("id") Long id, @Body ProductDTO product);
|
||||
|
||||
// Delete product
|
||||
@DELETE("api/v1/products/{id}")
|
||||
Call<Void> deleteProduct(@Path("id") Long id);
|
||||
|
||||
// Upload product image
|
||||
@Multipart
|
||||
@POST("api/v1/products/{id}/image")
|
||||
Call<Void> uploadProductImage(@Path("id") Long id, @Part MultipartBody.Part image);
|
||||
|
||||
// Delete product image
|
||||
@DELETE("api/v1/products/{id}/image")
|
||||
Call<Void> deleteProductImage(@Path("id") Long id);
|
||||
|
||||
// Get product dropdowns
|
||||
@GET("api/v1/dropdowns/products")
|
||||
Call<List<DropdownDTO>> getProductDropdowns();
|
||||
|
||||
// Get category dropdowns
|
||||
@GET("api/v1/dropdowns/categories")
|
||||
Call<List<DropdownDTO>> getCategoryDropdowns();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Retrofit interface for product-supplier endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ProductSupplierDTO;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.*;
|
||||
|
||||
// api calls for product-supplier relationships
|
||||
public interface ProductSupplierApi {
|
||||
|
||||
// Get all product-suppliers with filters
|
||||
@GET("api/v1/product-suppliers")
|
||||
Call<PageResponse<ProductSupplierDTO>> getAllProductSuppliers(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("productId") Long productId,
|
||||
@Query("supplierId") Long supplierId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
// Get product-supplier by composite id
|
||||
@GET("api/v1/product-suppliers/{productId}/{supplierId}")
|
||||
Call<ProductSupplierDTO> getProductSupplierById(
|
||||
@Path("productId") Long productId,
|
||||
@Path("supplierId") Long supplierId);
|
||||
|
||||
// Create product-supplier
|
||||
@POST("api/v1/product-suppliers")
|
||||
Call<ProductSupplierDTO> createProductSupplier(@Body ProductSupplierDTO dto);
|
||||
|
||||
// Update product-supplier
|
||||
@PUT("api/v1/product-suppliers/{productId}/{supplierId}")
|
||||
Call<ProductSupplierDTO> updateProductSupplier(
|
||||
@Path("productId") Long productId,
|
||||
@Path("supplierId") Long supplierId,
|
||||
@Body ProductSupplierDTO dto);
|
||||
|
||||
// Delete product-supplier
|
||||
@DELETE("api/v1/product-suppliers/{productId}/{supplierId}")
|
||||
Call<Void> deleteProductSupplier(
|
||||
@Path("productId") Long productId,
|
||||
@Path("supplierId") Long supplierId);
|
||||
|
||||
// Bulk delete product-suppliers
|
||||
@HTTP(method = "DELETE", path = "api/v1/product-suppliers", hasBody = true)
|
||||
Call<Void> bulkDeleteProductSuppliers(@Body BulkDeleteRequest request);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Retrofit interface for purchase order endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.PurchaseOrderDTO;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls for purchase orders
|
||||
public interface PurchaseOrderApi {
|
||||
|
||||
// Get all purchase orders with filters
|
||||
@GET("api/v1/purchase-orders")
|
||||
Call<PageResponse<PurchaseOrderDTO>> getAllPurchaseOrders(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
// Get purchase order by id
|
||||
@GET("api/v1/purchase-orders/{id}")
|
||||
Call<PurchaseOrderDTO> getPurchaseOrderById(@Path("id") Long id);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Retrofit interface for refund endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.RefundDTO;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// api calls for refunds
|
||||
public interface RefundApi {
|
||||
|
||||
// Get all refunds
|
||||
@GET("api/v1/refunds")
|
||||
Call<List<RefundDTO>> getAllRefunds();
|
||||
|
||||
// Get refund by id
|
||||
@GET("api/v1/refunds/{id}")
|
||||
Call<RefundDTO> getRefundById(@Path("id") Long id);
|
||||
|
||||
// Create refund
|
||||
@POST("api/v1/refunds")
|
||||
Call<RefundDTO> createRefund(@Body RefundDTO refund);
|
||||
|
||||
// Update refund
|
||||
@PUT("api/v1/refunds/{id}")
|
||||
Call<RefundDTO> updateRefund(@Path("id") Long id, @Body RefundDTO refund);
|
||||
|
||||
// Delete refund
|
||||
@DELETE("api/v1/refunds/{id}")
|
||||
Call<Void> deleteRefund(@Path("id") Long id);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.api.auth.AuthInterceptor;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
//Retrofit client Used for API calls
|
||||
public class RetrofitClient {
|
||||
//base URL
|
||||
public static final String BASE_URL = "http://10.0.2.2:8080"; //for emulator testing
|
||||
// public static final String BASE_URL = "http://10.0.0.200:8080/"; //for hardware testing
|
||||
|
||||
private static Retrofit retrofit = null;
|
||||
|
||||
public static Retrofit getClient(Context context) {
|
||||
//create an http logging using an interceptor
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.addInterceptor(interceptor)
|
||||
.addInterceptor(new AuthInterceptor(context))
|
||||
.build();
|
||||
|
||||
//build the retrofit object with all needed properties
|
||||
retrofit = new Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create()) //JSON converter
|
||||
.client(client) //logging interceptor - OkHttpClient
|
||||
.build();
|
||||
|
||||
return retrofit;
|
||||
}
|
||||
|
||||
//associate the retrofit object with the API interface
|
||||
public static PetApi getPetApi(Context context) {
|
||||
return getClient(context).create(PetApi.class);
|
||||
}
|
||||
|
||||
public static ServiceApi getServiceApi(Context context) {
|
||||
return getClient(context).create(ServiceApi.class);
|
||||
}
|
||||
|
||||
public static SupplierApi getSupplierApi(Context context) {
|
||||
return getClient(context).create(SupplierApi.class);
|
||||
}
|
||||
|
||||
public static AuthApi getAuthApi(Context context) {
|
||||
return getClient(context).create(AuthApi.class);
|
||||
}
|
||||
|
||||
public static ChatApi getChatApi(Context context) {
|
||||
return getClient(context).create(ChatApi.class);
|
||||
}
|
||||
|
||||
public static CustomerApi getCustomerApi(Context context) {
|
||||
return getClient(context).create(CustomerApi.class);
|
||||
}
|
||||
|
||||
public static MessageApi getMessageApi(Context context) {
|
||||
return getClient(context).create(MessageApi.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Retrofit interface for sale endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls for sales
|
||||
public interface SaleApi {
|
||||
|
||||
// Get all sales with filters
|
||||
@GET("api/v1/sales")
|
||||
Call<PageResponse<SaleDTO>> getAllSales(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("paymentMethod") String paymentMethod,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("isRefund") Boolean isRefund,
|
||||
@Query("customerId") Long customerId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
// Get sale by id
|
||||
@GET("api/v1/sales/{id}")
|
||||
Call<SaleDTO> getSaleById(@Path("id") Long id);
|
||||
|
||||
// Create sale
|
||||
@POST("api/v1/sales")
|
||||
Call<SaleDTO> createSale(@Body SaleDTO sale);
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
/*
|
||||
* Retrofit interface for service endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||
|
||||
@@ -7,6 +15,7 @@ import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
@@ -18,7 +27,9 @@ public interface ServiceApi {
|
||||
@GET("api/v1/services")
|
||||
Call<PageResponse<ServiceDTO>> getAllServices(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("sort") String sort
|
||||
);
|
||||
|
||||
// Get service by id
|
||||
@@ -36,4 +47,8 @@ public interface ServiceApi {
|
||||
// Delete service
|
||||
@DELETE("api/v1/services/{id}")
|
||||
Call<Void> deleteService(@Path("id") Long id);
|
||||
|
||||
// Bulk delete services
|
||||
@HTTP(method = "DELETE", path = "api/v1/services", hasBody = true)
|
||||
Call<Void> bulkDeleteServices(@Body BulkDeleteRequest request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Retrofit interface for store endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.DropdownDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls for stores
|
||||
public interface StoreApi {
|
||||
|
||||
// Get all stores with pagination
|
||||
@GET("api/v1/stores")
|
||||
Call<PageResponse<StoreDTO>> getAllStores(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size);
|
||||
|
||||
// Get store dropdowns
|
||||
@GET("api/v1/dropdowns/stores")
|
||||
Call<List<DropdownDTO>> getStoreDropdowns();
|
||||
|
||||
// Get employees of a specific store
|
||||
@GET("api/v1/dropdowns/stores/{storeId}/employees")
|
||||
Call<List<DropdownDTO>> getStoreEmployees(@Path("storeId") Long storeId);
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
/*
|
||||
* Retrofit interface for supplier endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||
|
||||
@@ -7,6 +15,7 @@ import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
@@ -18,7 +27,9 @@ public interface SupplierApi {
|
||||
@GET("api/v1/suppliers")
|
||||
Call<PageResponse<SupplierDTO>> getAllSuppliers(
|
||||
@Query("page") int page,
|
||||
@Query("size") int size
|
||||
@Query("size") int size,
|
||||
@Query("q") String query,
|
||||
@Query("sort") String sort
|
||||
);
|
||||
|
||||
// Get supplier by id
|
||||
@@ -36,4 +47,8 @@ public interface SupplierApi {
|
||||
// Delete supplier
|
||||
@DELETE("api/v1/suppliers/{id}")
|
||||
Call<Void> deleteSupplier(@Path("id") Long id);
|
||||
|
||||
// Bulk delete suppliers
|
||||
@HTTP(method = "DELETE", path = "api/v1/suppliers", hasBody = true)
|
||||
Call<Void> bulkDeleteSuppliers(@Body BulkDeleteRequest request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Retrofit interface for user endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api;
|
||||
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
// api calls for users
|
||||
public interface UserApi {
|
||||
// endpoint for downloading the user's avatar file
|
||||
String AVATAR_PATH = "api/v1/users/%d/avatar/file";
|
||||
|
||||
// Get all users with filters
|
||||
@GET("api/v1/users")
|
||||
Call<PageResponse<UserDTO>> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size);
|
||||
}
|
||||
@@ -1,17 +1,57 @@
|
||||
/*
|
||||
* Retrofit interface for login and registration endpoints.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api.auth;
|
||||
|
||||
import com.example.petstoremobile.dtos.AuthDTO;
|
||||
import com.example.petstoremobile.dtos.AvatarUploadResponse;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Part;
|
||||
|
||||
//Api for logging in and getting current user
|
||||
public interface AuthApi {
|
||||
|
||||
// endpoint for downloading the current user's avatar file
|
||||
String AVATAR_FILE_PATH = "api/v1/auth/me/avatar/file";
|
||||
|
||||
//login endpoint
|
||||
@POST("api/v1/auth/login")
|
||||
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
|
||||
|
||||
//get current user endpoint
|
||||
@GET("api/v1/auth/me")
|
||||
Call<AuthDTO.UserResponse> getCurrentUser();
|
||||
Call<UserDTO> getMe();
|
||||
|
||||
//update current user endpoint
|
||||
@PUT("api/v1/auth/me")
|
||||
Call<UserDTO> updateMe(@Body Map<String, String> updates);
|
||||
|
||||
//upload avatar endpoint
|
||||
@Multipart
|
||||
@POST("api/v1/auth/me/avatar")
|
||||
Call<AvatarUploadResponse> uploadAvatar(@Part MultipartBody.Part avatar);
|
||||
|
||||
//delete avatar endpoint
|
||||
@DELETE("api/v1/auth/me/avatar")
|
||||
Call<Void> deleteAvatar();
|
||||
|
||||
//forgot password endpoint
|
||||
@POST("api/v1/auth/forgot-password")
|
||||
Call<Void> forgotPassword(@Body Map<String, String> body);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.example.petstoremobile.api.auth;
|
||||
/*
|
||||
* Interceptor that attaches the auth token to outgoing requests.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
package com.example.petstoremobile.api.auth;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@@ -15,8 +20,8 @@ public class AuthInterceptor implements Interceptor {
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
public AuthInterceptor(Context context) {
|
||||
this.tokenManager = TokenManager.getInstance(context);
|
||||
public AuthInterceptor(TokenManager tokenManager) {
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -25,19 +30,26 @@ public class AuthInterceptor implements Interceptor {
|
||||
String token = tokenManager.getToken();
|
||||
String url = chain.request().url().toString();
|
||||
|
||||
if (url.contains("auth/login") || url.contains("auth/register")) {
|
||||
boolean isAuthEndpoint = url.contains("auth/login") || url.contains("auth/register");
|
||||
|
||||
if (isAuthEndpoint) {
|
||||
return chain.proceed(chain.request());
|
||||
}
|
||||
|
||||
//If we have a token then add it to the request
|
||||
Response response;
|
||||
if (token != null) {
|
||||
Request request = chain.request().newBuilder()
|
||||
.addHeader("Authorization", "Bearer " + token)
|
||||
.build();
|
||||
return chain.proceed(request);
|
||||
response = chain.proceed(request);
|
||||
} else {
|
||||
response = chain.proceed(chain.request());
|
||||
}
|
||||
|
||||
//If no token then just pass the request
|
||||
return chain.proceed(chain.request());
|
||||
if (response.code() == 401) {
|
||||
tokenManager.forceLogout();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
/*
|
||||
* Handles saving and retrieving the authentication token.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.api.auth;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
//Store login token in shared preferences
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext;
|
||||
|
||||
//Used to save and retrieve login data
|
||||
@Singleton
|
||||
public class TokenManager {
|
||||
public static final String ACTION_FORCE_LOGOUT = "com.example.petstoremobile.ACTION_FORCE_LOGOUT";
|
||||
|
||||
private static final String TOKEN_KEY = "token";
|
||||
private static final String USERNAME_KEY = "username";
|
||||
private static final String ROLE_KEY = "role";
|
||||
private static final String PREFS_NAME = "auth_prefs";
|
||||
private static final String USER_ID_KEY = "user_id";
|
||||
private static final String PRIMARY_STORE_ID_KEY = "primary_store_id";
|
||||
|
||||
private static TokenManager instance;
|
||||
private final Context context;
|
||||
private SharedPreferences prefs;
|
||||
|
||||
private TokenManager(Context context) {
|
||||
@Inject
|
||||
public TokenManager(@ApplicationContext Context context) {
|
||||
this.context = context;
|
||||
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static TokenManager getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
instance = new TokenManager(context);
|
||||
}
|
||||
return instance;
|
||||
public void forceLogout() {
|
||||
clearLoginData();
|
||||
Intent intent = new Intent(ACTION_FORCE_LOGOUT);
|
||||
intent.setPackage(context.getPackageName());
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
|
||||
//save login data after login
|
||||
@@ -56,6 +75,19 @@ public class TokenManager {
|
||||
prefs.edit().putLong(USER_ID_KEY, userId).apply();
|
||||
}
|
||||
|
||||
public void savePrimaryStoreId(Long storeId) {
|
||||
if (storeId != null) {
|
||||
prefs.edit().putLong(PRIMARY_STORE_ID_KEY, storeId).apply();
|
||||
} else {
|
||||
prefs.edit().remove(PRIMARY_STORE_ID_KEY).apply();
|
||||
}
|
||||
}
|
||||
|
||||
public Long getPrimaryStoreId() {
|
||||
long id = prefs.getLong(PRIMARY_STORE_ID_KEY, -1L);
|
||||
return id == -1L ? null : id;
|
||||
}
|
||||
|
||||
//Check if logged in
|
||||
public boolean isLoggedIn() {
|
||||
return getToken() != null;
|
||||
@@ -65,6 +97,4 @@ public class TokenManager {
|
||||
public void clearLoginData() {
|
||||
prefs.edit().clear().apply();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Sets up the Retrofit client and dependency injection for network calls.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.di;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import com.example.petstoremobile.BuildConfig;
|
||||
import com.example.petstoremobile.api.*;
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.api.auth.AuthInterceptor;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.hilt.InstallIn;
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext;
|
||||
import dagger.hilt.components.SingletonComponent;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
//Module to provide dependencies injection for the api
|
||||
@Module
|
||||
@InstallIn(SingletonComponent.class)
|
||||
public class NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("baseUrl")
|
||||
public static String provideBaseUrl() {
|
||||
return isEmulator() ? BuildConfig.EMULATOR_BACKEND_URL : BuildConfig.DEVICE_BACKEND_URL;
|
||||
}
|
||||
|
||||
// Check if the device is an emulator
|
||||
private static boolean isEmulator() {
|
||||
return Build.FINGERPRINT.startsWith("generic")
|
||||
|| Build.FINGERPRINT.startsWith("unknown")
|
||||
|| Build.MODEL.contains("google_sdk")
|
||||
|| Build.MODEL.contains("Emulator")
|
||||
|| Build.MODEL.contains("Android SDK built for x86")
|
||||
|| Build.MANUFACTURER.contains("Genymotion")
|
||||
|| Build.HARDWARE.contains("goldfish")
|
||||
|| Build.HARDWARE.contains("ranchu")
|
||||
|| Build.PRODUCT.contains("sdk")
|
||||
|| Build.PRODUCT.contains("sdk_gphone")
|
||||
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static OkHttpClient provideOkHttpClient(TokenManager tokenManager) {
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
|
||||
|
||||
return new OkHttpClient.Builder()
|
||||
.addInterceptor(interceptor)
|
||||
.addInterceptor(new AuthInterceptor(tokenManager))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
//build the retrofit instance
|
||||
@Provides
|
||||
@Singleton
|
||||
public static Retrofit provideRetrofit(@Named("baseUrl") String baseUrl, OkHttpClient client) {
|
||||
return new Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(client)
|
||||
.build();
|
||||
}
|
||||
|
||||
//associate the api with the retrofit instance
|
||||
@Provides
|
||||
@Singleton
|
||||
public static PetApi providePetApi(Retrofit retrofit) {
|
||||
return retrofit.create(PetApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static ServiceApi provideServiceApi(Retrofit retrofit) {
|
||||
return retrofit.create(ServiceApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static SupplierApi provideSupplierApi(Retrofit retrofit) {
|
||||
return retrofit.create(SupplierApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static AdoptionApi provideAdoptionApi(Retrofit retrofit) {
|
||||
return retrofit.create(AdoptionApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static AppointmentApi provideAppointmentApi(Retrofit retrofit) {
|
||||
return retrofit.create(AppointmentApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static ProductApi provideProductApi(Retrofit retrofit) {
|
||||
return retrofit.create(ProductApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static SaleApi provideSaleApi(Retrofit retrofit) {
|
||||
return retrofit.create(SaleApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static PurchaseOrderApi providePurchaseOrderApi(Retrofit retrofit) {
|
||||
return retrofit.create(PurchaseOrderApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static ProductSupplierApi provideProductSupplierApi(Retrofit retrofit) {
|
||||
return retrofit.create(ProductSupplierApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static InventoryApi provideInventoryApi(Retrofit retrofit) {
|
||||
return retrofit.create(InventoryApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static AuthApi provideAuthApi(Retrofit retrofit) {
|
||||
return retrofit.create(AuthApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static ChatApi provideChatApi(Retrofit retrofit) {
|
||||
return retrofit.create(ChatApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static CustomerApi provideCustomerApi(Retrofit retrofit) {
|
||||
return retrofit.create(CustomerApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static MessageApi provideMessageApi(Retrofit retrofit) {
|
||||
return retrofit.create(MessageApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static StoreApi provideStoreApi(Retrofit retrofit) {
|
||||
return retrofit.create(StoreApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static CategoryApi provideCategoryApi(Retrofit retrofit) {
|
||||
return retrofit.create(CategoryApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static UserApi provideUserApi(Retrofit retrofit) {
|
||||
return retrofit.create(UserApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static EmployeeApi provideEmployeeApi(Retrofit retrofit) {
|
||||
return retrofit.create(EmployeeApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static RefundApi provideRefundApi(Retrofit retrofit) {
|
||||
return retrofit.create(RefundApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static CouponApi provideCouponApi(Retrofit retrofit) {
|
||||
return retrofit.create(CouponApi.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public static ActivityLogApi provideActivityLogApi(Retrofit retrofit) {
|
||||
return retrofit.create(ActivityLogApi.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Data transfer object for activity log entries.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for activity logs.
|
||||
*/
|
||||
public class ActivityLogDTO {
|
||||
private Long logId;
|
||||
private String activity;
|
||||
private String fullName;
|
||||
private String fullNameSnapshot;
|
||||
private String logTimestamp;
|
||||
private String role;
|
||||
private String roleSnapshot;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private String storeNameSnapshot;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String usernameSnapshot;
|
||||
|
||||
public Long getLogId() { return logId; }
|
||||
public String getActivity() { return activity; }
|
||||
public String getFullName() { return fullName; }
|
||||
public String getFullNameSnapshot() { return fullNameSnapshot; }
|
||||
public String getLogTimestamp() { return logTimestamp; }
|
||||
public String getRole() { return role; }
|
||||
public String getRoleSnapshot() { return roleSnapshot; }
|
||||
public Long getStoreId() { return storeId; }
|
||||
public String getStoreName() { return storeName; }
|
||||
public String getStoreNameSnapshot() { return storeNameSnapshot; }
|
||||
public Long getUserId() { return userId; }
|
||||
public String getUsername() { return username; }
|
||||
public String getUsernameSnapshot() { return usernameSnapshot; }
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Data transfer object for adoption records.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for pet adoptions.
|
||||
*/
|
||||
public class AdoptionDTO {
|
||||
|
||||
private Long adoptionId;
|
||||
private Long petId;
|
||||
private String petName;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private Long employeeId;
|
||||
private String employeeName;
|
||||
private Long sourceStoreId;
|
||||
private String sourceStoreName;
|
||||
private String adoptionDate;
|
||||
private String adoptionStatus;
|
||||
private BigDecimal adoptionFee;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
public AdoptionDTO() {}
|
||||
|
||||
public AdoptionDTO(Long petId, Long customerId, Long employeeId, Long sourceStoreId,
|
||||
String adoptionDate, String adoptionStatus, BigDecimal adoptionFee) {
|
||||
this.petId = petId;
|
||||
this.customerId = customerId;
|
||||
this.employeeId = employeeId;
|
||||
this.sourceStoreId = sourceStoreId;
|
||||
this.adoptionDate = adoptionDate;
|
||||
this.adoptionStatus = adoptionStatus;
|
||||
this.adoptionFee = adoptionFee;
|
||||
}
|
||||
|
||||
public Long getAdoptionId() { return adoptionId; }
|
||||
public void setAdoptionId(Long adoptionId) { this.adoptionId = adoptionId; }
|
||||
|
||||
public Long getPetId() { return petId; }
|
||||
public void setPetId(Long petId) { this.petId = petId; }
|
||||
|
||||
public String getPetName() { return petName; }
|
||||
public void setPetName(String petName) { this.petName = petName; }
|
||||
|
||||
public Long getCustomerId() { return customerId; }
|
||||
public void setCustomerId(Long customerId) { this.customerId = customerId; }
|
||||
|
||||
public String getCustomerName() { return customerName; }
|
||||
public void setCustomerName(String customerName) { this.customerName = customerName; }
|
||||
|
||||
public Long getEmployeeId() { return employeeId; }
|
||||
public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; }
|
||||
|
||||
public String getEmployeeName() { return employeeName; }
|
||||
public void setEmployeeName(String employeeName) { this.employeeName = employeeName; }
|
||||
|
||||
public Long getSourceStoreId() { return sourceStoreId; }
|
||||
public void setSourceStoreId(Long sourceStoreId) { this.sourceStoreId = sourceStoreId; }
|
||||
|
||||
public String getSourceStoreName() { return sourceStoreName; }
|
||||
public void setSourceStoreName(String sourceStoreName) { this.sourceStoreName = sourceStoreName; }
|
||||
|
||||
public String getAdoptionDate() { return adoptionDate; }
|
||||
public void setAdoptionDate(String adoptionDate) { this.adoptionDate = adoptionDate; }
|
||||
|
||||
public String getAdoptionStatus() { return adoptionStatus; }
|
||||
public void setAdoptionStatus(String adoptionStatus) { this.adoptionStatus = adoptionStatus; }
|
||||
|
||||
public BigDecimal getAdoptionFee() { return adoptionFee; }
|
||||
public void setAdoptionFee(BigDecimal adoptionFee) { this.adoptionFee = adoptionFee; }
|
||||
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public String getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Data transfer object for appointments.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for appointments.
|
||||
*/
|
||||
public class AppointmentDTO {
|
||||
|
||||
private Long appointmentId;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private Long serviceId;
|
||||
private String serviceName;
|
||||
private Long employeeId;
|
||||
private String employeeName;
|
||||
private String appointmentDate;
|
||||
private String appointmentTime;
|
||||
private String appointmentStatus;
|
||||
private String petName;
|
||||
private Long petId;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
public AppointmentDTO(Long customerId, Long storeId, Long serviceId,
|
||||
String appointmentDate, String appointmentTime,
|
||||
String appointmentStatus, Long petId) {
|
||||
this(customerId, storeId, serviceId, null, appointmentDate, appointmentTime, appointmentStatus, petId);
|
||||
}
|
||||
|
||||
public AppointmentDTO(Long customerId, Long storeId, Long serviceId, Long employeeId,
|
||||
String appointmentDate, String appointmentTime,
|
||||
String appointmentStatus, Long petId) {
|
||||
this.customerId = customerId;
|
||||
this.storeId = storeId;
|
||||
this.serviceId = serviceId;
|
||||
this.employeeId = employeeId;
|
||||
this.appointmentDate = appointmentDate;
|
||||
this.appointmentTime = appointmentTime;
|
||||
this.appointmentStatus = appointmentStatus;
|
||||
this.petId = petId;
|
||||
}
|
||||
|
||||
public Long getAppointmentId() { return appointmentId; }
|
||||
|
||||
public Long getCustomerId() { return customerId; }
|
||||
|
||||
public String getCustomerName() { return customerName; }
|
||||
|
||||
public Long getStoreId() { return storeId; }
|
||||
|
||||
public String getStoreName() { return storeName; }
|
||||
|
||||
public Long getServiceId() { return serviceId; }
|
||||
|
||||
public String getServiceName() { return serviceName; }
|
||||
|
||||
public Long getEmployeeId() { return employeeId; }
|
||||
|
||||
public String getEmployeeName() { return employeeName; }
|
||||
|
||||
public String getAppointmentDate() { return appointmentDate; }
|
||||
|
||||
public String getAppointmentTime() { return appointmentTime; }
|
||||
|
||||
public String getAppointmentStatus() { return appointmentStatus; }
|
||||
|
||||
public String getPetName() { return petName; }
|
||||
|
||||
public Long getPetId() { return petId; }
|
||||
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
|
||||
public String getUpdatedAt() { return updatedAt; }
|
||||
|
||||
public Long getPetID() { return petId; }
|
||||
|
||||
public String getServiceType() { return serviceName; }
|
||||
|
||||
public Long getServiceID() { return serviceId; }
|
||||
|
||||
public String getStatus() { return appointmentStatus; }
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
/*
|
||||
* Data transfer object for authentication requests and responses.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
//Used to send login data to the backend
|
||||
/**
|
||||
* Data Transfer Object for authentication credentials.
|
||||
*/
|
||||
public class AuthDTO {
|
||||
public static class LoginRequest {
|
||||
private String username;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Response object returned after uploading an avatar image.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Response containing the URL of a newly uploaded avatar.
|
||||
*/
|
||||
public class AvatarUploadResponse {
|
||||
private String avatarUrl;
|
||||
private String message;
|
||||
|
||||
public AvatarUploadResponse() {
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Request object for deleting multiple items at once.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Request body for deleting multiple records at once.
|
||||
*/
|
||||
public class BulkDeleteRequest {
|
||||
private List<Long> ids;
|
||||
|
||||
public BulkDeleteRequest() {
|
||||
}
|
||||
|
||||
public BulkDeleteRequest(List<Long> ids) {
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
public List<Long> getIds() {
|
||||
return ids;
|
||||
}
|
||||
|
||||
public void setIds(List<Long> ids) {
|
||||
this.ids = ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Data transfer object for categories.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for product categories.
|
||||
*/
|
||||
public class CategoryDTO {
|
||||
private Long categoryId;
|
||||
private String categoryName;
|
||||
private String categoryType;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
public Long getCategoryId() {
|
||||
return categoryId;
|
||||
}
|
||||
|
||||
public String getCategoryName() {
|
||||
return categoryName;
|
||||
}
|
||||
|
||||
public String getCategoryType() {
|
||||
return categoryType;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
/*
|
||||
* Data transfer object for chat conversations.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for chat conversations.
|
||||
*/
|
||||
public class ConversationDTO {
|
||||
private Long id;
|
||||
private Long customerId;
|
||||
@@ -12,6 +22,14 @@ public class ConversationDTO {
|
||||
|
||||
public ConversationDTO() {}
|
||||
|
||||
public ConversationDTO(Long id, Long customerId, Long staffId, String lastMessage, String status) {
|
||||
this.id = id;
|
||||
this.customerId = customerId;
|
||||
this.staffId = staffId;
|
||||
this.lastMessage = lastMessage;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Data transfer object for coupons.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for coupons.
|
||||
*/
|
||||
public class CouponDTO {
|
||||
private Long couponId;
|
||||
private String couponCode;
|
||||
private String discountType;
|
||||
private BigDecimal discountValue;
|
||||
private BigDecimal minOrderAmount;
|
||||
private Boolean active;
|
||||
private String startsAt;
|
||||
private String endsAt;
|
||||
private Integer usageLimit;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
public CouponDTO() {}
|
||||
|
||||
public Long getCouponId() { return couponId; }
|
||||
public void setCouponId(Long couponId) { this.couponId = couponId; }
|
||||
|
||||
public String getCouponCode() { return couponCode; }
|
||||
public void setCouponCode(String couponCode) { this.couponCode = couponCode; }
|
||||
|
||||
public String getDiscountType() { return discountType; }
|
||||
public void setDiscountType(String discountType) { this.discountType = discountType; }
|
||||
|
||||
public BigDecimal getDiscountValue() { return discountValue; }
|
||||
public void setDiscountValue(BigDecimal discountValue) { this.discountValue = discountValue; }
|
||||
|
||||
public BigDecimal getMinOrderAmount() { return minOrderAmount; }
|
||||
public void setMinOrderAmount(BigDecimal minOrderAmount) { this.minOrderAmount = minOrderAmount; }
|
||||
|
||||
public Boolean getActive() { return active; }
|
||||
public void setActive(Boolean active) { this.active = active; }
|
||||
|
||||
public String getStartsAt() { return startsAt; }
|
||||
public void setStartsAt(String startsAt) { this.startsAt = startsAt; }
|
||||
|
||||
public String getEndsAt() { return endsAt; }
|
||||
public void setEndsAt(String endsAt) { this.endsAt = endsAt; }
|
||||
|
||||
public Integer getUsageLimit() { return usageLimit; }
|
||||
public void setUsageLimit(Integer usageLimit) { this.usageLimit = usageLimit; }
|
||||
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public String getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
@@ -1,59 +1,90 @@
|
||||
/*
|
||||
* Data transfer object for customers.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for customers.
|
||||
*/
|
||||
public class CustomerDTO {
|
||||
@SerializedName("customerId")
|
||||
@SerializedName("id")
|
||||
private Long customerId;
|
||||
|
||||
private String username;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String fullName;
|
||||
private String email;
|
||||
private String phone;
|
||||
private Boolean active;
|
||||
private Integer loyaltyPoints;
|
||||
private Long primaryStoreId;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private String password;
|
||||
private String role;
|
||||
|
||||
public CustomerDTO() {}
|
||||
|
||||
public Long getCustomerId() {
|
||||
return customerId;
|
||||
}
|
||||
|
||||
public void setCustomerId(Long customerId) {
|
||||
this.customerId = customerId;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
public CustomerDTO(String username, String password, String firstName, String lastName,
|
||||
String email, String phone) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public Long getCustomerId() { return customerId; }
|
||||
public void setCustomerId(Long customerId) { this.customerId = customerId; }
|
||||
|
||||
public String getUsername() { return username; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
|
||||
public String getFirstName() { return firstName; }
|
||||
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||
|
||||
public String getLastName() { return lastName; }
|
||||
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||
|
||||
public String getFullName() {
|
||||
return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "");
|
||||
if (fullName != null) return fullName;
|
||||
String f = firstName != null ? firstName : "";
|
||||
String l = lastName != null ? lastName : "";
|
||||
return (f + " " + l).trim();
|
||||
}
|
||||
public void setFullName(String fullName) { this.fullName = fullName; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public Boolean getActive() { return active; }
|
||||
public void setActive(Boolean active) { this.active = active; }
|
||||
|
||||
public Integer getLoyaltyPoints() { return loyaltyPoints; }
|
||||
public void setLoyaltyPoints(Integer loyaltyPoints) { this.loyaltyPoints = loyaltyPoints; }
|
||||
|
||||
public Long getPrimaryStoreId() { return primaryStoreId; }
|
||||
public void setPrimaryStoreId(Long primaryStoreId) { this.primaryStoreId = primaryStoreId; }
|
||||
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public String getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
|
||||
public String getRole() { return role; }
|
||||
public void setRole(String role) { this.role = role; }
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Data transfer object used to populate dropdown menus.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for simple dropdown selection lists.
|
||||
*/
|
||||
public class DropdownDTO {
|
||||
private Long id;
|
||||
private String label;
|
||||
|
||||
public DropdownDTO() {}
|
||||
|
||||
public DropdownDTO(Long id, String label) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Data transfer object for employees.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for employees.
|
||||
*/
|
||||
public class EmployeeDTO {
|
||||
|
||||
private Long id;
|
||||
private String username;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String fullName;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String role;
|
||||
private String staffRole;
|
||||
private Boolean active;
|
||||
private Integer loyaltyPoints;
|
||||
private Long primaryStoreId;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private String password;
|
||||
|
||||
public EmployeeDTO() {}
|
||||
|
||||
public EmployeeDTO(String username, String password, String firstName, String lastName,
|
||||
String email, String phone, String role, String staffRole, boolean active, Long primaryStoreId) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.email = email;
|
||||
this.phone = phone;
|
||||
this.role = role;
|
||||
this.staffRole = staffRole;
|
||||
this.active = active;
|
||||
this.primaryStoreId = primaryStoreId;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public void setFullName(String fullName) {
|
||||
this.fullName = fullName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public String getStaffRole() {
|
||||
return staffRole;
|
||||
}
|
||||
|
||||
public void setStaffRole(String staffRole) {
|
||||
this.staffRole = staffRole;
|
||||
}
|
||||
|
||||
public Boolean getActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(Boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public Integer getLoyaltyPoints() {
|
||||
return loyaltyPoints;
|
||||
}
|
||||
|
||||
public void setLoyaltyPoints(Integer loyaltyPoints) {
|
||||
this.loyaltyPoints = loyaltyPoints;
|
||||
}
|
||||
|
||||
public Long getPrimaryStoreId() {
|
||||
return primaryStoreId;
|
||||
}
|
||||
|
||||
public void setPrimaryStoreId(Long primaryStoreId) {
|
||||
this.primaryStoreId = primaryStoreId;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(String updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Represents an error response from the server.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Used to get messages of any errors from the backend.
|
||||
*/
|
||||
public class ErrorResponse {
|
||||
private String message;
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Data transfer object for inventory items.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for inventory stock.
|
||||
*/
|
||||
public class InventoryDTO {
|
||||
// Response fields (from backend InventoryResponse)
|
||||
private Long inventoryId;
|
||||
private Long prodId;
|
||||
private String productName;
|
||||
private String categoryName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private Integer quantity;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
public InventoryDTO() {
|
||||
}
|
||||
|
||||
// Constructor for create/update requests (matches InventoryRequest)
|
||||
public InventoryDTO(Long prodId, Long storeId, Integer quantity) {
|
||||
this.prodId = prodId;
|
||||
this.storeId = storeId;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public Long getInventoryId() {
|
||||
return inventoryId;
|
||||
}
|
||||
|
||||
public void setInventoryId(Long inventoryId) {
|
||||
this.inventoryId = inventoryId;
|
||||
}
|
||||
|
||||
public Long getProdId() {
|
||||
return prodId;
|
||||
}
|
||||
|
||||
public void setProdId(Long prodId) {
|
||||
this.prodId = prodId;
|
||||
}
|
||||
|
||||
public String getProductName() {
|
||||
return productName;
|
||||
}
|
||||
|
||||
public void setProductName(String productName) {
|
||||
this.productName = productName;
|
||||
}
|
||||
|
||||
public String getCategoryName() {
|
||||
return categoryName;
|
||||
}
|
||||
|
||||
public void setCategoryName(String categoryName) {
|
||||
this.categoryName = categoryName;
|
||||
}
|
||||
|
||||
public Long getStoreId() {
|
||||
return storeId;
|
||||
}
|
||||
|
||||
public void setStoreId(Long storeId) {
|
||||
this.storeId = storeId;
|
||||
}
|
||||
|
||||
public String getStoreName() {
|
||||
return storeName;
|
||||
}
|
||||
|
||||
public void setStoreName(String storeName) {
|
||||
this.storeName = storeName;
|
||||
}
|
||||
|
||||
public Integer getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public void setQuantity(Integer quantity) {
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(String updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,17 @@
|
||||
/*
|
||||
* Data transfer object for chat messages.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for chat messages.
|
||||
*/
|
||||
public class MessageDTO {
|
||||
|
||||
@SerializedName("id")
|
||||
@@ -22,6 +32,24 @@ public class MessageDTO {
|
||||
@SerializedName("isRead")
|
||||
private Boolean isRead;
|
||||
|
||||
@SerializedName("attachmentUrl")
|
||||
private String attachmentUrl;
|
||||
|
||||
@SerializedName("attachmentName")
|
||||
private String attachmentName;
|
||||
|
||||
@SerializedName("attachmentMimeType")
|
||||
private String attachmentMimeType;
|
||||
|
||||
@SerializedName("attachmentSizeBytes")
|
||||
private Long attachmentSizeBytes;
|
||||
|
||||
@SerializedName("senderRole")
|
||||
private String senderRole;
|
||||
|
||||
@SerializedName("senderDisplayName")
|
||||
private String senderDisplayName;
|
||||
|
||||
public MessageDTO() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
@@ -41,4 +69,22 @@ public class MessageDTO {
|
||||
|
||||
public Boolean getIsRead() { return isRead; }
|
||||
public void setIsRead(Boolean isRead) { this.isRead = isRead; }
|
||||
|
||||
public String getAttachmentUrl() { return attachmentUrl; }
|
||||
public void setAttachmentUrl(String attachmentUrl) { this.attachmentUrl = attachmentUrl; }
|
||||
|
||||
public String getAttachmentName() { return attachmentName; }
|
||||
public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; }
|
||||
|
||||
public String getAttachmentMimeType() { return attachmentMimeType; }
|
||||
public void setAttachmentMimeType(String attachmentMimeType) { this.attachmentMimeType = attachmentMimeType; }
|
||||
|
||||
public Long getAttachmentSizeBytes() { return attachmentSizeBytes; }
|
||||
public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; }
|
||||
|
||||
public String getSenderRole() { return senderRole; }
|
||||
public void setSenderRole(String senderRole) { this.senderRole = senderRole; }
|
||||
|
||||
public String getSenderDisplayName() { return senderDisplayName; }
|
||||
public void setSenderDisplayName(String senderDisplayName) { this.senderDisplayName = senderDisplayName; }
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
/*
|
||||
* Wrapper for paginated responses from the server.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
//Used to get data from the API
|
||||
/**
|
||||
* Generic response wrapper for paginated API results.
|
||||
*/
|
||||
public class PageResponse<T> {
|
||||
private List<T> content;
|
||||
private int totalPages;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/*
|
||||
* Data transfer object for pets.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object representing a pet.
|
||||
*/
|
||||
public class PetDTO {
|
||||
private Long petId;
|
||||
private String petName;
|
||||
@@ -7,9 +17,13 @@ public class PetDTO {
|
||||
private String petBreed;
|
||||
private Integer petAge;
|
||||
private String petStatus;
|
||||
private String petPrice;
|
||||
private Double petPrice;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
|
||||
public Long getPetId() { return petId; }
|
||||
public void setPetId(Long petId) { this.petId = petId; }
|
||||
@@ -29,12 +43,24 @@ public class PetDTO {
|
||||
public String getPetStatus() { return petStatus; }
|
||||
public void setPetStatus(String petStatus) { this.petStatus = petStatus; }
|
||||
|
||||
public String getPetPrice() { return petPrice; }
|
||||
public void setPetPrice(String petPrice) { this.petPrice = petPrice; }
|
||||
public Double getPetPrice() { return petPrice; }
|
||||
public void setPetPrice(Double petPrice) { this.petPrice = petPrice; }
|
||||
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public String getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public Long getCustomerId() { return customerId; }
|
||||
public void setCustomerId(Long customerId) { this.customerId = customerId; }
|
||||
|
||||
public String getCustomerName() { return customerName; }
|
||||
public void setCustomerName(String customerName) { this.customerName = customerName; }
|
||||
|
||||
public Long getStoreId() { return storeId; }
|
||||
public void setStoreId(Long storeId) { this.storeId = storeId; }
|
||||
|
||||
public String getStoreName() { return storeName; }
|
||||
public void setStoreName(String storeName) { this.storeName = storeName; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Data transfer object for products.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for products.
|
||||
*/
|
||||
public class ProductDTO {
|
||||
private Long prodId;
|
||||
private String prodName;
|
||||
private Long categoryId;
|
||||
private String categoryName;
|
||||
private String prodDesc;
|
||||
private BigDecimal prodPrice;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
public ProductDTO() {
|
||||
}
|
||||
|
||||
// Constructor for create/update
|
||||
public ProductDTO(String prodName, Long categoryId, String prodDesc, BigDecimal prodPrice) {
|
||||
this.prodName = prodName;
|
||||
this.categoryId = categoryId;
|
||||
this.prodDesc = prodDesc;
|
||||
this.prodPrice = prodPrice;
|
||||
}
|
||||
|
||||
public Long getProdId() {
|
||||
return prodId;
|
||||
}
|
||||
|
||||
public void setProdId(Long prodId) {
|
||||
this.prodId = prodId;
|
||||
}
|
||||
|
||||
public String getProdName() {
|
||||
return prodName;
|
||||
}
|
||||
|
||||
public void setProdName(String prodName) {
|
||||
this.prodName = prodName;
|
||||
}
|
||||
|
||||
public Long getCategoryId() {
|
||||
return categoryId;
|
||||
}
|
||||
|
||||
public void setCategoryId(Long categoryId) {
|
||||
this.categoryId = categoryId;
|
||||
}
|
||||
|
||||
public String getCategoryName() {
|
||||
return categoryName;
|
||||
}
|
||||
|
||||
public void setCategoryName(String categoryName) {
|
||||
this.categoryName = categoryName;
|
||||
}
|
||||
|
||||
public String getProdDesc() {
|
||||
return prodDesc;
|
||||
}
|
||||
|
||||
public void setProdDesc(String prodDesc) {
|
||||
this.prodDesc = prodDesc;
|
||||
}
|
||||
|
||||
public BigDecimal getProdPrice() {
|
||||
return prodPrice;
|
||||
}
|
||||
|
||||
public void setProdPrice(BigDecimal prodPrice) {
|
||||
this.prodPrice = prodPrice;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Data transfer object for product-supplier relationships.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for mapping products to suppliers.
|
||||
*/
|
||||
public class ProductSupplierDTO {
|
||||
private Long productId;
|
||||
private String productName;
|
||||
private Long supplierId;
|
||||
private String supplierName;
|
||||
private BigDecimal cost;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
// Constructor for create/update
|
||||
public ProductSupplierDTO(Long productId, Long supplierId, BigDecimal cost) {
|
||||
this.productId = productId;
|
||||
this.supplierId = supplierId;
|
||||
this.cost = cost;
|
||||
}
|
||||
|
||||
public Long getProductId() {
|
||||
return productId;
|
||||
}
|
||||
|
||||
public String getProductName() {
|
||||
return productName;
|
||||
}
|
||||
|
||||
public Long getSupplierId() {
|
||||
return supplierId;
|
||||
}
|
||||
|
||||
public String getSupplierName() {
|
||||
return supplierName;
|
||||
}
|
||||
|
||||
public BigDecimal getCost() {
|
||||
return cost;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Data transfer object for purchase orders.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for purchase orders.
|
||||
*/
|
||||
public class PurchaseOrderDTO {
|
||||
private Long purchaseOrderId;
|
||||
private Long supId;
|
||||
private String supplierName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private String orderDate;
|
||||
private String status;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
public Long getPurchaseOrderId() {
|
||||
return purchaseOrderId;
|
||||
}
|
||||
|
||||
public Long getSupId() {
|
||||
return supId;
|
||||
}
|
||||
|
||||
public String getSupplierName() {
|
||||
return supplierName;
|
||||
}
|
||||
|
||||
public Long getStoreId() {
|
||||
return storeId;
|
||||
}
|
||||
|
||||
public String getStoreName() {
|
||||
return storeName;
|
||||
}
|
||||
|
||||
public String getOrderDate() {
|
||||
return orderDate;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Data transfer object for refunds.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for refund processing.
|
||||
*/
|
||||
public class RefundDTO {
|
||||
// Response fields
|
||||
private Long id;
|
||||
private Long saleId;
|
||||
private Long customerId;
|
||||
private BigDecimal amount;
|
||||
private String reason;
|
||||
private String status;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
// Constructor for create request
|
||||
public RefundDTO(Long saleId, String reason) {
|
||||
this.saleId = saleId;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
// Constructor for update request
|
||||
public RefundDTO(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Long getSaleId() {
|
||||
return saleId;
|
||||
}
|
||||
|
||||
public Long getCustomerId() {
|
||||
return customerId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Data transfer object for sales.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for sales transactions.
|
||||
*/
|
||||
public class SaleDTO {
|
||||
// Response fields
|
||||
private Long saleId;
|
||||
private String saleDate;
|
||||
private Long employeeId;
|
||||
private String employeeName;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private BigDecimal totalAmount;
|
||||
private BigDecimal subtotalAmount;
|
||||
private BigDecimal couponDiscountAmount;
|
||||
private BigDecimal employeeDiscountAmount;
|
||||
private BigDecimal loyaltyDiscountAmount;
|
||||
private BigDecimal pointsDiscountAmount;
|
||||
private Integer pointsUsed;
|
||||
private String paymentMethod;
|
||||
private String channel;
|
||||
private Boolean isRefund;
|
||||
private Long originalSaleId;
|
||||
private Long cartId;
|
||||
private Long couponId;
|
||||
private Integer pointsEarned;
|
||||
private List<SaleItemDTO> items;
|
||||
private String createdAt;
|
||||
|
||||
// Constructor for create request
|
||||
public SaleDTO(Long storeId, String paymentMethod, List<SaleItemDTO> items,
|
||||
Boolean isRefund, Long originalSaleId, Long customerId) {
|
||||
this.storeId = storeId;
|
||||
this.paymentMethod = paymentMethod;
|
||||
this.items = items;
|
||||
this.isRefund = isRefund;
|
||||
this.originalSaleId = originalSaleId;
|
||||
this.customerId = customerId;
|
||||
}
|
||||
|
||||
public Long getSaleId() {
|
||||
return saleId;
|
||||
}
|
||||
|
||||
public String getSaleDate() {
|
||||
return saleDate;
|
||||
}
|
||||
|
||||
public Long getEmployeeId() {
|
||||
return employeeId;
|
||||
}
|
||||
|
||||
public String getEmployeeName() {
|
||||
return employeeName;
|
||||
}
|
||||
|
||||
public Long getStoreId() {
|
||||
return storeId;
|
||||
}
|
||||
|
||||
public String getStoreName() {
|
||||
return storeName;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getSubtotalAmount() {
|
||||
return subtotalAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getCouponDiscountAmount() {
|
||||
return couponDiscountAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getEmployeeDiscountAmount() {
|
||||
return employeeDiscountAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getLoyaltyDiscountAmount() {
|
||||
return loyaltyDiscountAmount;
|
||||
}
|
||||
|
||||
public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) {
|
||||
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
|
||||
}
|
||||
|
||||
public Integer getPointsUsed() {
|
||||
return pointsUsed;
|
||||
}
|
||||
|
||||
public void setPointsUsed(Integer pointsUsed) {
|
||||
this.pointsUsed = pointsUsed;
|
||||
}
|
||||
|
||||
public String getPaymentMethod() {
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
public String getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public Boolean getIsRefund() {
|
||||
return isRefund;
|
||||
}
|
||||
|
||||
public Long getOriginalSaleId() {
|
||||
return originalSaleId;
|
||||
}
|
||||
|
||||
public Long getCartId() {
|
||||
return cartId;
|
||||
}
|
||||
|
||||
public Long getCouponId() {
|
||||
return couponId;
|
||||
}
|
||||
|
||||
public void setCouponId(Long couponId) {
|
||||
this.couponId = couponId;
|
||||
}
|
||||
|
||||
public Integer getPointsEarned() {
|
||||
return pointsEarned;
|
||||
}
|
||||
|
||||
public List<SaleItemDTO> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Long getCustomerId() {
|
||||
return customerId;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public BigDecimal getPointsDiscountAmount() {
|
||||
return pointsDiscountAmount;
|
||||
}
|
||||
|
||||
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
|
||||
this.pointsDiscountAmount = pointsDiscountAmount;
|
||||
}
|
||||
|
||||
// Nested SaleItemDTO
|
||||
public static class SaleItemDTO {
|
||||
private Long saleItemId;
|
||||
private Long prodId;
|
||||
private String productName;
|
||||
private Integer quantity;
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
// Constructor for request
|
||||
public SaleItemDTO(Long prodId, Integer quantity) {
|
||||
this.prodId = prodId;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public Long getSaleItemId() {
|
||||
return saleItemId;
|
||||
}
|
||||
|
||||
public Long getProdId() {
|
||||
return prodId;
|
||||
}
|
||||
|
||||
public String getProductName() {
|
||||
return productName;
|
||||
}
|
||||
|
||||
public Integer getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public BigDecimal getUnitPrice() {
|
||||
return unitPrice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
/*
|
||||
* Request object for sending a new chat message.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Request body for sending a new chat message.
|
||||
*/
|
||||
public class SendMessageRequest {
|
||||
|
||||
private String content;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/*
|
||||
* Data transfer object for services.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for services.
|
||||
*/
|
||||
public class ServiceDTO {
|
||||
private Long serviceId;
|
||||
private String serviceName;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Data transfer object for store information.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for store information.
|
||||
*/
|
||||
public class StoreDTO {
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private String address;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String createdAt;
|
||||
private String updatedAt;
|
||||
|
||||
// Constructor for hardcoded fallback
|
||||
public StoreDTO(Long storeId, String storeName) {
|
||||
this.storeId = storeId;
|
||||
this.storeName = storeName;
|
||||
}
|
||||
|
||||
public Long getStoreId() { return storeId; }
|
||||
public String getStoreName() { return storeName; }
|
||||
public String getAddress() { return address; }
|
||||
public String getPhone() { return phone; }
|
||||
public String getEmail() { return email; }
|
||||
public String getCreatedAt() { return createdAt; }
|
||||
public String getUpdatedAt() { return updatedAt; }
|
||||
}
|
||||
@@ -1,5 +1,15 @@
|
||||
/*
|
||||
* Data transfer object for suppliers.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for suppliers.
|
||||
*/
|
||||
public class SupplierDTO {
|
||||
private Long supId;
|
||||
private String supCompany;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Request object for changing a conversation's status.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Request body for updating chat conversation status.
|
||||
*/
|
||||
public class UpdateConversationStatusRequest {
|
||||
private String status;
|
||||
|
||||
public UpdateConversationStatusRequest(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Data transfer object for user account information.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.dtos;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for user account details.
|
||||
*/
|
||||
public class UserDTO {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private String fullName;
|
||||
private String phone;
|
||||
private String avatarUrl;
|
||||
private String role;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
|
||||
// Getters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public Long getStoreId() {
|
||||
return storeId;
|
||||
}
|
||||
|
||||
public String getStoreName() {
|
||||
return storeName;
|
||||
}
|
||||
}
|
||||
@@ -1,407 +1,639 @@
|
||||
/*
|
||||
* Fragment for the real-time chat screen.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ChatAdapter;
|
||||
import com.example.petstoremobile.adapters.MessageAdapter;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.api.ChatApi;
|
||||
import com.example.petstoremobile.api.CustomerApi;
|
||||
import com.example.petstoremobile.api.MessageApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.databinding.FragmentChatBinding;
|
||||
import com.example.petstoremobile.dtos.ConversationDTO;
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.dtos.MessageDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SendMessageRequest;
|
||||
import com.example.petstoremobile.models.Chat;
|
||||
import com.example.petstoremobile.models.Message;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
import com.example.petstoremobile.utils.DialogUtils;
|
||||
import com.example.petstoremobile.utils.FileUtils;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.ChatListViewModel;
|
||||
import com.example.petstoremobile.websocket.StompChatManager;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import retrofit2.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
/**
|
||||
* Fragment for handling customer support chat.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener,
|
||||
StompChatManager.ConversationListener, StompChatManager.ConnectionListener {
|
||||
|
||||
private static final String TAG = "ChatFragment";
|
||||
|
||||
// View
|
||||
private DrawerLayout drawerLayout;
|
||||
private RecyclerView rvChatList, rvMessages;
|
||||
private EditText etMessage;
|
||||
private Button btnSend;
|
||||
private FragmentChatBinding binding;
|
||||
private ChatListViewModel viewModel;
|
||||
|
||||
// Adapters
|
||||
private ChatAdapter chatAdapter;
|
||||
private ChatAdapter activeChatAdapter;
|
||||
private ChatAdapter closedChatAdapter;
|
||||
private MessageAdapter messageAdapter;
|
||||
|
||||
// Data
|
||||
private final List<Chat> chatList = new ArrayList<>();
|
||||
private final List<Chat> activeChatList = new ArrayList<>();
|
||||
private final List<Chat> closedChatList = new ArrayList<>();
|
||||
private final List<Message> messageList = new ArrayList<>();
|
||||
private final Map<Long, String> customerNames = new HashMap<>();
|
||||
private Uri pendingAttachmentUri;
|
||||
|
||||
// APIs
|
||||
private ChatApi chatApi;
|
||||
private CustomerApi customerApi;
|
||||
private MessageApi messageApi;
|
||||
@Inject TokenManager tokenManager;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
// chat
|
||||
private Long currentUserId;
|
||||
private Long activeConversationId;
|
||||
private StompChatManager stompChatManager;
|
||||
private ActivityResultLauncher<Intent> attachmentLauncher;
|
||||
|
||||
/**
|
||||
* Initializes the view model and attachment launcher.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(ChatListViewModel.class);
|
||||
attachmentLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri uri = result.getData().getData();
|
||||
if (uri != null) showAttachmentPreview(uri);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout and sets up UI event listeners.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
binding = FragmentChatBinding.inflate(inflater, container, false);
|
||||
|
||||
View view = inflater.inflate(R.layout.fragment_chat, container, false);
|
||||
binding.btnHamburger.setOnClickListener(v -> binding.chatDrawerLayout.openDrawer(GravityCompat.START));
|
||||
|
||||
chatApi = RetrofitClient.getChatApi(requireContext());
|
||||
customerApi = RetrofitClient.getCustomerApi(requireContext());
|
||||
messageApi = RetrofitClient.getMessageApi(requireContext());
|
||||
binding.etMessage.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND || actionId == EditorInfo.IME_NULL) {
|
||||
binding.btnSend.performClick();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
drawerLayout = view.findViewById(R.id.chatDrawerLayout);
|
||||
rvChatList = view.findViewById(R.id.rvChatList);
|
||||
rvMessages = view.findViewById(R.id.rvMessages);
|
||||
etMessage = view.findViewById(R.id.etMessage);
|
||||
btnSend = view.findViewById(R.id.btnSend);
|
||||
binding.btnSend.setOnClickListener(v -> {
|
||||
if (pendingAttachmentUri != null) sendWithAttachment(pendingAttachmentUri);
|
||||
else sendMessage();
|
||||
});
|
||||
|
||||
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
|
||||
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
|
||||
btnSend.setOnClickListener(v -> sendMessage());
|
||||
binding.btnAttach.setOnClickListener(v -> selectAttachment());
|
||||
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
|
||||
binding.btnCloseChat.setOnClickListener(v -> closeChat());
|
||||
|
||||
setupDrawerToggles();
|
||||
setupRecyclerViews();
|
||||
observeViewModel();
|
||||
loadInitialData();
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private void setupRecyclerViews() {
|
||||
// Set up Drawer menu to select conversation
|
||||
chatAdapter = new ChatAdapter(chatList, this);
|
||||
rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
rvChatList.setAdapter(chatAdapter);
|
||||
/**
|
||||
* Sets up the logic to open and close the chat drawer.
|
||||
*/
|
||||
private void setupDrawerToggles() {
|
||||
binding.headerActiveChats.setOnClickListener(v -> {
|
||||
if (binding.rvActiveChats.getVisibility() == View.VISIBLE) {
|
||||
binding.rvActiveChats.setVisibility(View.GONE);
|
||||
binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_down_float);
|
||||
} else {
|
||||
binding.rvActiveChats.setVisibility(View.VISIBLE);
|
||||
binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_up_float);
|
||||
}
|
||||
});
|
||||
|
||||
binding.headerClosedChats.setOnClickListener(v -> {
|
||||
if (binding.rvClosedChats.getVisibility() == View.VISIBLE) {
|
||||
binding.rvClosedChats.setVisibility(View.GONE);
|
||||
binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_down_float);
|
||||
} else {
|
||||
binding.rvClosedChats.setVisibility(View.VISIBLE);
|
||||
binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_up_float);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the adapters and layout managers for chat lists and message history.
|
||||
*/
|
||||
private void setupRecyclerViews() {
|
||||
activeChatAdapter = new ChatAdapter(activeChatList, this);
|
||||
binding.rvActiveChats.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.rvActiveChats.setAdapter(activeChatAdapter);
|
||||
|
||||
closedChatAdapter = new ChatAdapter(closedChatList, this);
|
||||
binding.rvClosedChats.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.rvClosedChats.setAdapter(closedChatAdapter);
|
||||
|
||||
// set up RecyclerView for selected chat to show messages
|
||||
messageAdapter = new MessageAdapter(messageList, null);
|
||||
messageAdapter.setBaseUrl(baseUrl);
|
||||
|
||||
messageAdapter.setOnAttachmentClickListener(message -> {
|
||||
if (message.getAttachmentMimeType() != null && message.getAttachmentMimeType().startsWith("image/")) {
|
||||
showFullScreenImage(message);
|
||||
} else {
|
||||
downloadFile(message);
|
||||
}
|
||||
});
|
||||
|
||||
LinearLayoutManager lm = new LinearLayoutManager(getContext());
|
||||
lm.setStackFromEnd(true);
|
||||
rvMessages.setLayoutManager(lm);
|
||||
rvMessages.setAdapter(messageAdapter);
|
||||
setConversationActive(false);
|
||||
binding.rvMessages.setLayoutManager(lm);
|
||||
binding.rvMessages.setItemAnimator(null);
|
||||
binding.rvMessages.setAdapter(messageAdapter);
|
||||
setConversationActive(false, null);
|
||||
}
|
||||
|
||||
//Helper function to load token and user id then connect to websocket
|
||||
/**
|
||||
* Displays a full-screen image preview for message attachments.
|
||||
*/
|
||||
private void showFullScreenImage(Message message) {
|
||||
if (baseUrl == null || message.getId() == null) return;
|
||||
|
||||
Dialog dialog = new Dialog(requireContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen);
|
||||
dialog.setContentView(R.layout.dialog_full_screen_image);
|
||||
|
||||
ImageView imageView = dialog.findViewById(R.id.ivFullScreen);
|
||||
ImageButton closeButton = dialog.findViewById(R.id.btnClose);
|
||||
ImageButton downloadButton = dialog.findViewById(R.id.btnDownload);
|
||||
|
||||
String cleanBase = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
String downloadUrl = cleanBase + "/api/v1/chat/messages/" + message.getId() + "/attachment";
|
||||
|
||||
GlideUtils.loadImageWithToken(requireContext(), imageView, downloadUrl, tokenManager.getToken(), R.drawable.placeholder);
|
||||
|
||||
closeButton.setOnClickListener(v -> dialog.dismiss());
|
||||
downloadButton.setOnClickListener(v -> downloadFile(message));
|
||||
imageView.setOnClickListener(v -> dialog.dismiss());
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the download process for a message attachment.
|
||||
*/
|
||||
private void downloadFile(Message message) {
|
||||
if (message.getId() == null) return;
|
||||
|
||||
DialogUtils.showConfirmDialog(requireContext(), "Download Attachment",
|
||||
"Do you want to download \"" + message.getAttachmentName() + "\"?", () -> {
|
||||
Toast.makeText(requireContext(), "Downloading " + message.getAttachmentName() + "...", Toast.LENGTH_SHORT).show();
|
||||
|
||||
viewModel.downloadAttachment(message.getId()).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
saveFileToDownloads(resource.data, message.getAttachmentName(), message.getAttachmentMimeType());
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(requireContext(), "Download failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the downloaded file body to the device's downloads folder.
|
||||
*/
|
||||
private void saveFileToDownloads(ResponseBody body, String fileName, String mimeType) {
|
||||
android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
new Thread(() -> {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
|
||||
values.put(MediaStore.Downloads.MIME_TYPE, mimeType);
|
||||
values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
|
||||
|
||||
Uri uri = requireContext().getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
|
||||
if (uri != null) {
|
||||
try (OutputStream outputStream = requireContext().getContentResolver().openOutputStream(uri);
|
||||
InputStream inputStream = body.byteStream()) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
mainHandler.post(() -> Toast.makeText(requireContext(), "File saved to Downloads", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
} else {
|
||||
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
File file = new File(downloadsDir, fileName);
|
||||
try (OutputStream outputStream = new FileOutputStream(file);
|
||||
InputStream inputStream = body.byteStream()) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
mainHandler.post(() -> Toast.makeText(requireContext(), "File saved to Downloads: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show());
|
||||
}
|
||||
body.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error saving file", e);
|
||||
mainHandler.post(() -> Toast.makeText(requireContext(), "Error saving file", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes LiveData from the ViewModel to update chat lists and messages.
|
||||
*/
|
||||
private void observeViewModel() {
|
||||
viewModel.getActiveChats().observe(getViewLifecycleOwner(), list -> {
|
||||
activeChatList.clear();
|
||||
activeChatList.addAll(list);
|
||||
activeChatAdapter.notifyDataSetChanged();
|
||||
updateTitleAndStateIfActive(list);
|
||||
});
|
||||
|
||||
viewModel.getClosedChats().observe(getViewLifecycleOwner(), list -> {
|
||||
closedChatList.clear();
|
||||
closedChatList.addAll(list);
|
||||
closedChatAdapter.notifyDataSetChanged();
|
||||
updateTitleAndStateIfActive(list);
|
||||
});
|
||||
|
||||
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
|
||||
int prevSize = messageList.size();
|
||||
messageList.clear();
|
||||
messageList.addAll(list);
|
||||
if (prevSize > 0 && list.size() == prevSize + 1) {
|
||||
messageAdapter.notifyItemInserted(list.size() - 1);
|
||||
} else {
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
}
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), this::setLoading);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the progress bar.
|
||||
*/
|
||||
private void setLoading(boolean loading) {
|
||||
if (binding != null && binding.progressBar != null) {
|
||||
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the chat header and input state if the active conversation changes.
|
||||
*/
|
||||
private void updateTitleAndStateIfActive(List<Chat> list) {
|
||||
if (activeConversationId != null) {
|
||||
for (Chat chat : list) {
|
||||
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
setConversationActive(true, chat.getStatus());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads initial chat data and establishes WebSocket connection.
|
||||
*/
|
||||
private void loadInitialData() {
|
||||
TokenManager tm = TokenManager.getInstance(requireContext());
|
||||
String token = tm.getToken();
|
||||
currentUserId = tm.getUserId();
|
||||
String role = tm.getRole();
|
||||
String token = tokenManager.getToken();
|
||||
Long currentUserId = tokenManager.getUserId();
|
||||
String role = tokenManager.getRole();
|
||||
|
||||
messageAdapter.setCurrentUserId(currentUserId);
|
||||
messageAdapter.setToken(token);
|
||||
|
||||
// if token exist then connect to websocket
|
||||
if (token != null) {
|
||||
stompChatManager = new StompChatManager(token, role);
|
||||
stompChatManager = new StompChatManager(token, role, baseUrl);
|
||||
stompChatManager.setMessageListener(this);
|
||||
stompChatManager.setConversationListener(this);
|
||||
stompChatManager.setConnectionListener(this);
|
||||
stompChatManager.connect();
|
||||
}
|
||||
|
||||
if (getArguments() != null && getArguments().containsKey("conversation_id")) {
|
||||
activeConversationId = getArguments().getLong("conversation_id");
|
||||
} else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) {
|
||||
activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1);
|
||||
getActivity().getIntent().removeExtra("conversation_id");
|
||||
} else {
|
||||
Log.e(TAG, "No token found");
|
||||
activeConversationId = viewModel.getLastActiveConversationId();
|
||||
}
|
||||
|
||||
loadCustomers();
|
||||
viewModel.loadCustomers();
|
||||
|
||||
if (activeConversationId != null) {
|
||||
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
|
||||
viewModel.loadMessageHistory(activeConversationId);
|
||||
} else {
|
||||
setConversationActive(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
//Helper function to load customer names for it to be displayed on drawer menu
|
||||
private void loadCustomers() {
|
||||
customerApi.getAllCustomers(0, 100).enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<PageResponse<CustomerDTO>> call,
|
||||
@NonNull Response<PageResponse<CustomerDTO>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
for (CustomerDTO c : response.body().getContent()) {
|
||||
customerNames.put(c.getCustomerId(), c.getFullName());
|
||||
}
|
||||
}
|
||||
loadConversations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<PageResponse<CustomerDTO>> call,
|
||||
@NonNull Throwable t) {
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//helper function to load conversations entities to display with customer names in drawer menu
|
||||
private void loadConversations() {
|
||||
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<ConversationDTO>> call,
|
||||
@NonNull Response<List<ConversationDTO>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
chatList.clear();
|
||||
List<Chat> loaded = response.body().stream()
|
||||
.map(dto -> {
|
||||
String name = customerNames.getOrDefault(
|
||||
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
return new Chat(String.valueOf(dto.getId()),
|
||||
name, dto.getLastMessage(),
|
||||
dto.getCustomerId(), dto.getStaffId());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
chatList.addAll(loaded);
|
||||
chatAdapter.notifyDataSetChanged();
|
||||
if (activeConversationId == null) {
|
||||
messageList.clear();
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
setConversationActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<ConversationDTO>> call,
|
||||
@NonNull Throwable t) {
|
||||
Log.e(TAG, "Error loading conversations", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called when user taps a chat in the drawer
|
||||
// Loads messages for that chat selected
|
||||
/**
|
||||
* Handles clicks on a chat from the drawer to switch the active conversation.
|
||||
*/
|
||||
@Override
|
||||
public void onChatClick(Chat chat) {
|
||||
activeConversationId = Long.parseLong(chat.getChatId());
|
||||
setConversationActive(true);
|
||||
drawerLayout.closeDrawer(GravityCompat.START);
|
||||
viewModel.setLastActiveConversationId(activeConversationId);
|
||||
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.subscribeToConversation(activeConversationId);
|
||||
setConversationActive(true, chat.getStatus());
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
|
||||
|
||||
messageAdapter.setStaffId(chat.getStaffId());
|
||||
|
||||
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
|
||||
viewModel.loadMessageHistory(activeConversationId);
|
||||
}
|
||||
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
|
||||
//helper function to load messages for selected chat
|
||||
private void loadMessageHistory(Long conversationId) {
|
||||
messageApi.getMessages(conversationId).enqueue(new Callback<List<MessageDTO>>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<List<MessageDTO>> call,
|
||||
@NonNull Response<List<MessageDTO>> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
messageList.clear();
|
||||
for (MessageDTO dto : response.body()) {
|
||||
messageList.add(dtoToModel(dto));
|
||||
}
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<List<MessageDTO>> call,
|
||||
@NonNull Throwable t) {
|
||||
Log.e(TAG, "Error loading messages", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Helper function to send a message to the chat
|
||||
private void sendMessage() {
|
||||
//check if a chat is selected
|
||||
/**
|
||||
* Closes the active chat conversation.
|
||||
*/
|
||||
private void closeChat() {
|
||||
if (activeConversationId == null) return;
|
||||
|
||||
//get the message from text field
|
||||
String text = etMessage.getText().toString().trim();
|
||||
DialogUtils.showConfirmDialog(requireContext(), "Close Chat",
|
||||
"Are you sure you want to close this chat? This will notify the customer.", () -> {
|
||||
viewModel.sendMessage(activeConversationId, "The Chat has been closed").observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS) {
|
||||
viewModel.addMessageLocally(resource.data);
|
||||
|
||||
viewModel.closeConversation(activeConversationId).observe(getViewLifecycleOwner(), statusResource -> {
|
||||
if (statusResource == null) return;
|
||||
setLoading(statusResource.status == Resource.Status.LOADING);
|
||||
if (statusResource.status == Resource.Status.SUCCESS) {
|
||||
viewModel.loadConversations();
|
||||
setConversationActive(true, "CLOSED");
|
||||
} else if (statusResource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(requireContext(), "Failed to close chat: " + statusResource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a text message to the active conversation.
|
||||
*/
|
||||
private void sendMessage() {
|
||||
if (activeConversationId == null) return;
|
||||
String text = binding.etMessage.getText().toString().trim();
|
||||
if (text.isEmpty()) return;
|
||||
|
||||
//clear text field after sending
|
||||
etMessage.setText("");
|
||||
|
||||
//calls api to send the message
|
||||
messageApi.sendMessage(activeConversationId, new SendMessageRequest(text))
|
||||
.enqueue(new Callback<MessageDTO>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<MessageDTO> call,
|
||||
@NonNull Response<MessageDTO> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
messageList.add(dtoToModel(response.body()));
|
||||
messageAdapter.notifyItemInserted(messageList.size() - 1);
|
||||
scrollToBottom();
|
||||
loadConversations();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<MessageDTO> call,
|
||||
@NonNull Throwable t) {
|
||||
Log.e(TAG, "Send failed", t);
|
||||
binding.etMessage.setText("");
|
||||
viewModel.sendMessage(activeConversationId, text).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
viewModel.addMessageLocally(resource.data);
|
||||
viewModel.loadConversations();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// When a message is received updates the chat preview
|
||||
/**
|
||||
* Opens a file picker to select an attachment.
|
||||
*/
|
||||
private void selectAttachment() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
attachmentLauncher.launch(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a preview of the selected attachment.
|
||||
*/
|
||||
private void showAttachmentPreview(Uri uri) {
|
||||
pendingAttachmentUri = uri;
|
||||
binding.layoutAttachmentPreview.setVisibility(View.VISIBLE);
|
||||
String mimeType = requireContext().getContentResolver().getType(uri);
|
||||
binding.tvPreviewName.setText(FileUtils.getFileName(requireContext(), uri));
|
||||
if (mimeType != null && mimeType.startsWith("image/")) {
|
||||
binding.ivPreview.setVisibility(View.VISIBLE);
|
||||
Glide.with(this).load(uri).into(binding.ivPreview);
|
||||
} else {
|
||||
binding.ivPreview.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the currently selected attachment from the preview.
|
||||
*/
|
||||
private void removeAttachment() {
|
||||
pendingAttachmentUri = null;
|
||||
binding.layoutAttachmentPreview.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with a file attachment.
|
||||
*/
|
||||
private void sendWithAttachment(Uri uri) {
|
||||
if (activeConversationId == null) return;
|
||||
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) {
|
||||
Toast.makeText(requireContext(), "Failed to prepare file", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
String text = binding.etMessage.getText().toString().trim();
|
||||
|
||||
MultipartBody.Part contentPart = text.isEmpty()
|
||||
? null
|
||||
: MultipartBody.Part.createFormData("content", text);
|
||||
|
||||
String mimeType = requireContext().getContentResolver().getType(uri);
|
||||
if (mimeType == null) mimeType = "application/octet-stream";
|
||||
|
||||
RequestBody filePartBody = RequestBody.create(file, MediaType.parse(mimeType));
|
||||
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", file.getName(), filePartBody);
|
||||
|
||||
binding.etMessage.setText("");
|
||||
removeAttachment();
|
||||
|
||||
viewModel.sendMessageWithAttachment(activeConversationId, contentPart, filePart).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
viewModel.addMessageLocally(resource.data);
|
||||
viewModel.loadConversations();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(requireContext(), "Failed to send attachment: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when a new message is received through the WebSocket.
|
||||
*/
|
||||
@Override
|
||||
public void onMessageReceived(MessageDTO dto) {
|
||||
//if there is no active selected conversation or the message received is for another chat, then just update the preview of last message
|
||||
if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) {
|
||||
updateConversationPreview(dto.getConversationId(), dto.getContent());
|
||||
return;
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) {
|
||||
viewModel.addMessageLocally(dto);
|
||||
}
|
||||
updateConversationPreview(dto.getConversationId(), dto.getContent());
|
||||
|
||||
if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return;
|
||||
|
||||
//else add the message to the active chat if it's not from the current user
|
||||
messageList.add(dtoToModel(dto));
|
||||
messageAdapter.notifyItemInserted(messageList.size() - 1);
|
||||
scrollToBottom();
|
||||
viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
|
||||
});
|
||||
}
|
||||
|
||||
// When a new conversation is added, updates the chat preview
|
||||
/**
|
||||
* Callback for when a conversation's status or last message is updated.
|
||||
*/
|
||||
@Override
|
||||
public void onConversationUpdated(ConversationDTO dto) {
|
||||
boolean updated = false;
|
||||
String name = customerNames.getOrDefault(
|
||||
dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
|
||||
for (int i = 0; i < chatList.size(); i++) {
|
||||
Chat existing = chatList.get(i);
|
||||
if (existing.getChatId().equals(String.valueOf(dto.getId()))) {
|
||||
chatList.set(i, new Chat(
|
||||
String.valueOf(dto.getId()),
|
||||
name,
|
||||
dto.getLastMessage(),
|
||||
dto.getCustomerId(),
|
||||
dto.getStaffId()
|
||||
));
|
||||
chatAdapter.notifyItemChanged(i);
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
chatList.add(0, new Chat(
|
||||
String.valueOf(dto.getId()),
|
||||
name,
|
||||
dto.getLastMessage(),
|
||||
dto.getCustomerId(),
|
||||
dto.getStaffId()
|
||||
));
|
||||
chatAdapter.notifyItemInserted(0);
|
||||
}
|
||||
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
viewModel.updateConversationLocally(dto);
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
|
||||
setConversationActive(true);
|
||||
setConversationActive(true, dto.getStatus());
|
||||
binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when the WebSocket connection is successfully opened.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketOpened() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
loadConversations();
|
||||
if (activeConversationId != null) {
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
if (!isAdded()) return;
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
viewModel.loadConversations();
|
||||
if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when the WebSocket connection is closed.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketClosed() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
loadConversations();
|
||||
if (!isAdded()) return;
|
||||
requireActivity().runOnUiThread(viewModel::loadConversations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when a WebSocket error occurs.
|
||||
*/
|
||||
@Override
|
||||
public void onSocketError() {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
loadConversations();
|
||||
if (activeConversationId != null) {
|
||||
loadMessageHistory(activeConversationId);
|
||||
}
|
||||
if (!isAdded()) return;
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
viewModel.loadConversations();
|
||||
if (activeConversationId != null) viewModel.loadMessageHistory(activeConversationId);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to convert DTO to message
|
||||
private Message dtoToModel(MessageDTO dto) {
|
||||
Message m = new Message();
|
||||
m.setId(dto.getId());
|
||||
m.setConversationId(dto.getConversationId());
|
||||
m.setSenderId(dto.getSenderId());
|
||||
m.setContent(dto.getContent());
|
||||
m.setTimestamp(dto.getTimestamp());
|
||||
m.setIsRead(dto.getIsRead());
|
||||
return m;
|
||||
}
|
||||
|
||||
//Helper function to scroll to bottom of the chat
|
||||
/**
|
||||
* Scrolls the message list to the most recent message.
|
||||
*/
|
||||
private void scrollToBottom() {
|
||||
if (!messageList.isEmpty()) {
|
||||
rvMessages.post(() ->
|
||||
rvMessages.smoothScrollToPosition(messageList.size() - 1));
|
||||
binding.rvMessages.post(() ->
|
||||
binding.rvMessages.smoothScrollToPosition(messageList.size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to update the chat preview last message
|
||||
private void updateConversationPreview(Long conversationId, String lastMessage) {
|
||||
if (conversationId == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < chatList.size(); i++) {
|
||||
Chat existing = chatList.get(i);
|
||||
if (existing.getChatId().equals(String.valueOf(conversationId))) {
|
||||
Chat updated = new Chat(
|
||||
existing.getChatId(),
|
||||
existing.getCustomerName(),
|
||||
lastMessage,
|
||||
existing.getCustomerId(),
|
||||
existing.getStaffId()
|
||||
);
|
||||
chatList.set(i, updated);
|
||||
chatAdapter.notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Updates the UI state based on whether a conversation is active and its status.
|
||||
*/
|
||||
private void setConversationActive(boolean active, String status) {
|
||||
boolean isClosed = "CLOSED".equalsIgnoreCase(status);
|
||||
UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach);
|
||||
binding.btnCloseChat.setVisibility(active && !isClosed ? View.VISIBLE : View.GONE);
|
||||
|
||||
//Helper function to enable or disable the send button when there is no active chat
|
||||
private void setConversationActive(boolean active) {
|
||||
btnSend.setEnabled(active);
|
||||
etMessage.setEnabled(active);
|
||||
if (!active) {
|
||||
activeConversationId = null;
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.clearConversationSubscription();
|
||||
}
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
removeAttachment();
|
||||
if (binding != null && binding.tvChatTitle != null) binding.tvChatTitle.setText("Customer Chat");
|
||||
if (stompChatManager != null) stompChatManager.clearConversationSubscription();
|
||||
messageList.clear();
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
etMessage.setText("");
|
||||
etMessage.setHint("Select a chat to start messaging");
|
||||
binding.etMessage.setText("");
|
||||
binding.etMessage.setHint("Select a chat to start messaging");
|
||||
} else {
|
||||
etMessage.setHint("Type a message...");
|
||||
binding.etMessage.setHint(isClosed ? "This chat is closed" : "Type a message...");
|
||||
ChatNotificationService.activeConversationIdInUi = activeConversationId;
|
||||
}
|
||||
}
|
||||
|
||||
// When fragment is destroyed, disconnect from websocket
|
||||
/**
|
||||
* Cleans up resources and disconnects the WebSocket when the view is destroyed.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
if (stompChatManager != null) stompChatManager.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,82 @@
|
||||
/*
|
||||
* Base fragment that provides common list and search functionality.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentListBinding;
|
||||
|
||||
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.AdoptionFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.AppointmentFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.InventoryFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.ProductFragment;
|
||||
import javax.inject.Inject;
|
||||
|
||||
//The Fragment for the displaying the list of entities to be viewed
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment that serves as a container for various list-based screens, providing a navigation drawer.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ListFragment extends Fragment {
|
||||
|
||||
private DrawerLayout drawerLayout;
|
||||
private LinearLayout drawerPets, drawerServices, drawerSuppliers;
|
||||
private View touchBlocker;
|
||||
|
||||
// Adoptions, Appointments, Inventory, Products
|
||||
|
||||
private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts;
|
||||
private FragmentListBinding binding;
|
||||
private NavController innerNavController;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
/**
|
||||
* Inflates the fragment layout, initializes navigation drawers, and applies role-based access control.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_list, container, false);
|
||||
binding = FragmentListBinding.inflate(inflater, container, false);
|
||||
|
||||
//get controls from the layout
|
||||
drawerLayout = view.findViewById(R.id.drawerLayout);
|
||||
drawerPets = view.findViewById(R.id.drawerPets);
|
||||
drawerServices = view.findViewById(R.id.drawerServices);
|
||||
drawerSuppliers = view.findViewById(R.id.drawerSuppliers);
|
||||
drawerAdoptions = view.findViewById(R.id.drawerAdoptions);
|
||||
drawerAppointments = view.findViewById(R.id.drawerAppointments);
|
||||
drawerInventory = view.findViewById(R.id.drawerInventory);
|
||||
drawerProducts = view.findViewById(R.id.drawerProducts);
|
||||
|
||||
//needed to disable touches on the innerContainer while the drawer is open
|
||||
touchBlocker = view.findViewById(R.id.touchBlocker);
|
||||
|
||||
//Display pets fragment by default
|
||||
if (savedInstanceState == null) {
|
||||
loadFragment(new PetFragment());
|
||||
// Check user role and restrict access for STAFF
|
||||
String role = tokenManager.getRole();
|
||||
if ("STAFF".equalsIgnoreCase(role)) {
|
||||
binding.sectionAdmin.setVisibility(View.GONE);
|
||||
} else if ("ADMIN".equalsIgnoreCase(role)) {
|
||||
binding.sectionAdmin.setVisibility(View.VISIBLE);
|
||||
binding.drawerStaff.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// Default or other roles
|
||||
binding.sectionAdmin.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
//add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments)
|
||||
//while the drawer is open
|
||||
drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
|
||||
binding.drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
|
||||
|
||||
//When the drawer is opened, disable touches on the background
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
touchBlocker.setVisibility(View.VISIBLE);
|
||||
touchBlocker.setClickable(true);
|
||||
binding.touchBlocker.setVisibility(View.VISIBLE);
|
||||
binding.touchBlocker.setClickable(true);
|
||||
}
|
||||
|
||||
//When the drawer is closed, enable touches again
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
touchBlocker.setVisibility(View.GONE);
|
||||
touchBlocker.setClickable(false);
|
||||
binding.touchBlocker.setVisibility(View.GONE);
|
||||
binding.touchBlocker.setClickable(false);
|
||||
}
|
||||
|
||||
//unused methods
|
||||
@@ -83,63 +87,61 @@ public class ListFragment extends Fragment {
|
||||
});
|
||||
|
||||
// Click listeners for each drawer
|
||||
//Pets
|
||||
drawerPets.setOnClickListener(v -> {
|
||||
loadFragment(new PetFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
binding.drawerPets.setOnClickListener(v -> navigateTo(R.id.nav_pet));
|
||||
binding.drawerServices.setOnClickListener(v -> navigateTo(R.id.nav_service));
|
||||
binding.drawerSuppliers.setOnClickListener(v -> navigateTo(R.id.nav_supplier));
|
||||
binding.drawerAdoptions.setOnClickListener(v -> navigateTo(R.id.nav_adoption));
|
||||
binding.drawerAppointments.setOnClickListener(v -> navigateTo(R.id.nav_appointment));
|
||||
binding.drawerInventory.setOnClickListener(v -> navigateTo(R.id.nav_inventory));
|
||||
binding.drawerProducts.setOnClickListener(v -> navigateTo(R.id.nav_product));
|
||||
binding.drawerProductSupplier.setOnClickListener(v -> navigateTo(R.id.nav_product_supplier));
|
||||
binding.drawerPurchaseOrderView.setOnClickListener(v -> navigateTo(R.id.nav_purchase_order));
|
||||
binding.drawerSale.setOnClickListener(v -> navigateTo(R.id.nav_sale));
|
||||
binding.drawerStaff.setOnClickListener(v -> navigateTo(R.id.nav_staff));
|
||||
binding.drawerCustomers.setOnClickListener(v -> navigateTo(R.id.nav_customer));
|
||||
binding.drawerAnalytics.setOnClickListener(v -> navigateTo(R.id.nav_analytics));
|
||||
binding.drawerCoupons.setOnClickListener(v -> navigateTo(R.id.nav_coupon));
|
||||
binding.drawerActivityLogs.setOnClickListener(v -> navigateTo(R.id.nav_activity_log));
|
||||
|
||||
//Services
|
||||
drawerServices.setOnClickListener(v -> {
|
||||
loadFragment(new ServiceFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
|
||||
//Suppliers
|
||||
drawerSuppliers.setOnClickListener(v -> {
|
||||
loadFragment(new SupplierFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
|
||||
//Adoptions
|
||||
|
||||
drawerAdoptions.setOnClickListener(v -> {
|
||||
loadFragment(new AdoptionFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
|
||||
//Appoinment
|
||||
drawerAppointments.setOnClickListener(v -> {
|
||||
loadFragment(new AppointmentFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
|
||||
//Inventory
|
||||
drawerInventory.setOnClickListener(v -> {
|
||||
loadFragment(new InventoryFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
|
||||
//Products
|
||||
drawerProducts.setOnClickListener(v -> {
|
||||
loadFragment(new ProductFragment());
|
||||
drawerLayout.closeDrawers();
|
||||
});
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
//helper function to open the drawer
|
||||
/**
|
||||
* Cleans up the binding when the view is destroyed.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the NavController for the internal fragment container.
|
||||
*/
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
NavHostFragment navHostFragment = (NavHostFragment) getChildFragmentManager()
|
||||
.findFragmentById(R.id.inner_nav_host_fragment);
|
||||
if (navHostFragment != null) {
|
||||
innerNavController = navHostFragment.getNavController();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to a specific inner destination and closes all drawers.
|
||||
*/
|
||||
private void navigateTo(int destinationId) {
|
||||
if (innerNavController != null) {
|
||||
innerNavController.navigate(destinationId);
|
||||
}
|
||||
binding.drawerLayout.closeDrawers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically opens the navigation drawer.
|
||||
*/
|
||||
public void openDrawer() {
|
||||
drawerLayout.openDrawer(GravityCompat.START);
|
||||
}
|
||||
|
||||
// helper function to load the fragment into the display
|
||||
public void loadFragment(Fragment fragment) {
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.inner_fragment_container, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
binding.drawerLayout.openDrawer(GravityCompat.START);
|
||||
}
|
||||
}
|
||||
@@ -1,165 +1,116 @@
|
||||
/*
|
||||
* Fragment for viewing and editing the user's profile.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.fragments;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import android.provider.MediaStore;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.activities.MainActivity;
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentProfileBinding;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
import com.example.petstoremobile.utils.FileUtils;
|
||||
import com.example.petstoremobile.utils.GlideUtils;
|
||||
import com.example.petstoremobile.utils.ImagePickerHelper;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.AuthViewModel;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
/**
|
||||
* Fragment that displays and allows editing of the user's profile information.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ProfileFragment extends Fragment {
|
||||
|
||||
//initialize the view/controls
|
||||
private ImageView imgProfile;
|
||||
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
||||
private Button btnChangePhoto, btnEditEmail, btnEditPhone, btnLogout;
|
||||
private Uri photoUri;
|
||||
private FragmentProfileBinding binding;
|
||||
private UserDTO currentUser;
|
||||
private AuthViewModel viewModel;
|
||||
private boolean hasImage = false;
|
||||
|
||||
//Initialize the launchers for camera and gallery
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
private ActivityResultLauncher<Uri> cameraLauncher;
|
||||
private ActivityResultLauncher<String> permissionLauncher;
|
||||
@Inject TokenManager tokenManager;
|
||||
@Inject @Named("baseUrl") String baseUrl;
|
||||
|
||||
//Called when the fragment is created, sets up the launchers is set profile image
|
||||
private ImagePickerHelper imagePickerHelper;
|
||||
|
||||
/**
|
||||
* Initializes activity launchers and the ImagePickerHelper for camera and gallary.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(AuthViewModel.class);
|
||||
|
||||
// Launcher to open gallery to select profile image
|
||||
galleryLauncher = registerForActivityResult(
|
||||
//open gallery
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
//if the user selects an image and its not null
|
||||
if (result.getResultCode() == Activity.RESULT_OK
|
||||
&& result.getData() != null) {
|
||||
//get the selected image and set the image to the profile
|
||||
Uri selectedImage = result.getData().getData();
|
||||
imgProfile.setImageURI(selectedImage);
|
||||
//TODO: SAVE CHANGED PHOTO TO DATABASE
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher for camera to open and capture profile image
|
||||
cameraLauncher = registerForActivityResult(
|
||||
//open camera
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
//if a photo is taken set the image profile to it otherwise do nothing
|
||||
if (success) {
|
||||
//Clear the old image and set the new one
|
||||
imgProfile.setImageURI(null);
|
||||
imgProfile.setImageURI(photoUri);
|
||||
//TODO: SAVE CHANGED PHOTO TO DATABASE
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Launcher to request camera permission
|
||||
permissionLauncher = registerForActivityResult(
|
||||
//ask user for camera permission
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
//if the permission is granted launch the camera
|
||||
if (granted) {
|
||||
launchCamera();
|
||||
}
|
||||
else {
|
||||
//if the permission is denied then tell the user to grant it
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Permission Permission Required")
|
||||
.setMessage("Please grant camera permission to use this feature")
|
||||
.setPositiveButton("Open Settings", (dialog, which) ->{
|
||||
//open the settings page to grant the permission when they click open settings
|
||||
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null));
|
||||
startActivity(intent);
|
||||
})
|
||||
//close the dialog when the user clicks cancel
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: MAKE PROFILE VIEW DISPLAY PROFILE DATA FROM DATABASE
|
||||
imagePickerHelper = new ImagePickerHelper(this, "profile_photo.jpg", new ImagePickerHelper.ImagePickerListener() {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_profile, container, false);
|
||||
public void onImagePicked(Uri uri) {
|
||||
uploadAvatar(uri);
|
||||
}
|
||||
|
||||
//get all the controls from the view
|
||||
imgProfile = view.findViewById(R.id.imgProfile);
|
||||
tvProfileName = view.findViewById(R.id.tvProfileName);
|
||||
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
|
||||
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
|
||||
tvProfileRole = view.findViewById(R.id.tvProfileRole);
|
||||
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
|
||||
btnEditEmail = view.findViewById(R.id.btnEditEmail);
|
||||
btnEditPhone = view.findViewById(R.id.btnEditPhone);
|
||||
btnLogout = view.findViewById(R.id.btnLogout);
|
||||
@Override
|
||||
public void onImageRemoved() {
|
||||
deleteAvatar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the fragment layout and sets up listeners for profile.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentProfileBinding.inflate(inflater, container, false);
|
||||
|
||||
//Load Profile Data from backend
|
||||
loadProfileData();
|
||||
|
||||
//Set up listeners for the buttons
|
||||
//Change photo button
|
||||
btnChangePhoto.setOnClickListener(v -> {
|
||||
//Show alert dialog to user to select from gallery or camera
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Change Profile Photo")
|
||||
//set the options for the alert dialog
|
||||
.setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> {
|
||||
if (which == 0) {
|
||||
// Choose Camera
|
||||
//Checks if the user has granted the camera permission already
|
||||
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
//if the permission is already granted then launch the camera
|
||||
launchCamera();
|
||||
} else {
|
||||
//otherwise request the permission
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA);
|
||||
}
|
||||
} else {
|
||||
// Choose Gallery
|
||||
Intent intent = new Intent(Intent.ACTION_PICK,
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
galleryLauncher.launch(intent);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
//TODO: UPDATE PHOTO IN DATABASE
|
||||
binding.btnChangePhoto.setOnClickListener(v -> {
|
||||
imagePickerHelper.showImagePickerDialog("Change Profile Photo", hasImage);
|
||||
});
|
||||
|
||||
//Edit email button
|
||||
//When clicked open a dialog to change email
|
||||
btnEditEmail.setOnClickListener(v -> {
|
||||
binding.btnEditEmail.setOnClickListener(v -> {
|
||||
//Make a text field for the user to enter the new email
|
||||
EditText input = new EditText(requireContext());
|
||||
input.setPadding(30,30,30,30);
|
||||
input.setText(tvProfileEmail.getText().toString());
|
||||
input.setText(binding.tvProfileEmail.getText().toString());
|
||||
|
||||
//set input type to email
|
||||
input.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
|
||||
@@ -170,19 +121,10 @@ public class ProfileFragment extends Fragment {
|
||||
.setTitle("Edit Email")
|
||||
.setView(input)
|
||||
.setPositiveButton("Save", (dialog, which) -> {
|
||||
String newEmail = input.getText().toString();
|
||||
//if the new value is a valid email then set the email to the new value
|
||||
if (android.util.Patterns.EMAIL_ADDRESS.matcher(newEmail).matches()) {
|
||||
tvProfileEmail.setText(newEmail);
|
||||
//TODO: UPDATE THE EMAIL IN DATABASE
|
||||
}
|
||||
else {
|
||||
//tell the user to email is invalid
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Error")
|
||||
.setMessage("Email is invalid")
|
||||
.setPositiveButton("OK", null)
|
||||
.show();
|
||||
if (InputValidator.isValidEmail(input)) {
|
||||
updateProfileField("email", input.getText().toString());
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Email is invalid", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
@@ -191,18 +133,17 @@ public class ProfileFragment extends Fragment {
|
||||
|
||||
//Edit phone button
|
||||
//When clicked open a dialog to change phone
|
||||
btnEditPhone.setOnClickListener(v -> {
|
||||
binding.btnEditPhone.setOnClickListener(v -> {
|
||||
//Make a text field for the user to enter the new email
|
||||
EditText input = new EditText(requireContext());
|
||||
input.setPadding(30,30,30,30);
|
||||
input.setText(tvProfilePhone.getText().toString());
|
||||
input.setText(binding.tvProfilePhone.getText().toString());
|
||||
|
||||
//set input type to phone number
|
||||
input.setInputType(InputType.TYPE_CLASS_PHONE);
|
||||
input.setInputType(android.view.inputmethod.EditorInfo.TYPE_CLASS_PHONE);
|
||||
|
||||
//add canada phone number formatting to input (XXX) XXX-XXXX
|
||||
input.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA"));
|
||||
input.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
|
||||
//add canada phone number formatting to input
|
||||
UIUtils.formatPhoneInput(input);
|
||||
|
||||
|
||||
//Show alert dialog to user to enter new phone
|
||||
@@ -210,19 +151,10 @@ public class ProfileFragment extends Fragment {
|
||||
.setTitle("Edit Phone Number")
|
||||
.setView(input)
|
||||
.setPositiveButton("Save", (dialog, which) -> {
|
||||
String newPhone = input.getText().toString();
|
||||
//if the new value is format: (XXX) XXX-XXXX then set the phone to the new value
|
||||
if (newPhone.matches("\\(\\d{3}\\) \\d{3}-\\d{4}")) { //TODO MAKE VALIDATION CLASS INSTEAD FOR THIS
|
||||
tvProfilePhone.setText(newPhone);
|
||||
//TODO: UPDATE PHONE IN DATABASE
|
||||
}
|
||||
else {
|
||||
//tell the user to email cannot be empty
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle("Error")
|
||||
.setMessage("Phone number is invalid. Format: (XXX) XXX-XXXX")
|
||||
.setPositiveButton("OK", null)
|
||||
.show();
|
||||
if (InputValidator.isValidPhone(input)) {
|
||||
updateProfileField("phone", input.getText().toString());
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Phone number is invalid", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
@@ -230,26 +162,143 @@ public class ProfileFragment extends Fragment {
|
||||
});
|
||||
|
||||
//Logout button
|
||||
btnLogout.setOnClickListener(v -> {
|
||||
TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login
|
||||
binding.btnLogout.setOnClickListener(v -> {
|
||||
// Stop notification service before logging out so notifications stop
|
||||
android.content.Intent serviceIntent = new android.content.Intent(requireContext(), ChatNotificationService.class);
|
||||
requireContext().stopService(serviceIntent);
|
||||
|
||||
// clear the token for next login
|
||||
tokenManager.clearLoginData();
|
||||
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
|
||||
Intent intent = new Intent(getActivity(), MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
android.content.Intent intent = new android.content.Intent(getActivity(), MainActivity.class);
|
||||
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
//start the activity to go to login page and finish the current activity
|
||||
startActivity(intent);
|
||||
requireActivity().finish();
|
||||
});
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
//Helper function create a file in the cache directory to store the photo in then launch the camera to capture the photo
|
||||
private void launchCamera() {
|
||||
//create a file in the cache directory to store the photo in
|
||||
File photoFile = new File(requireContext().getCacheDir(), "profile_photo.jpg");
|
||||
//get the uri for the file made
|
||||
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
|
||||
//launch the camera to capture the photo and save the photo to photoUri
|
||||
cameraLauncher.launch(photoUri);
|
||||
/**
|
||||
* Toggles the visibility of the progress bar.
|
||||
*/
|
||||
private void setLoading(boolean loading) {
|
||||
if (binding != null && binding.progressBar != null) {
|
||||
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the binding when the view is destroyed.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches current user profile data from the API and then updates the UI.
|
||||
*/
|
||||
private void loadProfileData() {
|
||||
viewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
currentUser = resource.data;
|
||||
|
||||
//set the user data to the view
|
||||
binding.tvProfileName.setText(currentUser.getFullName());
|
||||
binding.tvProfileEmail.setText(currentUser.getEmail());
|
||||
binding.tvProfilePhone.setText(currentUser.getPhone());
|
||||
binding.tvProfileRole.setText(currentUser.getRole());
|
||||
|
||||
// get the avatar endpoint to load profile image and the token for authorization
|
||||
String avatarUrl = baseUrl + AuthApi.AVATAR_FILE_PATH;
|
||||
String token = tokenManager.getToken();
|
||||
|
||||
GlideUtils.loadImageWithToken(requireContext(), binding.imgProfile, avatarUrl, token, R.drawable.placeholder, new GlideUtils.ImageLoadListener() {
|
||||
@Override
|
||||
public void onResourceReady() {
|
||||
hasImage = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed() {
|
||||
hasImage = false;
|
||||
}
|
||||
});
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Failed to load profile: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the selected or captured image as the user's new avatar.
|
||||
*/
|
||||
private void uploadAvatar(Uri uri) {
|
||||
try {
|
||||
File file = FileUtils.getFileFromUri(requireContext(), uri);
|
||||
if (file == null) return;
|
||||
|
||||
// Create RequestBody for file upload
|
||||
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
|
||||
MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile);
|
||||
|
||||
//Call the backend to upload the avatar
|
||||
viewModel.uploadAvatar(body).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
Toast.makeText(getContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show();
|
||||
loadProfileData();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Upload failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e("UPLOAD_AVATAR", "Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API to delete the current user's avatar image.
|
||||
*/
|
||||
private void deleteAvatar() {
|
||||
viewModel.deleteAvatar().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
hasImage = false;
|
||||
binding.imgProfile.setImageResource(R.drawable.placeholder);
|
||||
Toast.makeText(getContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show();
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Removal failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a specific profile field (like email or phone) by sending a request to the API.
|
||||
*/
|
||||
private void updateProfileField(String fieldName, String value) {
|
||||
Map<String, String> updates = new HashMap<>();
|
||||
updates.put(fieldName, value);
|
||||
|
||||
viewModel.updateMe(updates).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource == null) return;
|
||||
setLoading(resource.status == Resource.Status.LOADING);
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
currentUser = resource.data;
|
||||
Toast.makeText(getContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
|
||||
// Update the view with the new data from backend
|
||||
binding.tvProfileEmail.setText(currentUser.getEmail());
|
||||
binding.tvProfilePhone.setText(currentUser.getPhone());
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
Toast.makeText(getContext(), "Update failed: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Fragment for browsing the activity log.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.app.DatePickerDialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.example.petstoremobile.adapters.ActivityLogAdapter;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentActivityLogBinding;
|
||||
import com.example.petstoremobile.dtos.ActivityLogDTO;
|
||||
import com.example.petstoremobile.dtos.DropdownDTO;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.ActivityLogListViewModel;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for viewing application activity logs with various filtering options.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class ActivityLogFragment extends Fragment {
|
||||
private FragmentActivityLogBinding binding;
|
||||
private ActivityLogListViewModel viewModel;
|
||||
private ActivityLogAdapter adapter;
|
||||
private final List<ActivityLogDTO> logList = new ArrayList<>();
|
||||
private List<DropdownDTO> storeList = new ArrayList<>();
|
||||
private String selectedStartDate = null;
|
||||
private String selectedEndDate = null;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
/**
|
||||
* Inflates the layout, checks for admin access, and initializes ViewModel and UI components.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentActivityLogBinding.inflate(inflater, container, false);
|
||||
|
||||
if (!"ADMIN".equalsIgnoreCase(tokenManager.getRole())) {
|
||||
Toast.makeText(requireContext(), "Access denied", Toast.LENGTH_SHORT).show();
|
||||
NavHostFragment.findNavController(this).popBackStack();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(ActivityLogListViewModel.class);
|
||||
|
||||
setupRecyclerView();
|
||||
setupFilters();
|
||||
observeViewModel();
|
||||
|
||||
binding.swipeRefreshActivityLog.setOnRefreshListener(() -> viewModel.loadInitialData());
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburgerActivityLog, this);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers initial data loading after the view is created.
|
||||
*/
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
viewModel.loadInitialData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the RecyclerView and its scroll listener for pagination.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new ActivityLogAdapter(logList);
|
||||
binding.recyclerViewActivityLog.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewActivityLog.setAdapter(adapter);
|
||||
|
||||
binding.recyclerViewActivityLog.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy <= 0) return;
|
||||
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewActivityLog.getLayoutManager();
|
||||
if (lm == null) return;
|
||||
int visible = lm.getChildCount();
|
||||
int total = lm.getItemCount();
|
||||
int firstVis = lm.findFirstVisibleItemPosition();
|
||||
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||
viewModel.loadLogs(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up filters for logs, including search, role, store, and date range.
|
||||
*/
|
||||
private void setupFilters() {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
|
||||
binding.etSearchLog, binding.spinnerRoleFilter, binding.spinnerStoreFilter);
|
||||
|
||||
UIUtils.attachSearch(binding.etSearchLog, () ->
|
||||
viewModel.setSearchQuery(binding.etSearchLog.getText().toString()));
|
||||
|
||||
String[] roles = {"All Roles", "Admin", "Staff", "Customer"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRoleFilter, roles, () ->
|
||||
viewModel.setRoleFilter(binding.spinnerRoleFilter.getSelectedItem() != null
|
||||
? binding.spinnerRoleFilter.getSelectedItem().toString() : "All Roles"));
|
||||
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreFilter, this::onStoreSelected);
|
||||
|
||||
binding.btnStartDate.setOnClickListener(v -> showDatePicker(true));
|
||||
binding.btnEndDate.setOnClickListener(v -> showDatePicker(false));
|
||||
binding.btnClearDates.setOnClickListener(v -> {
|
||||
selectedStartDate = null;
|
||||
selectedEndDate = null;
|
||||
binding.btnStartDate.setText("Start Date");
|
||||
binding.btnEndDate.setText("End Date");
|
||||
binding.btnClearDates.setVisibility(View.GONE);
|
||||
viewModel.setDateRange(null, null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a date picker dialog and updates the selected start or end date.
|
||||
*/
|
||||
private void showDatePicker(boolean isStart) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
new DatePickerDialog(requireContext(), (view, year, month, day) -> {
|
||||
String date = String.format("%04d-%02d-%02d", year, month + 1, day);
|
||||
String label = String.format("%02d/%02d/%04d", day, month + 1, year);
|
||||
if (isStart) {
|
||||
selectedStartDate = date;
|
||||
binding.btnStartDate.setText(label);
|
||||
} else {
|
||||
selectedEndDate = date;
|
||||
binding.btnEndDate.setText(label);
|
||||
}
|
||||
binding.btnClearDates.setVisibility(
|
||||
selectedStartDate != null || selectedEndDate != null ? View.VISIBLE : View.GONE);
|
||||
viewModel.setDateRange(selectedStartDate, selectedEndDate);
|
||||
}, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles store selection from the filter spinner.
|
||||
*/
|
||||
private void onStoreSelected() {
|
||||
int pos = binding.spinnerStoreFilter.getSelectedItemPosition();
|
||||
Long storeId = (pos > 0 && !storeList.isEmpty()) ? storeList.get(pos - 1).getId() : null;
|
||||
viewModel.setStoreFilter(storeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the ViewModel for log list updates, store options, and loading status.
|
||||
*/
|
||||
private void observeViewModel() {
|
||||
viewModel.getLogs().observe(getViewLifecycleOwner(), list -> {
|
||||
logList.clear();
|
||||
logList.addAll(list);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getStoreOptions().observe(getViewLifecycleOwner(), stores -> {
|
||||
storeList = stores;
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreFilter,
|
||||
stores, DropdownDTO::getLabel, "All Stores", -1L, DropdownDTO::getId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading ->
|
||||
binding.swipeRefreshActivityLog.setRefreshing(loading));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the binding reference.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
@@ -1,163 +1,347 @@
|
||||
/*
|
||||
* Fragment for browsing adoption records.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
// Added search/filter bar to filter adoptions by adopter name or pet name.
|
||||
// Added pull-to-refresh using SwipeRefreshLayout.
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.AdoptionAdapter;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.AdoptionDetailFragment;
|
||||
import com.example.petstoremobile.models.Adoption;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentAdoptionBinding;
|
||||
import com.example.petstoremobile.dtos.AdoptionDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.AdoptionListViewModel;
|
||||
import com.example.petstoremobile.utils.EventDecorator;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarDay;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarMode;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and managing a list of adoptions with a calendar view and filtering.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener {
|
||||
|
||||
private List<Adoption> adoptionList = new ArrayList<>();
|
||||
private List<Adoption> filteredList = new ArrayList<>();
|
||||
private FragmentAdoptionBinding binding;
|
||||
private List<AdoptionDTO> adoptionList = new ArrayList<>();
|
||||
private AdoptionAdapter adapter;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private EditText etSearch;
|
||||
private ImageButton hamburger;
|
||||
private AdoptionListViewModel viewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
private CalendarDay selectedCalendarDay;
|
||||
private boolean isMonthMode = false;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
/**
|
||||
* Initializes the ViewModel.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout and sets up UI components, calendar, and observers.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_adoption, container, false);
|
||||
binding = FragmentAdoptionBinding.inflate(inflater, container, false);
|
||||
|
||||
hamburger = view.findViewById(R.id.btnHamburger);
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupStatusFilter();
|
||||
setupStoreFilter();
|
||||
setupSwipeRefresh();
|
||||
setupCalendar();
|
||||
setupFilterToggle();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
loadAdoptionData();
|
||||
// Replace with actual API call when backend is ready
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
binding.fabAddAdoption.setOnClickListener(v -> openDetail(-1));
|
||||
|
||||
FloatingActionButton fabAddAdoption = view.findViewById(R.id.fabAddAdoption);
|
||||
fabAddAdoption.setOnClickListener(v -> openAdoptionDetails(-1));
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburgerAdoption, this);
|
||||
|
||||
//Make the hamburger button open the drawer from listFragment
|
||||
hamburger.setOnClickListener(v -> {
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
//if list fragment is found then use its helper function to open the drawer
|
||||
if (listFragment != null) {
|
||||
listFragment.openDrawer();
|
||||
}
|
||||
});
|
||||
binding.btnToggleCalendarModeAdoption.setOnClickListener(v -> toggleCalendarMode());
|
||||
|
||||
return view;
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
// Filters adoption list by adopter name or pet name
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchAdoption);
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterAdoptions(s.toString());
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
private void filterAdoptions(String query) {
|
||||
filteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(adoptionList);
|
||||
} else {
|
||||
String lower = query.toLowerCase();
|
||||
for (Adoption a : adoptionList) {
|
||||
if (a.getAdopterName().toLowerCase().contains(lower)
|
||||
|| a.getPetName().toLowerCase().contains(lower)
|
||||
|| a.getStatus().toLowerCase().contains(lower)) {
|
||||
filteredList.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAdoption);
|
||||
swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||
loadAdoptionData(); // TODO: Replace with actual API call
|
||||
filterAdoptions(etSearch.getText().toString());
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
});
|
||||
}
|
||||
|
||||
private void openAdoptionDetails(int position) {
|
||||
AdoptionDetailFragment detailFragment = new AdoptionDetailFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("position", position);
|
||||
|
||||
if (position != -1) {
|
||||
Adoption adoption = filteredList.get(position);
|
||||
int realPosition = adoptionList.indexOf(adoption);
|
||||
args.putInt("position", realPosition);
|
||||
args.putInt("adoptionId", adoption.getAdoptionId());
|
||||
args.putString("adopterName", adoption.getAdopterName());
|
||||
args.putString("adopterEmail", adoption.getAdopterEmail());
|
||||
args.putString("adopterPhone", adoption.getAdopterPhone());
|
||||
args.putString("petName", adoption.getPetName());
|
||||
args.putString("adoptionDate", adoption.getAdoptionDate());
|
||||
args.putString("status", adoption.getStatus());
|
||||
}
|
||||
|
||||
detailFragment.setArguments(args);
|
||||
detailFragment.setAdoptionFragment(this);
|
||||
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
if (listFragment != null) listFragment.loadFragment(detailFragment);
|
||||
}
|
||||
|
||||
public void onAdoptionSaved(int position, Adoption adoption) {
|
||||
if (position == -1) {
|
||||
adoptionList.add(adoption);
|
||||
} else {
|
||||
adoptionList.set(position, adoption);
|
||||
}
|
||||
filterAdoptions(etSearch.getText().toString());
|
||||
}
|
||||
|
||||
public void onAdoptionDeleted(int position) {
|
||||
adoptionList.remove(position);
|
||||
filterAdoptions(etSearch.getText().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdoptionClick(int position) {
|
||||
openAdoptionDetails(position);
|
||||
}
|
||||
|
||||
private void loadAdoptionData() {
|
||||
/**
|
||||
* Observes the ViewModel for adoption list, stores, and loading status.
|
||||
*/
|
||||
private void observeViewModel() {
|
||||
viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> {
|
||||
adoptionList.clear();
|
||||
adoptionList.add(new Adoption(1, "Sarah Connor", "sarah@email.com", "555-1234", "Luna", "2026-03-01", "Approved"));
|
||||
adoptionList.add(new Adoption(2, "Tom Hardy", "tom@email.com", "555-5678", "Bella", "2026-03-05", "Pending"));
|
||||
adoptionList.add(new Adoption(3, "Emily Clark", "emily@email.com", "555-9012", "Charlie", "2026-03-07", "Pending"));
|
||||
adoptionList.add(new Adoption(4, "Mike Ross", "mike@email.com", "555-3456", "Milo", "2026-02-20", "Rejected"));
|
||||
filteredList.clear();
|
||||
filteredList.addAll(adoptionList);
|
||||
adoptionList.addAll(list);
|
||||
updateCalendarDecorators();
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreAdoption, list,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshAdoption.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAdoptions);
|
||||
adapter = new AdoptionAdapter(filteredList, this);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
recyclerView.setAdapter(adapter);
|
||||
/**
|
||||
* Configures the bulk delete handler for multiple adoption record deletion.
|
||||
*/
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
binding.layoutBulkDelete,
|
||||
binding.tvSelectionCount,
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"adoption",
|
||||
viewModel::bulkDeleteAdoptions,
|
||||
() -> loadAdoptions(true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads adoption data and stores when the fragment resumes.
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadAdoptions(true);
|
||||
if (!isStaff()) viewModel.loadStores();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles between month and week display modes for the calendar.
|
||||
*/
|
||||
private void toggleCalendarMode() {
|
||||
isMonthMode = !isMonthMode;
|
||||
binding.calendarViewAdoption.state().edit()
|
||||
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter visibility toggle, considering user roles.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
if (isStaff()) {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
|
||||
binding.etSearchAdoption, binding.spinnerStatusAdoption);
|
||||
binding.spinnerStoreAdoption.setVisibility(View.GONE);
|
||||
} else {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilterAdoption, binding.layoutFilterAdoption,
|
||||
binding.etSearchAdoption, binding.spinnerStatusAdoption, binding.spinnerStoreAdoption);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the currently logged-in user has the STAFF role.
|
||||
*/
|
||||
private boolean isStaff() {
|
||||
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the calendar view for date-based filtering.
|
||||
*/
|
||||
private void setupCalendar() {
|
||||
binding.calendarViewAdoption.setOnDateChangedListener((widget, date, selected) -> {
|
||||
if (selected) {
|
||||
if (date.equals(selectedCalendarDay)) {
|
||||
selectedCalendarDay = null;
|
||||
binding.calendarViewAdoption.clearSelection();
|
||||
} else {
|
||||
selectedCalendarDay = date;
|
||||
}
|
||||
} else {
|
||||
selectedCalendarDay = null;
|
||||
}
|
||||
loadAdoptions(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates calendar decorators to highlight dates with adoptions.
|
||||
*/
|
||||
private void updateCalendarDecorators() {
|
||||
HashSet<CalendarDay> datesWithAdoptions = new HashSet<>();
|
||||
for (AdoptionDTO adoption : adoptionList) {
|
||||
try {
|
||||
if (adoption.getAdoptionDate() != null) {
|
||||
Date date = dateFormat.parse(adoption.getAdoptionDate());
|
||||
if (date != null) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(date);
|
||||
datesWithAdoptions.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
|
||||
}
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.e("AdoptionFragment", "Error parsing date: " + adoption.getAdoptionDate());
|
||||
}
|
||||
}
|
||||
binding.calendarViewAdoption.removeDecorators();
|
||||
binding.calendarViewAdoption.addDecorator(new EventDecorator(Color.RED, datesWithAdoptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the RecyclerView and its scroll listener for pagination.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new AdoptionAdapter(adoptionList, this);
|
||||
binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewAdoptions.setAdapter(adapter);
|
||||
|
||||
binding.recyclerViewAdoptions.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy <= 0) return;
|
||||
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewAdoptions.getLayoutManager();
|
||||
if (lm == null) return;
|
||||
int visible = lm.getChildCount();
|
||||
int total = lm.getItemCount();
|
||||
int firstVis = lm.findFirstVisibleItemPosition();
|
||||
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||
loadAdoptions(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the search input listener.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchAdoption, () -> loadAdoptions(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the status filter spinner.
|
||||
*/
|
||||
private void setupStatusFilter() {
|
||||
String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, () -> loadAdoptions(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, () -> loadAdoptions(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the swipe-to-refresh layout.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshAdoption.setOnRefreshListener(() -> loadAdoptions(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads adoption data based on current filters, search query, and selected date.
|
||||
*/
|
||||
private void loadAdoptions(boolean reset) {
|
||||
String query = binding.etSearchAdoption.getText().toString().trim();
|
||||
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
|
||||
|
||||
Long storeId;
|
||||
if (isStaff()) {
|
||||
storeId = tokenManager.getPrimaryStoreId();
|
||||
} else {
|
||||
storeId = null;
|
||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||
if (binding.spinnerStoreAdoption.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||
storeId = stores.get(binding.spinnerStoreAdoption.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
}
|
||||
|
||||
String selectedDateString = null;
|
||||
if (selectedCalendarDay != null) {
|
||||
selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
|
||||
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
|
||||
}
|
||||
|
||||
if (status.equals("All Statuses")) status = null;
|
||||
else status = status.toUpperCase();
|
||||
|
||||
viewModel.loadAdoptions(reset, query, status, storeId, selectedDateString, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the adoption detail screen.
|
||||
*/
|
||||
private void openDetail(int position) {
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1) {
|
||||
AdoptionDTO a = adoptionList.get(position);
|
||||
args.putLong("adoptionId", a.getAdoptionId());
|
||||
}
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_adoption_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adoption item clicks by opening details.
|
||||
*/
|
||||
@Override
|
||||
public void onAdoptionClick(int position) { openDetail(position); }
|
||||
|
||||
/**
|
||||
* Forwards selection changes to the bulk delete handler.
|
||||
*/
|
||||
@Override
|
||||
public void onSelectionChanged(int selectedCount) {
|
||||
if (bulkDeleteHandler != null) {
|
||||
bulkDeleteHandler.onSelectionChanged(selectedCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the binding reference.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Fragment for displaying store analytics and charts.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import javax.inject.Inject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Fragment for displaying business analytics, including revenue, transactions, and product performance.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class AnalyticsFragment extends Fragment {
|
||||
|
||||
@Inject
|
||||
TokenManager tokenManager;
|
||||
|
||||
private FragmentAnalyticsBinding binding;
|
||||
private AnalyticsViewModel viewModel;
|
||||
private boolean filtersExpanded = false;
|
||||
|
||||
private static final String[] TOP_N_OPTIONS = {"5", "10", "15", "20"};
|
||||
private static final int[] TOP_N_VALUES = { 5, 10, 15, 20 };
|
||||
|
||||
/**
|
||||
* Inflates the layout, initializes ViewModel, and sets up UI components and filters.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentAnalyticsBinding.inflate(inflater, container, false);
|
||||
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
|
||||
|
||||
setupFilterPanel();
|
||||
setupViewModeToggle();
|
||||
observeViewModel();
|
||||
viewModel.loadAnalytics();
|
||||
|
||||
binding.btnRefreshAnalytics.setOnClickListener(v -> viewModel.loadAnalytics());
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburgerAnalytics, this);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private static final int COLOR_SELECTED = 0xFF4ECDC4;
|
||||
private static final int COLOR_UNSELECTED = 0xFFCBD5E1;
|
||||
|
||||
/**
|
||||
* Configures the view mode toggle buttons (My Analytics vs Store Analytics).
|
||||
*/
|
||||
private void setupViewModeToggle() {
|
||||
updateViewModeButtonStyles(viewModel.getViewMode());
|
||||
|
||||
binding.btnMyAnalytics.setOnClickListener(v -> {
|
||||
viewModel.setViewMode("mine");
|
||||
updateViewModeButtonStyles("mine");
|
||||
updateStoreFilterVisibility("mine");
|
||||
});
|
||||
|
||||
binding.btnStoreAnalytics.setOnClickListener(v -> {
|
||||
viewModel.setViewMode("store");
|
||||
updateViewModeButtonStyles("store");
|
||||
updateStoreFilterVisibility("store");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the styles of the view mode buttons based on the selected mode.
|
||||
*/
|
||||
private void updateViewModeButtonStyles(String mode) {
|
||||
binding.btnMyAnalytics.setBackgroundTintList(
|
||||
android.content.res.ColorStateList.valueOf(mode.equals("mine") ? COLOR_SELECTED : COLOR_UNSELECTED));
|
||||
binding.btnStoreAnalytics.setBackgroundTintList(
|
||||
android.content.res.ColorStateList.valueOf(mode.equals("store") ? COLOR_SELECTED : COLOR_UNSELECTED));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visibility of the store filter based on the user's role and selected view mode.
|
||||
*/
|
||||
private void updateStoreFilterVisibility(String mode) {
|
||||
boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole());
|
||||
int vis = (isAdmin && mode.equals("store")) ? View.VISIBLE : View.GONE;
|
||||
binding.tvStoreFilterLabel.setVisibility(vis);
|
||||
binding.spinnerFilterStore.setVisibility(vis);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configures the filter panel, including date pickers, presets, and action buttons.
|
||||
*/
|
||||
private void setupFilterPanel() {
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerTopN, TOP_N_OPTIONS);
|
||||
|
||||
// Toggle expand/collapse
|
||||
binding.rowFilterHeader.setOnClickListener(v -> toggleFilters());
|
||||
|
||||
// Date pickers
|
||||
binding.etFilterStartDate.setOnClickListener(v ->
|
||||
UIUtils.showDatePicker(requireContext(), binding.etFilterStartDate, this::updateFilterSummary));
|
||||
binding.etFilterEndDate.setOnClickListener(v ->
|
||||
UIUtils.showDatePicker(requireContext(), binding.etFilterEndDate, this::updateFilterSummary));
|
||||
|
||||
// Quick presets
|
||||
binding.btnPresetToday.setOnClickListener(v -> applyPreset(0, 0));
|
||||
binding.btnPreset7D.setOnClickListener(v -> applyPreset(-6, 0));
|
||||
binding.btnPreset30D.setOnClickListener(v -> applyPreset(-29, 0));
|
||||
binding.btnPreset3M.setOnClickListener(v -> applyPreset(-89, 0));
|
||||
binding.btnPreset1Y.setOnClickListener(v -> applyPreset(-364, 0));
|
||||
binding.btnPresetAll.setOnClickListener(v -> {
|
||||
binding.etFilterStartDate.setText("");
|
||||
binding.etFilterEndDate.setText("");
|
||||
updateFilterSummary();
|
||||
});
|
||||
|
||||
binding.btnFilterApply.setOnClickListener(v -> applyFiltersFromUI());
|
||||
binding.btnFilterReset.setOnClickListener(v -> resetFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the filter content section.
|
||||
*/
|
||||
private void toggleFilters() {
|
||||
filtersExpanded = !filtersExpanded;
|
||||
binding.llFilterContent.setVisibility(filtersExpanded ? View.VISIBLE : View.GONE);
|
||||
binding.tvFilterToggleIcon.setText(filtersExpanded ? "▲" : "▼");
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a date range preset to the filter fields.
|
||||
*/
|
||||
private void applyPreset(int startOffset, int endOffset) {
|
||||
binding.etFilterStartDate.setText(getDateString(startOffset));
|
||||
binding.etFilterEndDate.setText(getDateString(endOffset));
|
||||
updateFilterSummary();
|
||||
applyFiltersFromUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads filter values from the UI and applies them to the ViewModel.
|
||||
*/
|
||||
private void applyFiltersFromUI() {
|
||||
AnalyticsViewModel.FilterState filter = new AnalyticsViewModel.FilterState();
|
||||
filter.startDate = binding.etFilterStartDate.getText().toString().trim();
|
||||
filter.endDate = binding.etFilterEndDate.getText().toString().trim();
|
||||
|
||||
Object pm = binding.spinnerFilterPayment.getSelectedItem();
|
||||
filter.paymentMethod = pm != null ? pm.toString() : "All";
|
||||
|
||||
int topNPos = binding.spinnerTopN.getSelectedItemPosition();
|
||||
filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
|
||||
|
||||
Object store = binding.spinnerFilterStore.getSelectedItem();
|
||||
viewModel.setStoreFilter(store != null ? store.toString() : "All Stores");
|
||||
|
||||
updateFilterSummary();
|
||||
viewModel.applyFilter(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all filters to their default values.
|
||||
*/
|
||||
private void resetFilters() {
|
||||
binding.etFilterStartDate.setText("");
|
||||
binding.etFilterEndDate.setText("");
|
||||
binding.spinnerTopN.setSelection(0);
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All");
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, "All Stores");
|
||||
updateFilterSummary();
|
||||
viewModel.resetFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the text summary of the currently selected date range.
|
||||
*/
|
||||
private void updateFilterSummary() {
|
||||
String start = binding.etFilterStartDate.getText().toString().trim();
|
||||
String end = binding.etFilterEndDate.getText().toString().trim();
|
||||
if (start.isEmpty() && end.isEmpty()) {
|
||||
binding.tvFilterSummary.setText("All time");
|
||||
} else if (start.isEmpty()) {
|
||||
binding.tvFilterSummary.setText("Up to " + shortDate(end));
|
||||
} else if (end.isEmpty()) {
|
||||
binding.tvFilterSummary.setText("From " + shortDate(start));
|
||||
} else {
|
||||
binding.tvFilterSummary.setText(shortDate(start) + " – " + shortDate(end));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date string into a shorter version for display.
|
||||
*/
|
||||
private String shortDate(String date) {
|
||||
return (date != null && date.length() >= 10) ? date.substring(5) : date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted date string for a given day.
|
||||
*/
|
||||
private String getDateString(int offsetDays) {
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.add(Calendar.DAY_OF_YEAR, offsetDays);
|
||||
return String.format(Locale.US, "%04d-%02d-%02d",
|
||||
c.get(Calendar.YEAR), c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Observes the ViewModel for analytics data, loading status, errors, and filter options.
|
||||
*/
|
||||
private void observeViewModel() {
|
||||
viewModel.getAnalyticsData().observe(getViewLifecycleOwner(), this::computeAndDisplay);
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
|
||||
if (loading) {
|
||||
binding.tvTotalRevenue.setText("Loading...");
|
||||
binding.tvTotalTransactions.setText("...");
|
||||
binding.tvAvgTransaction.setText("...");
|
||||
binding.tvTotalItems.setText("...");
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getErrorMessage().observe(getViewLifecycleOwner(), error -> {
|
||||
if (error != null) showError(error);
|
||||
});
|
||||
|
||||
viewModel.getAvailablePaymentMethods().observe(getViewLifecycleOwner(), methods -> {
|
||||
if (methods == null || methods.isEmpty()) return;
|
||||
String currentSelection = binding.spinnerFilterPayment.getSelectedItem() != null
|
||||
? binding.spinnerFilterPayment.getSelectedItem().toString() : "All";
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterPayment,
|
||||
methods.toArray(new String[0]));
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection);
|
||||
});
|
||||
|
||||
viewModel.getAvailableStores().observe(getViewLifecycleOwner(), stores -> {
|
||||
if (stores == null || stores.isEmpty()) return;
|
||||
String currentSelection = binding.spinnerFilterStore.getSelectedItem() != null
|
||||
? binding.spinnerFilterStore.getSelectedItem().toString() : "All Stores";
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterStore,
|
||||
stores.toArray(new String[0]));
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, currentSelection);
|
||||
updateStoreFilterVisibility(viewModel.getViewMode());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the binding reference.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Computes and displays analytics data in summary cards and bar charts.
|
||||
*/
|
||||
private void computeAndDisplay(AnalyticsViewModel.AnalyticsData data) {
|
||||
if (data == null) return;
|
||||
|
||||
// Summary cards
|
||||
binding.tvTotalRevenue.setText("$" + data.totalRevenue.setScale(2, RoundingMode.HALF_UP));
|
||||
binding.tvTotalTransactions.setText(String.valueOf(data.totalTransactions));
|
||||
binding.tvAvgTransaction.setText("$" + data.avgTransaction);
|
||||
binding.tvTotalItems.setText(String.valueOf(data.totalItems));
|
||||
|
||||
// Top Revenue Products
|
||||
binding.llTopRevenue.removeAllViews();
|
||||
if (data.topRevenueProducts != null && !data.topRevenueProducts.isEmpty()) {
|
||||
BigDecimal maxRev = data.topRevenueProducts.get(0).getValue();
|
||||
if (maxRev.compareTo(BigDecimal.ZERO) == 0) maxRev = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.topRevenueProducts) {
|
||||
addBarRow(binding.llTopRevenue, e.getKey(),
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxRev.floatValue(), "#ff6b35");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llTopRevenue, "No data");
|
||||
}
|
||||
|
||||
// Top Quantity Products
|
||||
binding.llTopQuantity.removeAllViews();
|
||||
if (data.topQuantityProducts != null && !data.topQuantityProducts.isEmpty()) {
|
||||
int maxQty = data.topQuantityProducts.get(0).getValue();
|
||||
if (maxQty == 0) maxQty = 1;
|
||||
for (Map.Entry<String, Integer> e : data.topQuantityProducts) {
|
||||
addBarRow(binding.llTopQuantity, e.getKey(), e.getValue() + " units",
|
||||
(float) e.getValue() / maxQty, "#4ecdc4");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llTopQuantity, "No data");
|
||||
}
|
||||
|
||||
// Payment Methods
|
||||
binding.llPaymentMethods.removeAllViews();
|
||||
if (data.paymentMethodStats != null && !data.paymentMethodStats.isEmpty()) {
|
||||
int maxPay = data.paymentMethodStats.stream().mapToInt(Map.Entry::getValue).max().orElse(1);
|
||||
String[] payColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" };
|
||||
int ci = 0;
|
||||
for (Map.Entry<String, Integer> e : data.paymentMethodStats) {
|
||||
addBarRow(binding.llPaymentMethods, e.getKey(),
|
||||
e.getValue() + " transactions",
|
||||
(float) e.getValue() / maxPay, payColors[ci++ % payColors.length]);
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llPaymentMethods, "No data");
|
||||
}
|
||||
|
||||
// Employee Performance
|
||||
boolean showEmployeeSection = viewModel.getViewMode().equals("store");
|
||||
View empParent = (View) binding.llEmployeePerformance.getParent();
|
||||
if (empParent != null) empParent.setVisibility(showEmployeeSection ? View.VISIBLE : View.GONE);
|
||||
if (showEmployeeSection) {
|
||||
binding.llEmployeePerformance.removeAllViews();
|
||||
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
|
||||
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
|
||||
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
|
||||
addBarRow(binding.llEmployeePerformance, e.getKey(),
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llEmployeePerformance, "No data");
|
||||
}
|
||||
}
|
||||
|
||||
// Daily Revenue
|
||||
binding.tvDailyRevenueTitle.setText(data.dailyRevenueTitle);
|
||||
binding.llDailyRevenue.removeAllViews();
|
||||
if (data.dailyRevenue != null && !data.dailyRevenue.isEmpty()) {
|
||||
BigDecimal maxDaily = data.dailyRevenue.stream()
|
||||
.map(Map.Entry::getValue).max(BigDecimal::compareTo).orElse(BigDecimal.ONE);
|
||||
if (maxDaily.compareTo(BigDecimal.ZERO) == 0) maxDaily = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.dailyRevenue) {
|
||||
String label = e.getKey().length() >= 10 ? e.getKey().substring(5) : e.getKey();
|
||||
addBarRow(binding.llDailyRevenue, label,
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxDaily.floatValue(), "#ff6b35");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llDailyRevenue, "No data");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dynamically adds a bar chart row to a given layout container.
|
||||
*/
|
||||
private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) {
|
||||
if (getContext() == null) return;
|
||||
LinearLayout row = new LinearLayout(getContext());
|
||||
row.setOrientation(LinearLayout.VERTICAL);
|
||||
row.setPadding(0, 6, 0, 6);
|
||||
|
||||
LinearLayout labelRow = new LinearLayout(getContext());
|
||||
labelRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
TextView tvLabel = new TextView(getContext());
|
||||
tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
|
||||
tvLabel.setText(label);
|
||||
tvLabel.setTextColor(Color.parseColor("#444441"));
|
||||
tvLabel.setTextSize(13f);
|
||||
|
||||
TextView tvValue = new TextView(getContext());
|
||||
tvValue.setText(value);
|
||||
tvValue.setTextColor(Color.parseColor("#444441"));
|
||||
tvValue.setTextSize(13f);
|
||||
tvValue.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
|
||||
|
||||
labelRow.addView(tvLabel);
|
||||
labelRow.addView(tvValue);
|
||||
|
||||
LinearLayout barBg = new LinearLayout(getContext());
|
||||
LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 12);
|
||||
bgParams.setMargins(0, 4, 0, 0);
|
||||
barBg.setLayoutParams(bgParams);
|
||||
barBg.setBackgroundColor(Color.parseColor("#EEEEEE"));
|
||||
|
||||
float safeRatio = Math.max(0f, Math.min(1f, ratio));
|
||||
View barFill = new View(getContext());
|
||||
barFill.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, safeRatio));
|
||||
barFill.setBackgroundColor(Color.parseColor(color));
|
||||
barBg.addView(barFill);
|
||||
|
||||
View spacer = new View(getContext());
|
||||
spacer.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - safeRatio));
|
||||
barBg.addView(spacer);
|
||||
|
||||
row.addView(labelRow);
|
||||
row.addView(barBg);
|
||||
parent.addView(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an empty message row to a given layout container.
|
||||
*/
|
||||
private void addEmptyRow(LinearLayout parent, String message) {
|
||||
if (getContext() == null) return;
|
||||
TextView tv = new TextView(getContext());
|
||||
tv.setText(message);
|
||||
tv.setTextColor(Color.parseColor("#888780"));
|
||||
tv.setTextSize(13f);
|
||||
parent.addView(tv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message and updates UI to reflect the error state.
|
||||
*/
|
||||
private void showError(String msg) {
|
||||
if (getContext() == null || binding == null) return;
|
||||
binding.tvTotalRevenue.setText("Error");
|
||||
binding.tvTotalTransactions.setText("—");
|
||||
binding.tvAvgTransaction.setText("—");
|
||||
binding.tvTotalItems.setText("—");
|
||||
Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
@@ -1,167 +1,380 @@
|
||||
/*
|
||||
* Fragment for browsing appointments.
|
||||
*
|
||||
* Author: Alex, Nikitha
|
||||
* Date: April 2026
|
||||
*/
|
||||
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
// Added search/filter bar to filter appointments by customer name or service type.
|
||||
// Added pull-to-refresh using SwipeRefreshLayout.
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.AppointmentAdapter;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment;
|
||||
import com.example.petstoremobile.models.Appointment;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentAppointmentBinding;
|
||||
import com.example.petstoremobile.dtos.AppointmentDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.AppointmentListViewModel;
|
||||
import com.example.petstoremobile.utils.EventDecorator;
|
||||
import com.example.petstoremobile.viewmodels.AuthViewModel;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarDay;
|
||||
import com.prolificinteractive.materialcalendarview.CalendarMode;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
|
||||
/**
|
||||
* Fragment for displaying and managing a list of appointments with calendar integration and filtering.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener {
|
||||
|
||||
private List<Appointment> appointmentList = new ArrayList<>(); // full data list
|
||||
private List<Appointment> filteredList = new ArrayList<>(); // filtered display list
|
||||
private FragmentAppointmentBinding binding;
|
||||
private List<AppointmentDTO> appointmentList = new ArrayList<>();
|
||||
|
||||
private AppointmentAdapter adapter;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private EditText etSearch;
|
||||
private ImageButton hamburger;
|
||||
private AppointmentListViewModel viewModel;
|
||||
private AuthViewModel authViewModel;
|
||||
private BulkDeleteHandler bulkDeleteHandler;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
private CalendarDay selectedCalendarDay;
|
||||
private boolean isMonthMode = false;
|
||||
private Long currentUserId = null;
|
||||
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
|
||||
|
||||
/**
|
||||
* Initializes ViewModels for appointment and authentication data.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(AppointmentListViewModel.class);
|
||||
authViewModel = new ViewModelProvider(this).get(AuthViewModel.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates the layout and sets up UI components, calendar, and observers.
|
||||
*/
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_appointment, container, false);
|
||||
binding = FragmentAppointmentBinding.inflate(inflater, container, false);
|
||||
|
||||
hamburger = view.findViewById(R.id.btnHamburger);
|
||||
setupRecyclerView();
|
||||
setupSearch();
|
||||
setupStatusFilter();
|
||||
setupStoreFilter();
|
||||
setupSwipeRefresh();
|
||||
setupCalendar();
|
||||
setupFilterToggle();
|
||||
setupMyAppointmentFilter();
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
loadAppointmentData(); // TODO: Replace with actual API call when backend is ready
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupSwipeRefresh(view);
|
||||
binding.fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
|
||||
|
||||
FloatingActionButton fabAddAppointment = view.findViewById(R.id.fabAddAppointment);
|
||||
fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1));
|
||||
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
|
||||
|
||||
//Make the hamburger button open the drawer from listFragment
|
||||
hamburger.setOnClickListener(v -> {
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
//if list fragment is found then use its helper function to open the drawer
|
||||
if (listFragment != null) {
|
||||
listFragment.openDrawer();
|
||||
}
|
||||
});
|
||||
binding.btnToggleCalendarMode.setOnClickListener(v -> toggleCalendarMode());
|
||||
|
||||
return view;
|
||||
loadCurrentUserInfo();
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
// Sets up the search bar to filter appointments by customer name or service type
|
||||
private void setupSearch(View view) {
|
||||
etSearch = view.findViewById(R.id.etSearchAppointment);
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterAppointments(s.toString());
|
||||
}
|
||||
@Override public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
// Filters the appointment list based on the search query
|
||||
private void filterAppointments(String query) {
|
||||
filteredList.clear();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(appointmentList);
|
||||
} else {
|
||||
String lower = query.toLowerCase();
|
||||
for (Appointment a : appointmentList) {
|
||||
if (a.getCustomerName().toLowerCase().contains(lower)
|
||||
|| a.getServiceType().toLowerCase().contains(lower)
|
||||
|| a.getPetName().toLowerCase().contains(lower)) {
|
||||
filteredList.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Observes the ViewModel for appointment list, stores, and loading status.
|
||||
*/
|
||||
private void observeViewModel() {
|
||||
viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> {
|
||||
appointmentList.clear();
|
||||
appointmentList.addAll(list);
|
||||
updateCalendarDecorators();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
|
||||
// Sets up pull-to-refresh: reloads data when user swipes down
|
||||
private void setupSwipeRefresh(View view) {
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment);
|
||||
swipeRefreshLayout.setOnRefreshListener(() -> {
|
||||
loadAppointmentData(); // TODO: Replace with actual API call when backend is ready
|
||||
filterAppointments(etSearch.getText().toString());
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
|
||||
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshAppointment.setRefreshing(loading);
|
||||
});
|
||||
}
|
||||
|
||||
private void openAppointmentDetails(int position) {
|
||||
AppointmentDetailFragment detailFragment = new AppointmentDetailFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("position", position);
|
||||
|
||||
if (position != -1) {
|
||||
Appointment appointment = filteredList.get(position);
|
||||
// Find the real position in the full list for save/delete callbacks
|
||||
int realPosition = appointmentList.indexOf(appointment);
|
||||
args.putInt("position", realPosition);
|
||||
args.putInt("appointmentId", appointment.getAppointmentId());
|
||||
args.putString("customerName", appointment.getCustomerName());
|
||||
args.putString("petName", appointment.getPetName());
|
||||
args.putString("serviceType", appointment.getServiceType());
|
||||
args.putString("appointmentDate", appointment.getAppointmentDate());
|
||||
args.putString("appointmentTime", appointment.getAppointmentTime());
|
||||
args.putString("status", appointment.getStatus());
|
||||
/**
|
||||
* Configures the bulk delete handler for multiple appointment deletion.
|
||||
*/
|
||||
private void setupBulkDelete() {
|
||||
bulkDeleteHandler = new BulkDeleteHandler(
|
||||
this,
|
||||
binding.layoutBulkDelete,
|
||||
binding.tvSelectionCount,
|
||||
binding.btnBulkDelete,
|
||||
adapter,
|
||||
"appointment",
|
||||
viewModel::bulkDeleteAppointments,
|
||||
() -> loadAppointmentData(true)
|
||||
);
|
||||
}
|
||||
|
||||
detailFragment.setArguments(args);
|
||||
detailFragment.setAppointmentFragment(this);
|
||||
|
||||
ListFragment listFragment = (ListFragment) getParentFragment();
|
||||
if (listFragment != null) listFragment.loadFragment(detailFragment);
|
||||
/**
|
||||
* Reloads appointment data and stores when the fragment resumes.
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadAppointmentData(true);
|
||||
if (!isStaff()) viewModel.loadStores();
|
||||
}
|
||||
|
||||
public void onAppointmentSaved(int position, Appointment appointment) {
|
||||
if (position == -1) {
|
||||
appointmentList.add(appointment);
|
||||
/**
|
||||
* Toggles between month and week display modes for the calendar.
|
||||
*/
|
||||
private void toggleCalendarMode() {
|
||||
isMonthMode = !isMonthMode;
|
||||
binding.calendarView.state().edit()
|
||||
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the "My Appointments" filter button.
|
||||
*/
|
||||
private void setupMyAppointmentFilter() {
|
||||
binding.btnMyAppointments.setOnClickListener(v -> {
|
||||
loadAppointmentData(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads information about the currently logged-in user.
|
||||
*/
|
||||
private void loadCurrentUserInfo() {
|
||||
authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
currentUserId = resource.data.getId();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the filter visibility toggle.
|
||||
*/
|
||||
private void setupFilterToggle() {
|
||||
if (isStaff()) {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, binding.spinnerStatus);
|
||||
binding.spinnerStore.setVisibility(View.GONE);
|
||||
} else {
|
||||
appointmentList.set(position, appointment);
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment,
|
||||
binding.spinnerStatus, binding.spinnerStore);
|
||||
}
|
||||
filterAppointments(etSearch.getText().toString());
|
||||
}
|
||||
|
||||
public void onAppointmentDeleted(int position) {
|
||||
appointmentList.remove(position);
|
||||
filterAppointments(etSearch.getText().toString());
|
||||
/**
|
||||
* Configures the calendar view for date-based filtering.
|
||||
*/
|
||||
private void setupCalendar() {
|
||||
binding.calendarView.setOnDateChangedListener((widget, date, selected) -> {
|
||||
if (selected) {
|
||||
if (date.equals(selectedCalendarDay)) {
|
||||
selectedCalendarDay = null;
|
||||
binding.calendarView.clearSelection();
|
||||
} else {
|
||||
selectedCalendarDay = date;
|
||||
}
|
||||
} else {
|
||||
selectedCalendarDay = null;
|
||||
}
|
||||
loadAppointmentData(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates calendar decorators to highlight dates with appointments.
|
||||
*/
|
||||
private void updateCalendarDecorators() {
|
||||
HashSet<CalendarDay> datesWithAppointments = new HashSet<>();
|
||||
for (AppointmentDTO appointment : appointmentList) {
|
||||
try {
|
||||
Date date = dateFormat.parse(appointment.getAppointmentDate());
|
||||
if (date != null) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(date);
|
||||
datesWithAppointments.add(CalendarDay.from(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.e("AppointmentFragment", "Error parsing date: " + appointment.getAppointmentDate());
|
||||
}
|
||||
}
|
||||
binding.calendarView.removeDecorators();
|
||||
binding.calendarView.addDecorator(new EventDecorator(Color.RED, datesWithAppointments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the search input listener.
|
||||
*/
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchAppointment, () -> loadAppointmentData(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the status filter spinner.
|
||||
*/
|
||||
private void setupStatusFilter() {
|
||||
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadAppointmentData(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the store filter spinner.
|
||||
*/
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadAppointmentData(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the swipe-to-refresh layout.
|
||||
*/
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshAppointment.setOnRefreshListener(() -> loadAppointmentData(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the appointment detail screen.
|
||||
*/
|
||||
private void openAppointmentDetails(int position) {
|
||||
Bundle args = new Bundle();
|
||||
if (position != -1) {
|
||||
AppointmentDTO a = appointmentList.get(position);
|
||||
args.putLong("appointmentId", a.getAppointmentId());
|
||||
}
|
||||
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles appointment item clicks by opening details.
|
||||
*/
|
||||
@Override
|
||||
public void onAppointmentClick(int position) {
|
||||
openAppointmentDetails(position);
|
||||
}
|
||||
|
||||
// Helper function to load hardcoded sample data
|
||||
// Replace with API call
|
||||
private void loadAppointmentData() {
|
||||
appointmentList.clear();
|
||||
appointmentList.add(new Appointment(1, "John Smith", "Buddy", "Grooming", "2026-03-10", "10:00 AM", "Confirmed"));
|
||||
appointmentList.add(new Appointment(2, "Jane Doe", "Luna", "Vet Checkup", "2026-03-11", "02:00 PM", "Pending"));
|
||||
appointmentList.add(new Appointment(3, "Bob Lee", "Max", "Training", "2026-03-12", "11:00 AM", "Confirmed"));
|
||||
appointmentList.add(new Appointment(4, "Alice Brown", "Milo", "Grooming", "2026-03-13", "03:00 PM", "Cancelled"));
|
||||
filteredList.clear();
|
||||
filteredList.addAll(appointmentList);
|
||||
/**
|
||||
* Forwards selection changes to the bulk delete handler.
|
||||
*/
|
||||
@Override
|
||||
public void onSelectionChanged(int count) {
|
||||
if (bulkDeleteHandler != null) {
|
||||
bulkDeleteHandler.onSelectionChanged(count);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupRecyclerView(View view) {
|
||||
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments);
|
||||
adapter = new AppointmentAdapter(filteredList, this); // adapter uses filteredList
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
recyclerView.setAdapter(adapter);
|
||||
/**
|
||||
* Checks if the currently logged-in user has the STAFF role.
|
||||
*/
|
||||
private boolean isStaff() {
|
||||
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads appointment data based on current filters, search query, and selected date.
|
||||
*/
|
||||
private void loadAppointmentData(boolean reset) {
|
||||
String query = binding.etSearchAppointment.getText().toString().trim();
|
||||
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
||||
|
||||
Long storeId;
|
||||
if (isStaff()) {
|
||||
storeId = tokenManager.getPrimaryStoreId();
|
||||
} else {
|
||||
storeId = null;
|
||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||
if (binding.spinnerStore.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||
storeId = stores.get(binding.spinnerStore.getSelectedItemPosition() - 1).getStoreId();
|
||||
}
|
||||
}
|
||||
|
||||
String selectedDateString = null;
|
||||
if (selectedCalendarDay != null) {
|
||||
selectedDateString = String.format(Locale.getDefault(), "%04d-%02d-%02d",
|
||||
selectedCalendarDay.getYear(), selectedCalendarDay.getMonth(), selectedCalendarDay.getDay());
|
||||
}
|
||||
|
||||
Long employeeId = null;
|
||||
if (binding.btnMyAppointments.isChecked()) {
|
||||
employeeId = currentUserId;
|
||||
}
|
||||
|
||||
if (status.equals("All Statuses")) status = null;
|
||||
else status = status.toUpperCase();
|
||||
|
||||
viewModel.loadAppointments(reset, query, status, storeId, selectedDateString, employeeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the RecyclerView and its scroll listener for pagination.
|
||||
*/
|
||||
private void setupRecyclerView() {
|
||||
adapter = new AppointmentAdapter(appointmentList, this);
|
||||
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.recyclerViewAppointments.setAdapter(adapter);
|
||||
|
||||
binding.recyclerViewAppointments.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy <= 0) return;
|
||||
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewAppointments.getLayoutManager();
|
||||
if (lm == null) return;
|
||||
int visible = lm.getChildCount();
|
||||
int total = lm.getItemCount();
|
||||
int firstVis = lm.findFirstVisibleItemPosition();
|
||||
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||
loadAppointmentData(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the binding reference.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user